JavaScript 原型机制
本文首发于个人博客:clloz.com
前言
原型链的概念相信大家都知道,ES6 出来以后可能关注度没有以前那么高的。虽然在 ES2015/ES6 中引入了 class 关键字,但那只是语法糖,JavaScript 仍然是基于原型的,作为 JavaScript 中的主要继承方式,我们有必要深入理解它。理解了原型之后,你对对象的理解也会更深入。
原型机制
原型机制说起来很简单,就是一个对象可以访问它原型对象上的属性和方法,从而实现属性和方法的复用。而原型对象又有自己的原型对象,这样原型就构成了一个链式结构,也就是我们说的原型链。一个对象可以访问自己原型链上的所有方法和属性。
JavaScript 中的继承只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 __proto__,引擎内部是 [[prototype]] )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
在 JavaScript 中,我们知道的数据类型有 Number, String, Undefined, Null, Boolean, BigInt, Symbol 七个基础类型,还有就是一个引用类型 Object。在内置对象比如 Function, Array, Date, RegExp 等中,Function 是一个特殊的内置对象。
我们将 JavaScript 中的对象分成两大类,一类是 Object ,一类就是 Function。我们来说一下他们之间的关系。
------------
我们创建对象有很多种方法,Object.create(),new Object(),new function(),和对象字面量等。但其实他们的本质都是 new Object() (关于 new 和对象创建的内容参考另外两篇文章:JavaScript对象属性类型和赋值细节 和 [JavaScript中new操作符的解析和实现](https://www.clloz.com/programming/front-end/js/2020/06/29/new-operator/ "JavaScript中new操作符的解析和实现"))。
Object.prototype._proto_
我们用 new Object() 创建一个空对象,它在 Chrome 中打印出的结果如下。

我们可以看到所谓的 空对象,并不是完全空的,它内部有一个 __proto__ 属性。但其实这个属性并不是它自身的,这个属性是 Object.prototype.__proto__,一个访问器属性(一个 getter 函数和一个 setter 函数), 暴露了通过它访问的对象的内部 [[Prototype]] (一个对象或 null)。
这个属性是由浏览器厂商提供的,并且目前绝大多数的浏览器都支持这个属性,所以 ECMAScript 2015 中也将其写入标准附录中,保持浏览器的兼容性。但是直接修改对象的 [[prototype]] 在任何引擎和浏览器中都是非常慢并且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的,并且性能消耗的时间也不是简单的花费在 obj.proto = ... 语句上, 它还会影响到所有继承来自该 [[Prototype]] 的对象。标准中还提供了两组关于读写原型对象的方法 Object.getPrototypeOf/Reflect.getPrototypeOf 和 Object.setPrototypeOf/Reflect.setPrototypeOf。不过写对象和上面说的一样,依然是一个影响性能的操作,如果你关心性能,不应该用这些方法。比较好的实践是用 Object.create() 来设置原型,用 Object.getPrototypeOf() 来读取原型对象。
>我们同样可以用对象字面量来设置 __proto__,也可以自定义 __proto__ 来覆盖 Object.prototype.proto。参考文章:JavaScript对象属性类型和赋值细节。
不同类型的对象其 [[prototype]] 是不同的,对于使用数组字面量创建的对象,这个值是 Array.prototype。对于 functions,这个值是 Function.prototype。对于使用 new fun 创建的对象,其中 fun 是由 js 提供的内建构造器函数之一(Array, Boolean, Date, Number, Object, String 等等),这个值总是 fun.prototype。对于用 js 定义的其他 js 构造器函数创建的对象,这个值就是该构造器函数的 prototype 属性。关于内置对象之间的关系,我们后面会详细讨论。
Object 和 Function
Object 和 Function 是 JavaScript 中最重要的两个对象,他们同时也是构造函数 function Object(), function Function()。几乎所有对象都是 function Object() 的实例,而所有函数都是 function Function() 的实例,包括 Object 也是由 Function 构造的。
我们上面说过对象内部有一个 [[prototype]] 属性指向它的源性对象;而每一个函数都有一个 prototype 属性,指向由这个函数构造出的对象的 [[prototype]]。更准确的说,在函数被创建的时候,就有一个 prototype 属性指向一个对象,这个对象本身只有一个 constructor 属性指向这个函数。当用 new func() 创建对象的时候,新对象的 [[prototype]] 就指向构造函数的 prototype 对应的对象。不过需要注意的是 prototype 和 constructor 都是可以 重写 的。

对于我们的自定义对象,这是很好理解的。那么内置对象之间的关系,特别是 Object 和 Function 之间的关系是怎么样的呢。先明确两点:
一切函数都是由
function Function()构造的,所以函数的[[prototype]]指向Function.prototype。所有由
function Object()构造的非函数对象的[[prototype]]指向Object.prototype。函数的创建的同时,会创建一个
function.prototype对象,该对象是一个由function Object()构造的对象。prototype属性可以任意指定,指定的对象内可能没有constructor属性或者是错误的constructor。所有非函数对象都是由构造函数通过
new运算符创建的(本质都是new Object(),很多内置对象可以省略new,比如Function,Object,Array,省略和不省略效果是一样的)。这个构造函数要么是自定义的(由function Function()构造),要么是function Object()。自定义函数手动指定
prototype为其它自定函数的实例, 可以让我们实现链式继承,这条链最终有一个节点会是有function Object()构造的对象,它的[[protottype]]指向Object.prototype。所以我们可以说,所有的非函数对象都是function Object()的实例。
其实记住这几点就可以应对绝大部分问题,如果你还对内置对象的关系有兴趣,可以继续往下看。
------------
根据我们上面的两条规律我们可以知道 Object 的 [[prototype]] 指向 Function.prototype,那么 function Function() ,Function.prototype 和 Object.prototype 的 [[prototype]] 都分别是什么呢?
先说 Object.prototype,它是所有非函数对象的 [[prototype]] 指向,而它自己的 [[prototype]] 指向的就是 null,也就是一切对象的原型链的终点。它的 constructor 属性指向 function Object()
而 function Function() 的 [[prototype]] 和其它的函数一样,指向 Function.prototype,也就是说 function Function() 的 prototype 和 [[prototype]]指向的是同一个对象 Function.prototype。
Function.prototype 的 [[prototype]] 指向的是 Object.prototype。consctructor 指向的是 function Function()。其实 Function.prototype 本身就是函数,可以直接调用,接受任何参数并返回 undefined。
为什么要这样呢?我认为是确保每一个函数对象,非函数对象,他们的原型链上都有 Object.prototype,都能够访问 Object.prototype 上定义的一些公有方法。
想要更清晰的看清楚我上面说的关系,可以借助于这张来自网上的图,画的非常好。

其他内置对象
最后在说一说其他的内置对象,绝大多数内置对象都是函数对象(BigInt,Math,JSON 和 Reflect 不是函数对象),虽然有些不能用 new 操作符(比如 Symbol,有些对象用不用 new 表现一样,比如 Object, Function,Array 等)。所以内置对象的 [[prototype]] 指向 function.prototype。内置对象的 prototype 一般来说就是一个普通的对象(用 function Object() 构造的)。这个对象上挂载了很多该类型可以使用的方法,比如 Array.prototype 有如下属性:
需要注意的一点是,几乎所有内置对象的属性都是不可枚举的,所以无论是 for ... in 还是 Object.keys() 都是无法枚举这些属性的。我们自己也可以在内置对象的 prototype 上添加属性或者方法,让所有该类型的对象都能使用。
基本包装类型 String,Number 和 Boolean 在一般情况下不要使用创建对象的方式来初始化对应的类型。使用这种方式创建的值都是对象(使用 typeof 返回 object),而且所有基本包装类型的对象都会被转换为布尔值 true。看下面的代码。
关于 constructor 和 prototype 有一个有趣的小题目,可以看一看:关于constructor和prototype的思考
参考文章
版权声明: 本文为 InfoQ 作者【Clloz】的原创文章。
原文链接:【http://xie.infoq.cn/article/2054036cbd23fbd12e5a6a372】。文章转载请联系作者。











评论