我们首先先来看一张图,如下

关于 JavaScript 中的原型,本质上来说其实就是 prototype、__proto__ 和 constructor 的三者之间的关系,上图第一眼看上去感觉十分复杂,但是其实说的也就是两句话的事,如下
1 | function Foo() { } |
下面我们就来逐一分析他们之间的关系
实例对象
通过构造函数的 new 操作创建的对象是实例对象,可以用一个构造函数,构造多个实例对象
1 | function Foo() { } |
prototype
构造函数有一个 prototype 属性,指向『实例对象的原型对象』,通过同一个构造函数实例化的多个对象具有相同的原型对象,经常使用原型对象来实现继承
1 | function Foo() { } |
proto
实例对象有一个 __proto__ 属性,指向『该实例对象对应的原型对象』(需要注意,实例对象也是对象)
1 | function Foo() { } |
不过需要注意是,如果实例对象 f1 是通过 Object.create() 创建的话,结果就不一样了
1 | function Foo() { } |
constructor
原型对象有一个 constructor 属性,指向『该原型对象对应的构造函数』
1 | function Foo() { } |
由于实例对象可以继承原型对象的属性,所以实例对象也拥有 constructor 属性,同样指向原型对象对应的构造函数
1 | function Foo() { } |
constructor 属性返回对创建此对象的数组函数的引用,它是不会影响任何 JavaScript 的内部属性的
看下面一段代码
1 | function Foo() { } |
看起来 a.constructor === Foo 为 true 则意味着 a 确实有一个指向 Foo 的 .constructor 属性,但是事实不是这样,实际上,.constructor 引用同样被委托给了 Foo.prototype,而 Foo.prototype.constructor 默认指向 Foo,Foo.prototype 的 .constructor 属性只是 Foo 函数在声明时的默认属性
如果你创建了一个新对象并替换了函数默认的 .prototype 对象引用,那么新对象并不会自动获得 .constructor 属性,思考下面的代码
1 | function Foo() { } |
a1 并没有 .constructor 属性,所以它会委托 prototype 链上的 Foo.prototype,但是这个对象也没有 .constructor 属性(不过默认的 Foo.prototype 对象有这个属性),所以它会继续委托,这次会委托给委托链顶端的 Object.prototype,这个对象有 .constructor 属性,指向内置的 Object() 函数,当然,你可以给 Foo.prototype 添加一个 .constructor 属性,不过这需要手动添加一个符合正常行为的不可枚举的属性
1 | function Foo() { } |
实际上,对象的 .constructor 会默认指向一个函数,这个函数可以通过对象的 .prototype 引用,.constructor 并不是一个不可变属性,它是不可枚举(参见上面的代码)的,但是它的值是可写的(可以被修改),此外,你可以给任意 prototype 链中的任意对象添加一个名为 constructor 的属性或者对其进行修改,你可以任意对其赋值
所以这是一个非常不可靠并且不安全的引用,通常来说要尽量避免使用这些引用,但是有的时候,为了将实例的构造器的原型对象暴露出来,比如写了一个插件,别人得到的都是你实例化后的对象,如果想扩展下对象,就可以用 instance.constructor.prototype 去修改或扩展原型对象
1 | var a, b |
因为 A 在闭包里,所以现在我们是不能直接访问 A 的,那如果我们想给类 A 增加新方法,那么就可以通过访问 constructor 就可以了
1 | // a.constructor.prototype 在 chrome 和 firefox 中可以通过 a.__proto__ 直接访问 |
或者我们想知道 a 的构造函数有几个参数?
1 | a.constructor.length |
或者再复杂点,我想知道 a 的构造函数的参数名是什么
1 | a.constructor.toString().match(/\(.*\)/).pop().slice(1, -1).split(',') |
实例与原型
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,以此类推,一直找到最顶层为止
1 | function Foo() { } |
在上面的例子中,我们给实例对象 f1 添加了 name 属性,当我们打印 f1.name 的时候,结果自然为 lisi,但是当我们删除了 f1 的 name 属性时,再次读取 f1.name 的时候,就会发现在 f1 对象中已经找不到 name 属性了,所以就会从 f1 的原型,也就是 f1.__proto__ 对应的 Foo.prototype 当中去进行查找,所幸的是我们找到了 name 属性,结果为 zhangsan
但是万一还没有找到呢?原型的原型又是什么呢?
原型的原型
在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是
1 | var obj = new Object() |
其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 __proto__ 指向构造函数的 prototype,那 Object.prototype 的原型又是指向哪里的呢?我们可以打印一下
1 | Object.prototype.__proto__ // null |
null 表示没有对象,即该处不应该有值,所以 Object.prototype.__proto__ 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思,所以查找属性的时候查到 Object.prototype 就可以停止查找了,这也就是开头部分的图片当中所表达的这个意思,来看下面这个示例
1 | var a = { |
自定义对象
1 | // 1. 默认情况下 |
以上两种情况都等于完全重写了 Foo.prototype,所以 Foo.prototype.constructor 也跟着改变了,于是乎 constructor 这个属性和原来的构造函数 Foo() 也就切断了联系
基于原型的一个实例
最后我们来看一个具体的实例和它的几个变种方式来巩固一下之前的知识点,如下
1 | function obj(name) { |
一般函数直接调用,默认 this 指向全局 window/global,通过 obj('name1') 调用,返回 this 引用,并传递给 a,此时 a 等于 window 对象,即可输出 name 值,new 操作,在没有参数的情况下 new obj 等价于 new obj() ,实例化一个对象,这时 this 指向 obj,要拿到 b.name 的值,需要保证 name 属性存在
属性查找原则是先查找当前实例有没有属性,如果有就直接使用,如果没有,就到原型上去找,在没有就接着原型链一步一步往上,这里为了和 a.name 作属性区别,使用了 if (name) 有条件的构建 this 的属性 name,所以,现在 name 属性提供给 a 使用,原型上的 name 提供给 b 使用
实例变体一
1 | // 去掉 if 判断 |
实例变体二 试试传个参数
1 | function obj(name) { |
总结
记住以下几点
- 函数(
Function也是函数)是new Function的结果,所以函数可以作为实例对象,其构造函数是Function(),原型对象是Function.prototype - 对象(函数也是对象)是
new Object的结果,所以对象可以作为实例对象,其构造函数是Object(),原型对象是Object.prototype Object.prototype的原型对象是null__proto__是每个对象都有的一个属性,而prototype是函数才会有的属性__proto__指向的是构造该对象的构造函数的原型,而prototype指向的,是以当前函数作为构造函数构造出来的对象的原型对象__proto__并不存在于你正在使用的对象中,实际上,它和其他的常用函数(toString()、isPrototypeOf(),等等)一样,存在于内置的Object.prototype中(它们是不可枚举的)__proto__看起来很像一个属性,但是实际上它更像一个getter/setter__proto__的实现大致上是类似下面这样的
1 | Object.defineProperty(Object.prototype, '__proto__', { |
明确以下三点
- 通过
Function.prototype.bind方法构造出来的函数是个例外,它没有prototype属性 Object.prototype这个对象,它的__proto__指向的是null- 通过
Object.create(null)创建出来的对象没有__proto__,如下
1 | var obj = Object.create(null) |