原文链接:https://medium.com/jspoint/what-are-internal-slots-and-internal-methods-in-javascript-f2f0f6b38de
ECMAScript 2015 规范增加了内部槽盒内部函数,用于指定在运行时对象不被暴露的内部属性和函数。
ECMAScript 2015 规范 6.1.7.2 一节讨论了对象(Object的子类)可以包含的一些奇怪的内部属性和内部函数。这些属性和函数由 JavaScript 引擎实现,但是从运行时抽象出来,因此,你不能像普通属性那样访问它们。
在 ECMAScript 规范中,这些属性被标记为[[<name>]],其中,name是内部属性或内部函数的名字。内部属性被称为内部槽 internal slot,通常包含与对象相关联的值,这个值表示该对象的某些状态 state。
下面看一个例子。所有对象都实现了[[GetPrototypeOf]]内部方法,其作用是返回这个对象的原型。当执行Reflect.getPrototypeOf(obj)时,其中,obj是需要检测原型的对象,JavaScript 引擎会调用对象obj的内部函数[[GetPrototypeOf]],该函数返回具有原型的对象的内部槽[[Prototype]]的值。
obj.__proto__同样指向obj的原型,访问这个属性就像访问obj的内部槽[[Prototype]]。
调用内部函数时使用的对象,比如上面的obj,被称为函数调用的“目标”。如果目标不支持该内部函数,比如,针对null调用Reflect.getPrototypeOf,就会抛出TypeError异常。
对象拥有多个内部槽和内部函数。ECMAScript 规范并没有规定这些内部函数如何实现,仅描述了这些函数的签名。下面的表格是 ES2015 规定的所有对象都必须实现的内部函数。这些内部函数可能需要某些特定的 ECMAScript 语言类型的参数,并且可能有返回值。
| 内部函数 | 签名 | 说明 |
[[GetPrototypeOf]] | () -> Object | Null | 返回一个对象,该对象为另外的对象提供继承属性。如果没有继承的属性,返回null。 |
[[SetPrototypeOf]] | (Object | Null) -> Boolean | 将一个对象与提供继承属性的对象关联起来。参数null表示没有继承的属性。如果操作成功,返回true;否则返回false。 |
[[IsExtensible]] | () -> Boolean | 控制是否允许向对象添加额外的属性。 |
[[PreventExtensions]] | () -> Boolean | 控制是否允许向对象添加新的属性。如果操作成功,返回true;否则返回false。 |
[[GetOwnProperty]] | (propertyKey) -> Undefined | PropertyDescriptor | 返回对象所属属性propertyKey的属性描述符;如果没有对应属性,返回undefined。 |
[[HasProperty]] | (propertyKey) -> Boolean | 返回一个布尔值,指明对象是否所有或继承一个键为propertyKey的属性。 |
[[Get]] | (propertyKey, Receiver) -> any | 返回对象键为propertyKey的属性值。如果需要执行 ECMAScript 代码才能获取该值,在代码执行中,Receiver会作为this的角色。 |
[[Set]] | (propertyKey, value, Receiver) -> Boolean | 设置对象propertyKey的属性值为value。如果需要执行 ECMAScript 代码才能设置该值,在代码执行中,Receiver会作为this的角色。如果操作成功,返回true;否则返回false。 |
[[Delete]] | (propertyKey) -> Boolean | 删除对象键为propertyKey的属性。如果属性没有删除,依然存在,返回false;如果属性被删除了,或者没有该属性,返回true。 |
[[DefineOwnProperty]] | (propertyKey, PropertyDescriptor) -> Boolean | 创建或修改键为propertyKey的所属属性为PropertyDescriptor所描述的状态。如果属性成功创建或修改,返回true;否则返回false。 |
[[Enumerate]] | () -> Object | 返回一个遍历器对象,用于以字符串形式枚举对象的可枚举属性的键。 |
[[OwnPropertyKeys]] | () -> List of propertyKey | 返回一个列表,包含对象所有所属属性的键。 |
| 内部函数 | 签名 | 说明 |
[[Call]] | (any, a List of any) -> any | 通过关联的对象执行代码。通过函数调用表达式调用。参数是this的值,以及由调用表达式传入的参数列表。实现了该内部函数的对象被称为可调用的 callable。 |
[[Construct]] | (a List of any, Object) -> Object | 创建一个对象。通过new或super调用。第一个参数是传给该函数的参数列表。第二个参数是由new创建的对象。实现了该内部函数的对象被称为构造函数 constructor。一个函数对象并不一定是构造函数,非构造函数的函数对象不需要有[[Construct]]内部函数。 |
以上是 ES2015 Function对象的内部函数。下面是属性描述符对象的内部槽,用于表示属性描述符的状态。这篇文章详细介绍了什么是属性描述符。
| 属性名 | 默认值 |
[[Value]] | undefined |
[[Get]] | undefined |
[[Set]] | undefined |
[[Writable]] | false |
[[Enumerable]] | false |
[[Configurable]] | false |
内部槽和内部函数是 ECMAScript 规范的辅助工具,它们在规范内部被使用,以描述恰当的行为。例如,当[[Property]]一词出现时,我们就知道,规范实在讨论一个包含对象原型的内部属性。
内部槽和内部函数有助于实现对象的一致行为,实现者(JavaScript引擎)可以通过查看规范以便正确提供这些实现。但是,一种实现并不是必须严格满足规范制定的签名。这里讨论了这一问题。
ES6 规范提供了Reflect对象,用来实现对象的内省和修改。例如,Reflect.has(target, key)函数用来检查target对象或其原型是否有一个key属性,这一行为与in运算符类似,只是以函数的形式提供。
当在target对象调用该函数时,JavaScript 引擎会执行target对象的内部函数[[HasProperty]]。这个函数是由 JavaScript 引擎实现的。不同的 JavaScript 引擎可能会有不同的实现,但是都需要遵守这个规范。
ES6 还增加了Proxy构造函数,用于创建target对象的代理。let proxy = new Proxy(target, handler);语句中,proxy对象基于handler对象,提供了基于target对象的代理接口。handler对象提供了一些函数,用于拦截proxy的操作,将其代理到target上面。
因此,handler函数被称为陷阱,因为它们拦截了target的操作。这些代理函数类似于Reflect的同名函数。例如,在proxy对象上使用in运算符时,handler.has实际被执行。
如果handler对象没有对应的函数,例如这里的has函数,JavaScript 引擎就会自动调用关联到has的内部函数(类似于Reflect.has),也就是target的[[HasProperty]]内部函数。
最后需要强调的是,JavaScript 引擎并不会将内部槽和内部函数暴露出来,它们被限定于在 ECMAScript 规范内部使用。但是,我们应该知道有这些东西,因为它们会不时出现在 JavaScript 文档中。
如果你想了解更深入一些,V8 团队编写了一系列文章,简要地介绍了如何阅读 ECMAScript 规范(当然也包含了内部槽和内部函数这部分内容)。强烈建议 JavaScript 开发者阅读一下这篇文章。