一般印象中的原型
原型真是个奇妙的东西。我现在已经工作了6个念头,依然可以学到一些东西。我不知道面试过多少人对原型一只半解,却觉得在面试中被问到原型是一件非常『丢份』的事情。有些人甚至没有办法说清楚原型链到底是怎么样的东西。
扯远了,我们来看看原型。
我们每创建一个函数,js引擎都会帮这个函数创造一个属于它自己的原型对象。
function Animal() {}
Animal.prototype // { constructor: f Animal(), [[Prototype]]: Object}
Animal.__proto__ == Function.prototype // true
在原型没有被修改过的前提下,我们可以认为,对象的__proto__
指向哪里,该对象就是__proto__.constructor
的实例。
这句话听起来可能比较绕口,我们来个例子;
function Animal() {}
let dog = new Animal
dog.__proto__.constructor == Animal // true
Animal.__proto__.constructor == Function // true
现在看看,dog确实是 Animal的实例,这很好理解;而 Animal 是函数的实例,所以 它的 __proto__
指向 Function.prototype
。我们常用的 apply
等方法就来自这里。
Function 的原型链
到目前为止都很简单,现在问题来了:
Function.__proto__.constructor == Function // true
这可就有点意思了:难不成 Function 是自己的实例?
如果你看过哪个著名的原型图,就应该可以发现 Function 的 __proto__
指向极其不自然。一般来说,对象的 __proto__
会指向原型,然后原型和其对应的函数有 prototype
和 constructor
相互关联,差不多是这样的结构:
foo ----__proto__----> Foo.prototype ----constructor----> Foo
然而,对于函数,这一原则被打破了。
Function ----__proto__----> Function.prototype ----constructor----> Function
这个函数本来应该是另外的某个东西(通常对应构造函数)的实例,但是在这里却把这个任务直接丢给了 Function。这其实也很好理解,Function 本来就是定义函数用的,我们总不能用另一个函数来定义函数吧?
换句话说,Function是JS继承的一个特例!Function.prototype是一个特殊的对象。如果你把它打印出来,会发现它其实是一个函数。
我们知道函数都是通过
new Function()
生成的,难道Function.prototype
也是通过new Function()
产生的吗?答案也是否定的,这个函数也是引擎自己创建的。首先引擎创建了Object.prototype
,然后创建了Function.prototype
,并且通过__proto__
将两者联系了起来。这里也很好的解释了上面的一个问题,为什么let fun = Function.prototype.bind()
没有prototype
属性。因为Function.prototype
是引擎创建出来的对象,引擎认为不需要给这个对象添加prototype
属性。所以我们又可以得出一个结论,不是所有函数都是
new Function()
产生的。有了
Function.prototype
以后才有了function Function()
,然后其他的构造函数都是function Function()
生成的。现在可以来解释
Function.__proto__ === Function.prototype
这个问题了。因为先有的Function.prototype
以后才有的function Function()
,所以也就不存在鸡生蛋蛋生鸡的悖论问题了。对于为什么Function.__proto__
会等于Function.prototype
,个人的理解是:其他所有的构造函数都可以通过原型链找到Function.prototype
,并且function Function()
本质也是一个函数,为了不产生混乱就将function Function()
的__proto__
联系到了Function.prototype
上。
从这里我们可以知道,至少有两个东西是 JS 引擎『凭空』创造出来的,它们就是 Object.prototype.__proto__
(null) 以及 function Function
。然后,Function.prototype.__proto__ -> Object.Prototype
。这不啻为 JS 原型原理的基石。
不可靠的 constructor
现在让我们看一个继承实现:
function Animal() {}
Animal.prototype = {
walk() { console.log('walk') }
}
function Dog() {}
Dog.prototye = new Animal()
现在看看这个继承有什么问题?
如果我们打印 Dog.constructor
,会发现输出是 Object
!
这是怎么回事?难道不应该是 Animal
吗?
这就是 constructor
不可靠的地方了。之前说了,当我们创建函数的时候,js引擎将帮我们创建一个 prototype
。此时这个prototype
里有一个constructor
属性,在这个例子中指向Animal
。
但是,我们在上面的代码中,直接替换了prototype
。替换的对象没有constructor
。于是,根据原型链规则,找到了 Object.prototype.constructor
。这便是原型链的破环。
所以,我们不应该直接替换原型链对象。如果这么做了,应给重新赋予constructor
属性。
另:constructor 是非引用类型时是只读的。这个鸡肋限制实在是很有槽点
更多的缺点和较好的写法
再观察一下上面的继承实现,假设 Animal 需要接受参数,这种方式就无法处理了。如果我们用的是 Class
,则可以通过 super
传递。
优化的写法:
function Animal(name) { this.name = name } Animal.prototype = { walk() { console.log('walk') } } function Dog(name) { Animal.call(this, name) } Dog.prototype = new Animal() Dog.prototype.constructor = Dog
它可以解决上述问题,同时重新指定了 constructor
. 但是父函数,在此例中也就是 Animal
,被调用了两次。
最终的写法:
function Animal(name) { this.name = name } Animal.prototype = { walk() { console.log('walk') } } function Dog(name) { Animal.call(this, name) } Dog.prototype = Object.create(Animal.prototype) Dog.prototype.constructor = Dog
es6 的 Class 会被 babel 编译成什么样?
可以直接在 babel 的官网试试看。target: ie11
class Foo { static a = 'a' static b () {} constructor() { this.foo = 42; } bar () { console.log('bar') } get b () { return this.a } } const foo = new Foo();
编译后:
"use strict"; // 一个检查。显然 class 虽然被编译成了 function ,但是不能直接调用,只能用 `new` 调用 function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } // 这是一个工具函数。用来给对应的对象增加属性。它会遍历 props ,依次声明 // definedProperty 大家应该都不陌生了。这里需要注意 enumerable 默认为 false function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } // 这是真正处理构造函数的地方。可以看到静态属性被定义在了 Function 上,其它属性被定义在了原型上。 function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; } // 另一个定义属性的方法。在定义非函数的静态属性时会用到 function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var Foo = /*#__PURE__*/ (function () { function Foo() { // constructor 就是这个函数的内容 _classCallCheck(this, Foo); this.foo = 42; // console.log('hi') } // 接着,静态方法被写入函数,方法则被写入 prototype _createClass(Foo, [ { key: "bar", value: function bar() { console.log("bar"); } // get b () { return this.a } } ], [ { key: "b", value: function b() {} } ]); return Foo; })(); // 最后,静态属性被写入 Foo _defineProperty(Foo, "a", "a");
这样处理之后,和普通的构造函数有什么不同呢?
- class 所有的方法(包括静态方法和实例方法)都是不可枚举的。
- 对 Foo 的 prototype 赋值是无效的。
- 由于 Foo 被包裹在一个 IIFE 里面,所以虽然会变量提升,但是不会赋值。(准确来说是存在一个临时死区,类似 let)。
- 在函数里对 Foo 进行赋值是无效的。
- class 只能用
new
调用
接下来,我们看看继承。
class Foo { constructor() { this.foo = 42; } bar () { console.log('bar') } } class Sub extends Foo { constructor() { super() super.bar() } } const foo = new Foo(); const sub = new Sub()
编译后:
"use strict"; function _typeof(obj) { // ... 为了正确返回 Symbol 做的 hack,基本等于 typeof } // 真正的继承 function _inherits(subClass, superClass) { // 首先是一个校验 if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } // 然后创建一个父类原型的实例,并进行原型链接 subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); // 一样这是不可重新赋值的 Object.defineProperty(subClass, "prototype", { writable: false }); // 赋予 __proto__. 那之后,Sub.__proto__ === Foo if (superClass) Object.setPrototypeOf(subClass, superClass); } // Derived 这里是 Sub。 function _createSuper(Derived) { // 调用 Super 时,即调用此方法. 调用时指定了 this 为 Sub 实例。 // 我们也可以看到,调用结果也是一个子类实例 return function _createSuperInternal() { // Super: Foo var Super = Object.getPrototypeOf(Derived), result; // this: <Sub>{},NewTarget: Sub Function var NewTarget = Object.getPrototypeOf(this).constructor; result = Super.apply(this, arguments); // 这里省略了校验代码 return result; }; } function _classCallCheck(instance, Constructor) { // ... } function _defineProperties(target, props) { // ... } function _createClass(Constructor, protoProps, staticProps) { // ... } var Foo = /*#__PURE__*/ (function () { // ... })(); var Sub = /*#__PURE__*/ (function (_Foo) { _inherits(Sub, _Foo); // <Sub>{} var _super = _createSuper(Sub); function Sub() { var _thisSuper, _this; _classCallCheck(this, Sub); _this = _super.call(this); // 对应 Super.bar Reflect.get.apply( (_thisSuper = _this), // Foo.prototype _getPrototypeOf(Sub.prototype)), "bar", _thisSuper ).call(_thisSuper); return _possibleConstructorReturn(_this); } return _createClass(Sub); })(Foo); var foo = new Foo(); var sub = new Sub();
这里的代码有点复杂,不过重点只有一个:ES6 Class,子类构造函数使用 __proto__
指向了父类的构造函数。调用Super时,使用原型链找到父类的原型,从中取值。
class Foo { static staticValue = 43 constructor() { this.foo = 42; } bar () { console.log('bar') } } class Sub extends Foo { constructor() { console.log(super().foo) // 42 console.log(super.foo) // undefined console.log(super.staticValue) // undefined console.log(super.bar()) // 'bar' } } const foo = new Foo(); const sub = new Sub() console.log(Sub.__proto__===Foo) // true
关于原生构造函数
这是一个被广为人知的区别。使用 es5
的经典继承方式无法继承内置构造函数,es6
的 class
则可以。然而,生产环境中的 class
大抵是转换成 es5
运行的。所以,这其实依然是代码层面的变化。
然而,真的是这样的吗?究竟是什么让经典继承无法继承内置构造函数?如果我们仔细观察,就会发现是这一句的作用:
Parent.call(this, args)
也就是在子类构造函数中,调用父类构造函数的这一步。
内置类型常常有严格的检查,如果是非原生构造函数对象调用原生构造函数方法会报错。
很多文章宣称这是因为 this
声明顺序的问题,这其实是值得商榷的。比如,我们可以尝试继承 Array
:
当然,一般提出的 this 差别是没有问题的:
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
所以如果我们正在构造我们自己的构造函数,那么我们必须调用 super,否则具有 this 的对象将不被创建,并报错。
从源码中,我们可以看到,调用 super
后,其中被赋值给了 _this
。我们在代码中进行的操作,实际上是对 _this
的操作。同时,class
确实可以解决原生构造函数的继承问题。