十一、属性:赋值 vs. 定义
原文:
exploringjs.com/deep-js/ch_property-assignment-vs-definition.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
11.1?赋值 vs. 定义
-
11.1.1?赋值
-
11.1.2?定义
-
-
11.2?在理论中的赋值和定义(可选)
-
11.2.1?分配给属性
-
11.2.2?定义属性
-
-
11.3?实践中的定义和赋值
-
11.3.1?只有定义允许我们创建具有任意属性的属性
-
11.3.2?赋值操作符不会更改原型中的属性
-
11.3.3?赋值调用 setter,定义不会
-
11.3.4?继承的只读属性阻止通过赋值创建自有属性
-
-
11.4?哪些语言结构使用定义,哪些使用赋值?
-
11.4.1?对象文字的属性是通过定义添加的
-
11.4.2?赋值操作符
= 总是使用赋值 -
11.4.3?公共类字段是通过定义添加的
-
-
11.5?进一步阅读和本章的来源
有两种方法可以创建或更改对象
-
赋值:
obj.prop = true -
定义:
Object.defineProperty(obj, '', {value: true})
本章解释了它们的工作原理。
必需知识:属性属性和属性描述符
对于本章,您应该熟悉属性属性和属性描述符。如果您不熟悉,请查看§9“属性属性:介绍”。
11.1?赋值 vs. 定义
11.1.1?赋值
我们使用赋值操作符
obj.prop = value
这个操作符的工作方式取决于
-
更改属性:如果存在自有数据属性
.prop ,赋值会将其值更改为value 。 -
调用 setter:如果存在自有或继承的 setter
.prop ,赋值会调用该 setter。 -
创建属性:如果没有自有数据属性
.prop ,也没有自有或继承的 setter,赋值会创建一个新的自有数据属性。
也就是说,赋值的主要目的是进行更改。这就是为什么它支持 setter。
11.1.2?定义
要定义对象
Object.defineProperty(obj, propKey, propDesc)
这种方法的工作方式取决于属性的外观:
-
更改属性:如果存在具有键
propKey 的自有属性,则定义将根据属性描述符propDesc (如果可能)更改其属性。 -
创建属性:否则,定义将创建一个具有
propDesc 指定属性的自有属性(如果可能)。
也就是说,定义的主要目的是创建一个自有属性(即使存在继承的 setter,它也会忽略)并改变属性的属性。
11.2?理论上的赋值和定义(可选)
ECMAScript 规范中的属性描述符
在规范操作中,属性描述符不是 JavaScript 对象,而是Records,这是一个规范内部的数据结构,具有fields。字段的键用双括号括起来。例如,
11.2.1 分配属性
通过ECMAScript 规范中的以下操作来处理属性的赋值工作:
OrdinarySetWithOwnDescriptor(O, P, V, Receiver, ownDesc)
这些是参数:
-
O 是当前正在访问的对象。 -
P 是我们正在赋值的属性的键。 -
V 是我们正在赋值的值。 -
Receiver 是赋值开始的对象。 -
ownDesc 是O[P] 的描述符,如果该属性不存在则为null 。
返回值是一个布尔值,指示操作是否成功。如本章后面所述,严格模式赋值如果
这是算法的高级摘要:
-
它遍历
Receiver 的原型链,直到找到键为P 的属性。遍历是通过递归调用OrdinarySetWithOwnDescriptor() 来完成的。在递归过程中,O 会改变并指向当前正在访问的对象,但Receiver 保持不变。 -
根据遍历的结果,在
Receiver (递归开始的地方)中创建一个自有属性,或者发生其他事情。
更详细地说,这个算法的工作方式如下:
-
如果
ownDesc 是undefined ,那么我们还没有找到一个带有键P 的属性:-
如果
O 有一个原型parent ,那么我们返回parent.[[Set]](P, V, Receiver) 。这将继续我们的搜索。该方法调用通常最终会递归调用OrdinarySetWithOwnDescriptor() 。 -
否则,我们对
P 的搜索失败了,并将ownDesc 设置如下:{ [[Value]]: undefined, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }
有了这个
ownDesc ,下一个if 语句将在Receiver 中创建一个自有属性。
-
-
如果
ownDesc 指定了一个数据属性,那么我们已经找到了一个属性:-
如果
ownDesc.[[Writable]] 是false ,则返回false 。这意味着任何不可写的属性P (自有的或继承的)都会阻止赋值。 -
让
existingDescriptor 为Receiver.[[GetOwnProperty]](P) 。也就是说,检索赋值开始的属性的描述符。现在我们有:-
当前对象
O 和当前属性描述符ownDesc 。 -
原始对象
Receiver 和另一方面的原始属性描述符existingDescriptor 。
-
-
如果
existingDescriptor 不是undefined :-
(如果我们到了这里,那么我们仍然在原型链的开始处 - 只有在
Receiver 没有属性P 时才会递归。) -
以下两个
if 条件永远不应该为true ,因为ownDesc 和existingDesc 应该相等:-
如果
existingDescriptor 指定了一个访问器,则返回false 。 -
如果
existingDescriptor.[[Writable]] 是false ,则返回false 。
-
-
返回
Receiver.[[DefineOwnProperty]](P, { [[Value]]: V }) 。这个内部方法执行定义,我们用它来改变属性Receiver[P] 的值。定义算法在下一小节中描述。
-
-
否则:
-
(如果我们到了这里,那么
Receiver 没有一个带有键P 的自有属性。) -
返回
CreateDataProperty(Receiver, P, V) 。(此操作 在其第一个参数中创建自有数据属性。)
-
-
-
(如果我们到达这里,那么
ownDesc 描述的是自有或继承的访问器属性。) -
让
setter 为ownDesc.[[Set]] 。 -
如果
setter 是undefined ,返回false 。 -
执行
Call(setter, Receiver, ?V?) 。Call() 调用函数对象setter ,并将this 设置为Receiver ,单个参数V (规范中使用法文引号?? 表示列表)。 -
返回
true 。
11.2.1.1 从赋值到 OrdinarySetWithOwnDescriptor() 的过程是如何进行的?
在不涉及解构的赋值中,涉及以下步骤:
-
在规范中,评估从赋值表达式的运行时语义部分开始。该部分处理为匿名函数提供名称、解构等。
-
如果没有解构模式,则使用
PutValue() 进行赋值。 -
对于属性赋值,
PutValue() 调用内部方法.[[Set]]() 。 -
对于普通对象,
.[[Set]]() 调用OrdinarySet() (调用OrdinarySetWithOwnDescriptor() )并返回结果。
值得注意的是,在严格模式下,如果
11.2.2 定义属性
定义属性的实际工作是通过 ECMAScript 规范中的以下操作处理的:
ValidateAndApplyPropertyDescriptor(O, P, extensible, Desc, current)
参数是:
-
我们要定义属性的对象
O 。这里有一个特殊的仅验证模式,其中O 是undefined 。我们在这里忽略了这种模式。 -
我们要定义的属性的属性键
P 。 -
extensible 表示O 是否可扩展。 -
Desc 是指定属性所需的属性描述符。 -
如果存在,则
current 包含自有属性O[P] 的属性描述符。否则,current 是undefined 。
操作的结果是一个布尔值,指示操作是否成功。失败可能会产生不同的后果。有些调用者会忽略结果。其他调用者,如
这是算法的摘要:
-
如果
current 是undefined ,则属性P 目前不存在,必须创建。-
如果
extensible 是false ,返回false 表示无法添加属性。 -
否则,检查
Desc 并创建数据属性或访问器属性。 -
返回
true 。
-
-
如果
Desc 没有任何字段,则返回true ,表示操作成功(因为不需要进行任何更改)。 -
如果
current.[[Configurable]] 是false :-
(
Desc 不允许改变除value 之外的属性。) -
如果
Desc.[[Configurable]] 存在,则它必须与current.[[Configurable]] 具有相同的值。如果不是,则返回false 。 -
相同的检查:
Desc.[[Enumerable]]
-
-
接下来,我们验证属性描述符
Desc :current 描述的属性是否可以更改为Desc 指定的值?如果不能,返回false 。如果可以,继续。-
如果描述符是通用的(没有特定于数据属性或访问器属性的属性),则验证成功,我们可以继续。
-
否则,如果一个描述符指定了数据属性,另一个指定了访问器属性:
-
当前属性必须是可配置的(否则其属性无法按需更改)。如果不是,返回
false 。 -
将当前属性从数据属性更改为访问器属性,反之亦然。在这样做时,
.[[Configurable]] 和.[[Enumerable]] 的值被保留,所有其他属性获得默认值(对象值属性为undefined ,布尔值属性为false )。
-
-
否则,如果两个描述符都指定数据属性:
-
如果
current.[[Configurable]] 和current.[[Writable]] 都为false ,则不允许进行任何更改,Desc 和current 必须指定相同的属性:-
(由于
current.[[Configurable]] 为false ,Desc.[[Configurable]] 和Desc.[[Enumerable]] 已经在先前检查过,并且具有正确的值。) -
如果
Desc.[[Writable]] 存在且为true ,则返回false 。 -
如果
Desc.[[Value]] 存在且与current.[[Value]] 的值不同,则返回false 。 -
没有其他事情可做。返回
true 表示算法成功。 -
(请注意,通常情况下,我们不能更改非可配置属性的任何属性,除了它的值。这个规则的一个例外是,我们总是可以从可写变为不可写。该算法正确处理了这个例外。)
-
-
-
否则(两个描述符都指定访问器属性):
-
如果
current.[[Configurable]] 为false ,则不允许进行任何更改,Desc 和current 必须指定相同的属性:-
(由于
current.[[Configurable]] 为false ,Desc.[[Configurable]] 和Desc.[[Enumerable]] 已经在先前检查过,并且具有正确的值。) -
如果
Desc.[[Set]] 存在,则它必须与current.[[Set]] 具有相同的值。如果不是,则返回false 。 -
相同的检查:
Desc.[[Get]] -
没有其他事情可做。返回
true 表示算法成功。
-
-
-
-
将属性
P 的属性设置为Desc 指定的值。由于验证,我们可以确保所有更改都是允许的。 -
返回
true 。
11.3?定义和实践中的赋值
本节描述了属性定义和赋值的一些后果。
11.3.1?只有定义允许我们创建具有任意属性的属性
如果我们通过赋值创建自有属性,它总是创建属性,其属性
const obj = {}; obj.dataProp = 'abc'; assert.deepEqual( Object.getOwnPropertyDescriptor(obj, 'dataProp'), { value: 'abc', writable: true, enumerable: true, configurable: true, });
因此,如果我们想指定任意属性,我们必须使用定义。
虽然我们可以在对象文字中创建 getter 和 setter,但我们不能通过赋值后来添加它们。在这里,我们也需要定义。
11.3.2?赋值运算符不会更改原型中的属性
让我们考虑以下设置,其中
const proto = { prop: 'a' }; const obj = Object.create(proto);
我们不能通过给
assert.deepEqual( Object.keys(obj), []); obj.prop = 'b'; // The assignment worked: assert.equal(obj.prop, 'b'); // But we created an own property and overrode proto.prop, // we did not change it: assert.deepEqual( Object.keys(obj), ['prop']); assert.equal(proto.prop, 'a');
这种行为的原因如下:原型可以具有其所有后代共享的属性值。如果我们只想在一个后代中更改这样的属性,我们必须通过覆盖来进行非破坏性地更改。然后,更改不会影响其他后代。
11.3.3?赋值调用 setter,定义不会
定义
如果我们定义,那么我们的意图是要创建或更改
let setterWasCalled = false; const proto = { get prop() { return 'protoGetter'; }, set prop(x) { setterWasCalled = true; }, }; const obj = Object.create(proto); assert.equal(obj.prop, 'protoGetter'); // Defining obj.prop: Object.defineProperty( obj, 'prop', { value: 'objData' }); assert.equal(setterWasCalled, false); // We have overridden the getter: assert.equal(obj.prop, 'objData');
相反,如果我们对
let setterWasCalled = false; const proto = { get prop() { return 'protoGetter'; }, set prop(x) { setterWasCalled = true; }, }; const obj = Object.create(proto); assert.equal(obj.prop, 'protoGetter'); // Assigning to obj.prop: obj.prop = 'objData'; assert.equal(setterWasCalled, true); // The getter still active: assert.equal(obj.prop, 'protoGetter');
11.3.4?继承的只读属性阻止通过赋值创建自己的属性
如果原型中的
const proto = Object.defineProperty( {}, 'prop', { value: 'protoValue', writable: false, });
在从
const obj = Object.create(proto); assert.throws( () => obj.prop = 'objValue', /^TypeError: Cannot assign to read only property 'prop'/);
为什么我们不能赋值?理由是通过创建自己的属性来覆盖继承的属性可以被视为非破坏性地更改继承的属性。可以说,如果属性是不可写的,我们就不应该能够这样做。
然而,定义
Object.defineProperty( obj, 'prop', { value: 'objValue' }); assert.equal(obj.prop, 'objValue');
没有 setter 的访问器属性也被认为是只读的:
const proto = { get prop() { return 'protoValue'; } }; const obj = Object.create(proto); assert.throws( () => obj.prop = 'objValue', /^TypeError: Cannot set property prop of #<Object> which has only a getter$/);
“覆盖错误”:优缺点
只读属性在原型链中较早阻止赋值的事实被称为覆盖错误:
-
它是在 ECMAScript 5.1 中引入的。
-
一方面,这种行为与原型继承和 setter 的工作方式一致。(因此,可以说这不是一个错误。)
-
另一方面,使用这种行为,深冻结全局对象会导致不希望的副作用。
-
曾经有尝试改变这种行为,但那破坏了 Lodash 库并被放弃了(GitHub 上的拉取请求)。
-
背景知识:
-
GitHub 上的拉取请求
-
ECMAScript.org 上的维基页面 (存档)
-
11.4 语言构造使用定义,哪些使用赋值?
在本节中,我们检查语言何时使用定义以及何时使用赋值。我们通过跟踪是否调用继承的 setter 来检测使用的操作。有关更多信息,请参见§11.3.3 “赋值调用 setter,定义不调用”。
11.4.1 对象文字的属性是通过定义添加的
当我们通过对象文字创建属性时,JavaScript 总是使用定义(因此从不调用继承的 setter):
let lastSetterArgument; const proto = { set prop(x) { lastSetterArgument = x; }, }; const obj = { __proto__: proto, prop: 'abc', }; assert.equal(lastSetterArgument, undefined);
11.4.2 赋值运算符= 总是使用赋值
赋值运算符
let lastSetterArgument; const proto = { set prop(x) { lastSetterArgument = x; }, }; const obj = Object.create(proto); // Normal assignment: obj.prop = 'abc'; assert.equal(lastSetterArgument, 'abc'); // Assigning via destructuring: [obj.prop] = ['def']; assert.equal(lastSetterArgument, 'def');
11.4.3 公共类字段是通过定义添加的
遗憾的是,即使公共类字段具有与赋值相同的语法,它们不使用赋值来创建属性,它们使用定义(就像对象文字中的属性一样):
let lastSetterArgument1; let lastSetterArgument2; class A { set prop1(x) { lastSetterArgument1 = x; } set prop2(x) { lastSetterArgument2 = x; } } class B extends A { prop1 = 'one'; constructor() { super(); this.prop2 = 'two'; } } new B(); // The public class field uses definition: assert.equal(lastSetterArgument1, undefined); // Inside the constructor, we trigger assignment: assert.equal(lastSetterArgument2, 'two');
11.5 本章的进一步阅读和来源
-
“原型链”部分 in “JavaScript for impatient programmers”
-
Allen Wirfs-Brock 发送给 es-discuss 邮件列表的电子邮件:“赋值和定义之间的区别[…]在 ES 只有数据属性且 ES 代码无法操作属性属性时并不重要。”[这在 ECMAScript 5 中发生了变化。]
评论
十二、属性的可枚举性
原文:
exploringjs.com/deep-js/ch_enumerability.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
12.1?可枚举性如何影响属性迭代构造
-
12.1.1?只考虑可枚举属性的操作
-
12.1.2?同时考虑可枚举和不可枚举属性的操作
-
12.1.3?内省操作的命名规则
-
-
12.2?预定义和创建属性的可枚举性
-
12.3?可枚举性的用例
-
12.3.1?用例:隐藏属性不被
for-in 循环处理 -
12.3.2?用例:标记不需要被复制的属性
-
12.3.3?将属性标记为私有
-
12.3.4?隐藏自有属性不被
JSON.stringify() 处理
-
-
12.4?结论
可枚举性是对象属性的属性。在本章中,我们将更仔细地看看它是如何使用的,以及它如何影响
必需知识:属性特性
在本章中,您应该熟悉属性特性。如果不熟悉,请查看§9“属性特性:介绍”。
12.1?可枚举性如何影响属性迭代构造
为了演示各种操作受可枚举性的影响,我们使用以下对象
const protoEnumSymbolKey = Symbol('protoEnumSymbolKey'); const protoNonEnumSymbolKey = Symbol('protoNonEnumSymbolKey'); const proto = Object.defineProperties({}, { protoEnumStringKey: { value: 'protoEnumStringKeyValue', enumerable: true, }, [protoEnumSymbolKey]: { value: 'protoEnumSymbolKeyValue', enumerable: true, }, protoNonEnumStringKey: { value: 'protoNonEnumStringKeyValue', enumerable: false, }, [protoNonEnumSymbolKey]: { value: 'protoNonEnumSymbolKeyValue', enumerable: false, }, }); const objEnumSymbolKey = Symbol('objEnumSymbolKey'); const objNonEnumSymbolKey = Symbol('objNonEnumSymbolKey'); const obj = Object.create(proto, { objEnumStringKey: { value: 'objEnumStringKeyValue', enumerable: true, }, [objEnumSymbolKey]: { value: 'objEnumSymbolKeyValue', enumerable: true, }, objNonEnumStringKey: { value: 'objNonEnumStringKeyValue', enumerable: false, }, [objNonEnumSymbolKey]: { value: 'objNonEnumSymbolKeyValue', enumerable: false, }, });
12.1.1?只考虑可枚举属性的操作
表 2:忽略不可枚举属性的操作。
操作 | 字符串键 | 符号键 | 继承 | |
---|---|---|---|---|
ES5 | ||||
ES2017 | ||||
ES2017 | ||||
扩展 |
||||
ES6 | ||||
ES5 | ||||
ES1 |
以下操作(在 tbl. 2 中总结)只考虑可枚举属性:
-
Object.keys() ^([ES5]) 返回可枚举自有字符串键属性的键。> Object.keys(obj) [ 'objEnumStringKey' ]
-
Object.values() ^([ES2017]) 返回可枚举自有字符串键属性的值。> Object.values(obj) [ 'objEnumStringKeyValue' ]
-
Object.entries() ^([ES2017]) 返回可枚举自有字符串键属性的键值对。(请注意,Object.fromEntries() 接受符号作为键,但只创建可枚举属性。)> Object.entries(obj) [ [ 'objEnumStringKey', 'objEnumStringKeyValue' ] ]
-
扩展到对象字面量 ^([ES2018]) 只考虑自有可枚举属性(带有字符串键或符号键)。
> const copy = {...obj}; > Reflect.ownKeys(copy) [ 'objEnumStringKey', objEnumSymbolKey ]
-
Object.assign() ^([ES6]) 只会复制可枚举的自有属性(带有字符串键或符号键)。> const copy = Object.assign({}, obj); > Reflect.ownKeys(copy) [ 'objEnumStringKey', objEnumSymbolKey ]
-
JSON.stringify() ^([ES5]) 只会将可枚举的自有属性与字符串键字符串化。> JSON.stringify(obj) '{"objEnumStringKey":"objEnumStringKeyValue"}'
-
for-in 循环 ^([ES1]) 遍历自有和继承的可枚举字符串键属性。const propKeys = []; for (const propKey in obj) { propKeys.push(propKey); } assert.deepEqual( propKeys, ['objEnumStringKey', 'protoEnumStringKey']);
12.1.2?同时考虑可枚举和不可枚举属性的操作
表 3:同时考虑可枚举和不可枚举属性的操作。
操作 | 字符串键 | 符号键 | 继承 | |
---|---|---|---|---|
ES5 | ||||
ES6 | ||||
ES6 | ||||
ES2017 |
以下操作(在 tbl. 3 中总结)考虑了可枚举和不可枚举的属性:
-
Object.getOwnPropertyNames() ^([ES5]) 列出所有自有字符串键属性的键。> Object.getOwnPropertyNames(obj) [ 'objEnumStringKey', 'objNonEnumStringKey' ]
-
Object.getOwnPropertySymbols() ^([ES6]) 列出所有自有符号键属性的键。> Object.getOwnPropertySymbols(obj) [ objEnumSymbolKey, objNonEnumSymbolKey ]
-
Reflect.ownKeys() ^([ES6]) 列出所有自有属性的键。> Reflect.ownKeys(obj) [ 'objEnumStringKey', 'objNonEnumStringKey', objEnumSymbolKey, objNonEnumSymbolKey ]
-
Object.getOwnPropertyDescriptors() ^([ES2017]) 列出所有自有属性的属性描述符。> Object.getOwnPropertyDescriptors(obj) { objEnumStringKey: { value: 'objEnumStringKeyValue', writable: false, enumerable: true, configurable: false }, objNonEnumStringKey: { value: 'objNonEnumStringKeyValue', writable: false, enumerable: false, configurable: false }, [objEnumSymbolKey]: { value: 'objEnumSymbolKeyValue', writable: false, enumerable: true, configurable: false }, [objNonEnumSymbolKey]: { value: 'objNonEnumSymbolKeyValue', writable: false, enumerable: false, configurable: false } }
12.1.3?内省操作的命名规则
内省使程序能够在运行时检查值的结构。这是元编程:普通编程是关于编写程序;元编程是关于检查和/或更改程序。
在 JavaScript 中,常见的内省操作具有简短的名称,而很少使用的操作具有较长的名称。忽略不可枚举的属性是规范,这就是为什么执行这种操作的名称很短,而不执行这种操作的名称很长的原因:
-
Object.keys() 忽略不可枚举的属性。 -
Object.getOwnPropertyNames() 列出所有自有属性的字符串键。
然而,
此外,自 ES6 以来,还有以下区别:
-
属性键要么是字符串,要么是符号。
-
属性名称是字符串键的属性键。
-
属性符号是符号键的属性键。
因此,
12.2?预定义和创建属性的可枚举性
在本节中,我们将像这样缩写
const desc = Object.getOwnPropertyDescriptor.bind(Object);
大多数数据属性都是使用以下属性创建的:
{ writable: true, enumerable: false, configurable: true, }
这包括:
-
赋值
-
对象字面量
-
公共类字段
-
Object.fromEntries()
最重要的不可枚举属性是:
-
内置类的原型属性
> desc(Object.prototype, 'toString').enumerable false
-
通过用户定义的类创建的原型属性
> desc(class {foo() {}}.prototype, 'foo').enumerable false
-
数组的属性
.length :> Object.getOwnPropertyDescriptor([], 'length') { value: 0, writable: true, enumerable: false, configurable: false }
-
字符串的属性
.length (注意原始值的所有属性都是只读的):> Object.getOwnPropertyDescriptor('', 'length') { value: 0, writable: false, enumerable: false, configurable: false }
接下来我们将看一下可枚举性的使用案例,这将告诉我们为什么有些属性是可枚举的,而其他的不是。
12.3?可枚举性的使用案例
可枚举性是一个不一致的特性。它确实有用例,但总是有某种注意事项。在本节中,我们将看看使用案例和注意事项。
12.3.1?使用案例:隐藏for-in 循环中的属性
一般来说,最好避免使用
function listPropertiesViaForIn(obj) { const result = []; for (const key in obj) { result.push(key); } return result; }
12.3.1.1?使用for-in 遍历对象的注意事项
const proto = {enumerableProtoProp: 1}; const obj = { __proto__: proto, enumerableObjProp: 2, }; assert.deepEqual( listPropertiesViaForIn(obj), ['enumerableObjProp', 'enumerableProtoProp']);
对于普通的普通对象,
const obj = {}; assert.deepEqual( listPropertiesViaForIn(obj), []);
在用户定义的类中,所有继承的属性也是不可枚举的,因此被忽略:
class Person { constructor(first, last) { this.first = first; this.last = last; } getName() { return this.first + ' ' + this.last; } } const jane = new Person('Jane', 'Doe'); assert.deepEqual( listPropertiesViaForIn(jane), ['first', 'last']);
结论: 在对象中,
12.3.1.2?使用for-in 遍历数组的注意事项
数组和字符串中的自有属性
> listPropertiesViaForIn(['a', 'b']) [ '0', '1' ] > listPropertiesViaForIn('ab') [ '0', '1' ]
然而,通常不安全使用
const arr1 = ['a', 'b']; assert.deepEqual( listPropertiesViaForIn(arr1), ['0', '1']); const arr2 = ['a', 'b']; arr2.nonIndexProp = 'yes'; assert.deepEqual( listPropertiesViaForIn(arr2), ['0', '1', 'nonIndexProp']);
**结论:**不应该使用
-
如果您对数组的键感兴趣,请使用数组方法
.keys() :> [...['a', 'b', 'c'].keys()] [ 0, 1, 2 ]
-
如果要遍历数组的元素,请使用
for-of 循环,这样还可以与其他可迭代的数据结构一起使用。
12.3.2?用例:标记不要复制的属性
通过使属性不可枚举,我们可以将它们从某些复制操作中隐藏起来。让我们首先检查两个历史复制操作,然后再转向更现代的复制操作。
12.3.2.1?历史复制操作:Prototype 的Object.extend()
Prototype是一个 JavaScript 框架,由 Sam Stephenson 于 2005 年 2 月创建,作为 Ruby on Rails 中 Ajax 支持的基础的一部分。
Prototype 的
function extend(destination, source) { for (var property in source) destination[property] = source[property]; return destination; }
如果我们使用
const proto = Object.defineProperties({}, { enumProtoProp: { value: 1, enumerable: true, }, nonEnumProtoProp: { value: 2, enumerable: false, }, }); const obj = Object.create(proto, { enumObjProp: { value: 3, enumerable: true, }, nonEnumObjProp: { value: 4, enumerable: false, }, }); assert.deepEqual( extend({}, obj), {enumObjProp: 3, enumProtoProp: 1});
12.3.2.2?历史复制操作:jQuery 的$.extend()
jQuery 的
-
它将
source1 的所有可枚举的自有和继承属性复制到target 的自有属性中。 -
然后它对
source2 执行相同的操作。 -
等等。
12.3.2.3?可枚举性驱动复制的缺点
基于可枚举性进行复制有几个缺点:
-
虽然可枚举性对于隐藏继承属性很有用,但主要是以这种方式使用,因为我们通常只想将自有属性复制到自有属性中。忽略继承属性可以更好地实现相同的效果。
-
要复制哪些属性通常取决于手头的任务;为所有用例提供单个标志很少有意义。更好的选择是提供一个带有谓词(返回布尔值的回调)的复制操作,告诉它何时忽略属性。
-
可枚举性在复制时方便地隐藏了数组的自有属性
.length 。但这是一个极为罕见的特例:一个既影响兄弟属性又受其影响的魔术属性。如果我们自己实现这种魔术,我们将使用(继承的)getter 和/或 setter,而不是(自有的)数据属性。
12.3.2.4?Object.assign() ^([ES5])
在 ES6 中,
关于可枚举性,
Object.assign 将为所有已经在流通中的extend() API 铺平道路。我们认为在这些情况下不复制可枚举方法的先例足以证明Object.assign 具有这种行为的足够理由。
换句话说:
12.3.2.5 非可枚举性在复制时有用的罕见例子
非可枚举性有助的情况很少。一个罕见的例子是库
-
内置的 Node.js 模块
fs 有一个包含基于 Promise 的fs API 版本的对象的属性.promises 。在问题出现时,读取.promise 会导致以下警告被记录到控制台:ExperimentalWarning: The fs.promises API is experimental
-
除了提供自己的功能外,
fs-extra 还将fs 中的所有内容重新导出。对于 CommonJS 模块,这意味着将fs 的所有属性复制到fs-extra 的module.exports 中(通过Object.assign() )。当fs-extra 这样做时,就会触发警告。这很令人困惑,因为每次加载fs-extra 时都会发生这种情况。 -
一个快速的修复是将属性
fs.promises 设为不可枚举。之后,fs-extra 就忽略了它。
12.3.3 将属性标记为私有
如果我们使一个属性不可枚举,它就不能被
然而,这种方法存在几个问题:
-
在复制对象时,我们通常希望复制私有属性。这与使不应该被复制的属性不可枚举相冲突(见上一节)。
-
属性并不是真正私有的。获取、设置和其他几种机制对可枚举和不可枚举的属性没有区别。
-
在处理代码时,无论是作为源代码还是交互式地,我们无法立即看到属性是否可枚举。命名约定(例如给属性名加上下划线前缀)更容易发现。
-
我们不能使用可枚举性来区分公共和私有方法,因为原型中的方法默认是不可枚举的。
12.3.4 隐藏自有属性不被JSON.stringify() 处理
作为可枚举性的替代方案,对象可以实现方法
class Point { static fromJSON(json) { return new Point(json[0], json[1]); } constructor(x, y) { this.x = x; this.y = y; } toJSON() { return [this.x, this.y]; } } assert.equal( JSON.stringify(new Point(8, -3)), '[8,-3]' );
我觉得
12.4 结论
我们已经看到几乎所有非可枚举性的应用都是现在有其他更好的解决方案的变通方法。
对于我们自己的代码,我们通常可以假装可枚举性不存在:
-
通过对象字面量和赋值创建属性总是创建可枚举的属性。
-
通过类创建的原型属性总是不可枚举的。
也就是说,我们自动遵循最佳实践。
评论
第四部分:OOP:技术
原文:
exploringjs.com/deep-js/pt_oop-techniques.html 译者:飞龙
协议:CC BY-NC-SA 4.0
接下来:13 实例化类的技术
十三、实例化类的技术
原文:
exploringjs.com/deep-js/ch_creating-class-instances.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
13.1?问题:异步初始化属性
-
13.2?解决方案:基于 Promise 的构造函数
- 13.2.1?使用立即调用的异步箭头函数
-
13.3?解决方案:静态工厂方法
-
13.3.1?改进:通过秘密令牌私有构造函数
-
13.3.2?改进:构造函数抛出,工厂方法借用类原型
-
13.3.3?改进:实例默认处于非活动状态,由工厂方法激活
-
13.3.4?变体:单独的工厂函数
-
-
13.4?基于 Promise 的构造函数的子类化(可选)
-
13.5?结论
-
13.6?进一步阅读
在本章中,我们将研究创建类实例的几种方法:构造函数,工厂函数等。我们通过多次解决一个具体的问题来做到这一点。本章的重点是类,因此忽略了类的替代方案。
13.1?问题:异步初始化属性
以下容器类应该异步接收其属性
class DataContainer { #data; // (A) constructor() { Promise.resolve('downloaded') .then(data => this.#data = data); // (B) } getData() { return 'DATA: '+this.#data; // (C) } }
此代码的关键问题:属性
const dc = new DataContainer(); assert.equal(dc.getData(), 'DATA: undefined'); setTimeout(() => assert.equal( dc.getData(), 'DATA: downloaded'), 0);
在 A 行,我们声明了私有字段
13.2?解决方案:基于 Promise 的构造函数
如果我们延迟访问
class DataContainer { #data; constructor() { return Promise.resolve('downloaded') .then(data => { this.#data = data; return this; // (A) }); } getData() { return 'DATA: '+this.#data; } } new DataContainer() .then(dc => assert.equal( // (B) dc.getData(), 'DATA: downloaded'));
现在我们必须等到可以访问我们的实例(B 行)。在数据“下载”后(A 行)将其传递给我们。此代码中可能出现两种错误:
-
下载可能失败并产生拒绝。
-
在第一个
.then() 回调的主体中可能会抛出异常。
在任何一种情况下,错误都会成为从构造函数返回的 Promise 的拒绝。
利弊:
-
这种方法的好处是,只有在完全初始化后才能访问实例。没有其他方法可以创建
DataContainer 的实例。 -
一个缺点是构造函数返回一个 Promise 而不是一个实例。
13.2.1?使用立即调用的异步箭头函数
不是直接使用 Promise API 来创建从构造函数返回的 Promise,我们也可以使用一个异步箭头函数,我们立即调用:
constructor() { return (async () => { this.#data = await Promise.resolve('downloaded'); return this; })(); }
13.3?解决方案:静态工厂方法
类
-
.create() : 创建一个新实例。示例:Object.create() -
.from() : 创建一个基于不同对象的新实例,通过复制和/或转换它。示例:Array.from() -
.of() : 通过使用参数指定的值创建一个新实例。示例:Array.of()
在下面的示例中,
class DataContainer { #data; static async create() { const data = await Promise.resolve('downloaded'); return new this(data); } constructor(data) { this.#data = data; } getData() { return 'DATA: '+this.#data; } } DataContainer.create() .then(dc => assert.equal( dc.getData(), 'DATA: downloaded'));
这次,所有异步功能都包含在
优缺点:
-
这种方法的一个好处是构造函数变得简单。
-
这种方法的一个缺点是现在可能会创建不正确设置的实例,通过
new DataContainer() 。
13.3.1?改进:通过秘密令牌的私有构造函数
如果我们想要确保实例始终正确设置,我们必须确保只有
const secretToken = Symbol('secretToken'); class DataContainer { #data; static async create() { const data = await Promise.resolve('downloaded'); return new this(secretToken, data); } constructor(token, data) { if (token !== secretToken) { throw new Error('Constructor is private'); } this.#data = data; } getData() { return 'DATA: '+this.#data; } } DataContainer.create() .then(dc => assert.equal( dc.getData(), 'DATA: downloaded'));
如果
优缺点:
-
优点:安全且直观。
-
缺点:稍微冗长。
13.3.2?改进:构造函数抛出,工厂方法借用类原型
我们解决方案的以下变体禁用了
class DataContainer { static async create() { const data = await Promise.resolve('downloaded'); return Object.create(this.prototype)._init(data); // (A) } constructor() { throw new Error('Constructor is private'); } _init(data) { this._data = data; return this; } getData() { return 'DATA: '+this._data; } } DataContainer.create() .then(dc => { assert.equal(dc instanceof DataContainer, true); // (B) assert.equal( dc.getData(), 'DATA: downloaded'); });
在内部,
优缺点:
-
优点:优雅;
instanceof 有效。 -
缺点:
-
无法完全阻止创建实例。不过公平地说,通过
Object.create() 的解决方案也可以用于我们之前的解决方案。 -
我们不能在
DataContainer 中使用私有字段和私有方法,因为这些只对通过构造函数创建的实例正确设置。
-
13.3.3?改进:实例默认处于非活动状态,由工厂方法激活
另一种更冗长的变体是,默认情况下,实例通过标志
class DataContainer { #data; static async create() { const data = await Promise.resolve('downloaded'); return new this().#init(data); } #active = false; constructor() { } #init(data) { this.#active = true; this.#data = data; return this; } getData() { this.#check(); return 'DATA: '+this.#data; } #check() { if (!this.#active) { throw new Error('Not created by factory'); } } } DataContainer.create() .then(dc => assert.equal( dc.getData(), 'DATA: downloaded'));
标志
这种解决方案的主要缺点是冗长。还有一个风险,就是在每个方法中忘记调用
13.3.4?变体:单独的工厂函数
为了完整起见,我将展示另一种变体:不使用静态方法作为工厂,您也可以使用一个单独的独立函数。
const secretToken = Symbol('secretToken'); class DataContainer { #data; constructor(token, data) { if (token !== secretToken) { throw new Error('Constructor is private'); } this.#data = data; } getData() { return 'DATA: '+this.#data; } } async function createDataContainer() { const data = await Promise.resolve('downloaded'); return new DataContainer(secretToken, data); } createDataContainer() .then(dc => assert.equal( dc.getData(), 'DATA: downloaded'));
独立函数作为工厂偶尔是有用的,但在这种情况下,我更喜欢静态方法:
-
独立函数无法访问
DataContainer 的私有成员。 -
我更喜欢
DataContainer.create() 的方式。
13.4?以 Promise 为基础的构造函数进行子类化(可选)
一般来说,子类化是一种需要谨慎使用的东西。
使用单独的工厂函数,相对容易扩展
然而,使用基于 Promise 的构造函数扩展类会导致严重的限制。在下面的示例中,我们对
class DataContainer { #data; constructor() { return Promise.resolve('downloaded') .then(data => { this.#data = data; return this; // (A) }); } getData() { return 'DATA: '+this.#data; } } class SubDataContainer extends DataContainer { #moreData; constructor() { super(); const promise = this; return promise .then(_this => { return Promise.resolve('more') .then(moreData => { _this.#moreData = moreData; return _this; }); }); } getData() { return super.getData() + ', ' + this.#moreData; } }
哎呀,我们无法实例化这个类:
assert.rejects( () => new SubDataContainer(), { name: 'TypeError', message: 'Cannot write private member #moreData ' + 'to an object whose class did not declare it', } );
为什么会失败?构造函数总是将其私有字段添加到其
然而,如果
13.5 结论
在本章研究的场景中,我更喜欢使用基于 Promise 的构造函数或静态工厂方法加上通过秘密令牌的私有构造函数。
然而,这里介绍的其他技术在其他场景中仍然可能有用。
13.6 进一步阅读
-
异步编程:
-
《JavaScript 程序员的急切指南》中的“用于异步编程的 Promise”章节
-
《JavaScript 程序员的急切指南》中的“异步函数”章节
-
《JavaScript 程序员的急切指南》中的“立即调用的异步箭头函数”部分
-
-
面向对象编程:
-
《JavaScript 程序员的急切指南》中的“原型链和类”章节
-
“JavaScript 类中的私有字段的 ES 提案”博文
-
“JavaScript 类中的私有方法和访问器的 ES 提案”博文
-
评论
十四、复制类的实例:.clone() vs. 复制构造函数
原文:
exploringjs.com/deep-js/ch_copying-class-instances.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
14.1?
.clone() 方法 -
14.2?静态工厂方法
-
14.3?致谢
在本章中,我们将介绍两种实现类实例复制的技术:
-
.clone() 方法 -
所谓的复制构造函数,即接收当前类的另一个实例并用它来初始化当前实例的构造函数。
14.1?.clone() 方法
这种技术为每个需要被复制的类引入了一个
class Point { constructor(x, y) { this.x = x; this.y = y; } clone() { return new Point(this.x, this.y); } } class Color { constructor(name) { this.name = name; } clone() { return new Color(this.name); } } class ColorPoint extends Point { constructor(x, y, color) { super(x, y); this.color = color; } clone() { return new ColorPoint( this.x, this.y, this.color.clone()); // (A) } }
A 行展示了这种技术的一个重要方面:复合实例属性值也必须递归地被克隆。
14.2?静态工厂方法
复制构造函数是一种使用当前类的另一个实例来设置当前实例的构造函数。复制构造函数在静态语言(如 C++和 Java)中很受欢迎,因为你可以通过静态重载提供构造函数的多个版本。在这里,静态意味着选择使用哪个版本是在编译时做出的。
在 JavaScript 中,我们必须在运行时做出决定,这导致了不优雅的代码:
class Point { constructor(...args) { if (args[0] instanceof Point) { // Copy constructor const [other] = args; this.x = other.x; this.y = other.y; } else { const [x, y] = args; this.x = x; this.y = y; } } }
这是你如何使用这个类的方式:
const original = new Point(-1, 4); const copy = new Point(original); assert.deepEqual(copy, original);
静态工厂方法是构造函数的一种替代方法,在这种情况下效果更好,因为我们可以直接调用所需的功能。(这里,静态意味着这些工厂方法是类方法。)
在下面的例子中,三个类
class Point { constructor(x, y) { this.x = x; this.y = y; } static from(other) { return new Point(other.x, other.y); } } class Color { constructor(name) { this.name = name; } static from(other) { return new Color(other.name); } } class ColorPoint extends Point { constructor(x, y, color) { super(x, y); this.color = color; } static from(other) { return new ColorPoint( other.x, other.y, Color.from(other.color)); // (A) } }
在 A 行,我们再次进行递归复制。
这就是
const original = new ColorPoint(-1, 4, new Color('red')); const copy = ColorPoint.from(original); assert.deepEqual(copy, original);
14.3?致谢
- Ron Korvig提醒我在 JavaScript 中进行深复制时要使用静态工厂方法而不是重载构造函数。
评论
十五、不可变集合的包装器
原文:
exploringjs.com/deep-js/ch_immutable-collection-wrappers.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
15.1 包装对象
- 15.1.1 通过包装使集合变为不可变
-
15.2 Map 的不可变包装器
-
15.3 数组的不可变包装器
通过包装集合的不可变包装器使该集合变为不可变。在本章中,我们将探讨其工作原理以及其有用之处。
15.1 包装对象
如果有一个我们想要减少接口的对象,我们可以采取以下方法:
-
创建一个新对象,将原始对象存储在私有字段中。新对象被称为包装器,原始对象被称为被包装对象。
-
包装器只将它接收到的一些方法调用转发给被包装对象。
包装的样子如下:
class Wrapper { #wrapped; constructor(wrapped) { this.#wrapped = wrapped; } allowedMethod1(...args) { return this.#wrapped.allowedMethod1(...args); } allowedMethod2(...args) { return this.#wrapped.allowedMethod2(...args); } }
相关软件设计模式:
-
包装与四人帮设计模式Facade有关。
-
我们使用转发来实现委托。委托意味着一个对象让另一个对象(委托)处理它的一些工作。这是共享代码的继承的替代方法。
15.1.1 通过包装使集合变为不可变
要使集合变为不可变,我们可以使用包装并从其接口中删除所有破坏性操作。
这种技术的一个重要用例是一个具有内部可变数据结构的对象,它希望安全地导出而不复制它。导出是“活动的”也可能是一个目标。对象可以通过包装内部数据结构并使其不可变来实现其目标。
接下来的两节展示了 Map 和数组的不可变包装器。它们都有以下限制:
-
它们只是草图。需要更多的工作使它们适用于实际使用:更好的检查,支持更多的方法等。
-
它们的工作是浅层的:每个都使被包装对象不可变,但不影响其返回的数据。这可以通过包装一些方法返回的结果来修复。
15.2 不可变的 Map 包装器
类
class ImmutableMapWrapper { static _setUpPrototype() { // Only forward non-destructive methods to the wrapped Map: for (const methodName of ['get', 'has', 'keys', 'size']) { ImmutableMapWrapper.prototype[methodName] = function (...args) { return this.#wrappedMapmethodName; } } } #wrappedMap; constructor(wrappedMap) { this.#wrappedMap = wrappedMap; } } ImmutableMapWrapper._setUpPrototype();
原型的设置必须由静态方法执行,因为我们只能从类内部访问私有字段
这是
const map = new Map([[false, 'no'], [true, 'yes']]); const wrapped = new ImmutableMapWrapper(map); // Non-destructive operations work as usual: assert.equal( wrapped.get(true), 'yes'); assert.equal( wrapped.has(false), true); assert.deepEqual( [...wrapped.keys()], [false, true]); // Destructive operations are not available: assert.throws( () => wrapped.set(false, 'never!'), /^TypeError: wrapped.set is not a function$/); assert.throws( () => wrapped.clear(), /^TypeError: wrapped.clear is not a function$/);
15.3 不可变的数组包装器
对于数组
const RE_INDEX_PROP_KEY = /^[0-9]+$/; const ALLOWED_PROPERTIES = new Set([ 'length', 'constructor', 'slice', 'concat']); function wrapArrayImmutably(arr) { const handler = { get(target, propKey, receiver) { // We assume that propKey is a string (not a symbol) if (RE_INDEX_PROP_KEY.test(propKey) // simplified check! || ALLOWED_PROPERTIES.has(propKey)) { return Reflect.get(target, propKey, receiver); } throw new TypeError(`Property "${propKey}" can’t be accessed`); }, set(target, propKey, value, receiver) { throw new TypeError('Setting is not allowed'); }, deleteProperty(target, propKey) { throw new TypeError('Deleting is not allowed'); }, }; return new Proxy(arr, handler); }
让我们来包装一个数组:
const arr = ['a', 'b', 'c']; const wrapped = wrapArrayImmutably(arr); // Non-destructive operations are allowed: assert.deepEqual( wrapped.slice(1), ['b', 'c']); assert.equal( wrapped[1], 'b'); // Destructive operations are not allowed: assert.throws( () => wrapped[1] = 'x', /^TypeError: Setting is not allowed$/); assert.throws( () => wrapped.shift(), /^TypeError: Property "shift" can’t be accessed$/);
评论
第六部分:正则表达式
原文:
exploringjs.com/deep-js/pt_regular-expressions.html 译者:飞龙
协议:CC BY-NC-SA 4.0
接下来:16?正则表达式:通过示例了解环视断言
十六、正则表达式:通过示例了解先行断言
原文:
exploringjs.com/deep-js/ch_regexp-lookaround-assertions.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
16.1?速查表:先行断言
-
16.2?本章警告
-
16.3?示例:指定匹配项之前或之后的内容(正向先行断言)
-
16.4?示例:指定匹配项之前或之后的内容(负向先行断言)
- 16.4.1?没有简单的替代方案来使用负向先行断言
-
16.5?插曲:将先行断言指向内部
-
16.6?示例:匹配不以’abc’开头的字符串
-
16.7?示例:匹配不包含’.mjs’的子字符串
-
16.8?示例:跳过带有注释的行
-
16.9?示例:智能引号
- 16.9.1?通过反斜杠支持转义
-
16.10?致谢
-
16.11?进一步阅读
在本章中,我们使用示例来探讨正则表达式中的先行断言。先行断言是非捕获的,必须匹配(或不匹配)输入字符串中当前位置之前(或之后)的内容。
16.1?速查表:先行断言
表 4:可用先行断言的概述。
模式 | 名称 | |
---|---|---|
正向先行断言 | ES3 | |
负向先行断言 | ES3 | |
正向后行断言 | ES2018 | |
负向后行断言 | ES2018 |
有四个先行断言(表格 4)
-
先行断言(ECMAScript 3):
-
正向先行断言:
(?=?pattern?) 如果pattern 匹配输入字符串中当前位置之后的内容,则匹配成功。 -
负向先行断言:
(?!?pattern?) 如果pattern 不匹配输入字符串中当前位置之后的内容,则匹配成功。
-
-
后行断言(ECMAScript 2018):
-
正向后行断言:
(?<=?pattern?) 如果pattern 匹配输入字符串中当前位置之前的内容,则匹配成功。 -
负向后行断言:
(?<!?pattern?) 如果pattern 不匹配输入字符串中当前位置之前的内容,则匹配成功。
-
16.2?本章警告
-
这些示例展示了通过先行断言可以实现的内容。然而,正则表达式并不总是最佳解决方案。另一种技术,比如适当的解析,可能是更好的选择。
-
后行断言是一个相对较新的功能,可能不被您所针对的所有 JavaScript 引擎支持。
-
先行断言可能会对性能产生负面影响,特别是如果它们的模式匹配长字符串。
16.3?示例:指定匹配项之前或之后的内容(正向先行断言)
在以下交互中,我们提取带引号的单词:
> 'how "are" "you" doing'.match(/(?<=")[a-z]+(?=")/g) [ 'are', 'you' ]
两个先行断言在这里帮助了我们:
-
(?<=") “必须由一个引号前导” -
(?=") “必须跟随一个引号”
环视断言在
> 'how "are" "you" doing'.match(/"([a-z]+)"/g) [ '"are"', '"you"' ]
16.4?示例:指定匹配之前或之后不出现的内容(负环视)
我们如何实现与前一节相反的操作,并从字符串中提取所有未引用的单词?
-
输入:
'how "are" "you" doing' -
输出:
['how', 'doing']
我们的第一个尝试是将正环视断言简单地转换为负环视断言。然而,这种方法失败了:
> 'how "are" "you" doing'.match(/(?<!")[a-z]+(?!")/g) [ 'how', 'r', 'o', 'doing' ]
问题在于我们提取了不被引号括起来的字符序列。这意味着在字符串
我们可以通过声明前缀和后缀不能是引号或字母来解决这个问题:
> 'how "are" "you" doing'.match(/(?<!["a-z])[a-z]+(?!["a-z])/g) [ 'how', 'doing' ]
另一个解决方案是通过
> 'how "are" "you" doing'.match(/(?<!")[a-z]+(?!")/g) [ 'how', 'doing' ]
负回顾断言和负前瞻断言的一个好处是它们也可以分别在字符串的开头或结尾工作,正如示例中所演示的那样。
16.4.1?没有负环视断言的简单替代方案
负环视断言是一个强大的工具,通常无法通过其他正则表达式手段来模拟。
如果我们不想使用它们,通常必须采取完全不同的方法。例如,在这种情况下,我们可以将字符串拆分为(带引号和不带引号的)单词,然后过滤这些单词:
const str = 'how "are" "you" doing'; const allWords = str.match(/"?[a-z]+"?/g); const unquotedWords = allWords.filter( w => !w.startsWith('"') || !w.endsWith('"')); assert.deepEqual(unquotedWords, ['how', 'doing']);
这种方法的好处:
-
它适用于旧版引擎。
-
这很容易理解。
16.5?插曲:指向内部的环视断言
到目前为止,我们所看到的所有示例都有一个共同点,即环视断言规定了匹配之前或之后必须出现的内容,但不包括这些字符在匹配中。
本章其余部分显示的正则表达式是不同的:它们的环视断言指向内部并限制了匹配中的内容。
16.6?示例:匹配不以'abc' 开头的字符串
假设我们想匹配所有不以
这对
> /^(?!abc)/.test('xyz') true
然而,
> /^(?!abc)/.exec('xyz') { 0: '', index: 0, input: 'xyz', groups: undefined }
问题在于像环视断言这样的断言不会扩展匹配的文本。也就是说,它们不会捕获输入字符,它们只是对输入中的当前位置提出要求。
因此,解决方案是添加一个能够捕获输入字符的模式:
> /^(?!abc).*$/.exec('xyz') { 0: 'xyz', index: 0, input: 'xyz', groups: undefined }
如期望的那样,这个新的正则表达式拒绝了以
> /^(?!abc).*$/.exec('abc') null > /^(?!abc).*$/.exec('abcd') null
它接受了不具有完整前缀的字符串:
> /^(?!abc).*$/.exec('ab') { 0: 'ab', index: 0, input: 'ab', groups: undefined }
16.7?示例:匹配不包含'.mjs' 的子字符串
在下面的示例中,我们想要找到
import ··· from '?module-specifier?';
其中
const code = ` import {transform} from './util'; import {Person} from './person.mjs'; import {zip} from 'lodash'; `.trim(); assert.deepEqual( code.match(/^import .*? from '[^']+(?<!.mjs)';$/umg), [ "import {transform} from './util';", "import {zip} from 'lodash';", ]);
在这里,回顾断言
16.8?示例:跳过带有注释的行
场景:我们想解析带有设置的行,同时跳过注释。例如:
const RE_SETTING = /^(?!#)([^:]*):(.*)$/ const lines = [ 'indent: 2', // setting '# Trim trailing whitespace:', // comment 'whitespace: trim', // setting ]; for (const line of lines) { const match = RE_SETTING.exec(line); if (match) { const key = JSON.stringify(match[1]); const value = JSON.stringify(match[2]); console.log(`KEY: ${key} VALUE: ${value}`); } } // Output: // 'KEY: "indent" VALUE: " 2"' // 'KEY: "whitespace" VALUE: " trim"'
我们是如何得到正则表达式
我们从以下正则表达式开始处理设置:
/^([^:]*):(.*)$/
直观地,它是以下部分的序列:
-
行的开头
-
非冒号(零个或多个)
-
一个冒号
-
任何字符(零个或多个)
-
行的末尾
这个正则表达式确实拒绝了一些注释:
> /^([^:]*):(.*)$/.test('# Comment') false
但它接受其他的(其中有冒号):
> /^([^:]*):(.*)$/.test('# Comment:') true
我们可以通过在前面加上
新的正则表达式按预期工作:
> /^(?!#)([^:]*):(.*)$/.test('# Comment:') false
16.9?示例:智能引号
假设我们想将成对的直引号转换为弯引号:
-
输入:
"yes" and "no" -
输出:
“yes” and “no”
这是我们的第一次尝试:
> `The words "must" and "should".`.replace(/"(.*)"/g, '“$1”') 'The words “must" and "should”.'
只有第一个引号和最后一个引号是卷曲的。问题在于
如果我们在
> `The words "must" and "should".`.replace(/"(.*?)"/g, '“$1”') 'The words “must” and “should”.'
16.9.1 通过反斜杠支持转义
如果我们想要通过反斜杠允许引号的转义怎么办?我们可以在引号之前使用保护符
> const regExp = /(?<!\)"(.*?)(?<!\)"/g; > String.raw`"straight" and "curly"`.replace(regExp, '“$1”') '\"straight\" and “curly”'
作为后处理步骤,我们仍然需要做:
.replace(/\"/g, `"`)
然而,当有一个反斜杠转义的反斜杠时,这个正则表达式可能会失败:
> String.raw`Backslash: "\"`.replace(/(?<!\)"(.*?)(?<!\)"/g, '“$1”') 'Backslash: "\\"'
第二个反斜杠阻止了引号变成卷曲的形状。
如果我们让我们的保护符更复杂一些,我们可以解决这个问题(
(?<=^\*)
新的保护符允许在引号之前有一对反斜杠:
> const regExp = /(?<=^\*)"(.*?)(?<=^\*)"/g; > String.raw`Backslash: "\"`.replace(regExp, '“$1”') 'Backslash: “\\”'
还有一个问题。这个保护符会阻止第一个引号在字符串开头时被匹配到:
> const regExp = /(?<=^\*)"(.*?)(?<=^\*)"/g; > `"abc"`.replace(regExp, '“$1”') '"abc"'
我们可以通过将第一个保护符改为:
> const regExp = /(?<=^\*|^)"(.*?)(?<=^\*)"/g; > `"abc"`.replace(regExp, '“$1”') '“abc”'
16.10 致谢
- 第一个处理引号前转义反斜杠的正则表达式是由
@jonasraoni 在 Twitter 上提出的。
16.11 进一步阅读
- 章节“正则表达式(
RegExp )” 在“JavaScript 程序员的急切指南”中
评论
第七部分:杂项主题
原文:
exploringjs.com/deep-js/pt_miscellaneous.html 译者:飞龙
协议:CC BY-NC-SA 4.0
接下来:17?通过实现来探索 Promises
十七、通过实现 Promise 来探索 Promise
原文:
exploringjs.com/deep-js/ch_implementing-promises.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
17.1?复习:Promise 的状态
-
17.2?版本 1:独立的 Promise
-
17.2.1?方法
.then() -
17.2.2?方法
.resolve()
-
-
17.3?版本 2:链接
.then() 调用 -
17.4?便捷方法
.catch() -
17.5?省略反应
-
17.6?实现
-
17.7?版本 3:扁平化从
.then() 回调返回的 Promise-
17.7.1?从
.then() 的回调中返回 Promise -
17.7.2?扁平化使 Promise 状态更加复杂
-
17.7.3?实现 Promise 扁平化
-
-
17.8?版本 4:在反应回调中抛出异常
-
17.9?版本 5:揭示构造函数模式
所需知识:Promise
在本章中,您应该对 Promise 有一定了解,但这里也复习了许多相关知识。如果需要,您可以阅读“JavaScript for impatient programmers”中关于 Promise 的章节。
在这一章中,我们将从不同的角度来接触 Promise:我们将创建一个简单的实现。这种不同的角度曾经帮助我很大地理解 Promise。
Promise 的实现是
带有代码的存储库
17.1?复习:Promise 的状态
图 11:Promise 的状态(简化版本):Promise 最初是 pending 状态。如果我们解决它,它就会变成 fulfilled。如果我们拒绝它,它就会变成 rejected。
我们从一个简化版本开始解释 Promise 状态的工作方式(图 11):
-
Promise 最初是pending状态。
-
如果一个 Promise 被值
v resolved,它就会变成fulfilled(稍后,我们将看到解决也可以拒绝)。v 现在是 Promise 的fulfillment value。 -
如果一个 Promise 被错误
e rejected,它就会变成rejected。e 现在是 Promise 的rejection value。
17.2?版本 1:独立的 Promise
我们的第一个实现是一个独立的 Promise,具有最小的功能:
-
我们可以创建一个 Promise。
-
我们可以解决或拒绝一个 Promise,而且只能做一次。
-
我们可以通过
.then() 注册reactions(回调)。注册必须独立于 Promise 是否已经解决或未解决而做正确的事情。 -
.then() 目前不支持链接,它不返回任何东西。
-
ToyPromise1.prototype.resolve(value) -
ToyPromise1.prototype.reject(reason) -
ToyPromise1.prototype.then(onFulfilled, onRejected)
也就是说,
这是第一个实现的用法:
// .resolve() before .then() const tp1 = new ToyPromise1(); tp1.resolve('abc'); tp1.then((value) => { assert.equal(value, 'abc'); });
// .then() before .resolve() const tp2 = new ToyPromise1(); tp2.then((value) => { assert.equal(value, 'def'); }); tp2.resolve('def');
图 12 说明了我们的第一个
Promises 中的数据流图是可选的
图表的动机是为 Promises 的工作原理提供一个视觉解释。但它们是可选的。如果你觉得它们令人困惑,你可以忽略它们,专注于代码。
图 12:
17.2.1 方法.then()
让我们先来看一下
-
如果 Promise 仍处于挂起状态,则会排队调用
onFulfilled 和onRejected 。它们将在 Promise 解决时使用。 -
如果 Promise 已经被满足或拒绝,
onFulfilled 或onRejected 可以立即被调用。
then(onFulfilled, onRejected) { const fulfillmentTask = () => { if (typeof onFulfilled === 'function') { onFulfilled(this._promiseResult); } }; const rejectionTask = () => { if (typeof onRejected === 'function') { onRejected(this._promiseResult); } }; switch (this._promiseState) { case 'pending': this._fulfillmentTasks.push(fulfillmentTask); this._rejectionTasks.push(rejectionTask); break; case 'fulfilled': addToTaskQueue(fulfillmentTask); break; case 'rejected': addToTaskQueue(rejectionTask); break; default: throw new Error(); } }
上一个代码片段使用以下辅助函数:
function addToTaskQueue(task) { setTimeout(task, 0); }
Promise 必须始终异步解决。这就是为什么我们不直接执行任务,而是将它们添加到事件循环的任务队列中(浏览器、Node.js 等)。请注意,真正的 Promise API 不使用普通任务(如
17.2.2 方法.resolve()
resolve(value) { if (this._promiseState !== 'pending') return this; this._promiseState = 'fulfilled'; this._promiseResult = value; this._clearAndEnqueueTasks(this._fulfillmentTasks); return this; // enable chaining }
_clearAndEnqueueTasks(tasks) { this._fulfillmentTasks = undefined; this._rejectionTasks = undefined; tasks.map(addToTaskQueue); }
17.3 版本 2:链接.then() 调用
图 13:
我们实现的下一个特性是链接(图 13):我们从满足反应或拒绝反应中返回的值可以由后续的
在下面的示例中:
-
第一个
.then() :我们在满足反应中返回一个值。 -
第二个
.then() :我们通过满足反应接收该值。
new ToyPromise2() .resolve('result1') .then(x => { assert.equal(x, 'result1'); return 'result2'; }) .then(x => { assert.equal(x, 'result2'); });
在下面的示例中:
-
第一个
.then() :我们在拒绝反应中返回一个值。 -
第二个
.then() :我们通过满足反应接收该值。
new ToyPromise2() .reject('error1') .then(null, x => { assert.equal(x, 'error1'); return 'result2'; }) .then(x => { assert.equal(x, 'result2'); });
17.4 便利方法.catch()
新版本引入了一个方便的方法
如果我们使用它,上一个示例看起来更好(A 行):
new ToyPromise2() .reject('error1') .catch(x => { // (A) assert.equal(x, 'error1'); return 'result2'; }) .then(x => { assert.equal(x, 'result2'); });
以下两个方法调用是等效的:
.catch(rejectionReaction) .then(null, rejectionReaction)
这就是
catch(onRejected) { // [new] return this.then(null, onRejected); }
17.5 省略反应
新版本还会在我们省略满足反应时转发满足,并在我们省略拒绝反应时转发拒绝。这有什么用呢?
以下示例演示了如何传递拒绝:
someAsyncFunction() .then(fulfillmentReaction1) .then(fulfillmentReaction2) .catch(rejectionReaction);
以下示例演示了如何传递满足:
someAsyncFunction() .catch(rejectionReaction) .then(fulfillmentReaction);
如果
如果
17.6?实现
所有这些是如何在底层处理的?
-
.then() 返回一个 Promise,该 Promise 解析为onFulfilled 或onRejected 返回的内容。 -
如果
onFulfilled 或onRejected 丢失,无论它们将接收到什么都会传递给由.then() 返回的 Promise。
只有
then(onFulfilled, onRejected) { const resultPromise = new ToyPromise2(); // [new] const fulfillmentTask = () => { if (typeof onFulfilled === 'function') { const returned = onFulfilled(this._promiseResult); resultPromise.resolve(returned); // [new] } else { // [new] // `onFulfilled` is missing // => we must pass on the fulfillment value resultPromise.resolve(this._promiseResult); } }; const rejectionTask = () => { if (typeof onRejected === 'function') { const returned = onRejected(this._promiseResult); resultPromise.resolve(returned); // [new] } else { // [new] // `onRejected` is missing // => we must pass on the rejection value resultPromise.reject(this._promiseResult); } }; ··· return resultPromise; // [new] }
-
fulfillmentTask 的工作方式不同。现在完成后会发生什么:-
如果提供了
onFullfilled ,则调用它并使用其结果来解析resultPromise 。 -
如果
onFulfilled 丢失,我们使用当前 Promise 的完成值来解析resultPromise 。
-
-
rejectionTask 的工作方式不同。这是拒绝后现在发生的事情:-
如果提供了
onRejected ,则调用它并使用其结果来解析resultPromise 。请注意,resultPromise 不会被拒绝:我们假设onRejected() 修复了任何问题。 -
如果
onRejected 丢失,我们使用当前 Promise 的拒绝值来拒绝resultPromise 。
-
17.7?版本 3:扁平化从.then() 回调返回的 Promises
17.7.1?从.then() 回调返回 Promises
Promise 扁平化主要是为了使链接更加方便:如果我们想要将一个值从一个
如果我们从
asyncFunc1() .then((result1) => { assert.equal(result1, 'Result of asyncFunc1()'); return asyncFunc2(); // (A) }) .then((result2Promise) => { result2Promise .then((result2) => { // (B) assert.equal( result2, 'Result of asyncFunc2()'); }); });
这一次,将 A 行返回的值放入由
asyncFunc1() .then((result1) => { assert.equal(result1, 'Result of asyncFunc1()'); return asyncFunc2(); // (A) }) .then((result2) => { // result2 is the fulfillment value, not the Promise assert.equal( result2, 'Result of asyncFunc2()'); });
在 A 行,我们返回了一个 Promise。由于 Promise 扁平化,
17.7.2?扁平化使 Promise 状态变得更加复杂
在 ECMAScript 规范中扁平化 Promises
在 ECMAScript 规范中,扁平化 Promises 的细节在“Promise Objects”部分中有描述。
Promise API 如何处理扁平化?
如果 Promise P 用 Promise Q 解析,那么 P 不会包装 Q,P“变成”Q:P 的状态和解决值现在总是与 Q 的相同。这有助于我们理解
P 如何变成 Q?通过锁定Q:P 变得外部无法解决,Q 的解决会触发 P 的解决。锁定是一个额外的不可见的 Promise 状态,使状态变得更加复杂。
Promise API 还有一个额外的特性:Q 不必是一个 Promise,只需是一个所谓的thenable。thenable 是一个带有方法
图[14](#fig:promise-states-all)可视化了新的状态。
图 14:Promise 的所有状态:Promise 扁平化引入了不可见的伪状态“锁定”。如果 Promise P 用 thenable Q 解析,那么 P 的状态和解决值总是与 Q 相同。
请注意,解决的概念也变得更加复杂。现在解决一个 Promise 只意味着它不能直接被解决:
-
解决可能会拒绝一个 Promise:我们可以用一个被拒绝的 Promise 来解决一个 Promise。
-
解决甚至可能不会解决一个 Promise:我们可以用另一个始终处于挂起状态的 Promise 来解决一个 Promise。
ECMAScript 规范是这样规定的:“一个未解决的 Promise 始终处于挂起状态。已解决的 Promise 可能是挂起的、已实现的或已拒绝的。”
17.7.3 实现 Promise 扁平化
图 15 显示了
图 15:
我们通过这个函数检测 thenables:
function isThenable(value) { // [new] return typeof value === 'object' && value !== null && typeof value.then === 'function'; }
为了实现锁定,我们引入了一个新的布尔标志
resolve(value) { // [new] if (this._alreadyResolved) return this; this._alreadyResolved = true; if (isThenable(value)) { // Forward fulfillments and rejections from `value` to `this`. // The callbacks are always executed asynchronously value.then( (result) => this._doFulfill(result), (error) => this._doReject(error)); } else { this._doFulfill(value); } return this; // enable chaining }
如果
-
如果
value 以结果实现,当前 Promise 也将以该结果实现。 -
如果
value 以错误拒绝,当前 Promise 也将以该错误拒绝。
通过私有方法
_doFulfill(value) { // [new] assert.ok(!isThenable(value)); this._promiseState = 'fulfilled'; this._promiseResult = value; this._clearAndEnqueueTasks(this._fulfillmentTasks); }
这里没有显示
17.8 版本 4:反应回调中抛出的异常
图 16:
作为我们的最终特性,我们希望我们的 Promises 能够将用户代码中的异常作为拒绝处理(图 16)。在本章中,“用户代码”指的是
new ToyPromise4() .resolve('a') .then((value) => { assert.equal(value, 'a'); throw 'b'; // triggers a rejection }) .catch((error) => { assert.equal(error, 'b'); })
const fulfillmentTask = () => { if (typeof onFulfilled === 'function') { this._runReactionSafely(resultPromise, onFulfilled); // [new] } else { // `onFulfilled` is missing // => we must pass on the fulfillment value resultPromise.resolve(this._promiseResult); } };
_runReactionSafely(resultPromise, reaction) { // [new] try { const returned = reaction(this._promiseResult); resultPromise.resolve(returned); } catch (e) { resultPromise.reject(e); } }
17.9 版本 5:揭示构造函数模式
我们跳过了最后一步:如果我们想要将
const promise = new Promise( (resolve, reject) => { // executor // ··· });
如果执行程序抛出异常,则
Comments
十八、元编程与代理
原文:
exploringjs.com/deep-js/ch_proxies.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
18.1 概述
-
18.2 编程与元编程
- 18.2.1 元编程的种类
-
18.3 代理解释
-
18.3.1 一个例子
-
18.3.2 特定函数陷阱
-
18.3.3 拦截方法调用
-
18.3.4 可撤销代理
-
18.3.5 代理作为原型
-
18.3.6 转发拦截操作
-
18.3.7 陷阱:并非所有对象都可以被代理透明包装
-
-
18.4 代理的用例
-
18.4.1 跟踪属性访问(
get ,set ) -
18.4.2 关于未知属性的警告(
get ,set ) -
18.4.3 负数组索引(
get ) -
18.4.4 数据绑定(
set ) -
18.4.5 访问 restful web 服务(方法调用)
-
18.4.6 可撤销引用
-
18.4.7 在 JavaScript 中实现 DOM
-
18.4.8 更多用例
-
18.4.9 使用代理的库
-
-
18.5 代理 API 的设计
-
18.5.1 分层:保持基本级别和元级别分开
-
18.5.2 虚拟对象与包装器
-
18.5.3 透明虚拟化和处理程序封装
-
18.5.4 元对象协议和代理陷阱
-
18.5.5 强制代理的不变性
-
-
18.6 常见问题:代理
- 18.6.1
enumerate 陷阱在哪里?
- 18.6.1
-
18.7 参考:代理 API
-
18.7.1 创建代理
-
18.7.2 处理程序方法
-
18.7.3 处理程序方法的不变性
-
18.7.4 影响原型链的操作
-
18.7.5 反射
-
-
18.8 结论
-
18.9 进一步阅读
18.1 概述
代理使我们能够拦截和定制对对象执行的操作(例如获取属性)。它们是一种元编程特性。
在以下示例中:
-
proxy 是一个空对象。 -
通过实现特定方法,
handler 可以拦截对proxy 执行的操作。 -
如果处理程序不拦截操作,则将其转发到
target 。
我们只拦截一个操作 -
const logged = []; const target = {size: 0}; const handler = { get(target, propKey, receiver) { logged.push('GET ' + propKey); return 123; } }; const proxy = new Proxy(target, handler);
当我们获取属性
assert.equal( proxy.size, 123); assert.deepEqual( logged, ['GET size']);
查看完整 API 的参考以获取可以拦截的操作列表。
18.2 编程与元编程
在我们深入了解代理是什么以及它们为何有用之前,我们首先需要了解什么是元编程。
在编程中,有不同的层次:
-
在基础级别(也称为:应用级别),代码处理用户输入。
-
在元级别,代码处理基础级别的代码。
基础和元级别可以是不同的语言。在下面的元程序中,元编程语言是 JavaScript,基础编程语言是 Java。
const str = 'Hello' + '!'.repeat(3); console.log('System.out.println("'+str+'")');
元编程可以采用不同的形式。在前面的示例中,我们已经将 Java 代码打印到控制台。让我们将 JavaScript 用作元编程语言和基础编程语言。这方面的经典示例是
> eval('5 + 2') 7
其他 JavaScript 操作可能看起来不像元编程,但实际上是的,如果我们仔细看的话:
// Base level const obj = { hello() { console.log('Hello!'); }, }; // Meta level for (const key of Object.keys(obj)) { console.log(key); }
程序在运行时检查其自身的结构。这看起来不像元编程,因为 JavaScript 中编程构造和数据结构之间的分离是模糊的。所有的
18.2.1 元编程的种类
反射元编程意味着程序处理自身。Kiczales 等人[2]区分了三种反射元编程:
-
**内省:**我们对程序的结构具有只读访问权限。
-
**自修改:**我们可以改变那个结构。
-
**介入:**我们可以重新定义一些语言操作的语义。
让我们看一些例子。
示例:内省。
**示例:自修改。**以下函数
function moveProperty(source, propertyName, target) { target[propertyName] = source[propertyName]; delete source[propertyName]; }
const obj1 = { color: 'blue' }; const obj2 = {}; moveProperty(obj1, 'color', obj2); assert.deepEqual( obj1, {}); assert.deepEqual( obj2, { color: 'blue' });
ECMAScript 5 不支持介入;代理被创建来填补这一空白。
18.3 代理解释
代理将介入 JavaScript。它们的工作原理如下。我们可以对对象
-
获取对象
obj 的属性prop (obj.prop ) -
检查对象
obj 是否具有属性prop ('prop' in obj )
代理是特殊的对象,允许我们定制其中一些操作。代理是由两个参数创建的:
-
handler :对于每个操作,都有一个相应的处理程序方法,如果存在,就执行该操作。这样的方法拦截了操作(在其传递到目标的途中),并被称为陷阱——这个术语是从操作系统的领域借来的。 -
target :如果处理程序不拦截操作,那么它将在目标上执行。也就是说,它充当处理程序的后备。在某种程度上,代理包装了目标。
注意:“介入”的动词形式是“介入”。介入是双向的。拦截是单向的。
18.3.1 一个示例
在下面的示例中,处理程序拦截了
const logged = []; const target = {}; const handler = { /** Intercepts: getting properties */ get(target, propKey, receiver) { logged.push(`GET ${propKey}`); return 123; }, /** Intercepts: checking whether properties exist */ has(target, propKey) { logged.push(`HAS ${propKey}`); return true; } }; const proxy = new Proxy(target, handler);
如果我们获取属性(行 A)或使用
assert.equal(proxy.age, 123); // (A) assert.equal('hello' in proxy, true); // (B) assert.deepEqual( logged, [ 'GET age', 'HAS hello', ]);
处理程序没有实现
proxy.age = 99; assert.equal(target.age, 99);
18.3.2 特定于函数的陷阱
如果目标是一个函数,可以拦截两个额外的操作:
-
apply :进行函数调用。通过以下方式触发:-
proxy(···) -
proxy.call(···) -
proxy.apply(···)
-
-
construct :进行构造函数调用。通过以下方式触发:new proxy(···)
之所以只为函数目标启用这些陷阱的原因很简单:否则,我们将无法转发操作
18.3.3 拦截方法调用
如果我们想通过代理拦截方法调用,我们面临一个挑战:没有方法调用的陷阱。相反,方法调用被视为两个操作的序列:
-
get 检索函数 -
一个
apply 来调用那个函数
因此,如果我们想要拦截方法调用,我们需要拦截两个操作:
-
首先,我们拦截
get 并返回一个函数。 -
其次,我们拦截该函数的调用。
以下代码演示了如何实现这一点。
const traced = []; function traceMethodCalls(obj) { const handler = { get(target, propKey, receiver) { const origMethod = target[propKey]; return function (...args) { // implicit parameter `this`! const result = origMethod.apply(this, args); traced.push(propKey + JSON.stringify(args) + ' -> ' + JSON.stringify(result)); return result; }; } }; return new Proxy(obj, handler); }
我们并没有使用代理进行第二次拦截;我们只是将原始方法包装在一个函数中。
让我们使用以下对象来尝试
const obj = { multiply(x, y) { return x * y; }, squared(x) { return this.multiply(x, x); }, }; const tracedObj = traceMethodCalls(obj); assert.equal( tracedObj.squared(9), 81); assert.deepEqual( traced, [ 'multiply[9,9] -> 81', 'squared[9] -> 81', ]);
甚至在
这不是最有效的解决方案。例如,可以缓存方法。此外,代理本身会影响性能。
18.3.4?可撤销的代理
代理可以被撤销(关闭):
const {proxy, revoke} = Proxy.revocable(target, handler);
第一次调用函数
const target = {}; // Start with an empty object const handler = {}; // Don’t intercept anything const {proxy, revoke} = Proxy.revocable(target, handler); // `proxy` works as if it were the object `target`: proxy.city = 'Paris'; assert.equal(proxy.city, 'Paris'); revoke(); assert.throws( () => proxy.prop, /^TypeError: Cannot perform 'get' on a proxy that has been revoked$/ );
18.3.5?代理作为原型
代理
const proto = new Proxy({}, { get(target, propertyKey, receiver) { console.log('GET '+propertyKey); return target[propertyKey]; } }); const obj = Object.create(proto); obj.weight; // Output: // 'GET weight'
在
18.3.6?转发拦截的操作
处理程序没有实现的操作的陷阱会自动转发到目标。有时,除了转发操作之外,我们还想执行一些任务。例如,拦截和记录所有操作,而不阻止它们到达目标:
const handler = { deleteProperty(target, propKey) { console.log('DELETE ' + propKey); return delete target[propKey]; }, has(target, propKey) { console.log('HAS ' + propKey); return propKey in target; }, // Other traps: similar }
18.3.6.1?改进:使用Reflect.*
对于每个陷阱,我们首先记录操作的名称,然后通过手动执行它来转发它。JavaScript 有一个类似模块的对象
对于每个陷阱:
handler.trap(target, arg_1, ···, arg_n)
Reflect.trap(target, arg_1, ···, arg_n)
如果我们使用
const handler = { deleteProperty(target, propKey) { console.log('DELETE ' + propKey); return Reflect.deleteProperty(target, propKey); }, has(target, propKey) { console.log('HAS ' + propKey); return Reflect.has(target, propKey); }, // Other traps: similar }
18.3.6.2?改进:使用代理实现处理程序
现在每个陷阱的作用如此相似,以至于我们可以通过代理来实现处理程序:
const handler = new Proxy({}, { get(target, trapName, receiver) { // Return the handler method named trapName return (...args) => { console.log(trapName.toUpperCase() + ' ' + args[1]); // Forward the operation return ReflecttrapName; }; }, });
对于每个陷阱,代理通过
让我们使用基于代理的处理程序:
const target = {}; const proxy = new Proxy(target, handler); proxy.distance = 450; // set assert.equal(proxy.distance, 450); // get // Was `set` operation correctly forwarded to `target`? assert.equal( target.distance, 450); // Output: // 'SET distance' // 'GETOWNPROPERTYDESCRIPTOR distance' // 'DEFINEPROPERTY distance' // 'GET distance'
18.3.7?陷阱:并非所有对象都可以被代理透明包装
代理对象可以被视为拦截对其目标对象执行的操作 - 代理包装目标。代理的处理程序对象就像代理的观察者或监听器。它通过实现相应的方法(
因此,如果处理程序是空对象,则代理应该透明地包装目标。然而,这并不总是有效。
18.3.7.1?包装对象会影响this
在深入研究之前,让我们快速回顾一下包装目标如何影响
const target = { myMethod() { return { thisIsTarget: this === target, thisIsProxy: this === proxy, }; } }; const handler = {}; const proxy = new Proxy(target, handler);
如果我们直接调用
assert.deepEqual( target.myMethod(), { thisIsTarget: true, thisIsProxy: false, });
如果我们通过代理调用该方法,
assert.deepEqual( proxy.myMethod(), { thisIsTarget: false, thisIsProxy: true, });
也就是说,如果代理将方法调用转发到目标,
18.3.7.2?无法透明包装的对象
通常,具有空处理程序的代理会透明地包装目标:我们不会注意到它们的存在,它们也不会改变目标的行为。
然而,如果目标通过代理无法控制的机制与
例如,以下
const _name = new WeakMap(); class Person { constructor(name) { _name.set(this, name); } get name() { return _name.get(this); } }
const jane = new Person('Jane'); assert.equal(jane.name, 'Jane'); const proxy = new Proxy(jane, {}); assert.equal(proxy.name, undefined);
class Person2 { constructor(name) { this._name = name; } get name() { return this._name; } } const jane = new Person2('Jane'); assert.equal(jane.name, 'Jane'); const proxy = new Proxy(jane, {}); assert.equal(proxy.name, 'Jane');
18.3.7.3 包装内置构造函数的实例
大多数内置构造函数的实例也使用代理无法拦截的机制。因此,它们也无法被透明地包装。如果我们使用
const target = new Date(); const handler = {}; const proxy = new Proxy(target, handler); assert.throws( () => proxy.getFullYear(), /^TypeError: this is not a Date object.$/ );
代理不受影响的机制称为内部槽。这些槽是与实例关联的类似属性的存储。规范将这些槽处理为具有方括号名称的属性。例如,以下方法是内部的,可以在所有对象
O.[[GetPrototypeOf]]()
与属性不同,访问内部槽不是通过正常的“获取”和“设置”操作完成的。如果通过代理调用
对于
除非另有规定,下面定义的 Date 原型对象的方法不是通用的,传递给它们的
this 值必须是已初始化为时间值的具有[[DateValue]] 内部槽的对象。
18.3.7.4 解决方法
作为解决方法,我们可以改变处理程序如何转发方法调用,并有选择地将
const handler = { get(target, propKey, receiver) { if (propKey === 'getFullYear') { return target.getFullYear.bind(target); } return Reflect.get(target, propKey, receiver); }, }; const proxy = new Proxy(new Date('2030-12-24'), handler); assert.equal(proxy.getFullYear(), 2030);
这种方法的缺点是,方法在
18.3.7.5 数组可以被透明地包装
与其他内置对象不同,数组可以被透明地包装:
const p = new Proxy(new Array(), {}); p.push('a'); assert.equal(p.length, 1); p.length = 0; assert.equal(p.length, 0);
数组可包装的原因是,即使属性访问被定制以使
18.4 代理的用例
本节演示了代理可以用于什么。这将使我们有机会看到 API 的实际应用。
18.4.1 跟踪属性访问(get ,set )
假设我们有一个函数
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `Point(${this.x}, ${this.y})`; } } // Trace accesses to properties `x` and `y` const point = new Point(5, 7); const tracedPoint = tracePropertyAccesses(point, ['x', 'y']);
获取和设置被跟踪对象
assert.equal(tracedPoint.x, 5); tracedPoint.x = 21; // Output: // 'GET x' // 'SET x=21'
有趣的是,当
assert.equal( tracedPoint.toString(), 'Point(21, 7)'); // Output: // 'GET x' // 'GET y'
18.4.1.1 不使用代理实现tracePropertyAccesses()
如果没有代理,我们将如下实现
function tracePropertyAccesses(obj, propKeys, log=console.log) { // Store the property data here const propData = Object.create(null); // Replace each property with a getter and a setter propKeys.forEach(function (propKey) { propData[propKey] = obj[propKey]; Object.defineProperty(obj, propKey, { get: function () { log('GET '+propKey); return propData[propKey]; }, set: function (value) { log('SET '+propKey+'='+value); propData[propKey] = value; }, }); }); return obj; }
参数
const obj = {}; const logged = []; tracePropertyAccesses(obj, ['a', 'b'], x => logged.push(x)); obj.a = 1; assert.equal(obj.a, 1); obj.c = 3; assert.equal(obj.c, 3); assert.deepEqual( logged, [ 'SET a=1', 'GET a', ]);
18.4.1.2 使用代理实现tracePropertyAccesses()
代理给了我们一个更简单的解决方案。我们拦截属性的获取和设置,不需要改变实现。
function tracePropertyAccesses(obj, propKeys, log=console.log) { const propKeySet = new Set(propKeys); return new Proxy(obj, { get(target, propKey, receiver) { if (propKeySet.has(propKey)) { log('GET '+propKey); } return Reflect.get(target, propKey, receiver); }, set(target, propKey, value, receiver) { if (propKeySet.has(propKey)) { log('SET '+propKey+'='+value); } return Reflect.set(target, propKey, value, receiver); }, }); }
18.4.2 关于未知属性的警告(get ,set )
在访问属性方面,JavaScript 非常宽容。例如,如果我们尝试读取一个属性并拼错它的名称,我们不会得到异常 - 我们会得到结果
我们可以使用代理在这种情况下得到一个异常。工作原理如下。我们将代理作为对象的原型。如果在对象中找不到属性,则会触发代理的
-
如果在代理之后的原型链中甚至不存在属性,则确实缺少该属性,我们会抛出异常。
-
否则,我们返回继承属性的值。我们通过将
get 操作转发到目标(代理从目标获取其原型)来这样做。
这是这种方法的一个实现:
const propertyCheckerHandler = { get(target, propKey, receiver) { // Only check string property keys if (typeof propKey === 'string' && !(propKey in target)) { throw new ReferenceError('Unknown property: ' + propKey); } return Reflect.get(target, propKey, receiver); } }; const PropertyChecker = new Proxy({}, propertyCheckerHandler);
让我们为一个对象使用
const jane = { __proto__: PropertyChecker, name: 'Jane', }; // Own property: assert.equal( jane.name, 'Jane'); // Typo: assert.throws( () => jane.nmae, /^ReferenceError: Unknown property: nmae$/); // Inherited property: assert.equal( jane.toString(), '[object Object]');
18.4.2.1?PropertyChecker 作为一个类
如果我们将
// We can’t change .prototype of classes, so we are using a function function PropertyChecker2() {} PropertyChecker2.prototype = new Proxy({}, propertyCheckerHandler); class Point extends PropertyChecker2 { constructor(x, y) { super(); this.x = x; this.y = y; } } const point = new Point(5, 7); assert.equal(point.x, 5); assert.throws( () => point.z, /^ReferenceError: Unknown property: z/);
这是
const p = Object.getPrototypeOf.bind(Object); assert.equal(p(point), Point.prototype); assert.equal(p(p(point)), PropertyChecker2.prototype); assert.equal(p(p(p(point))), Object.prototype);
18.4.2.2?防止意外创建属性
如果我们担心意外创建属性,我们有两个选择:
-
我们可以将代理包装在捕获
set 的对象周围。 -
或者我们可以通过
Object.preventExtensions(obj) 使对象obj 不可扩展,这意味着 JavaScript 不允许我们向obj 添加新的(自有)属性。
18.4.3?负数组索引(get )
一些数组方法允许我们通过
> ['a', 'b', 'c'].slice(-1) [ 'c' ]
然而,当通过括号运算符(
function createArray(...elements) { const handler = { get(target, propKey, receiver) { if (typeof propKey === 'string') { const index = Number(propKey); if (index < 0) { propKey = String(target.length + index); } } return Reflect.get(target, propKey, receiver); } }; // Wrap a proxy around the Array return new Proxy(elements, handler); } const arr = createArray('a', 'b', 'c'); assert.equal( arr[-1], 'c'); assert.equal( arr[0], 'a'); assert.equal( arr.length, 3);
18.4.4?数据绑定(set )
数据绑定是关于在对象之间同步数据的。一个常见的用例是基于 MVC(模型视图控制器)模式的小部件:通过数据绑定,视图(小部件)会保持最新状态,如果我们改变模型(小部件可视化的数据)。
为了实现数据绑定,我们必须观察并对对象所做的更改做出反应。以下代码片段是对如何观察数组的更改进行工作的草图。
function createObservedArray(callback) { const array = []; return new Proxy(array, { set(target, propertyKey, value, receiver) { callback(propertyKey, value); return Reflect.set(target, propertyKey, value, receiver); } }); } const observedArray = createObservedArray( (key, value) => console.log( `${JSON.stringify(key)} = ${JSON.stringify(value)}`)); observedArray.push('a'); // Output: // '"0" = "a"' // '"length" = 1'
18.4.5?访问 restful web 服务(方法调用)
代理可以用来创建一个可以调用任意方法的对象。在以下示例中,函数
const service = createWebService('http://example.com/data'); // Read JSON data in http://example.com/data/employees service.employees().then((jsonStr) => { const employees = JSON.parse(jsonStr); // ··· });
以下代码是
function createWebService(baseUrl, propKeys) { const service = {}; for (const propKey of propKeys) { service[propKey] = () => { return httpGet(baseUrl + '/' + propKey); }; } return service; }
使用代理,
function createWebService(baseUrl) { return new Proxy({}, { get(target, propKey, receiver) { // Return the method to be called return () => httpGet(baseUrl + '/' + propKey); } }); }
这两种实现都使用以下函数来进行 HTTP GET 请求(其工作原理在JavaScript for impatient programmers中有解释)。
function httpGet(url) { return new Promise( (resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.onload = () => { if (xhr.status === 200) { resolve(xhr.responseText); // (A) } else { // Something went wrong (404, etc.) reject(new Error(xhr.statusText)); // (B) } } xhr.onerror = () => { reject(new Error('Network error')); // (C) }; xhr.open('GET', url); xhr.send(); }); }
18.4.6?可撤销的引用
可撤销的引用的工作原理如下:客户端不允许直接访问重要资源(对象),只能通过引用(中间对象,资源的包装器)访问。通常,对引用应用的每个操作都会转发到资源。客户端完成后,通过撤销引用来保护资源,关闭它。此后,对引用应用操作会抛出异常,不再转发。
在以下示例中,我们为一个资源创建了一个可撤销的引用。然后,我们通过引用读取了资源的一个属性。这是有效的,因为引用授予了我们访问权限。接下来,我们撤销了引用。现在引用不再让我们读取属性。
const resource = { x: 11, y: 8 }; const {reference, revoke} = createRevocableReference(resource); // Access granted assert.equal(reference.x, 11); revoke(); // Access denied assert.throws( () => reference.x, /^TypeError: Cannot perform 'get' on a proxy that has been revoked/ );
代理非常适合实现可撤销的引用,因为它们可以拦截和转发操作。这是一个基于代理的
function createRevocableReference(target) { let enabled = true; return { reference: new Proxy(target, { get(target, propKey, receiver) { if (!enabled) { throw new TypeError( `Cannot perform 'get' on a proxy that has been revoked`); } return Reflect.get(target, propKey, receiver); }, has(target, propKey) { if (!enabled) { throw new TypeError( `Cannot perform 'has' on a proxy that has been revoked`); } return Reflect.has(target, propKey); }, // (Remaining methods omitted) }), revoke: () => { enabled = false; }, }; }
通过上一节的代理作为处理程序技术,可以简化代码。这一次,处理程序基本上是
function createRevocableReference(target) { let enabled = true; const handler = new Proxy({}, { get(_handlerTarget, trapName, receiver) { if (!enabled) { throw new TypeError( `Cannot perform '${trapName}' on a proxy` + ` that has been revoked`); } return Reflect[trapName]; } }); return { reference: new Proxy(target, handler), revoke: () => { enabled = false; }, }; }
但是,我们不必自己实现可撤销的引用,因为代理可以被撤销。这一次,撤销发生在代理中,而不是在处理程序中。处理程序所要做的就是将每个操作转发到目标。正如我们已经看到的,如果处理程序没有实现任何陷阱,那么这将自动发生。
function createRevocableReference(target) { const handler = {}; // forward everything const { proxy, revoke } = Proxy.revocable(target, handler); return { reference: proxy, revoke }; }
18.4.6.1 膜
膜基于可撤销引用的想法构建:用于安全运行不受信任代码的库在该代码周围包装一个膜,以隔离它并保持系统的其余部分安全。对象在两个方向上通过膜传递:
-
不受信任的代码可能会从外部接收对象(“干燥对象”)。
-
或者它可能将对象(“湿对象”)交给外部。
在这两种情况下,可撤销的引用被包装在对象周围。由包装函数或方法返回的对象也被包装。此外,如果将包装的对象传回膜中,则会被解包。
一旦不受信任的代码完成,所有可撤销的引用都被撤销。因此,外部的代码将不再被执行,它引用的外部对象也将停止工作。Caja 编译器是“用于使第三方 HTML、CSS 和 JavaScript 安全嵌入到您的网站中的工具”。它使用膜来实现这一目标。
18.4.7 在 JavaScript 中实现 DOM
浏览器的文档对象模型(DOM)通常是由 JavaScript 和 C++混合实现的。在纯 JavaScript 中实现它对于以下情况很有用:
-
模拟浏览器环境,例如在 Node.js 中操作 HTML。jsdom 是一个可以实现这一功能的库。
-
加快 DOM 的速度(在 JavaScript 和 C++之间切换需要时间)。
然而,标准的 DOM 可以做一些在 JavaScript 中不容易复制的事情。例如,大多数 DOM 集合都是对 DOM 当前状态的动态更改的实时视图。因此,纯 JavaScript 实现的 DOM 并不是非常高效的。向 JavaScript 添加代理的原因之一是为了实现更高效的 DOM。
18.4.8 更多用例
代理还有更多的用例。例如:
-
远程:本地占位符对象将方法调用转发到远程对象。这个用例类似于 Web 服务的例子。
-
数据库的数据访问对象:读取和写入对象会读取和写入数据库。这个用例类似于 Web 服务的例子。
-
分析:拦截方法调用以跟踪每个方法花费的时间。这个用例类似于跟踪的例子。
18.4.9 使用代理的库
-
Immer(由 Michel Weststrate)有助于非破坏性地更新数据。应用的更改是通过调用方法、设置属性、设置数组元素等来指定的。草案状态是通过代理实现的。
-
MobX 让您观察数据结构(如对象、数组和类实例)的更改。这是通过代理实现的。
-
Alpine.js(由 Caleb Porzio)是一个前端库,通过代理实现数据绑定。
-
on-change(由 Sindre Sorhus)观察对象的更改(通过代理)并报告它们。
-
Env utility(由 Nicholas C. Zakas)允许您通过属性访问环境变量,并在它们不存在时抛出异常。这是通过代理实现的。
-
LDflex(由 Ruben Verborgh 和 Ruben Taelman)提供了一个用于链接数据(考虑语义网络)的查询语言。流畅的查询 API 是通过代理实现的。
18.5?代理 API 的设计
在本节中,我们将更深入地了解代理的工作原理以及为什么它们以这种方式工作。
18.5.1?分层:保持基本级别和元级别分开
Firefox 曾经支持一种有限的元编程形式:如果对象
const calc = { __noSuchMethod__: function (methodName, args) { switch (methodName) { case 'plus': return args.reduce((a, b) => a + b); case 'times': return args.reduce((a, b) => a * b); default: throw new TypeError('Unsupported: ' + methodName); } } }; // All of the following method calls are implemented via // .__noSuchMethod__(). assert.equal( calc.plus(3, 5, 2), 10); assert.equal( calc.times(2, 3, 4), 24); assert.equal( calc.plus('Parts', ' of ', 'a', ' string'), 'Parts of a string');
因此,
即使在标准的 ECMAScript 中,基本级别和元级别有时会混合在一起。例如,以下元编程机制可能会失败,因为它们存在于基本级别:
-
obj.hasOwnProperty(propKey) : 如果原型链中的属性覆盖了内置实现,则此调用可能会失败。例如,在以下代码中,obj 会导致失败:const obj = { hasOwnProperty: null }; assert.throws( () => obj.hasOwnProperty('width'), /^TypeError: obj.hasOwnProperty is not a function/ );
这些是调用
.hasOwnProperty() 的安全方式:assert.equal( Object.prototype.hasOwnProperty.call(obj, 'width'), false); // Abbreviated version: assert.equal( {}.hasOwnProperty.call(obj, 'width'), false);
-
func.call(···) ,func.apply(···) : 对于这两种方法,问题和解决方案与.hasOwnProperty() 相同。 -
obj.__proto__ : 在普通对象中,__proto__ 是一个特殊属性,它允许我们获取和设置接收者的原型。因此,当我们将普通对象用作字典时,我们必须避免将__proto__ 作为属性键。
到目前为止,应该很明显,使(基本级别)属性键特殊是有问题的。因此,代理是分层的:基本级别(代理对象)和元级别(处理程序对象)是分开的。
18.5.2?虚拟对象与包装器
代理有两种角色:
-
作为包装器,它们包装它们的目标,控制对它们的访问。包装器的示例包括:可撤销资源和通过代理进行跟踪。
-
作为虚拟对象,它们只是具有特殊行为的对象,它们的目标并不重要。一个例子是代理,它将方法调用转发到远程对象。
代理 API 的早期设计将代理视为纯粹的虚拟对象。然而,事实证明,即使在这种角色中,目标也是有用的,用于强制执行不变量(稍后解释)并作为处理程序没有实现的陷阱的后备。
18.5.3?透明虚拟化和处理程序封装
代理有两种方式进行屏蔽:
-
无法确定对象是否是代理(透明虚拟化)。
-
我们无法通过其代理访问处理程序(处理程序封装)。
这两个原则赋予了代理模式相当大的权力,可以模拟其他对象。强制执行不变量(稍后解释)的一个原因是为了控制这种权力。
如果我们确实需要一种方法来区分代理和非代理,我们必须自己实现。以下代码是一个模块
// lib.mjs const proxies = new WeakSet(); export function createProxy(obj) { const handler = {}; const proxy = new Proxy(obj, handler); proxies.add(proxy); return proxy; } export function isProxy(obj) { return proxies.has(obj); }
该模块使用数据结构
下一个示例展示了如何使用
// main.mjs import { createProxy, isProxy } from './lib.mjs'; const proxy = createProxy({}); assert.equal(isProxy(proxy), true); assert.equal(isProxy({}), false);
18.5.4?元对象协议和代理陷阱
在本节中,我们将研究 JavaScript 的内部结构以及选择 Proxy 陷阱集的方式。
在编程语言和 API 设计的上下文中,协议是一组接口加上使用它们的规则。ECMAScript 规范描述了如何执行 JavaScript 代码。它包括一个处理对象的协议。这个协议在元级别上运行,有时被称为元对象协议(MOP)。JavaScript MOP 由所有对象都具有的内部方法组成。 “内部”意味着它们只存在于规范中(JavaScript 引擎可能有也可能没有),并且无法从 JavaScript 访问。内部方法的名称用双方括号写成。
获取属性的内部方法称为
// Method definition __Get__(propKey, receiver) { const desc = this.__GetOwnProperty__(propKey); if (desc === undefined) { const parent = this.__GetPrototypeOf__(); if (parent === null) return undefined; return parent.__Get__(propKey, receiver); // (A) } if ('value' in desc) { return desc.value; } const getter = desc.get; if (getter === undefined) return undefined; return getter.__Call__(receiver, []); }
在这段代码中调用的 MOP 方法有:
-
[[GetOwnProperty]] (陷阱getOwnPropertyDescriptor ) -
[[GetPrototypeOf]] (陷阱getPrototypeOf ) -
[[Get]] (陷阱get ) -
[[Call]] (陷阱apply )
在 A 行中,我们可以看到原型链中的代理是如何找到
**基本与派生操作。**我们可以看到
18.5.4.1?代理的元对象协议
代理的元对象协议与普通对象的不同。对于普通对象,派生操作调用其他操作。对于代理,每个操作(无论是基本还是派生)都会被处理程序方法拦截或转发到目标。
哪些操作应该通过代理进行拦截?
-
一种可能性是只为基本操作提供陷阱。
-
另一种选择是包括一些派生操作。
这样做的好处是可以提高性能并更加方便。例如,如果没有
包括派生陷阱的一个缺点是可能导致代理行为不一致。例如,
18.5.4.2?选择性拦截:哪些操作应该是可拦截的?
代理的拦截是选择性的:我们无法拦截每个语言操作。为什么有些操作被排除在外?让我们看两个原因。
首先,稳定操作不太适合拦截。如果一个操作总是对相同的参数产生相同的结果,则该操作是稳定的。如果代理可以拦截稳定操作,它可能会变得不稳定,因此不可靠。严格相等(
不进行更多操作的拦截的另一个原因是,拦截意味着在通常不可能的情况下执行自定义代码。代码的交错发生越多,理解和调试程序就越困难。它还会对性能产生负面影响。
18.5.4.3?陷阱:get 与invoke
如果我们想通过代理创建虚拟方法,我们必须从
-
通过
obj.prop 获取属性(陷阱get ) -
通过
obj.prop() 调用方法(陷阱invoke )
有两个原因不这样做。
首先,并非所有实现都区分
其次,提取方法并稍后通过
// Variant 1: call via dynamic dispatch const result1 = obj.m(); // Variant 2: extract and call directly const m = obj.m; const result2 = m.call(obj);
18.5.4.3.1?invoke 的用例
有些事情只有在我们能够区分
**自动绑定。**通过将代理设置为对象
-
通过
obj.m 获取方法m 的值将返回一个this 绑定到obj 的函数。 -
obj.m() 执行方法调用。
自动绑定有助于使用方法作为回调。例如,前面示例中的第 2 个变体变得更简单:
const boundMethod = obj.m; const result = boundMethod();
拦截丢失的方法。
-
如果通过
obj.prop 读取该属性,则不会发生拦截,返回undefined 。 -
如果我们进行方法调用
obj.prop() ,那么代理会拦截,并且,例如,通知一个回调。
18.5.5?强制执行代理的不变量
在我们讨论不变量是什么以及如何通过代理来强制执行它们之前,让我们回顾一下通过非可扩展性和非可配置性来保护对象的方法。
18.5.5.1?保护对象
保护对象的两种方法:
-
非可扩展性保护对象:如果一个对象是非可扩展的,我们就不能添加属性,也不能改变它的原型。
-
非可配置性保护属性(或者说,它们的属性):
-
布尔属性
writable 控制属性的值是否可以更改。 -
布尔属性
configurable 控制属性的属性是否可以更改。
-
有关此主题的更多信息,请参见§10“保护对象免受更改”。
18.5.5.2?强制执行不变量
传统上,非可扩展性和非可配置性是:
-
通用:它们适用于所有对象。
-
单调:一旦打开,就不能再关闭。
这些以及其他在语言操作面前保持不变的特征被称为不变量。通过代理很容易违反不变量,因为它们不是通过非可扩展性等固有地受限制的。代理 API 通过检查目标对象和处理程序方法的结果来防止这种情况发生。
接下来的两个小节描述了四个不变量。不变量的详尽列表在本章末尾给出。
18.5.5.3?通过目标对象强制执行的两个不变量
以下两个不变性涉及不可扩展性和不可配置性。这些是通过使用目标对象进行记录来强制执行的:处理程序方法返回的结果必须与目标对象大部分同步。
-
不变性:如果
Object.preventExtensions(obj) 返回true ,则所有未来的调用必须返回false ,并且obj 现在必须是不可扩展的。- 通过抛出
TypeError 来强制执行代理,如果处理程序返回true ,但目标对象不可扩展。
- 通过抛出
-
不变性:一旦对象被设置为不可扩展,
Object.isExtensible(obj) 必须始终返回false 。- 通过抛出
TypeError 来强制执行代理,如果处理程序返回的结果(在强制转换后)与Object.isExtensible(target) 不同。
- 通过抛出
18.5.5.4 通过检查返回值强制执行的两个不变性
通过检查返回值强制执行的两个不变性是:
-
不变性:
Object.isExtensible(obj) 必须返回一个布尔值。- 通过强制处理程序返回的值转换为布尔值来强制执行代理。
-
不变性:
Object.getOwnPropertyDescriptor(obj, ···) 必须返回一个对象或undefined 。- 通过抛出
TypeError 来强制执行代理,如果处理程序没有返回适当的值。
- 通过抛出
18.5.5.5 不变性的好处
强制执行不变性具有以下好处:
-
代理与其他对象一样,关于可扩展性和可配置性。因此,保持了普遍性。这是在不阻止代理虚拟(冒充)受保护对象的情况下实现的。
-
受保护的对象不能通过包装代理来误导。误导可能是由错误或恶意代码引起的。
接下来的两节给出了强制执行不变性的示例。
18.5.5.6 示例:不可扩展目标的原型必须被忠实地表示
在响应
为了演示这个不变性,让我们创建一个处理程序,返回一个与目标原型不同的原型:
const fakeProto = {}; const handler = { getPrototypeOf(t) { return fakeProto; } };
如果目标是可扩展的,则伪造原型可以起作用:
const extensibleTarget = {}; const extProxy = new Proxy(extensibleTarget, handler); assert.equal( Object.getPrototypeOf(extProxy), fakeProto);
但是,如果我们为不可扩展的对象伪造原型,就会出现错误。
const nonExtensibleTarget = {}; Object.preventExtensions(nonExtensibleTarget); const nonExtProxy = new Proxy(nonExtensibleTarget, handler); assert.throws( () => Object.getPrototypeOf(nonExtProxy), { name: 'TypeError', message: "'getPrototypeOf' on proxy: proxy target is" + " non-extensible but the trap did not return its" + " actual prototype", });
18.5.5.7 示例:不可写不可配置的目标属性必须被忠实地表示
如果目标具有不可写不可配置的属性,则处理程序必须在
const handler = { get(target, propKey) { return 'abc'; } }; const target = Object.defineProperties( {}, { manufacturer: { value: 'Iso Autoveicoli', writable: true, configurable: true }, model: { value: 'Isetta', writable: false, configurable: false }, }); const proxy = new Proxy(target, handler);
属性
assert.equal( proxy.manufacturer, 'abc');
但是,属性
assert.throws( () => proxy.model, { name: 'TypeError', message: "'get' on proxy: property 'model' is a read-only and" + " non-configurable data property on the proxy target but" + " the proxy did not return its actual value (expected" + " 'Isetta' but got 'abc')", });
18.6 常见问题:代理
18.6.1 enumerate 陷阱在哪里?
ECMAScript 6 最初有一个名为
18.7 参考:代理 API
本节是代理 API 的快速参考:
-
全局对象
Proxy -
全局对象
Reflect
引用使用以下自定义类型:
type PropertyKey = string | symbol;
18.7.1 创建代理
有两种创建代理的方法:
-
const proxy = new Proxy(target, handler) 使用给定的目标和给定的处理程序创建一个新的代理对象。
-
const {proxy, revoke} = Proxy.revocable(target, handler) 创建一个可以通过函数
revoke 撤销的代理。revoke 可以被多次调用,但只有第一次调用会产生效果并关闭proxy 。之后,对proxy 执行的任何操作都会导致抛出TypeError 。
18.7.2 处理程序方法
本小节解释了处理程序可以实现的陷阱以及触发它们的操作。几个陷阱返回布尔值。对于
所有对象的陷阱:
-
defineProperty(target, propKey, propDesc): boolean Object.defineProperty(proxy, propKey, propDesc)
-
deleteProperty(target, propKey): boolean -
delete proxy[propKey] -
delete proxy.someProp
-
-
get(target, propKey, receiver): any -
receiver[propKey] -
receiver.someProp
-
-
getOwnPropertyDescriptor(target, propKey): undefined|PropDesc Object.getOwnPropertyDescriptor(proxy, propKey)
-
getPrototypeOf(target): null|object Object.getPrototypeOf(proxy)
-
has(target, propKey): boolean propKey in proxy
-
isExtensible(target): boolean Object.isExtensible(proxy)
-
ownKeys(target): Array<PropertyKey> -
Object.getOwnPropertyPropertyNames(proxy) (仅使用字符串键) -
Object.getOwnPropertyPropertySymbols(proxy) (仅使用符号键) -
Object.keys(proxy) (仅使用可枚举的字符串键;通过Object.getOwnPropertyDescriptor 检查可枚举性)
-
-
preventExtensions(target): boolean Object.preventExtensions(proxy)
-
set(target, propKey, value, receiver): boolean -
receiver[propKey] = value -
receiver.someProp = value
-
-
setPrototypeOf(target, proto): boolean Object.setPrototypeOf(proxy, proto)
函数的陷阱(仅当目标是函数时可用):
-
apply(target, thisArgument, argumentsList): any -
proxy.apply(thisArgument, argumentsList) -
proxy.call(thisArgument, ...argumentsList) -
proxy(...argumentsList)
-
-
construct(target, argumentsList, newTarget): object new proxy(..argumentsList)
18.7.2.1 基本操作与派生操作
以下操作是基本的,它们不使用其他操作来完成工作:
所有其他操作都是派生的,它们可以通过基本操作来实现。例如,
18.7.3 处理程序方法的不变量
不变量是处理程序的安全约束。本小节记录了代理 API 强制执行的不变量以及其工作原理。在下面每当我们读到“处理程序必须执行 X”时,这意味着如果处理程序没有执行 X,则会抛出
-
如果期望布尔值,则使用强制转换将非布尔值转换为合法值。
-
在所有其他情况下,非法值会导致
TypeError 。
这是强制执行的不变量的完整列表:
-
apply(target, thisArgument, argumentsList): any -
不强制执行任何不变量。
-
仅当目标可调用时才激活。
-
-
construct(target, argumentsList, newTarget): object -
处理程序返回的结果必须是对象(而不是
null 或任何其他原始值)。 -
仅当目标可构造时才激活。
-
-
defineProperty(target, propKey, propDesc): boolean -
如果目标不可扩展,则无法添加新属性。
-
如果
propDesc 将configurable 属性设置为false ,则目标必须具有一个不可配置的自有属性,其键为propKey 。 -
如果
propDesc 将configurable 和writable 属性都设置为false ,则目标必须具有一个键为propKey 的自有属性,该属性不可配置且不可写。 -
如果目标具有键为
propKey 的自有属性,则propDesc 必须与该属性兼容:如果我们使用描述符重新定义目标属性,则不得抛出异常。
-
-
deleteProperty(target, propKey): boolean -
如果:
-
目标对象具有一个键为
propKey 的不可配置的自有属性。 -
目标对象是不可扩展的,并且具有一个键为
propKey 的自有属性。
-
-
-
get(target, propKey, receiver): any -
如果目标对象具有一个自有的、不可写的、不可配置的数据属性,其键为
propKey ,则处理程序必须返回该属性的值。 -
如果目标对象有一个自有的、不可配置的、没有 getter 的访问器属性,那么处理程序必须返回
undefined 。
-
-
getOwnPropertyDescriptor(target, propKey): undefined|PropDesc -
处理程序必须返回
undefined 或一个对象。 -
目标对象的不可配置的自有属性不能被处理程序报告为不存在。
-
如果目标对象是不可扩展的,则处理程序必须报告目标对象的自有属性存在。
-
如果处理程序报告一个属性为不可配置,则该属性必须是目标对象的不可配置的自有属性。
-
如果处理程序报告一个属性为不可配置且不可写,那么该属性必须是目标对象的不可配置不可写的自有属性。
-
-
getPrototypeOf(target): null|object -
结果必须是
null 或者一个对象。 -
如果目标对象不可扩展,则处理程序必须返回目标对象的原型。
-
-
has(target, propKey): boolean -
目标对象的不可配置的自有属性不能被处理程序报告为不存在。
-
如果目标对象是不可扩展的,那么目标对象的自有属性不能被报告为不存在。
-
-
isExtensible(target): boolean - 在转换为布尔值后,处理程序返回的值必须与
target.isExtensible() 相同。
- 在转换为布尔值后,处理程序返回的值必须与
-
ownKeys(target): Array<PropertyKey> -
处理程序必须返回一个对象,该对象被视为类似数组,并转换为数组。
-
生成的数组不能包含重复条目。
-
结果的每个元素必须是字符串或符号。
-
结果必须包含目标对象的所有不可配置的自有属性的键。
-
如果目标对象不可扩展,则结果必须恰好包含目标对象的自有属性的键(没有其他值)。
-
-
preventExtensions(target): boolean - 如果
target.isExtensible() 为false ,则处理程序只能返回一个真值(表示成功更改)。
- 如果
-
set(target, propKey, value, receiver): boolean -
如果目标对象具有一个不可写的、不可配置的数据属性,其键为
propKey ,则处理程序必须返回该属性的值。 -
如果相应的目标对象属性是一个不可配置的访问器且没有 setter,则无法以任何方式设置该属性。
-
-
setPrototypeOf(target, proto): boolean - 如果目标对象不可扩展,则原型不能被更改。这是如何实施的:如果目标对象不可扩展且处理程序返回一个真值(表示成功更改),则
proto 必须与目标对象的原型相同。否则,将抛出TypeError 。
- 如果目标对象不可扩展,则原型不能被更改。这是如何实施的:如果目标对象不可扩展且处理程序返回一个真值(表示成功更改),则
ECMAScript 规范中的不变量
在规范中,不变量在“代理对象内部方法和内部插槽”部分中列出。
18.7.4?影响原型链的操作
普通对象的以下操作在原型链上执行操作。因此,如果该链中的一个对象是代理,则会触发其陷阱。规范将这些操作实现为内部自有方法(对 JavaScript 代码不可见)。但在本节中,我们假装它们是具有与陷阱相同名称的普通方法。参数
-
target.get(propertyKey, receiver) 如果
target 没有具有给定键的自有属性,则在target 的原型上调用get 。 -
target.has(propertyKey) 类似于
get ,如果target 没有具有给定键的自有属性,则在target 的原型上调用has 。 -
target.set(propertyKey, value, receiver) 类似于
get ,如果target 没有具有给定键的自有属性,则在target 的原型上调用set 。
所有其他操作只影响自有属性,对原型链没有影响。
ECMAScript 规范中的内部操作
在规范中,这些(和其他)操作在“普通对象内部方法和内部插槽”一节中有描述。
18.7.5?Reflect
全局对象
-
Reflect.apply(target, thisArgument, argumentsList): any 类似于
Function.prototype.apply() 。 -
Reflect.construct(target, argumentsList, newTarget=target): object new 操作符作为一个函数。target 是要调用的构造函数,可选参数newTarget 指向启动当前构造函数调用链的构造函数。 -
Reflect.defineProperty(target, propertyKey, propDesc): boolean 类似于
Object.defineProperty() 。 -
Reflect.deleteProperty(target, propertyKey): boolean delete 操作符作为一个函数。但它的工作方式略有不同:如果成功删除属性或属性从未存在,则返回true 。如果属性无法删除且仍然存在,则返回false 。保护属性免受删除的唯一方法是使它们不可配置。在松散模式下,delete 操作符返回相同的结果。但在严格模式下,它会抛出TypeError 而不是返回false 。 -
Reflect.get(target, propertyKey, receiver=target): any 一个获取属性的函数。可选参数
receiver 指向获取开始的对象。当get 在原型链中后面达到 getter 时,需要它。然后它为this 提供值。 -
Reflect.getOwnPropertyDescriptor(target, propertyKey): undefined|PropDesc 与
Object.getOwnPropertyDescriptor() 相同。 -
Reflect.getPrototypeOf(target): null|object 与
Object.getPrototypeOf() 相同。 -
Reflect.has(target, propertyKey): boolean in 操作符作为一个函数。 -
Reflect.isExtensible(target): boolean 与
Object.isExtensible() 相同。 -
Reflect.ownKeys(target): Array<PropertyKey> 以数组形式返回所有自有属性键:所有自有可枚举和不可枚举属性的字符串键和符号键。
-
Reflect.preventExtensions(target): boolean 类似于
Object.preventExtensions() 。 -
Reflect.set(target, propertyKey, value, receiver=target): boolean 一个设置属性的函数。
-
Reflect.setPrototypeOf(target, proto): boolean 设置对象原型的新标准方式。目前大多数引擎中有效的非标准方式是设置特殊属性
__proto__ 。
几种方法具有布尔结果。对于
18.7.5.1?Reflect 的用例除了转发
除了转发操作,为什么
-
不同的返回值:
Reflect 复制了Object 的以下方法,但其方法返回布尔值,指示操作是否成功(而Object 方法返回被修改的对象)。-
Object.defineProperty(obj, propKey, propDesc): object -
Object.preventExtensions(obj): object -
Object.setPrototypeOf(obj, proto): object
-
-
作为函数的运算符:以下
Reflect 方法实现了通过运算符才能实现的功能:-
Reflect.construct(target, argumentsList, newTarget=target): object -
Reflect.deleteProperty(target, propertyKey): boolean -
Reflect.get(target, propertyKey, receiver=target): any -
Reflect.has(target, propertyKey): boolean -
Reflect.set(target, propertyKey, value, receiver=target): boolean
-
-
apply() 的简短版本:如果我们想完全安全地调用函数的apply() 方法,我们不能通过动态分发来做到这一点,因为函数可能具有一个具有键'apply' 的自有属性:func.apply(thisArg, argArray) // not safe Function.prototype.apply.call(func, thisArg, argArray) // safe
使用
Reflect.apply() 比安全版本更短:Reflect.apply(func, thisArg, argArray)
-
删除属性时不会抛出异常:在严格模式下,如果我们尝试删除一个不可配置的自有属性,
delete 运算符会抛出异常。在这种情况下,Reflect.deleteProperty() 会返回false 。
18.7.5.2 Object.* 与Reflect.*
未来,
18.8 结论
这结束了我们对代理 API 的深入研究。需要注意的一点是,代理会减慢代码。如果性能很重要,这可能很重要。
另一方面,性能通常并不是关键,拥有代理赋予我们的元编程能力是很好的。
致谢:
-
Allen Wirfs-Brock 指出了§18.3.7“陷阱:并非所有对象都可以被代理透明地包装”中解释的陷阱。
-
§18.4.3“通过代理使用负数组索引(
get )”的想法来自Hemanth.HM的博客文章。 -
André Jaenisch 为使用代理的库列表做出了贡献。
18.9 进一步阅读
-
[1] “关于 ECMAScript 反射 API 设计” by Tom Van Cutsem and Mark Miller. Technical report, 2012. [本章的重要来源。]
-
[2] “元对象协议的艺术” by Gregor Kiczales, Jim des Rivieres and Daniel G. Bobrow. Book, 1991.
-
[3] “将元类应用于工作:面向对象编程的新维度” by Ira R. Forman and Scott H. Danforth. Book, 1999.
-
[4] “Harmony-reflect: 为什么我应该使用这个库?” by Tom Van Cutsem. [解释了为什么
Reflect 很有用。]
评论
十九、剩下的章节在哪里?
原文:
exploringjs.com/deep-js/ch_missing-chapters-site.html 译者:飞龙
协议:CC BY-NC-SA 4.0
您正在阅读本书的免费在线版本。
-
离线版本 - 包括三个额外的奖励章节 - 可以购买。
-
您可以查看完整的目录(也链接到书的主页)。