本文为《人人都能读标准》—— ECMAScript篇的第13篇。我在这个仓库中系统地介绍了标准的阅读规则以及使用方式,并深入剖析了标准对JavaScript核心原理的描述。
JavaScript是一门面向对象编程语言,绝大多数的操作都是基于对象完成的。
本节,我会先讲ECMAScript对象的内部模型,这个模型可以帮助我们理解对象的内部行为。然后,我们会使用这个模型来实现类型判断。最后,我会讲基于这个模型,标准是如何对对象进行分类的。
对象的内部模型
在ECMAScript中,每个对象都有自己的内部方法(internal methods) 以及内部插槽(internal slots) 。内部方法表示对象在运行时上的行为,内部插槽则表示对象的状态,你也可以把它们理解为对象内更为底层的方法和属性。“内部”二字表示内部插槽和内部方法都可以使用“规范类型”,且都不能被ECMAScript程序直接访问。在标准中,所有的内部方法和内部插槽都使用[[]]
表示。
内部方法
所有的对象都必须有以下的内部方法,这些方法称为基础内部方法(Essential Internal Methods) :
基础内部方法 | 描述 |
---|---|
[[GetPrototypeOf]] | 获取对象的原型 |
[[SetPrototypeOf]] | 设置对象的原型 |
[[IsExtensible]] | 判断对象是否可以增加新的属性 |
[[PreventExtensions]] | 控制对象是否允许增加新的属性 |
[[GetOwnProperty]] | 返回某个自身属性的属性描述符(Property Descriptor) |
[[DefineOwnProperty]] | 用一个属性描述符来创建或者修改一个自身属性 |
[[HasProperty]] | 判断对象是否有某个属性 |
[[Get]] | 获取对象的属性 |
[[Set]] | 设置对象属性 |
[[Delete]] | 删除对象属性 |
[[OwnPropertyKeys]] | 获取对象所有的自身属性 |
从以上列表可以看出,这些都是一些非常基础的操作,用于完成对象属性的增、删、改、查。许多暴露在Object构造器以及Object.prototype上的方法,都是对这些内部方法的封装。比如,用以获得对象原型的静态方法Object.getPrototypeOf(O):
除了以上的内部方法,对象可能还有两个特殊的内部方法:
[[Call]]
方法:实现了这个内部方法的对象是函数对象,这个方法会由函数调用表达式触发,并执行一段绑定在对象上的逻辑。关于[[Call]]
方法,我们会在14.函数中进行深入研究。[[Construct]]
方法:实现了这个内部方法的对象是构造器对象,这个方法由new表达式或super方法触发,会创建一个新的对象。关于[[Construct]]
方法,我们会在15.类中进行深入研究。
内部插槽
最重要的内部插槽是[[Prototype]]
,该插槽指向另一个对象或者null。当对象A的[[Prototype]]
指向对象B时,B即为A的原型对象。此时,对A来说,A自身的属性称为自有属性(own properties) ,而B的属性都为A的继承属性(inherit properties) 。当然,B也有自己的[[Prototype]]
。于是,所有通过[[Prototype]]
连接起来的对象就构成了A的原型链。
除了[[Prototype]]
,常见的内部插槽还有:
[[Extensible]]
:用来表示对象是否可扩展。当这个插槽值为false时,对象不能添加属性、不能修改[[prototype]]
插槽、不能修改[[Extensible]]
插槽为true。这个插槽可以使用Object.isExtensible(Obj)与Object.preventExtensions(Obj)间接访问与修改。[[privateElements]]
:用于存储对象上的私有属性方法。
应用:类型判断
基于对象内部方法以及内部插槽,我们可以完成一些类型判断的工作。主要有两种思路:
- 基于对象特有的内部插槽或内部方法,判断对象的类型。
- 基于
[[Prototype]]
内部插槽,判断对象的类型。
我在12.原始类型中提到过,在原始类型上调用方法,会使得原始类型通过抽象操作ToObject转化为特定的对象,而原始类型对应的每一种对象,都有自己特有的内部插槽。比如布尔对象有一个特有的[[BooleanData]]
内部插槽,Number对象有一个特有的[[NumberData]]
内部插槽。。。因而,我们可以根据这些特有的内部插槽识别出数据类型。
当然,我们无法直接访问内部插槽,但是有的语言API会帮助我们间接完成访问,比如