区分JS中的__proto__和prototype

Category:
发表:
大纲
  1. 1. __proto__和prototype的概念
  2. 2. new的过程
  3. 3. 示例
    1. 3.1. 第一个示例
    2. 3.2. 第二个示例
    3. 3.3. 示例总结
  4. 4. 一些额外的问题
    1. 4.1. Object.create方法
    2. 4.2. instanceof背后的逻辑
  5. 5. 参考列表

我们知道javascript是基于原型(prototype)继承的语言。关于javascript原型继承的更多内容请参阅之前的这篇文章浅谈Javascript中的原型继承,内容绝对不会让你失望。

不过本篇文章将会详细阐述javascript中用于原型继承的两个重要的东西__proto__prototype,以及由它们延伸出的一点容易让人疑惑的方面。

__proto__prototype的概念

prototype就是原型的意思,就是函数(构造器)的一个属性,每个函数都将会有一个prototype属性。此属性是一个引用类型(指针),它指向函数的原型集对象。我们一般可以通过prototype修改函数的原型属性。而__proto__是一个对象(可以是某个构造器的实例)的属性,它在对象(实例)被创建时跟随被创建,它也是一个引用类型(指针),指向函数(构造器)的prototype属性。

两者的关系如下,

1
2
3
4
5
6
7
function Foo() {

}

var foo = new Foo();

console.log(foo.__proto__ === Foo.prototype); // true

额外提一点,__proto__属性目前在IE浏览器中貌似不能直接访问,不过在Chrome和Firefox中是可以直接访问的。

new的过程

如下代码,

1
2
3
4
5
function Foo() {

}

var foo = new Foo();

我们使用new创建了构造器Foo的一个实例foo。那么这个过程究竟是怎么样的呢?它将分为以下几步,

  • 创建一个对象foo,并且foo = {}
  • 将构造器的原型链到foo__proto__上,即foo.__proto__ = Foo.prototype。这一步至关重要,为实例能够访问构造器原型方法原型链查找等操作做好了铺垫。
  • 执行构造器Foo,并将对象foo绑定到其上下文环境中,即Foo.call(foo)。其实就是将Foo中的this指针指向foo

示例

第一个示例

下面让我们来看个完整的例子来探索一下prototype__proto属性的相互关系,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Animal() {
this.eats = true;
}

var cat = new Animal();
Animal.prototype.jumps = true;

console.log(cat.eats); // true;
console.log(cat.jumps); // true


// change constructor's prototype
Animal.prototype = {
bark: true
};

var dog = new Animal();

console.log(cat.bark); // undefined
console.log(dog.bark); // true

console.log(cat.__proto === Animal.prototype); // false;
console.log(dog.__proto === Animal.prototype); // true;

下面是一张关于上述代码中Animal(构造器)及两个实例(catdog)的原型关系图,

从图中我们可以看出,

  • catdogAnimal构造出的实例
  • cat__proto__属性指向一个对象,此对象是Animal改变prototype之的指向。
  • dog__proto__属性指向一个对象,此对象是Animal改变prototype之的指向。和Animal.prototype的指向一致。
  • 通过cat或者dog__proto__属性,经过层层查找,最终的都会指向同一个对象,即Object.prototype,而Object.prototype__proto__指向null,此时原型链已经到顶。

第二个示例

下面我们再来看个例子来探索一下javascript是如何在其原型链上进行属性查找的,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person() {
this.name = 'gejiawen';

this.secret = function() {
console.log('I am a secret!');
};
};

Person.prototype.sayName = function() {
console.log('My name is ', this.name);
};
Person.prototype.secret = function() {
console.log('I am a secret on prototype!');
};

var p = new Person();

console.log(p.name); // gejiawen
console.log(p.secret()); // I am a secret!
console.log(p.sayName()); // My name is gejiawen

下面是实例对象p在Chrome console中的打印,

所以,各个打印的解释如下,

  • p.name,直接在对象p中找到了,将其打印出来。
  • p.secret,在对象p中也直接找到了,执行相应方法。
  • p.sayName,在对象p中并未找到相关定义,此时将会开始检索p.__proto__中对象,发现了sayName的定义,此时就终止检索,执行相应函数。

有两点需要额外提出来,

  1. 我们可以看到在构造器和prototype中定义了一个同名方法secret,从上述代码中可以看出,是不会执行prototype中的secret函数。因为javascript在示例对象p自身的定义找到secret时就终止检索了。
  2. 针对上面的第三点解释,如果我们在p.__proto__仍然未找到sayName的相关定义,那么javascript会继续向上检索,继续检查p.__proto__.__proto__,如此反复,直至到Object.prototype

示例总结

通过上面的示例,我们可以看出,示例对象的__proto__在javascript的原型继承模型中扮演着不可或缺的角色。

可以说__proto__就是将一个个的对象串成一条完整原型链的粘合器。

个人觉得prototype相对于__proto__更加像是一个开放的接口,而__proto__更倾向是原型链模型的内部实现。我们在平时进行javascript开发时,可以在适当的时候应用一些prototype的知识来提高代码质量。

一些额外的问题

Object.create方法

Object.create是ES5中引入的方法,

1
Object.create(proto[, propertiesObject])

MDN上给出的定义如下,

The Object.create() method creates a new object with the specified prototype object and properties.

意思就是我们可以在创建对象时,可以自定义其原型。(需要注意的是,当给Object.create传入的参数不是一个对象或者null时,它将会抛出一个错误)

看下面的代码,

1
2
3
4
5
6
7
var animal = {
eats: true
};

var rabbit = Object.create(animal);

console.log(rabbit.eats); // true

此时,rabbit.__proto__上将会有eats属性,如关系如下图,

请注意各个对象的__proto__之间的关系。

instanceof背后的逻辑

我们知道instanceof可以判断某个实例对象是否是某个构造器的实例。那么instanceof判断的依据是什么呢?

1
2
3
4
5
6
7
function A() {

}

var a = new A();

console.log(a instanceof A); // true

instanceof的过程如下,

  1. 对比a.__proto__A.prototype
  2. 若第一步的返回为true,则表示a即为A的实例。
  3. 若第一步的返回为false,此时执行类似这样的操作a = a.__proto__,然后重复第一步。

其实说白了,instanceof的实质就是比较__proto__prototype,我们来看个示例来看下是否能说明这个问题,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Animal() {
this.name = 'wangcai';
}

function Dog() {

}

Dog.prototype = new Animal();

var dog = new Dog();

console.log(dog.name); // 'wangcai'
console.log(dog instanceof Animal); // true

// change the prototype of Class
Dog.prototype = {
name: 'yingcai'
};

console.log(dog instanceof Dog); // false
console.log(dog instanceof Animal); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true

这个例子中,我们中间改变了Dogprototype,此时造成的后果就是破坏了原先dog对象的__proto__Dog.prototype的引用关系,从而dog instanceof Dog返回的结果为false

参考列表