抛砖引玉

以下是JavaScript中的一个简单的new操作

// ES5中通过构造函数
function Foo() {
    // ...
}
let f1 = new Foo();
// ES6中通过class
class Foo2() {
    constructor() {
        // ...
    }
}
let f2 = new Foo2();


如图所示,尽管只是一个非常简单且常规的操作,但是他在JS中的执行过程是十分的复杂的,而这也就涉及这五个概念:prototype、[[Prototype]]、\_\_proto\_\_、constructor、原型链,以下将围绕这张图对其进行逐一讲解,建议先看最后面的总结,再来看以下内容,最后再理解一遍总结内容加深理解

图片中函数后面加括号仅用于更清楚的表示其为一个函数,不代表函数执行后的返回值,下文也是如此

prototype

原型,又叫原型对象,类型是一个对象(Object),仅函数(Function)有该属性(类Class的本质也是函数)

用于构造函数为其实例对象(通过关键字new创建)提供公共的属性和方法

[[Prototype]]

隐式原型仅对象才拥有,它指向自己的prototype原型对象(或者说父对象),可以通过Object.getPrototypeOf()Object.setPrototypeOf()方法进行访问和设置

\_\_proto\_\_

[[prototype]]是规范和一些书籍对隐式原型的表示方法,\_\_proto\_\_ 是浏览器厂商对隐式原型的实现,其实就是一个东西;过去浏览器显示为__proto__,现在改为另外一种显示呢,等同于[[Prototype]],是许多浏览器和JavaScritp引擎的非ECMAScript标准实现
__proto是对象所独有的,通过直接访问该属性即可更改

console.log(Object.getPrototypeOf(f1) === f1.__proto__) // true
但是这是非ECMAScript标准,会有安全问题,因此更推荐使用Object.getPrototypeOf()Object.setPrototypeOf()方法,这里将使用__proto__进行说明

它的作用也是一致的,指向自己的构造函数的prototype原型对象(或者说父对象),因此下列输出成立

console.log(f1.__proto__ === Foo.prototype) // true

有了以上知识我们可以正式开始讲解该图片了,先不管别的,来看最左边的一列,我们说过,prototype它是一个对象,因此它也有__proto__属性,而它的属性指向哪了?显然,只要我们没有特意设置它的指向的话,那便是Object.prototype,再继续向下找__proto__那便是null,这一个接连一个的查找形成了一个链条,这便是大名鼎鼎的原型链,而null便是原型链的终点。

这里扩展一下,我们说了,prototype是对象类型的,而终点null是一种特殊的存在,表示不可在继续查找下去。null 虽然用typeof打印出来的类型为 "object", 但它的真实类型应该是Null ,表示一个空对象指针。它不是一个对象类型,不能调用对象的方法。
console.log(typeof null) // object

这里我们换一个例子来进一步理解原型链

let arr = new Array();
console.log(arr.__proto__ === Array.prototype);
console.log(Array.prototype.__proto__ === Object.prototype);

使用Array()构造函数来创造了一个数组实例arr,数组arr的隐式原型指向数组原型对象Array.prototype,因此第一个输出成立,然后Array.prototype又指向对象的prototype,再次成立,最后指向null

关于函数

如果只是对象的话,那么这个图便不会如此复杂,但因为函数的存在大大增加了这个图的复杂度,函数的本质也是一个对象,因此函数既有__proto__和prototype,这也是许多人迟迟搞不懂原型链的原因,不用想那么多,一个函数的__proto__与prototype是完全没有任何关系的

现在先不管所有的constructorFoo()函数的__proto__显而易见的指向了一个叫做Function.prototype的东西,没错他便是构造函数Function()的原型对象,而一切的函数都由Function()创造,如图所示,它的__proto__又指向了Object()函数的原型对象Object.prototype,再而指向null,相反的,Object()是一个构造函数,因此__proto__又指向Function.prototype

关于函数还有一个需要注意的一点,那就是图中Function的prototype和__proto__是相等的。可以理解为,所有函数的__proto__都指向Function()构造函数的原型对象,而Function()本身也是函数又是对象,所以自己的__proto__属性指向自己的原型对象Function.prototype

console.log(Function.__proto__ === Function.prototype); // true

constructor

与__proto__类似,对象独有的属性,用于原型对象(prototype)指回其构造函数本身。如图所示,一个的函数prototype和它的constructor的关系是:函数通过prototype属性访问其原型对象,而原型对象上有一个constructor属性指回其构造函数本身

而实例对象(如图中的f1)它的constructor也指向Foo(),但只是继承而来的并不真正拥有(因为f1它是Foo()的实例)

需要注意一点,所有的函数的constructor最终都继承来自Function()构造函数,而Function()constructor又继承于自身

总结

  • prototype仅函数(class类也是函数)拥有,用于给自己实例化对象提供公共公告属性或对象
  • \_\_proto\_\_constructor仅对象拥有,因为函数本质也是对象所以函数同时拥有以上三者。\_\_proto\_\_指向自己的原型对象(即prototype),因此f1.__proto__ === Foo.prototype
  • 查找一个实例对象的属性或方法先从自身开始查找,若没有则查找自己构造函数的prototype;若没有,继续查找函数的prototype属性里的__proto__属性的又一个原型对象,直至查找到Object.prototype,再往下的__proto__便为null,这样的链式查找方式便是原型链null是原型链的终点
  • constructor用于prototype里指回自己的构造函数,实例对象继承于自己的构造函数,即Foo === Foo.prototype.constuctor
  • 所有对象的prototype最终指向Object.prototype,所有对象的constuctor最终指向Function()构造函数,Function()的constructor继承于自己

鸣谢

本文参考了以下链接中的内容并综合自己的个人理解,如若有误欢迎指出,排名分先后