我们首先先来看一张图,如下
关于 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) |