JavaScript原型链(从构造函数、原型、对象实例的关系说起)

一开始看MDN的JavaScript指南,没看明白。主要原因是关于构造函数(constructor)、原型(prototype)、对象实例(object, instance)之间关系的描述太少;直接就给我整个原型链让我挺懵逼的。
于是靠百度来搞懂。我觉得先从这三者关系入手,然后回头理解原型链更容易。
相关资料:
(侧重关系)构造函数、对象实例、原型对象三者之间的关系
(侧重原型链)JS重点整理之JS原型链彻底搞清楚
一、关系:一个层级中构造函数、原型、对象实例的关系
我认为在原型链、层级体系中,最不重要的反而是对象实例。但是,它能起到入口的作用,帮助我们访问、探寻原型、构造函数。同时,要讨论三者关系,就只讨论一个层级内的三者,先忘掉继承、其他层级的事情。
1、预备:构建原型链、层级的“柴犬代码”
下面先用代码构建Shiba->Dog->Animal这条原型链,共三个层级:(我们称这段代码为“柴犬代码”,可以先跳过柴犬代码,看下面构造函数、原型、对象实例三者相互访问的代码)

function Animal() { this.type = 'animal'; this.name = 'default' this.age = 0; } Animal.prototype.sayHi = function() { console.log('Hi, I am a(n) ' + this.type + '. My name is ' + this.name + '.'); }function Dog() { this.type = 'dog'; } Dog.prototype = new Animal(); function Shiba(name='default', age=0) { this.type = 'shiba'; this.name = name; this.age = age; } Shiba.prototype = new Dog();

2、从对象实例出发访问原型、构造函数
我们声明一个Shiba的对象实例gougou,然后通过gougou访问Shiba这一层级的原型、构造函数:
var gougou = new Shiba('gougou', 2); console.log(gougou.__proto__); // 原型 console.log(gougou.__proto__.constructor); // 构造函数 console.log(gougou.__proto__.constructor === Shiba); // true

这里要说明的问题是:
(1) 对象实例可以通过__proto__访问原型。(不要问为什么)
(2) 原型可以通过constructor访问构造函数。(因为最后一行输出为true)
2、从构造函数出发访问原型
实际上,构造函数和原型之间可以双向访问:
console.log(Shiba.prototype); // 原型 console.log(Shiba.prototype === gougou.__proto__); // true

这里说明的问题是:
构造函数可以通过prototype访问原型。(因为第二行输出为true)
3、小结三者的访问方式
小结:
(1) 对象实例:自己声明的变量名。
(2) 原型:使用对象实例的__proto__属性访问;如果知道构造函数名称(类名),可以用构造函数的prototype属性访问。
(3) 构造函数:如果是自定义的构造函数,自己就知道构造函数名称;但很多时候是JavaScript内置的构造函数,可以通过原型的constructor属性访问。
【JavaScript原型链(从构造函数、原型、对象实例的关系说起)】一般情况下,要想访问当前层次的构造函数、原型,是以对象实例作为入口,先访问原型,再从原型访问构造函数。
4、构造函数、原型、对象实例的关系、分工
我认为,构造函数、原型、对象实例三者的访问方式已经能说明他们的关系了。那么,为什么要有这样的结构分工呢?好处是什么?
从作用上来说:
(1) 构造函数居核心、支配地位,持有原型,并指导对象实例的生成(new)。
(2) 原型一方面帮助构造函数存储方法,供构造函数生成的对象调用,避免对象实例各自存储一份方法(避免浪费空间);另一方面直接形成原型链,构建了层级结构、继承关系。
(3) 对象实例应该作为原型链、层级体系的附属品。对象实例的属性是构造函数指导生成的;对象实例的方法,是构造函数持有的原型提供的。构造函数可以生产无数个对象实例。
二、原型链:将原型串成链、搭建层级
1、原型链继承:“显式串联”层级
我们回头看那段柴犬代码。代码中已经构建好Shiba->Dog->Animal这条原型链,注意到“串起来”的两行代码:
Dog.prototype = new Animal(); Shiba.prototype = new Dog();

这里要说明的问题是:
将Dog层级中的原型,指定为Animal层级的一个对象实例,就形成了Dog->Animal这段原型链,Dog继承了Animal的属性、方法。(Dog对Animal说:拿来吧你)
Shiba和Dog的关系也一样。因此,Shiba->Dog->Animal这条原型链构建好了。
2、继续上溯:堆__proto__就完事
既然当前层级的原型是上一层级的对象实例,那么我们可以顺藤摸瓜,沿着原型链上溯各个层次直到null。null没有原型,它是原型链的最顶端或者终点。
在柴犬代码中,以对象实例gougou为入口,访问它的原型gougou.__proto__,这是在Shiba层级;同时,原型又是Dog层级的对象实例,继续通过__proto__属性访问……如此逐层往上即可。代码如下:
console.log('Shiba层级的原型:\n', gougou.__proto__); console.log('Shiba层级的构造函数:\n', gougou.__proto__.constructor); console.log('Dog层级的原型:\n', gougou.__proto__.__proto__); console.log('Dog层级的构造函数:\n', gougou.__proto__.__proto__.constructor); console.log('Animal层级的原型:\n', gougou.__proto__.__proto__.__proto__); console.log('Animal层级的构造函数:\n', gougou.__proto__.__proto__.__proto__.constructor);

这里已经有点数不清有多少个__proto__了。反正这里说明的问题是:
(1) 确实可以沿着原型链往上访问各个层级的原型,以及构造函数。
(2) 当前层级的原型,就是更高层级的对象实例(柴犬代码中用type属性做了标记,可以分辨是Animal对象实例还是Dog对象实例)。
3、完整的原型链:上溯至null
注意到Animal层级打印的原型,直接就是一个对象字面量了。实际上,这就是Objct层级的一个对象实例。完整的原型链是:Shiba->Dog->Animal->Object->null。
不想再从gougou出发写那么多__proto__了,还有另一种方式访问Animal层级的原型:构造函数Animal的prototype属性。上溯代码如下,这次我们验证为主:
console.log(Animal.prototype.__proto__ === Object.prototype); // true console.log(Object.prototype.__proto__ === null); // true

这里说明的问题是:
通过Animal.prototype.__proto__获取到了上一层级的原型,刚好等于Object层级的原型,也就是说Animal的上一层级就是Object。但是Object层级再往上就没有所谓层级了,只剩一个null了。“null层级”只有原型null,没有构造函数。
完成。
最后,分享下关于JavaScript原型链学习的个人感受:
(1) 摆脱传统继承中子类继承父类的惯性思维。JavaScript中的构造函数看着像是“类名”,实际上和继承没啥关系,原型链继承靠的是原型。
(2) 摆脱“原型”字眼隐含的继承含义。这可能是我自己想当然了,看到到“对象的原型”这种字眼就想当然认为原型是对象的“父类”。实际上,原型和对象应该放到同一层级内讨论;不同层级我们用原型说话。

    推荐阅读