理解 prototype 和 __proto__

理解 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
    9
    Number.__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
2
Math.__proto__ === Object.prototype;  // true
JSON.__proto__ === Object.prototype; // true

以上介绍都是原生的 javascript 的构造器(函数),当然自定义的构造器也满足:

1
2
3
4
function 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
    10
    var 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
5
function Person(name) {
this.name = name
}
var person = new Person('aikin');
person.__proto__ === Person.prototype; // true

对象原型链图

new 关键字做了什么?

new 运算符接受一个函数 F 及其参数:new F(arguments...)。这一过程分为三步:

  1. 创建类的实例。这步是把一个空的对象的__proto__属性设置为F.prototype
  2. 初始化实例。函数 F 被传入参数并调用,关键字this指向该实例。
  3. 返回实例 当然你也可以return自定义的对象。
  • new 函数的伪实现:
    1
    2
    3
    4
    5
    6
    7
    function new (f) {
    var ins = { '__proto__': f.prototype }; /*第一步*/
    return function() {
    f.apply(ins, arguments); /*第二步*/
    return ins; /*第三步*/
    };
    }

干货呈上(来几道 quiz 吧)

  • quiz-1
1
2
3
4
5
6
7
8
9
10
function Person() {
this.name = 'aikin';
return {
name: 'ulaijn'
};
}

var me = new Person();

console.log(me.name); // ?
  • quiz-2
1
2
3
4
5
6
7
8
function Person() {
this.name = 'aikin';
return 'ulaijn';
}

var me = new Person();

console.log(me.name); // ?
  • quiz-1quiz-2运行结果分别是:ulaijnaikin。这两个测试题主要考察的是对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.nameaikin
  • quiz-3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person() {}

Person.prototype = {
constructor: Person,
name : 'aikin',
age : 23
};

var aMe = new Person();
var bMe = new Person();

aMe.name = 'luna';
bMe.name = 'tom';

console.log(aMe.name); // ?
console.log(bMe.name); // ?
  • quiz-4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Person() {}

Person.prototype = {
constructor: Person,
info : {
name : 'aikin',
age : 23
}
};

var aMe = new Person();
var bMe = new Person();

aMe.info.name = 'luna';
bMe.info.name = 'tom';

console.log(aMe.info.name); // ?
console.log(bMe.info.name); // ?
  • quiz-5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person() {
this.info = {
name : 'aikin',
age : 23
}
}

var aMe = new Person();
var bMe = new Person();

aMe.info.name = 'luna';
bMe.info.name = 'tom';

console.log(aMe.info.name); // ?
console.log(bMe.info.name); // ?
  • quiz-6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Person() {
this.info = {
name : 'aikin',
age : 23
}
}

Person.prototype = {
constructor: Person,
info : {
name : 'aikin',
age : 23
}
}

var aMe = new Person();
var bMe = new Person();

aMe.info.name = 'luna';
bMe.info.name = 'tom';

console.log(aMe.info.name); // ?
console.log(bMe.info.name); // ?
  • quiz-3quiz-5quiz-6 的运行结果是: lunatom,而 quiz-4 的运行结果是: tomtom。这四道题主要考察的是对prototype理解。
  • quiz-3aMe.name = 'luna'是赋值的过程,会在aMe中创建name属性,不会修改aMe.__proto__.name,当然bMe也一样。
  • 对于 quiz-4 来说,aMe.info.name = 'luna'是在对aMe.info赋值的过程,所以要先查找出aMe.info对象,因为aMe对象自己没有info属性,所以会从aMe.__proto__ 获取到info对象,并修改掉infoname属性。而bMe.info.name = tom也是这个过程,由于aMe.__proto__ === bMe.__proto__,所以bMe.info.name = tom执行后会修改掉bMe.__proto__.info.name,从而导致aMe.info.name也被修改成tom
  • quiz-5quiz-6 是考察对原型链查找的规则理解。当实例对象自己有相应的属性,就不会去获取原型链上的属性。就像 quiz-6 里面的aMe实例对象,aMe对象拥有info属性,同时在aMe.__proto__也用拥有info属性,但是aMe.info.name,不会获取aMe.__proto__.info.name,因为在原型链获取相应对象或者函数时,会从对象(实例)本身开始沿着原型链向上查找,只要找到了,就好停止继续查找。

使用 console.dir(Function) 打印出你的原型链。

参考