在 ECMAScript 5 之前,JavaScript 环境中具有不可枚举和不可写的对象属性,但是开发者不能定义不可枚举或不可写的属性。ECMAScript 5 添加的 Object.defineProperty()
方法允许开发者定义不可枚举和不可写的对象属性。
ECMAScript 6 给予了开发者先前仅内建对象具有的访问 JavaScript 引擎的能力。语言通过代理(proxies)暴露了对象的内部工作方式(inner workings),代理是包装器,可以拦截和改变 JavaScript 引擎的底层(low-level)操作。本章以详细描述代理可以解决的问题作为开头,然后讨论如何有效地创建和使用代理。
数组问题
ECMAScript 6 之前,开发者不能自定义对象模仿 JavaScript 数组对象的行为。当给数组指定项赋值时,会影响数组的 length
属性,也可以通过修改 length
属性从而改变数组。例如:
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
colors
数组开始有 3 个元素。赋值 "black"
给 colors[3]
会自动递增 length
属性,此时属性值为 4。设置 length
属性值为 2 会移除数组中最后面的两个元素,数组的前两个元素不变。在 ECMAScript 5 中,开发者不能实现这种行为,但是代理可以。
在 ECMAScript 6 中,这种非标准的行为是数组作为 exotic 对象的原因。
代理和反射是什么
调用 new Proxy()
可以创建一个代理,然后使用它取代另一个对象(称为目标对象)。代理虚拟化了(virtualizes)目标对象,以至于使用了代理后,代理和目标对象看起来是同一个对象。
代理允许拦截目标对象的底层操作,这些操作通常由 JavaScript 引擎内部处理。一般使用 trap 拦截这些底层操作。trap 是响应特定操作的函数。
Reflect
对象表示的反射(reflection)API 是一组方法,提供了和代理可以重写的底层操作相同的操作的默认行为。每个代理 trap 有一个 Reflect
方法。这些方法和对应的 trap 具有相同的名字,接收的参数也相同。
可以重写的操作 | 代理 Trap | 默认行为 |
---|---|---|
读属性值 | get |
Reflect.get() |
写属性 | set |
Reflect.set() |
in 操作符 |
has |
Reflect.has() |
delete 操作符 |
deleteProperty |
Reflect.deleteProperty() |
Object.getPrototypeOf() |
getPrototypeOf |
Reflect.getPrototypeOf() |
Object.setPrototypeOf() |
setPrototypeOf |
Reflect.setPrototypeOf() |
Object.isExtensible() |
isExtensible |
Reflect.isExtensible() |
Object.preventExtensions() |
preventExtensions |
Reflect.preventExtensions() |
Object.getOwnPropertyDescriptor() |
getOwnPropertyDescriptor |
Reflect.getOwnPropertyDescriptor() |
Object.defineProperty() |
defineProperty |
Reflect.defineProperty |
Object.keys , Object.getOwnPropertyNames() , Object.getOwnPropertySymbols() |
ownKeys |
Reflect.ownKeys() |
调用函数 | apply |
Reflect.apply() |
使用 new 调用函数 |
construct |
Reflect.construct() |
每个 trap 重写了某个 JavaScript 对象的内建行为,允许拦截和修改这个行为。如果仍然需要使用内建行为,可以使用对应的反射 API 方法。
创建简单代理
使用 Proxy
构造器创建代理时,需要传入两个参数:目标对象和处理器(handler)。处理器是定义一或多个 trap 的对象。除了 trap 重写的操作之外,代理会对所有操作使用默认行为。创建简单的转发(forwarding)代理,使用不含 trap 的处理器:
let target = {};
let proxy = new Proxy(target, {});
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
target.name = "target";
console.log(proxy.name); // "target"
console.log(target.name); // "target"
在这个例子中,proxy
直接转发所有操作给 target
。将 "proxy" 赋值给 name
属性时,target
上会创建 name
属性。代理自身没有这个属性,它只是简单地转发操作给 target
。类似地,proxy.name
和 target.name
的值相同,因为两者都引用 target.name
。这也意味着设置 target.name
的值会让 proxy.name
的值作出相同的改变。
使用 set
trap 验证属性
假设想要创建一个对象,这个对象的属性值必须是数值。这意味着每次向这个对象添加新的属性,需要验证属性值,如果值不是数值则必须抛出错误。为了完成这个操作,可以定义 set
trap 重写设置一个值的默认行为。 set
trap 接收4 个参数:
trapTarget
接收属性的对象(代理的目标对象)key
将要设置的属性的键(string 或 symbol 类型)value
写入属性的值receiver
这个对象上执行操作(通常是代理)
Reflect.set()
是 set
trap 对应的反射方法,是这个操作的默认行为。Reflect.set()
方法接收和 set
trap 相同的 4 个参数,使得方法在 trap 中易于使用。当属性设置成功,trap 应该返回 true,否则返回 false。(Reflect.set()
方法基于操作是否成功返回对应的值。)
为了验证属性值,使用 set
trap,检查传入的 value
参数。例子如下:
let target = {
name: "target"
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
// 忽略对象已有的属性,这些属性不受影响
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("Property must be a number.");
}
}
// 设置属性
return Reflect.set(trapTarget, key, value, receiver);
}
});
// 添加新属性
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
// 可以向 name 属性赋值,因为它是目标对象的已有属性
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 抛出错误
proxy.anotherName = "proxy";
这段代码定义了一个 set
trap 验证添加到 target
对象上的新属性的值。执行 proxy.count = 1
时,调用 set
trap。 trapTarget
值等于 target
, key
值是 “count",value
值是 1, receiver
(这个例子中没有使用) 值是 proxy
。target
中没有名为 count 的属性,代理传入 value
给 isNaN()
进行验证。如果结果是 NaN
,则属性值不是数值,抛出错误。由于 count 的值是 1,代理传入和 set
trap 相同的 4 个参数调用 Reflect.set()
方法,添加新属性。
当向 proxy.name
赋值一个字符串时,操作成功完成。由于 target
已经有 name
属性,在验证过程中调用的trapTarget.hasOwnProperty()
方法忽略该属性。这确保了仍然支持先前已存在的非数值属性值。
然而,向 proxy.anotherName
赋值字符串时,抛出错误。目标对象中不存在anotherName
属性,所以需要验证它的值。在验证过程中,因为 "proxy" 不是数值,抛出错误。
当写入属性时,可以使用代理的 set
trap 拦截这个操作。当读属性时,可以使用 get
trap 拦截这个操作。
使用 get
Trap 验证 Object Shape
在 JavaScript 中,读一个不存在的属性不会抛出错误。此时,将得到一个 undefined 值。如下所示:
let target = {};
console.log(target.name); // undefined
在大多数语言中,因为 target.name
属性不存在,读取这个属性会抛出错误。但是 JavaScript 会得到 undefined 值。
对象 shape 是对象可用的一组属性和方法。JavaScript 引擎使用对象 shape 优化代码,通常创建类表示对象。如果可以安全地假定一个对象在它创建之后,它的属性和方法不会改变(使用 Object.preventExtensions()
、Object.seal()
和 Object.freeze()
方法可以实现这个行为),则访问一个不存在的属性时抛出错误是有帮助的。代理使得对象 shape 的验证容易。
仅当读取属性时,属性验证发生,可以使用 get
trap。读取属性时,get
trap 调用,即使对象没有对应的属性。它接收 3 个参数:
trapTarget
具有读取的属性的对象(代理的目标对象)。key
读取的属性的键(string 或 symbol)。receiver
操作发生的对象(通常是代理)。
这些属性和 set
trap 的参数类似,也有不同之处。因为 get
trap 不向属性写入值,没有 value
参数。 Reflect.get()
方法接收和 get
trap 相同的 3 个参数,返回属性的默认值。
当目标对象上不存在读取的属性时,可以使用 get
trap 和 Reflect.get()
方法抛出错误,如下所示:
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.");
}
return Reflect.get(trapTarget, key, receiver);
}
});
// 添加属性
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// 读取不存在的属性抛出错误
console.log(proxy.nme); // throws error
这个例子中,get
trap 拦截了属性的读操作。in 操作符确定 receiver
中是否存在指定属性。in 操作符和 receiver
而不是 trapTarget
一起使用,因为此时 receiver
具有 has
trap。在这种情况下使用 trapTarget
会绕过 has
trap,得到错误的结果。如果属性不存在,抛出错误;否则,发生默认行为。
这段代码允许添加、写入和读取像 proxy.name
这样的新属性。最后一行因为 nme
这个属性不存在,抛出错误。
使用 has
Trap 隐藏存在的属性
in 操作符确定指定对象是否有指定属性,如果自有属性或原型链中属性和指定属性名匹配(string 或 symbol),返回 true。例如:
let target = {
value: 42;
}
console.log("value" in target); // true
console.log("toString" in target); // true
value
和 toString
在对象中存在,所以两者使用 in 操作符返回 true。value
属性是自有属性,toString
属性是原型链中的属性(继承自 Object
)。代理允许使用 has
trap 拦截 in 操作,在 in 操作中返回不同的值。
无论什么时候 in 操作符执行时,has
trap 被调用。调用时会传入两个参数:
trapTarget
读取的属性所在的对象(代理的目标对象)。key
检查的属性的键(string 或 symbol)。
Reflect.has()
方法接收相同的参数,返回默认的 in 操作符的响应。使用 has
trap 和 Reflect.has()
方法可以改变 in 操作符对部分属性的行为,其他属性保持默认行为。例如,假设想要隐藏 value
属性,可以这样做:
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
has(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.has(trapTarget, key);
}
}
});
console.log("value" in proxy); // false
console.log("name" in proxy); // true
console.log("toString" in proxy); // true
代理的 has
trap 检查如果键为 "value",返回 false。否则,调用 Reflect.has()
方法执行默认行为。因此,即使 value 属性实际上在目标对象中存在,in 操作符也会返回 false。name 和 toString 使用 in 操作符时返回 true。
deleteProperty
Trap 阻止删除属性
delete
操作符从对象上移除指定属性,如果移除成功,返回 true,否则返回 false。在严格模式下,移除一个不可配置属性会抛出错误;在非严格模式下,移除一个不可配置属性返回 false。例子如下:
let target = {
name: "target",
value: 42
};
Object.defineProperty(target, "name", { configurable: false });
console.log("value" in target); // true
let result1 = delete target.value;
console.log(result1); // true
console.log("value" in target); // false
// 下述代码在严格模式下抛出错误
let result2 = delete target.name;
console.log(result2); // false
console.log("name" in target); // true
使用 delete
操作符删除了 value
属性,因此,在第三次调用 console.log()
时,in 操作符返回 false。不可配置属性 name
不能删除,所以 delete
操作符返回 false(如果在严格模式下,会抛出错误)。在代理中使用 deleteProperty
trap 可以改变这个行为。
对对象的属性使用 delete
操作符时,deleteProperty
trap 被调用。这个 trap 接收 2 个参数:
trapTarget
删除的属性所在的对象(代理的目标对象)。key
删除的属性的键(string 或 symbol)。
Reflect.deleteProperty()
方法提供了 deleteProperty
trap 的默认实现,接收相同的参数。组合使用 Reflect.deleteProperty()
方法和 deleteProperty
trap 可以改变 delete
操作符的行为。例如,可以保证不删除 value
属性:
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
deleteProperty(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.deleteProperty(trapTarget, key);
}
}
});
// 尝试删除 proxy.value
console.log("value" in proxy); // true
let result1 = delete proxy.value;
console.log(result1); // false
console.log("value" in proxy); // true
// 尝试删除 proxy.name
console.log("name" in proxy); // true
let result2 = delete proxy.name;
console.log(result2); // true
console.log("name" in proxy); // false
这段代码非常类似于 has
trap 的例子,deleteProperty
trap 检查属性的键是否为 "value",如果是则返回 false;否则,调用 Reflect.deleteProperty()
方法执行默认行为。value
属性不能通过 proxy
删除,因为操作经过 trap,trap 中不会对 value
属性执行删除操作,但 name
属性会被删除。当想要在严格模式下,保证删除属性时不会抛出错误,可以使用这个方法。
代理作为原型时的 Trap
ECMAScript 6 添加的 Object.setPrototypeOf()
方法作为 Object.getPrototypeOf()
方法的补充。代理允许通过 setPrototypeOf
和 getPrototypeOf
trap 拦截这两个方法的执行。在这两种情况下,Object 的方法会在代理上调用对应名称的 trap,允许改变方法的行为。
由于有两个 trap 和原型代理关联,每种类型的 trap 都有一组方法与之关联。setPrototypeOf
trap 接收的参数如下:
trapTarget
设置的原型所在的对象(代理的目标对象)。proto
作为原型使用的对象。
Object.setPrototypeOf()
和 Reflect.setPrototypeOf()
方法接收相同的参数。getPrototypeOf
trap 仅接收 trapTarget
参数,Object.setPrototypeOf()
和 Reflect.setPrototypeOf()
方法也有这个参数。
这些 trap 有一些限制。首先,getPrototypeOf
trap 必须返回一个对象或 null,返回其他值会导致运行时错误。返回值检查确保 Object.getPrototypeOf()
总是返回所期望的值。类似的,如果操作不成功,setPrototypeOf
trap 的返回值必须是 false。当 setPrototypeOf
返回 false 时,Object.setPrototypeOf()
抛出错误。如果 setPrototypeOf
返回除了 false 之外的其他值,Object.setPrototypeOf()
确保操作成功。
下述例子总是返回 null 隐藏代理的原型,不允许改变原型:
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return null;
},
setPrototypeOf(trapTarget, proto) {
return false;
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
// 成功
Object.setPrototypeOf(target, {});
// 抛出错误
Object.setPrototypeOf(proxy, {});
这段代码强调了 target 和 proxy 的不同行为。Object.getPrototypeOf()
方法传入 target 调用时,返回一个值;传入 proxy 调用时,返回 null,因为此时 getPrototypeOf
trap 被调用。类似的,Object.setPrototypeOf()
方法传入 target 调用,成功;传入 proxy 调用则抛出错误,由于 setPrototypeOf
trap。
如果想要使用这两个 trap 的默认行为,可以使用 Reflect 对象的对应方法。例如,这段代码实现了 getPrototypeOf
和 setPrototypeOf
trap 的默认行为:
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return Reflect.getPrototypeOf(trapTarget);
},
setPrototypeOf(trapTarget, proto) {
return Reflect.setPrototypeOf(trapTarget, proto);
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // true
// 成功
Object.setPrototypeOf(target, {});
// 成功
Object.setPrototypeOf(proxy, {});
这个例子中,可以相互交替地使用 target 和 proxy,得到相同的结果。因为 getPrototypeOf
和 setPrototypeOf
trap 直接使用了默认实现。重要的是,由于某些重要的不同之处,本例中使用了 Reflect.getPrototypeOf()
和 Reflect.setPrototypeOf()
方法而不是 Object 对象的同名方法。
Reflect.getPrototypeOf()
和 Reflect.setPrototypeOf()
方法看起来类似于 Object.getPrototypeOf()
和 Object.setPrototypeOf()
方法。这两对方法执行类似地操作,有一些不同之处。
Object.getPrototypeOf()
和 Object.setPrototypeOf()
方法是较高层次(higher-level)操作,提供给开发者使用。Reflect.getPrototypeOf()
和 Reflect.setPrototypeOf()
方法是低层次(lower-level)操作,使得开发者可以访问先前仅内部可用的 [[GetPrototypeOf]] 和 [[SetPrototypeOf]] 操作。Reflect.getPrototypeOf()
方法是内部的 [[GetPrototypeOf]] 操作(和一些输入验证)的包装器。Reflect.setPrototypeOf()
方法和 [[SetPrototypeOf]] 操作之间的关系和前述类似。Object 上对应的方法也调用了 [[GetPrototypeOf]] 和 [[SetPrototypeOf]] 操作,同时在调用之前执行了某些操作,检查返回值确定行为。
传入 Reflect.getPrototypeOf()
方法的参数不是对象时,抛出错误。Object.getPrototypeOf()
方法首先将参数强制转换为对象,然后执行操作。如果向这两个方法传入数值,会得到不同的结果:
let result1 = Object.getPrototypeOf(1);
console.log(result1 === Number.prototype); // true
// 抛出错误
Reflect.getPrototypeOf(1);
Object.getPrototypeOf()
方法从数字 1 上检索原型,因为它首先将值转换为 Number 对象,然后返回 Number.prototype
。Reflect.getPrototypeOf()
方法不会将参数转换为对象,由于 1 不是对象,它抛出错误。
Reflect.setPrototypeOf()
方法和 Object.setPrototypeOf()
方法之间也有些许不同。Reflect.setPrototypeOf()
方法返回布尔值表示操作是否成功,true 表示成功,false 表示失败。如果 Object.setPrototypeOf()
方法操作失败,抛出错误。
setPrototypeOf
trap 返回 false 会造成 Object.setPrototypeOf()
方法抛出错误。Object.setPrototypeOf()
方法会将它的第一个参数作为返回值返回,不适合实现代理 setPrototypeOf
trap 的默认行为。下述代码演示了两者的不同:
let target1 = {};
let result1 = Object.setPrototypeOf(target1, {});
console.log(result1 === target1); // true
let target2 = {};
let result2 = Reflect.setPrototypeOf(target2, {});
console.log(result2 === target2); // false
console.log(result2); // true
本例中, Object.setPrototypeOf()
将 target1
作为返回值返回,Reflect.setPrototypeOf()
返回 true
。这微妙的不同很重要。Object 和 Reflect 之间有许多同名的方法,总是确保在代理 trap 中使用 Reflect 的方法。
对象扩展性 Trap
ECMAScript 5 添加了 Object.preventExtensions()
和 Object.isExtensible()
方法用于修改对象的可扩展性,ECMAScript 6 允许代理通过 preventExtensions
和 isExtensible
trap 拦截这些方法。这两个方法接收一个名为 trapTarget
的参数,表示调用方法的对象。 isExtensible
trap 必须返回布尔值表示对象是否可扩展。preventExtensions
trap 必须返回布尔值表示操作是否成功。
Reflect.preventExtensions()
和 Reflect.isExtensible()
实现了对应操作的默认行为。两者都返回布尔值,所以可以直接在对应的 trap 中使用。
考虑下面的代码,观察对象可扩展性 trap 的行为,实现了 isExtensible
and preventExtensions
trap 的默认行为:
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return Reflect.preventExtensions(trapTarget);
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false
这个例子展示了 Object.preventExtensions()
和 Object.isExtensible()
接收 proxy 和 target 的行为。当然,也可以改变这个行为。例如,如果不想 Object.preventExtensions()
方法对 proxy 生效,可以在 preventExtensions
trap 中返回 false:
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return false
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
此时,对 Object.preventExtensions(proxy)
的调用被忽略,因为 preventExtensions
trap 返回 false。操作并没有转发给 target,所以 Object.isExtensible()
返回 true。
Object.isExtensible()
方法和 Reflect.isExtensible()
方法相似,除了当接收一个非对象参数时的表现不同。当接收非对象参数时,Object.isExtensible()
返回 false,Reflect.isExtensible()
抛出错误。例子如下:
let result1 = Object.isExtensible(2);
console.log(result1); // false
// 抛出错误
let result2 = Reflect.isExtensible(2);
底层方法比对应的较高层方法有更严格的错误检查。
Object.preventExtensions()
方法总是返回传入它的参数,即使这个参数不是对象。Reflect.preventExtensions()
方法则当参数不是对象时抛出错误,参数是对象时,如果执行成功返回 true,否则返回 false。例如:
let result1 = Object.preventExtensions(2);
console.log(result1); // 2
let target = {};
let result2 = Reflect.preventExtensions(target);
console.log(result2); // true
// 抛出错误
let result3 = Reflect.preventExtensions(2);
属性描述符 Trap
Object.defineProperty()
方法用于定义对象属性的性质。Object.getOwnPropertyDescriptor()
方法则可以查询属性的性质。
使用 defineProperty
、getOwnPropertyDescriptor
trap 可以拦截对 Object.defineProperty()
、 Object.getOwnPropertyDescriptor()
方法的调用。defineProperty
trap 接收如下参数:
trapTarget
定义的属性所位于的对象(代理的目标对象)。key
属性的键,string 或 symbol。descriptor
属性的描述符(descriptor)对象。
当操作成功时, defineProperty
trap 返回 true,否则返回 false。getOwnPropertyDescriptor
trap 接收参数 trapTarget
和 key
,返回描述符。 Reflect.defineProperty()
和 Reflect.getOwnPropertyDescriptor()
方法接收和对应的 trap 完全相同的参数。下述例子仅实现了每个 trap 的默认行为:
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
return Reflect.defineProperty(trapTarget, key, descriptor);
},
getOwnPropertyDescriptor(trapTarget, key) {
return Reflect.getOwnPropertyDescriptor(trapTarget, key);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
console.log(descriptor.value); // "proxy"
defineProperty
trap 返回 true 时,Object.defineProperty()
执行成功;返回 false 时,Object.defineProperty()
抛出错误。可以使用这个功能限制 Object.defineProperty()
可以定义的属性的类型。例如,如果不允许定义 symbol 属性,可以检查属性的键类型是否为 string,是则返回 true,否则返回 false:
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
if (typeof key === "symbol") {
return false;
}
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let nameSymbol = Symbol("name");
// 抛出错误
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
});
也可以让 Object.defineProperty()
静默地失败,此时返回 true 但并不调用 Reflect.defineProperty()
方法。这种方式会抑制错误,实际上不会定义属性。
为确保使用 Object.defineProperty()
和 Object.getOwnPropertyDescriptor()
方法行为的一致性,传递给 defineProperty
trap 的描述符对象需要正则化。基于同样的原因,从 getOwnPropertyDescriptor
trap 返回的对象总是已验证的。
无论传递什么样的对象作为 Object.defineProperty()
的第三个参数,这个对象的 enumerable
, configurable
, value
, writable
, get
, 和 set
属性有效,会传递给 defineProperty
trap。例如:
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
console.log(descriptor.value); // "proxy"
console.log(descriptor.name); // undefined
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy",
name: "custom"
});
调用 defineProperty
trap 时,descriptor
对象不是引用了传给 Object.defineProperty()
方法的第 3 个参数,而是一个仅包含允许的属性的新对象。 Reflect.defineProperty()
方法也会忽略描述符对象中的非标准属性。
getOwnPropertyDescriptor
trap 的返回值必须是 null、undefined 或对象。如果返回的是对象,则这个对象的自有属性必须是: enumerable
, configurable
, value
, writable
, get
和 set
。如果返回的对象的自有属性具有上述的这些属性之外的属性,抛出错误,正如下述代码所示:
let proxy = new Proxy({}, {
getOwnPropertyDescriptor(trapTarget, key) {
return {
name: "proxy"
};
}
});
// 抛出错误
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
Object.defineProperty()
和 Reflect.defineProperty()
除了返回值不同,其他作用相同。 Object.defineProperty()
返回传入给它的第一个参数,Reflect.defineProperty()
返回布尔值表示操作是否成功。例如:
let target = {};
let result1 = Object.defineProperty(target, "name", { value: "target "});
console.log(target === result1); // true
let result2 = Reflect.defineProperty(target, "name", { value: "reflect" });
console.log(result2); // true
由于 defineProperty
trap 返回一个布尔值,必要的时候,最好使用Reflect.defineProperty()
实现默认行为。
传入 Object.getOwnPropertyDescriptor()
方法的第一个参数如果是原始值(primitive value),则会强制将其转换成对象,然后再执行操作。当传入 Reflect.getOwnPropertyDescriptor()
方法的第一个参数是原始值时,抛出错误。例子如下:
let descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
console.log(descriptor1); // undefined
// 抛出错误
let descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");
因为将 2 强制转换成对象后,没有名为 name 的属性,所以输出 descriptor1 的结果为 undefined。
ownKeys
Trap
ownKeys
trap 拦截内部的 [[OwnPropertyKeys]]
方法,可以重写返回由属性名构成的数组这一行为。有 4 个方法会用到这个数组:Object.keys()
、 Object.getOwnPropertyNames()
、 Object.getOwnPropertySymbols()
和 Object.assign()
( Object.assign()
使用这个数组来确定复制哪些属性)方法。
Reflect.ownKeys()
方法实现了 ownKeys
trap 的默认行为,返回所有自有属性的键名构成的数组,包括 string 和 symbol 类型的键。 Object.getOwnProperyNames()
和 Object.keys()
方法返回自有属性中仅由 string 类型的键名构成的数组,Object.getOwnPropertySymbols()
方法返回自有属性中仅由 symbol 类型的键名构成的数组。 Object.assign()
方法返回所有自有属性的键名构成的数组,包括 string 和 symbol 类型的键。
ownKeys
trap 接收一个参数,这个参数是目标对象,必须返回一个数组或类数组对象,否则抛出错误。例如,使用 ownKeys
trap 可以在使用 Object.keys()
、 Object.getOwnPropertyNames()
、 Object.getOwnPropertySymbols()
和 Object.assign()
方法时过滤掉不想要的指定属性的键。假设不想包含以下划线开头的属性,通常这些属性是私有属性,可以使用 ownKeys
trap 过滤这些属性。如下所示:
let proxy = new Proxy({}, {
ownKeys(trapTarget) {
return Reflect.ownKeys(trapTarget).filter(key => {
return typeof key !== "string" || key[0] !== "_";
});
}
});
let nameSymbol = Symbol("name");
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
let names = Object.getOwnPropertyNames(proxy),
keys = Object.keys(proxy),
symbols = Object.getOwnPropertySymbols(proxy);
console.log(names.length); // 1
console.log(names[0]); // "name"
console.log(keys.length); // 1
console.log(keys[0]); // "name"
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"
ownKeys
trap 也会影响到 for-in
循环语句。这个 trap 决定哪些键会在循环内部出现。
函数代理中的 apply
and construct
Trap
在所有的 trap 中,只有 apply
和 construct
trap 需要代理的目标对象是函数。函数有两个内部方法 [[Call]] 和 [[Construct]]。当不使用 new 操作符调用函数时,执行 [[Call]] 方法;使用 new 操作符调用函数时,执行 [[Construct]] 方法。apply
和 construct
trap 分别对应这两种调用方式,可以重写对应的内部方法。当没有使用 new 操作符调用函数时,apply
trap 和 Reflect.apply()
接收的参数如下:
trapTarget
执行的函数(代理的目标对象)。thisArg
在调用过程中,函数内部的this
表示的值。argumentsList
传递给函数的参数构成的数组。
使用 new 操作符调用函数时,construct
trap被调用,接收的参数如下:
trapTarget
执行的函数(代理的目标对象)。argumentsList
传递给函数的参数构成的数组。
Reflect.construct()
方法的前 2 个参数与之相同,有可选的第三个参数 newTarget
,表示函数中 new.target
的值。
apply
和 construct
trap 完全控制了代理的目标函数的行为。模仿函数的默认行为,可以这样做:
let target = function() { return 42 },
proxy = new Proxy(target, {
apply: function(trapTarget, thisArg, argumentList) {
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList);
}
});
// 目标对象是函数的代理对象看起来是一个函数
console.log(typeof proxy); // "function"
console.log(proxy()); // 42
var instance = new proxy();
console.log(instance instanceof proxy); // true
console.log(instance instanceof target); // true
instanceof 使用原型链判断类型,原型链查找不受代理的影响。
apply
和 construct
trap 可以改变函数执行的方式。假设想要验证所有参数是否为指定类型,可以在 apply
trap 中检查:
// 相加所有参数
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0);
}
let sumProxy = new Proxy(sum, {
apply: function(trapTarget, thisArg, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
throw new TypeError("This function can't be called with new.");
}
});
console.log(sumProxy(1, 2, 3, 4)); // 10
// 抛出错误
console.log(sumProxy(1, "2", 3, 4));
// 也会抛出错误
let result = new sumProxy();
也可以让一个函数必须使用 new 操作符调用,验证它的参数必须是数值:
function Numbers(...values) {
this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
apply: function(trapTarget, thisArg, argumentList) {
throw new TypeError("This function must be called with new.");
},
construct: function(trapTarget, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.construct(trapTarget, argumentList);
}
});
let instance = new NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// 抛出错误
NumbersProxy(1, 2, 3, 4);
不使用代理,使用 new.target
也可以完成这个操作。
new.target
表示使用 new 操作符调用的函数,通过检查这个值可以判断函数是否使用了 new 操作符调用:
function Numbers(...values) {
if (typeof new.target === "undefined") {
throw new TypeError("This function must be called with new.");
}
this.values = values;
}
let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// 抛出错误
Numbers(1, 2, 3, 4);
假设 Numbers()
函数定义在无法修改的代码中,已知代码中使用了 new.target
,如果想要在调用函数时不执行 new.target
相关的语句,可以使用 apply
trap:
function Numbers(...values) {
if (typeof new.target === "undefined") {
throw new TypeError("This function must be called with new.");
}
this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
apply: function(trapTarget, thisArg, argumentsList) {
return Reflect.construct(trapTarget, argumentsList);
}
});
let instance = NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
NumbersProxy()
函数允许调用 Numbers()
函数时不使用 new 操作符。此时的行为相当于使用了 new 操作符调用。因为 apply
trap 中调用了 Reflect.construct()
方法,这个方法是函数使用 new 操作符调用时的默认行为。此时 Numbers()
中的 new.target
等于函数自身。
可以向 Reflect.construct()
方法传入第 3 个参数,表示 new.target
的值。当在函数中需要检查 new.target
的值时可以使用这种方式。在抽象基类构造器中,new.target
的值必须不等于类构造器自身,如下所示:
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
}
}
class Numbers extends AbstractNumbers {}
let instance = new Numbers(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// 抛出错误
new AbstractNumbers(1, 2, 3, 4);
调用 new AbstractNumbers()
时,new.target
等于 AbstractNumbers
,调用 new Numbers()
时,new.target
等于 Numbers
。使用代理手动赋值给 new.target
可以绕过这个限制:
class AbstractNumbers {
constructor(...values) {
if (new.target === AbstractNumbers) {
throw new TypeError("This function must be inherited from.");
}
this.values = values;
}
}
let AbstractNumbersProxy = new Proxy(AbstractNumbers, {
construct: function(trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList, function() {});
}
});
let instance = new AbstractNumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
上述代码中,new.target
的值是一个空函数。
类构造器必须使用 new 操作符调用,因为类构造器的内部方法 [[Call]] 执行时会抛出错误。代理可以拦截对 [[Call]] 方法的调用,使用代理可以创建一个可回调的类构造器。例如,如果不想使用 new 操作符调用类构造器,可以使用 apply
trap 创建新实例。例子如下:
class Person {
constructor(name) {
this.name = name;
}
}
let PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList);
}
});
let me = PersonProxy("Nicholas");
console.log(me.name); // "Nicholas"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
上述代码中,不使用 new 操作符调用 PersonProxy()
时,会返回一个目标对象的新实例。此时如果不使用 new 操作符调用 Person()
,仍然会抛出错误。
可撤销代理
一般情况下,代理创建之后不能和它的目标对象解绑。当代理不再需要的时候,需要撤销。比如,有一个对象遍及整个 API,为了安全性和可维护性,使用某些功能时不允许访问这个对象。此时可以撤销代理。
使用 Proxy.revocable()
方法可以创建一个可撤销代理,它接收和 Proxy()
构造器相同的参数:一个目标对象和一个处理器。作为返回值的对象有如下属性:
proxy
可撤销的代理对象。revoke
调用这个函数可以撤销代理。
调用 revoke()
函数后,不能通过 proxy
执行任何操作。此时使用 proxy
时触发的任何 trap 都会抛出错误。例如:
let target = {
name: "target"
};
let { proxy, revoke } = Proxy.revocable(target, {});
console.log(proxy.name); // "target"
revoke();
// 抛出错误
console.log(proxy.name);
解决数组问题
使用代理和反射 API 可以创建一个对象,使得它的行为和内建的 Array
对象添加和移除属性时的行为相同。代理可以模仿的行为如下:
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
本例中有两个特别重要的行为需要注意:
- 给
colors[3]
赋值时,length
属性递增到 4。 - 设置
length
属性为 2 时,数组的最后两项被删除。
整数作为属性的键是数组特有的。ECMAScript 6 标准给定了判断属性的键是否为数组索引的方式:
一个字符串属性名
P
是数组索引当且仅当ToString(ToUint32(T))
等于P
且ToUint32(P)
不等于 \(2^{32} - 1\)。
这个操作的实现方式如下:
// 将值转换为无符号 32 位整数
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
前述的两个数组行为依赖于属性赋值,意味着仅需要使用 set
trap。下述例子实现了使用的数组索引大于 length - 1
时,递增 length
属性:
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
function createMyArray(length=0) {
return new Proxy({ length }, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
// 设置 length
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
}
// 设置元素
return Reflect.set(trapTarget, key, value);
}
});
}
let colors = createMyArray(3);
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
当设置 length
属性的值小于先前的值时,移除数组中的元素。这不仅涉及到改变 length
属性的值,也要删除数组中先前存在的元素。例如,如果数组的 length
值为 4,然后将值修改为 2,则索引 2 和 3 所在的数组元素需要删除。这个操作可以在 set
trap 中完成。将先前的例子修改后如下所示:
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
function createMyArray(length=0) {
return new Proxy({ length }, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
// 判断使用的索引和 length 之间的关系
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
// 设置 length 时判断其值
} else if (key === "length") {
if (value < currentLength) {
for (let index = currentLength - 1; index >= value; index--) {
Reflect.deleteProperty(trapTarget, index);
}
}
}
// 修改元素
return Reflect.set(trapTarget, key, value);
}
});
}
let colors = createMyArray(3);
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"
实现了上述两个行为,可以很容易地创建一个模仿内建数组行为的对象。将这些行为封装成一个类更好。
创建一个使用代理的类的最简单的方式是定义这个类,从构造器中返回代理。使用这种方式,当实例化类时,返回的对象是代理而不是这个类的实例(实例是构造器中 this
的值)。此时实例是代理的目标对象,返回的是代理。实例完全私有,不能直接访问它,可以通过代理间接访问实例。
从类的构造器中返回类的代理的例子如下:
class Thing {
constructor() {
return new Proxy(this, {});
}
}
let myThing = new Thing();
console.log(myThing instanceof Thing); // true
创建一个数组类相对直接。此时,在类构造器中使用代理代码。完整的例子如下:
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
class MyArray {
constructor(length=0) {
this.length = length;
return new Proxy(this, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
// 判断属性键是否是索引
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
} else if (key === "length") {
if (value < currentLength) {
for (let index = currentLength - 1; index >= value; index--) {
Reflect.deleteProperty(trapTarget, index);
}
}
}
// 设置元素值
return Reflect.set(trapTarget, key, value);
}
});
}
}
let colors = new MyArray(3);
console.log(colors instanceof MyArray); // true
console.log(colors.length); // 3
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"
虽然从类的构造器中返回代理很容易,但这也意味着对于每个实例都会创建一个新的代理对象。也可以让所有实例共享同一个代理:将代理作为原型。
使用代理作为原型
将代理作为原型时,仅当默认操作涉及到原型执行时,trap 才会被调用。这限制了作为原型的代理的能力。考虑如下例子:
let target = {};
let newTarget = Object.create(new Proxy(target, {
// 从不调用
defineProperty(trapTarget, name, descriptor) {
// 调用时造成错误
return false;
}
}));
Object.defineProperty(newTarget, "name", {
value: "newTarget"
});
console.log(newTarget.name); // "newTarget"
console.log(newTarget.hasOwnProperty("name")); // true
创建 newTarget
对象时,将代理作为原型。代理的目标对象 target
实际上是 newTarget
的原型,因为代理是透明的。仅当 newTarget
上的操作传递给 target
执行时,trap 会被调用。
Object.defineProperty()
方法在 newTarget
上创建了自有属性 name
。在一个对象上定义属性不涉及这个对象的原型,所以 defineProperty
trap 不会被调用。
代理作为原型时有严格的限制,少数几个 trap 仍然有用。
调用内部方法 [[Get]] 读取属性时,首先在对象的自有属性中查找指定属性,如果没有,在对象的原型中查找,如果还没有,则沿着原型链查找,直到找到了或所有的原型都查找过了。
创建了 get
trap 后,无论什么时候读取一个给定属性,这个属性在自有属性中不存在时,trap 会在原型上调用。访问的属性不能保证在对象中存在时,可以使用 get
trap 阻止不想发生的行为。如下创建一个对象,无论什么时候访问这个对象中不存在的属性时,抛出错误:
let target = {};
let thing = Object.create(new Proxy(target, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
}));
thing.name = "thing";
console.log(thing.name); // "thing"
// 抛出错误
let unknown = thing.unknown;
重要的是需要理解 trapTarget
和 receiver
是不同的对象。将代理作为原型使用时,trapTarget
是原型对象自身,receiver
是实例对象。在这个例子中,trapTarget
是 target
对象,receiver
是 thing
对象。
内部方法 [[Set]] 也会检查自有属性和原型(如果必要的话)。给一个对象属性赋值时,如果这个属性是对象的自有属性,则修改这个属性的值;如果自有属性中没有这个属性,会在原型中查找该属性。即使会在原型中查找,但默认情况下,仍然会在对象上创建这个属性,然后赋值。创建的属性是对象的自有属性。原型中是否有这个属性不重要。
下面的例子显示了默认行为:
let target = {};
let thing = Object.create(new Proxy(target, {
set(trapTarget, key, value, receiver) {
return Reflect.set(trapTarget, key, value, receiver);
}
}));
console.log(thing.hasOwnProperty("name")); // false
// 调用了 set trap
thing.name = "thing";
console.log(thing.name); // "thing"
console.log(thing.hasOwnProperty("name")); // true
// 没调用 set trap
thing.name = "boo";
console.log(thing.name); // "boo"
给 thing.name
赋值 "thing" 时,set
trap 被调用,因为 thing
没有名为 name
的自有属性。在 set
trap 中,trapTarget
等于 target
,receiver
等于 thing
。最终会在 thing
上创建一个名为 name
的自有属性。Reflect.set()
实现了这个默认行为。
has
trap 拦截了 in 操作符的操作。当给定一个属性名时,in 操作符首先在对象的自有属性中查找,如果自有属性中不存在,则去原型中查找,如果依然不存在,则沿着原型链查找,直到找到了或在所有原型上都找过了。
仅当查找操作在原型上执行时,has
trap 才会被调用。将代理作为原型时,当给定属性名在自有属性中不存在时这个行为才会发生。例如:
let target = {};
let thing = Object.create(new Proxy(target, {
has(trapTarget, key) {
return Reflect.has(trapTarget, key);
}
}));
// 触发 has trap
console.log("name" in thing); // false
thing.name = "thing";
// 不触发 has trap
console.log("name" in thing); // true
类不能直接修改原型使得它的原型是代理,因为它的 prototype
属性不可写入。通过继承创建一个类,使得它的原型是代理。先使用构造器函数创建一个类型,然后重写原型,将原型修改为代理。例子如下:
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
let thing = new NoSuchProperty();
// 抛出错误,get trap 中执行
let result = thing.name;
NoSuchProperty
函数作为基类。函数的 prototype
属性没有限制,所以可以使用代理作为原型。
下一步是创建一个类继承 NoSuchProperty
。如下所示:
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let area1 = shape.length * shape.width;
console.log(area1); // 12
// 抛出错误,因为 wdth 属性不存在
let area2 = shape.length * shape.wdth;
这段代码中,作为原型的代理位于 Square
类的原型链中。代理不是 shape
的原型。如下所示:
function NoSuchProperty() {
// empty
}
// 引用作为原型的代理
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
NoSuchProperty.prototype = proxy;
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let shapeProto = Object.getPrototypeOf(shape);
console.log(shapeProto === proxy); // false
let secondLevelProto = Object.getPrototypeOf(shapeProto);
console.log(secondLevelProto === proxy); // true
shape
的原型是 Square.prototype
,Square.prototype
的原型是代理,继承自 NoSuchProperty
。
只有当查找操作到达原型时,才会调用 get
trap。如果查找在 Square.prototype
上完成时,get
trap 不会调用。如下所示:
function NoSuchProperty() {
// empty
}
NoSuchProperty.prototype = new Proxy({}, {
get(trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
getArea() {
return this.length * this.width;
}
}
let shape = new Square(2, 6);
let area1 = shape.length * shape.width;
console.log(area1); // 12
let area2 = shape.getArea();
console.log(area2); // 12
// 抛出错误,wdth 属性不存在
let area3 = shape.length * shape.wdth;
此时, getArea()
方法自动成为 Square.prototype
的属性。
参考
[1] Zakas, Understanding ECMAScript 6, 2017.