理解 prototype 和 __proto__
一个对象的真正原型是被对象内部的
[[Prototype]]
属性所持有。ECMAScript 引入了标准对象原型访问器Object.getPrototype(object)
,到目前为止只有 Firefox 和 Chrome 实现了此访问器。除了IE,其他的浏览器支持非标准的访问器__proto__
什么是 prototype
和 __proto__
?
prototype
为构造器(构造函数)的原型,是一个 javascript 的原生对象,它是通过调用构造函数而创建的那个对象实例的原型对象。 其他的对象可以通过他实现属性继承。
__proto__
是对象的内部原型,所有对象的__proto__
都指向该对象的构造器的prototype
。
在调用对象的方法时,会通过该对象__proto__
在原型链中查找。而构造器的prototype
会用于创建通过new
关键字创建的对象的__proto__
。
- 所有构造器(函数)的
__proto__
都指向Function.prototype
1
2
3
4
5
6
7
8
9Number.__proto__ === Function.prototype; // true
Boolean.__proto__ === Function.prototype; // true
String.__proto__ === Function.prototype; // true
Object.__proto__ === Function.prototype; // true
Function.__proto__ === Function.prototype; // true
Array.__proto__ === Function.prototype; // true
RegExp.__proto__ === Function.prototype; // true
Error.__proto__ === Function.prototype; // true
Date.__proto__ === Function.prototype; // true
注意: JavaScript中有内置(build-in)构造器/对象共计12个(ES5中新加了JSON),这里列举了可访问的8个构造器。剩下如Global
不能直接访问,Arguments
仅在函数调用时由JS引擎创建,Math、JSON
是以对象形式存在的,无需new
。它们的__proto__
是Object.prototype
。如下:1
2Math.__proto__ === Object.prototype; // true
JSON.__proto__ === Object.prototype; // true
以上介绍都是原生的 javascript 的构造器(函数),当然自定义的构造器也满足:1
2
3
4function Person() {}
var Man = function() {};
Person.__proto__ === Function.prototype; // true
Man.__proto__ === Function.prototype; // true
- 所有对象的
__proto__
都指向其构造函数的prototype
1
2
3
4
5
6
7
8
9
10var obj = {name: 'aikin'};
var arr = [1,2,3];
var reg = /hello/g;
var date = new Date;
var err = new Error('exception');
obj.__proto__ === Object.prototype; // true
arr.__proto__ === Array.prototype; // true
reg.__proto__ === RegExp.prototype; // true
date.__proto__ === Date.prototype; // true
err.__proto__ === Error.prototype; // true
自定义构造函数的实例对象也是如此:1
2
3
4
5function Person(name) {
this.name = name
}
var person = new Person('aikin');
person.__proto__ === Person.prototype; // true
new
关键字做了什么?
new 运算符接受一个函数 F 及其参数:
new F(arguments...)
。这一过程分为三步:
- 创建类的实例。这步是把一个空的对象的
__proto__
属性设置为F.prototype
。- 初始化实例。函数 F 被传入参数并调用,关键字
this
指向该实例。- 返回实例 当然你也可以
return
自定义的对象。
- new 函数的伪实现:
1
2
3
4
5
6
7function new (f) {
var ins = { '__proto__': f.prototype }; /*第一步*/
return function() {
f.apply(ins, arguments); /*第二步*/
return ins; /*第三步*/
};
}
干货呈上(来几道 quiz 吧)
- quiz-1
1 | function Person() { |
- quiz-2
1 | function Person() { |
quiz-1
和quiz-2
运行结果分别是:ulaijn
和aikin
。这两个测试题主要考察的是对new
关键字的理解。- 在
quiz-1
中,在调用new Person()
时,Person
构造函数内的this
指向是Person
的实例,由于Person
构造函数返回了一个对象类型的{ name: 'ulaijn' }
,导致new Person()
返回的不是Person
构造函数的实例,而是{name: ulaijn}
,因为只要当构造函数自定义return
的值是对象类型(不为null
)时,这样将导致使用new
关键字调用构造函数后的返回值,替换掉本该返回的构造函数的实例,所以这里的me
为{name: 'ulaijn'}
。 - 在
quiz-2
中,Person
构造函数内的this
,在调用new Person()
时和quiz-1
一样都是指向Person
的实例,但是由于Person
构造函数返回了一个不是对象类型,而是字符串类型的ulaijn
,导致new Person()
生成的实例无法被替换,所以me.name
为aikin
。
- quiz-3
1 | function Person() {} |
- quiz-4
1 | function Person() {} |
- quiz-5
1 | function Person() { |
- quiz-6
1 | function Person() { |
- quiz-3、quiz-5 和 quiz-6 的运行结果是:
luna
和tom
,而 quiz-4 的运行结果是:tom
和tom
。这四道题主要考察的是对prototype
理解。 - quiz-3 中
aMe.name = 'luna'
是赋值的过程,会在aMe
中创建name
属性,不会修改aMe.__proto__.name
,当然bMe
也一样。 - 对于 quiz-4 来说,
aMe.info.name = 'luna'
是在对aMe.info
赋值的过程,所以要先查找出aMe.info
对象,因为aMe
对象自己没有info
属性,所以会从aMe.__proto__
获取到info
对象,并修改掉info
的name
属性。而bMe.info.name = tom
也是这个过程,由于aMe.__proto__ === bMe.__proto__
,所以bMe.info.name = tom
执行后会修改掉bMe.__proto__.info.name
,从而导致aMe.info.name
也被修改成tom
。 - quiz-5 和 quiz-6 是考察对原型链查找的规则理解。当实例对象自己有相应的属性,就不会去获取原型链上的属性。就像 quiz-6 里面的
aMe
实例对象,aMe
对象拥有info
属性,同时在aMe.__proto__
也用拥有info
属性,但是aMe.info.name
,不会获取aMe.__proto__.info.name
,因为在原型链获取相应对象或者函数时,会从对象(实例)本身开始沿着原型链向上查找,只要找到了,就好停止继续查找。
使用 console.dir(Function)
打印出你的原型链。