JS 模板字符串含反引号的处理与 Object.defineProperty Hook window 属性

模板字符串里包含反引号

反引号是模板字符串的开始/结束符,中间再出现反引号会直接语法错误:

// ❌ 语法错误
`
asdasd``
`

方案一:转义(最标准)

const str = `asd\`asd`;

方案二:String.raw(不处理转义)

const str = String.raw`asd\`asd`;

String.raw 是标签模板,适合存放正则、shell 命令、SQL、Prompt 等原始文本,不会解释 \n\t 等转义序列。

方案三:数组 join(不用转义,推荐)

const s = [
  '`',
  'asdasd``',
  '`'
].join('\n');

大型项目里代码生成器常用这种方式,完全不需要考虑转义问题,任意字符都行。

方案四:外部文件

内容包含大量反引号、${} 或换行的情况,最推荐直接存外部文件:

// Vite / 现代构建工具
import txt from './prompt.txt?raw';

// Node.js
const txt = fs.readFileSync('./prompt.txt', 'utf8');

AI prompt、HTML 模板、大段 SQL 都适合这种做法。

unsafeWindow 与 Proxy

油猴脚本里直接 window = unsafeWindow 会在某些环境报错,推荐用 Proxy:

const windowProxy = new Proxy(unsafeWindow, {
  get(target, prop) {
    return target[prop];
  },
  set(target, prop, value) {
    target[prop] = value;
    return true;
  }
});

关键限制new Proxy(window) 只影响局部变量 windowProxy,其他代码里的 window.xxx 仍然访问原生 window,不会走代理。真正要”全局 hook”必须用 Object.defineProperty

Object.defineProperty Hook window 属性

这是浏览器里最常用的 hook 方式,直接修改全局对象的属性描述符:

监听自定义属性的 get/set

let value;

Object.defineProperty(window, 'myToken', {
  configurable: true,
  get() {
    console.log('GET myToken');
    return value;
  },
  set(v) {
    console.log('SET myToken', v);
    value = v;
  }
});

其他脚本只要访问 window.myToken 就会触发上面的拦截器。

Hook fetch

const rawFetch = window.fetch;

Object.defineProperty(window, 'fetch', {
  configurable: true,
  get() {
    return function (...args) {
      console.log('fetch called:', args[0]);
      return rawFetch.apply(this, args);
    };
  }
});

Hook document.cookie

const rawSetter = Document.prototype.__lookupSetter__('cookie');

Document.prototype.__defineSetter__('cookie', function (val) {
  console.log('cookie set:', val);
  rawSetter.call(this, val);
});

通用 hook 工具函数

function hookGlobal(name) {
  let value;
  Object.defineProperty(window, name, {
    configurable: true,
    get() {
      console.log('GET', name);
      return value;
    },
    set(v) {
      console.log('SET', name, v);
      value = v;
    }
  });
}

hookGlobal('abc');

操作前先检查 configurable

部分内置属性(locationtopselfdocument)不可重定义,直接操作会抛错:

const desc = Object.getOwnPropertyDescriptor(window, 'fetch');
console.log(desc.configurable, desc.writable);

configurable: true 才能用 defineProperty 重新定义;writable: true 才能直接赋值覆盖。

Proxy vs Object.defineProperty 选哪个

场景推荐方式
Hook 单个全局 API(fetch、XHR)Object.defineProperty
局部作用域内代理对象Proxy
监控任意属性读写Proxy(但不适用于真正的 window)
跨越沙箱访问页面 windowunsafeWindow + Proxyscript 注入

整体代理 window 在浏览器里基本行不通,原生 window 是宿主对象,有大量不可配置属性。工程上的最佳实践是 hook 关键 API(fetch、XMLHttpRequest、WebSocket、JSON.parse)而不是整体代理。