杂项
Proxy 与 Reflect
Proxy 是 JavaScript 的元编程利器:它包装目标对象,拦截并自定义底层操作(属性读写、函数调用、
in运算符等)。
Reflect 是与 Proxy 配套的 API,提供与 Proxy 捕捉器一一对应的方法,用于安全转发操作到目标对象。
核心概念
| 概念 | 说明 |
|---|---|
| 目标对象 (target) | 被包装的原对象,可以是任何类型(对象、数组、函数、类实例)。 |
| 处理器 (handler) | 包含捕捉器 (traps) 的对象,定义拦截逻辑。 |
| 捕捉器 (trap) | 对应内部方法(如 [[Get]]、[[Set]])的拦截函数。 |
| 透明转发 | 若 handler 为空或某捕捉器缺失,操作直接透传给 target。 |
| 不变量 (Invariant) | 必须遵守的强制规则(如 set 成功必须返回 true)。 |
基础语法与透明代理
let target = { name: 'John' };
let proxy = new Proxy(target, {}); // 空 handler = 完全透明
proxy.age = 30; // 写入 proxy → 实际写入 target
console.log(target); // { name: 'John', age: 30 }
console.log(proxy.name); // 'John'(读取也透传)
// Proxy 自身无属性,只是 target 的透明包装
console.log(proxy === target); // false(不同对象)
关键原则:代理后应彻底替换对 target 的引用,避免混用导致混乱。
帇用捕捉器速查表
| 内部方法 | 捕捉器 | 触发场景 | 典型用途 |
|---|---|---|---|
[[Get]] | get | 读取属性 obj.prop | 默认值、访问控制、计算属性 |
[[Set]] | set | 写入属性 obj.prop = val | 验证、观察变化、私有保护 |
[[HasProperty]] | has | 'prop' in obj | 虚拟属性存在性检查 |
[[Delete]] | deleteProperty | delete obj.prop | 保护属性不被删除 |
[[Call]] | apply | 函数调用 fn() | 装饰器、延迟、参数预处理 |
[[Construct]] | construct | new Class() | 单例模式、参数验证 |
[[OwnPropertyKeys]] | ownKeys | Object.keys, for...in | 隐藏特定属性 |
[[GetOwnProperty]] | getOwnPropertyDescriptor | 获取属性描述符 | 配合 ownKeys 使虚拟属性可枚举 |
实战模式
1. 默认值模式(get 捕捉器)
// 数组越界返回 0,而非 undefined
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
return prop in target ? target[prop] : 0;
}
});
console.log(numbers[1]); // 1
console.log(numbers[99]); // 0(优雅降级)
// 词典:未翻译短语原样返回
let dictionary = { 'Hello': 'Hola' };
dictionary = new Proxy(dictionary, {
get(target, phrase) {
return phrase in target ? target[phrase] : phrase;
}
});
console.log(dictionary['Hello']); // Hola
console.log(dictionary['Welcome']); // Welcome(未翻译原样返回)
2. 验证模式(set 捕捉器)
// 纯数字数组:写入非数字抛出 TypeError
let numbers = [];
numbers = new Proxy(numbers, {
set(target, prop, val) {
if (typeof val !== 'number') {
return false; // 触发 TypeError
}
target[prop] = val;
return true; // 必须返回 true 表示成功
}
});
numbers.push(1); // OK
numbers.push('oops'); // TypeError: 'set' on proxy returned false
注意:数组方法(
push、pop等)内部使用[[Set]],因此自动被拦截,无需重写。
3. 私有属性保护(多捕捉器组合)
let user = {
name: 'John',
_password: 'secret123'
};
user = new Proxy(user, {
// 拦截读取
get(target, prop) {
if (prop.startsWith('_')) throw new Error('Access denied');
const val = target[prop];
// 方法需绑定 target,否则内部访问 _password 会触发代理
return typeof val === 'function' ? val.bind(target) : val;
},
// 拦截写入
set(target, prop, val) {
if (prop.startsWith('_')) throw new Error('Access denied');
target[prop] = val;
return true;
},
// 拦截删除
deleteProperty(target, prop) {
if (prop.startsWith('_')) throw new Error('Access denied');
delete target[prop];
return true;
},
// 拦截遍历:隐藏 _ 开头属性
ownKeys(target) {
return Object.keys(target).filter(k => !k.startsWith('_'));
}
});
console.log(Object.keys(user)); // ['name']
// console.log(user._password); // Error: Access denied
⚠️ 局限性:
bind方案会暴露原始 target,可能破坏代理链。现代 JS 推荐直接使用#private私有字段(但#private与 Proxy 也有兼容问题,见下文)。
4. 范围检查(has 捕捉器)
let range = { start: 1, end: 10 };
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
console.log(5 in range); // true
console.log(50 in range); // false
5. 函数装饰器(apply 捕捉器)
// 延迟执行装饰器,保留函数元属性(length、name)
function delay(fn, ms) {
return new Proxy(fn, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
console.log(`Hi, ${user}`);
}
const delayedSayHi = delay(sayHi, 3000);
console.log(delayedSayHi.length); // 1(传统包装函数会丢失)
delayedSayHi('John'); // 3秒后输出
Reflect:安全转发操作
为什么需要 Reflect?
| 场景 | 直接访问的问题 | Reflect 的优势 |
|---|---|---|
| Getter 继承 | target[prop] 会丢失 receiver(正确的 this) | Reflect.get 传递 receiver,确保 getter 中 this 指向正确 |
| 统一 API | 操作符(new、delete)无法作为函数调用 | Reflect.construct、Reflect.deleteProperty 等提供函数式调用 |
| 参数一致性 | 手动转发易遗漏参数 | 与 Proxy 捕捉器同名同参,可用 Reflect[trap](...arguments) 一键转发 |
典型用法
let user = {
_name: 'Guest',
get name() { return this._name; }
};
let userProxy = new Proxy(user, {
// ❌ 错误:getter 中的 this 指向 user,而非继承者
// get(target, prop) { return target[prop]; }
// ✅ 正确:传递 receiver,确保 this 指向实际调用者
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver);
// 或简写:return Reflect.get(...arguments);
}
});
let admin = {
__proto__: userProxy,
_name: 'Admin'
};
console.log(admin.name); // 'Admin'(正确)
Proxy 的局限性与解决方案
| 局限性 | 原因 | 解决方案 |
|---|---|---|
| 内置对象失效(Map、Set、Date 等) | 内置方法使用内部槽 [[MapData]],代理对象没有 | get 捕捉器中将方法绑定到 target:value.bind(target) |
私有字段 #private 失效 | 私有字段通过内部槽实现,不经过 [[Get]] | 同上,方法绑定到 target(但会暴露 target) |
严格相等 === 无法拦截 | 语言规范限制 | 无法解决,避免用 Proxy 作为 Map/Set 的键 |
| 性能开销 | 拦截增加调用层数 | 仅对需要元编程的对象使用,避免热路径 |
内置对象兼容示例
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
const val = Reflect.get(...arguments);
// 将方法绑定到原始 target,确保内部槽可访问
return typeof val === 'function' ? val.bind(target) : val;
}
});
proxy.set('key', 'value');
console.log(proxy.get('key')); // 'value'
可撤销代理(Revocable Proxy)
适用于临时访问控制(如租赁资源、安全沙箱):
let { proxy, revoke } = Proxy.revocable({ data: 'Sensitive' }, {});
console.log(proxy.data); // 'Sensitive'
revoke(); // 永久断开 proxy 与 target 的连接
console.log(proxy.data); // TypeError: Cannot perform 'get' on a proxy that has been revoked
可用
WeakMap<proxy, revoke>管理多个可撤销代理,避免内存泄漏。
任务实现参考
1. 读取不存在属性报错
function wrap(target) {
return new Proxy(target, {
get(target, prop) {
if (!(prop in target)) {
throw new ReferenceError(`Property doesn't exist: "${prop}"`);
}
return target[prop];
}
});
}
let user = wrap({ name: 'John' });
console.log(user.name); // John
// console.log(user.age); // ReferenceError: Property doesn't exist: "age"
2. 负索引数组
let array = [1, 2, 3];
array = new Proxy(array, {
get(target, prop) {
// 仅处理负整数索引
if (typeof prop === 'string' && /^-\d+$/.test(prop)) {
prop = target.length + parseInt(prop);
}
return target[prop];
}
});
console.log(array[-1]); // 3
console.log(array[-2]); // 2
array.push(4);
console.log(array[-1]); // 4(动态计算,始终指向末尾)
3. 可观察对象(Observable)
function makeObservable(target) {
const handlers = new Set();
return new Proxy(target, {
set(target, prop, val) {
const result = Reflect.set(...arguments);
// 触发所有观察者
handlers.forEach(handler => handler(prop, val));
return result;
},
// 通过方法添加观察者(利用 proxy 可扩展性)
get(target, prop) {
if (prop === 'observe') {
return (handler) => handlers.add(handler);
}
return target[prop];
}
});
}
let user = makeObservable({});
user.observe((key, value) => {
console.log(`SET ${key}=${value}`);
});
user.name = 'John'; // SET name=John
user.age = 30; // SET age=30
总结
Proxy = 对象的"拦截层"
在语言最底层劫持操作,实现验证、观察、虚拟化、访问控制等元编程能力;
Reflect = 可靠的"转发器"
确保在拦截后正确恢复默认行为,特别是处理继承和方法调用时不可或缺。
Eval:字符串执行
允许直接运行字符串:
eval可以访问外部变量,因为运行在当前词法环境,因此可以更改外部变量的值。- 严格模式下外部环境无法访问
eval内部定义的变量。 - 不启用严格模式,
eval没有属于自己的词法环境,因此可以从外部访问变量x和函数f。
let code = 'console.info("Hello")';
eval(code); // Hello
// 允许包含换行符、函数声明和变量等
let value = eval('let i = 0; ++i');
console.info(value); // 1
危害与建议
降低代码压缩率:
- 压缩工具会将变量名称缩短,以减小代码包体积。
- 通过
eval直接访问外部变量,压缩工具对所有可能被eval访问的变量不进行压缩处理,导致压缩率降低。
使用建议:
- 未使用外部变量情况下,使用
window.eval(...)的形式调用eval。 - 使用局部变量时,采用
new Function进行调用:
let f = new Function('a', 'console.info(a)');
f(5); // 5
其他字符串执行代码方案
在 纯 JavaScript 环境(浏览器 + Node.js 通用,不依赖特定 API)中,执行字符串代码的方式:
| 方式 | 说明 | 示例 |
|---|---|---|
eval(code) | 直接执行字符串代码,当前作用域 | eval("console.log('hello')") |
new Function(code) | 创建函数执行,全局作用域 | new Function("console.log('hello')")() |
Function.constructor | 绕过 new Function 检测 | Function("console.log('hello')")() |
setTimeout(code, delay) | 延迟执行字符串(浏览器/Node) | setTimeout("console.log('hello')", 0) |
setInterval(code, delay) | 定时执行字符串(浏览器/Node) | setInterval("console.log('hello')", 1000) |
注意:
setImmediate、queueMicrotask等 Node.js 特有,requestAnimationFrame、import()等 浏览器特有,均不属于"纯 JS 环境"通用方案。
防范策略
对这些方法进行覆写:
"use strict";
eval = undefined; // 可覆写
Function = undefined; // 可覆写
但以下方式难以完全禁用:
- 间接调用
eval(如(0, eval)(code)) - 通过原型链获取
Function构造函数
柯里化(Currying)
柯里化只是一种高阶技术规范,一种概念,并非局限某一个语言。
- 一种函数转换,从可调用的
f(a, b, c)转换为可调用的f(a)(b)(c) - 不会调用函数,只对函数进行转换
高级通用实现:lodash 库的
_.curry函数
为什么要柯里化
更好的创建部分应用函数或者部分函数,对于多参数函数,可以更加方便地创建易调用的版本。
假设一个日志打印方法:
function log(date, importance, message) {
console.info(`[${date.getHours()}:${date.getMinutes()}] [${importance}] ${message}`);
}
现在进行柯里化:
log = _.curry(log);
无论是柯里化调用还是正常调用都支持:
// 正常传参调用
log(new Date(), "DEBUG", "some debug"); // log(a, b, c)
// 柯里化调用
log(new Date())("DEBUG")("some debug"); // log(a)(b)(c)
创建部分应用函数,可以快速地调用对应场景的函数分支:
// logNow 会是带有固定第一个参数的日志的部分应用函数
let logNow = log(new Date());
logNow("INFO", "message"); // [HH:mm] INFO message
let debugNow = logNow("DEBUG");
debugNow("message"); // [HH:mm] DEBUG message
高级柯里化实现
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
};
}
- 传入参数
args长度与原始函数func.length参数长度相同或者更长时,使用apply转发到func上即可。 - 少于原始函数长度时,需要创建一个部分函数版本(目前未调用原始函数),返回的是另一个包装器,对原始函数进行重新包装。
柯里化仅支持转换固定长度参数的函数,使用 rest 参数(
...args)的方法不能以此进行柯里化。
Reference Type
主要是对
this动态丢失的详细讲解
错误示例
let user = {
name: "John",
hi() { console.info(this.name); },
bye() { console.info("Bye"); }
};
user.hi(); // 正常运行
// 现在让我们基于 name 来选择调用 user.hi 或 user.bye
(user.name == "John" ? user.hi : user.bye)(); // Error!
此处调用导致了一个错误,因为在该调用中 "this" 的值变成了 undefined。
调用对象方法分为两个操作:
- 点
'.'取了属性obj.method的值 ()执行了它
这就涉及到 this 从第一步传递到第二步,和以前一样分开调用就会丢失 this。
在
apply/call章节提到过
let user = {
name: "John",
hi() { console.info(this.name); }
};
// 把获取方法和调用方法拆成两行
let hi = user.hi;
hi(); // 报错了,因为 this 的值是 undefined
因为:点 '.' 返回的不是一个函数,而是一个特殊的 Reference Type 的值
Reference Type:
- ECMA 中的一个“规范类型”,无法使用,存在于语言内部
- 三个值的组合
(base, name, strict)base是对象name是属性名strict在use strict模式下为true
因此,user.hi 的结果不是一个函数,而是一个 Reference Type 的值,严格模式下为:
// Reference Type 的值
(user, "hi", true)
- 当
()被在 Reference Type 上调用时,会接收到关于对象和对象的方法的完整信息,然后可以设置正确的this(在此处=user) - Reference Type 充当“中间人”内部类型,目的是从
.传递信息给()调用 hi = user.hi等其他的操作,都会将 Reference Type 作为一个整体丢弃掉,而会取user.hi(一个函数)的值并继续传递,因此this发生了丢失
BigInt
特殊的数字类型,提供了对任意长度整数的支持。
创建方式:
const bigint = 1234567890123456789012345678901234567890n;
const sameBigint = BigInt("1234567890123456789012345678901234567890");
const bigintFromNumber = BigInt(10); // 与 10n 相同
常规数字支持的运算,BigInt 全都支持,但不能将 BigInt 与常规数字进行混用。
注意:
BigInt 不支持一元加法,无法使用
+bigint在非严格相等时:
console.info(1 == 1n); // true严格相等时:
console.info(1 === 1n); // false
console.info(1n + 2n); // 3
// BigInt 运算返回也是 BigInt,
// 结果向零进行舍入,舍入后得到的结果没有了小数部分
console.info(5n / 2n); // 2
混用前必须手动进行显式转换:
let bigint = 1n;
let number = 2;
// 将 number 转换为 bigint
console.info(bigint + BigInt(number)); // 3
// 将 bigint 转换为 number
console.info(Number(bigint) + number); // 3
BigInt 替代方案(三方库实现):JSBI
Unicode 字符串内幕
JavaScript 的字符串是基于 Unicode 的:每个字符由 1-4 个字节的字节序列表示。
十六进制 Unicode 编码
-
\xXX:XX 必须是介于00与FF之间的两位十六进制数,\xXX表示 Unicode 编码为 XX 的字符。由于只能表示两位,只能用于前 256 个 Unicode 字符
console.info("\x7A"); // z console.info("\xA9"); // © (版权符号) -
\uXXXX:XXXX 必须是 4 位十六进制数,值介于0000和FFFF之间。此时,\uXXXX便表示 Unicode 编码为 XXXX 的字符。console.info("\u00A9"); // ©, 等同于 \xA9,只是使用了四位十六进制数表示而已 console.info("\u044F"); // я(西里尔字母) console.info("\u2191"); // ↑(上箭头符号) -
\u{X…XXXXXX}:X…XXXXXX 必须是介于0和10FFFF(Unicode 定义的最高码位)之间的 1 到 6 个字节的十六进制值。能够轻松地表示所有现有的 Unicode 字符
console.info("\u{20331}"); // 佫, 不常见的中文字符(长 Unicode) console.info("\u{1F60D}"); // 😍
代理对
- 所有常用字符都有对应的 2 字节长度的编码(4 位十六进制数)
- 大多数欧洲语言的字母、数字、以及基本统一的 CJK 表意文字集(CJK —— 来自中文、日文和韩文书写系统)中的字母,均有对应的 2 字节长度的 Unicode 编码
JavaScript 是基于 UTF-16 编码的,只允许每个字符占 2 个字节长度。但 2 个字节只允许 65536 种组合,这对于表示 Unicode 里每个可能符号来说,是不够的。
对于超过 2 个字符的稀有字符则使用一对 2 字节长度的字符编码,它被称为“代理对”。(但这些符号的长度依旧为 2)
console.info('𝒳'.length); // 2, 大写的数学符号 X
console.info('😂'.length); // 2, 笑哭的表情
console.info('𩷶'.length); // 2, 一个少见的中文字符
无法通过简单的下标获取代理对的部分内容,会显示毫无意义的内容,代理对单个部分内容无意义,必须成对出现。
console.info('𝒳'[0]); // 显示出了一个奇怪的符号...
console.info('𝒳'[1]); // ...代理对的片段
正确处理代理对:
本质上与
String.fromCharCode和str.charCodeAt相同,但它们可以正确地处理代理对
// charCodeAt 不会考虑代理对,所以返回了 𝒳 前半部分的编码:
console.info('𝒳'.charCodeAt(0).toString(16)); // d835
// codePointAt 可以正确处理代理对
console.info('𝒳'.codePointAt(0).toString(16)); // 1d4b3,读取到了完整的代理对
变音符号
很多语言都有由基础字符及其上方/下方的标记所组成的符号。
Unicode 标准允许使用多个 Unicode 字符:基础字符后跟着一个或多个“装饰”它的“标记”字符。
比如 字母
a就是这些字符àáâäãåā的基础字符。
console.info('S\u0307'); // Ṡ
console.info('S\u0307\u0323'); // Ṩ
let s1 = 'S\u0307\u0323'; // Ṩ, S + 上方点符号 + 下方点符号
let s2 = 'S\u0323\u0307'; // Ṩ, S + 下方点符号 + 上方点符号
console.info(s1 == s2); // 视觉上是相同的,但结果却是 false
规范化处理:
normalize()将 3 个字符的序列合并为了一个字符
console.info(
"S\u0307\u0323".normalize() == "S\u0323\u0307".normalize()
); // true
console.info("S\u0307\u0323".normalize().length); // 1
console.info("S\u0307\u0323".normalize() == "\u1e68"); // true
Unicode 标准附录:Unicode 规范化形式
无需特意学习,很少使用