Proxy 和 Reflect
一个 Proxy
对象包装另一个对象并拦截诸如读取/写入属性和其他操作,可以选择自行处理它们,或者透明地允许该对象处理它们。
- Proxy
- let proxy = new Proxy(target, handler)
target
—— 是要包装的对象,可以是任何东西,包括函数。handler
—— 代理配置:带有“捕捉器”(“traps”,即拦截操作的方法)的对象。比如get
捕捉器用于读取target
的属性,set
捕捉器用于写入target
的属性,等等。
- 所有对
proxy
的操作都直接转发给了target
。- 写入操作
proxy.test=
会将值写入target
。 - 读取操作
proxy.test
会从target
返回对应的值。 - 迭代
proxy
会从target
返回对应的值。
- 写入操作
Proxy
是一种特殊的“奇异对象(exotic object)”。它没有自己的属性。如果handler
为空,则透明地将操作转发给target
。- Proxy 捕捉器会拦截这些方法的调用。它们在 proxy 规范 和下表中被列出。
- 不变量(Invariant)
[[Set]]
如果值已成功写入,则必须返回true
,否则返回false
。[[Delete]]
如果已成功删除该值,则必须返回true
,否则返回false
。- ……依此类推,我们将在下面的示例中看到更多内容。
- 应用于代理(proxy)对象的
[[GetPrototypeOf]]
,必须返回与应用于被代理对象的[[GetPrototypeOf]]
相同的值。换句话说,读取代理对象的原型必须始终返回被代理对象的原型。
内部方法 | Handler 方法 | 何时触发 |
---|---|---|
[[Get]] | get | 读取属性 |
[[Set]] | set | 写入属性 |
[[HasProperty]] | has | in 操作符 |
[[Delete]] | deleteProperty | delete 操作符 |
[[Call]] | apply | 函数调用 |
[[Construct]] | construct | new 操作符 |
[[GetPrototypeOf]] | getPrototypeOf | Object.getPrototypeOf |
[[SetPrototypeOf]] | setPrototypeOf | Object.setPrototypeOf |
[[IsExtensible]] | isExtensible | Object.isExtensible |
[[PreventExtensions]] | preventExtensions | Object.preventExtensions |
[[DefineOwnProperty]] | defineProperty | Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]] | getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor, for..in , Object.keys/values/entries |
[[OwnPropertyKeys]] | ownKeys | Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in , Object.keys/values/entries |
- 带有 “get” 捕捉器的默认值
- 最常见的捕捉器是用于读取/写入的属性。
target
—— 是目标对象,该对象被作为第一个参数传递给new Proxy
,property
—— 目标属性名,receiver
—— 如果目标属性是一个 getter 访问器属性,则receiver
就是本次读取属性所在的this
对象。通常,这就是proxy
对象本身(或者,如果我们从 proxy 继承,则是从该 proxy 继承的对象)。现在我们不需要此参数,因此稍后我们将对其进行详细介绍
- 最常见的捕捉器是用于读取/写入的属性。
JavaScript
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // 默认值
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0(没有这个数组项)
使用 “set” 捕捉器进行验证
- 假设我们想要一个专门用于数字的数组。如果添加了其他类型的值,则应该抛出一个错误。当写入属性时
set
捕捉器被触发。set(target, property, value, receiver)
:target
—— 是目标对象,该对象被作为第一个参数传递给new Proxy
,property
—— 目标属性名称,value
—— 目标属性的值,receiver
—— 与get
捕捉器类似,仅与 setter 访问器属性相关。
- 如果写入操作(setting)成功,
set
捕捉器应该返回true
,否则返回false
(触发TypeError
)。
- 假设我们想要一个专门用于数字的数组。如果添加了其他类型的值,则应该抛出一个错误。当写入属性时
使用 “ownKeys” 和 “getOwnPropertyDescriptor” 进行迭代
Object.keys
,for..in
循环和大多数其他遍历对象属性的方法都使用内部方法[[OwnPropertyKeys]]
(由ownKeys
捕捉器拦截) 来获取属性列表。Object.getOwnPropertyNames(obj)
返回非 symbol 键。Object.getOwnPropertySymbols(obj)
返回 symbol 键。Object.keys/values()
返回带有enumerable
标志的非 symbol 键/值(属性标志在 属性标志和属性描述符 一章有详细讲解)。for..in
循环遍历所有带有enumerable
标志的非 symbol 键,以及原型对象的键。
具有 “deleteProperty” 和其他捕捉器的受保护属性
- 有一个普遍的约定,即以下划线
_
开头的属性和方法是内部的。不应从对象外部访问它们。 - 我们使用代理来防止对以
_
开头的属性的任何访问。我们将需要以下捕捉器:get
读取此类属性时抛出错误,set
写入属性时抛出错误,deleteProperty
删除属性时抛出错误,ownKeys
在使用for..in
和像Object.keys
这样的方法时排除以_
开头的属性。
JavaScriptlet user = { name: "John", _password: "***" }; user = new Proxy(user, { get(target, prop) { if (prop.startsWith('_')) { throw new Error("Access denied"); } let value = target[prop]; return (typeof value === 'function') ? value.bind(target) : value; // (*) }, set(target, prop, val) { // 拦截属性写入 if (prop.startsWith('_')) { throw new Error("Access denied"); } else { target[prop] = val; return true; } }, deleteProperty(target, prop) { // 拦截属性删除 if (prop.startsWith('_')) { throw new Error("Access denied"); } else { delete target[prop]; return true; } }, ownKeys(target) { // 拦截读取属性列表 return Object.keys(target).filter(key => !key.startsWith('_')); } }); // "get" 不允许读取 _password try { alert(user._password); // Error: Access denied } catch(e) { alert(e.message); } // "set" 不允许写入 _password try { user._password = "test"; // Error: Access denied } catch(e) { alert(e.message); } // "deleteProperty" 不允许删除 _password try { delete user._password; // Error: Access denied } catch(e) { alert(e.message); } // "ownKeys" 将 _password 过滤出去 for(let key in user) alert(key); // name
- 类的私有属性
- 现代 JavaScript 引擎原生支持 class 中的私有属性,这些私有属性以
#
为前缀。它们在 私有的和受保护的属性和方法 一章中有详细描述。无需代理(proxy)。但是,此类属性有其自身的问题。特别是,它们是不可继承的。
- 现代 JavaScript 引擎原生支持 class 中的私有属性,这些私有属性以
- 有一个普遍的约定,即以下划线
带有 “has” 捕捉器的 “in range”
JavaScript
let range = {
start: 1,
end: 10
};
想使用 `in` 操作符来检查一个数字是否在 `range` 范围内。
`has` 捕捉器会拦截 `in` 调用。
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
- 包装函数:"apply"
- 我们也可以将代理(proxy)包装在函数周围。
apply(target, thisArg, args)
捕捉器能使代理以函数的方式被调用target
是目标对象(在 JavaScript 中,函数就是一个对象),thisArg
是this
的值。args
是参数列表。
- 我们也可以将代理(proxy)包装在函数周围。
- Reflect
Reflect
是一个内建对象,可简化Proxy
的创建。- 前面所讲过的内部方法,例如
[[Get]]
和[[Set]]
等,都只是规范性的,不能直接调用。 Reflect
对象使调用这些内部方法成为了可能。它的方法是内部方法的最小包装。- 对于每个可被
Proxy
捕获的内部方法,在Reflect
中都有一个对应的方法,其名称和参数与Proxy
捕捉器相同。
- 代理一个 getter
- 让我们看一个示例,来说明为什么
Reflect.get
更好。此外,我们还将看到为什么get/set
有第三个参数receiver
,而且我们之前从来没有使用过它。 - 其
get
捕捉器在这里是“透明的”,它返回原来的属性,不会做任何其他的事。
- 让我们看一个示例,来说明为什么
- Proxy 的局限性
- 代理提供了一种独特的方法,可以在最底层更改或调整现有对象的行为。但是,它并不完美。有局限性。
- 内建对象:内部插槽(Internal slot)
- 许多内建对象,例如
Map
,Set
,Date
,Promise
等,都使用了所谓的“内部插槽”。 Array
没有内部插槽
- 许多内建对象,例如
- 私有字段
#
字段名
- Proxy != target
- Proxy 无法拦截严格相等性检查
===
- 可撤销 Proxy
- 一个 可撤销 的代理是可以被禁用的代理。
- 假设我们有一个资源,并且想随时关闭对该资源的访问。我们可以做的是将它包装成可一个撤销的代理,没有任何捕捉器。这样的代理会将操作转发给对象,并且我们可以随时将其禁用。
- let {proxy, revoke} = Proxy.revocable(target, handler)
- 参考官方文档
Eval:执行代码字符串
- 内建函数
eval
允许执行一个代码字符串。- let result = eval(code);
JavaScript
let code = 'alert("Hello")';
eval(code); // Hello
- 使用 “eval”(现在几乎不用)
柯里化(Currying)
柯里化(Currying)是一种关于函数的高阶技术。它不仅被用于 JavaScript,还被用于其他编程语言。 柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c)
转换为可调用的 f(a)(b)(c)
。 柯里化不会调用函数。它只是对函数进行转换。
- 柯里化?目的是什么?
- 我们有一个用于格式化和输出信息的日志(logging)函数
log(date, importance, message)
。在实际项目中,此类函数具有很多有用的功能,例如通过网络发送日志(log),在这儿我们仅使用alert
- 我们有一个用于格式化和输出信息的日志(logging)函数
- 高级柯里化实现
- 柯里化要求函数具有固定数量的参数。
Reference Type
一个动态执行的方法调用可能会丢失 this
- Reference type 解读
- 仔细看的话,我们可能注意到
obj.method()
语句中的两个操作:- 首先,点
'.'
取了属性obj.method
的值。
- 首先,点
- 接着
()
执行了它。
- 接着
- 为确保
user.hi()
调用正常运行,JavaScript 玩了个小把戏 —— 点'.'
返回的不是一个函数,而是一个特殊的 Reference Type 的值。
- 仔细看的话,我们可能注意到
BigInt
BigInt
是一种特殊的数字类型,它提供了对任意长度整数的支持。 创建 bigint 的方式有两种:在一个整数字面量后面加 n
或者调用 BigInt
函数,该函数从字符串、数字等中生成 bigint。
- 数学运算符
- 对 bigint 的所有操作,返回的结果也是 bigint。
- 如果有其他需要,我们应该显式地转换它们:使用
BigInt()
或者Number()
,
- BigInt 不支持一元加法
- 一元加法运算符
+value
,是大家熟知的将value
转换成数字类型的方法。
- 一元加法运算符
- 比较运算符
- 比较运算符,例如
<
和>
,使用它们来对 bigint 和 number 类型的数字进行比较没有问题 - 但是请注意,由于 number 和 bigint 属于不同类型,它们可能在进行
==
比较时相等,但在进行===
(严格相等)比较时不相等
- 比较运算符,例如
- 布尔运算
- 当在
if
或其他布尔运算中时,bigint 的行为类似于 number。
- 当在
- Polyfill
- Polyfilling bigint 比较棘手。原因是许多 JavaScript 运算符,比如
+
和-
等,在对待 bigint 的行为上与常规 number 相比有所不同。- 例如,bigint 的除法总是返回 bigint(如果需要,会进行舍入)。
- Polyfilling bigint 比较棘手。原因是许多 JavaScript 运算符,比如
运算 | 原生 BigInt | JSBI |
---|---|---|
从 Number 创建 | a = BigInt(789) | a = JSBI.BigInt(789) |
加法 | c = a + b | c = JSBI.add(a, b) |
减法 | c = a - b | c = JSBI.subtract(a, b) |
Unicode —— 字符串内幕
JavaScript 的字符串是基于 Unicode 的:每个字符由 1-4 个字节的字节序列表示。
- 代理对
- 所有常用字符都有对应的 2 字节长度的编码(4 位十六进制数)。
- JavaScript 是基于 UTF-16 编码的,只允许每个字符占 2 个字节长度。但 2 个字节只允许 65536 种组合,这对于表示 Unicode 里每个可能符的号来说,是不够的。
- 因此,需要使用超过 2 个字节长度来表示的稀有符号,我们则使用一对 2 字节长度的字符编码,它被称为“代理对”(surrogate pair)。
- 变音符号和规范化
- 很多语言都有由基础字符及其上方/下方的标记所组成的符号。