你不知道的JS读书笔记4—原型与原型链

JS中一提到面向对象编程,就避免不了谈到原型链,因为在JS中类的继承是基于原型链的。花了些时间整理原型,原型链,可能还是有点绕。不当之处,欢迎指出。

简单例子

我们先从一个简单的例子入手,尝试阐述这些概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Animal(name, species, n){
this.name = name;
this.species = species || 'animal';
this.legsNum = n;
}
Animal.prototype.sayHello = function(){
console.log('Hello, I am a ' + this.species + ', my name is ' + this.name);
}
var dog = new Animal('Belly', 'dog', 4);
console.log(dog);
console.log(dog.__proto__);
console.log(Animal.prototype);
console.log(dog.__proto__ === Animal.prototype);
console.log(Animal.prototype.constructor);
dog.sayHello();

我们来仔细分析一下这个例子的过程。
首先定义了一个Animal的构造函数,构造函数本质上就是普通的函数,只是用new来调用之后我们称之为构造函数。

然后定义了Animal构造函数的prototype属性,prototype是函数才有的属性。prototype是对象,默认有个constructor属性,该属性指向构造函数本身。所以第4个输出就很清楚了,直接输出Animal对象。

用new Animal()定义一个新对象dog,这个过程主要发生了以下几件事:新建一个空的对象,将这个对象__proto__属性设置为Animal.prototype,将this设置为这个对象,如果没有对象就默认返回this对象。用代码来描述就是下面的过程。

1
2
3
var dog = new Object();
dog.__proto__ = Animal.prototype;
Animal.call(dog,'Belly', 'dog', 4);

明白这个过程之后,dog就拥有name属性,species属性,legsNum属性以及__proto__属性。显然dog.__proto__与Animal.prototype是相同的引用。

最后一个,dog.sayHello() ,我们知道dog对象并没有定义sayHello属性,但为何它还能执行呢?原因就在于__proto__属性,它就是原型链查找的关键,当dog对象上找不到sayHello属性时,它在__proto__对象中查找,而__proto__就是Animal.prototype。正巧,Animal.prototype上定义了sayHello,所以顺利执行了该函数。如果Animal.prototype还找不到,就会继续在dog.__proto__.__proto__上查找,即Animal.prototype.__proto__。

最后的输出结果如下:

结果图

prototype, __proto__, constructor三者关系

从上述例子可以整理出prototype, __proto__, constructor这三者之间的关系。每个函数都有prototype属性,当这个函数作为构造函数创建了一个实例后,该实例的__proto__属性指向构造函数的prototype。换句话来说,函数的prototype属性中所定义的变量和方法是被由它实例化出来的对象所共享的,记录在实例的__proto__属性中。

P.S. __proto__属性也用[[Prototype]]属性来描述。为了防止把[[Prototype]]属性和prototype搞混,这里我们用__proto__来统一表示。但是,__proto__其实是Chrome和Firefox浏览器提供的非标准的属性,标准的访问方法为Object.getPrototypeOf (obj )来访问。

默认情况下,prototype中包含一个constructor属性,指向构造函数本身。constructor可以被篡改,所以我并不知道这个属性的意义在哪里。在网上看到一个应用,如果你不能直接访问构造函数本身,你得到的是一个实例化对象,如果你想扩展构造函数,你可以通过instance.constructor.prototype来访问修改。但前提也是constructor指向正确的前提下,所以我觉得还是没有说服力。

原型链

总而言之,原型就是指__proto__属性,原型对象是指prototype属性。那么,原型链抽象来说,就是__proto__.__proto__.__proto__….。它的尽头是什么呢?这个属性是对象才有的属性,所以它们的尽头应该是Object.prototype,然后Object.prototype.__proto__指向null。这就是为何,普通的对象都会有toString,valueOf这些方法。

我们来深究对象在不同创建方法下的原型链分析:普通方式,构造函数方式,Object.create方式。我们来看些简单的例子。

//普通方式
var o = {a:1};
// o --> Object.prototype --> null
var a = [1, 2, 3];
// a --> Array.prototype  --> Object.prototype --> null
function f(){
}
// f --> Function.prototype  --> Object.prototype --> null

// 顺带提一下比较好玩的
Function.__proto__ === Function.prototype; // true
Function instanceof Function; //true

typeof Object; // 'function'
// new构造器创建
function Person(name){
    this.name = name;
}
var me  = new Person('Claire');
// me --> Person.prototype --> Object.prototype --> null

ES5中定义了一个新的方法Object.create,用来创建一个指定原型的对象。另外,ES6中定义了Object.setPrototypeOf(a, b),将a的原型指向b。

// Object.create方式
var a = {}; 
var b = Object.create(a);
//  b --> a --> Object.prototype --> null
var c = Object.create(b);
//  c -->b --> a --> Object.prototype --> null
var d = = Object.create(null);
// d --> null

现在对原型链应该有所了解了吧。

属性查找,设置与属性屏蔽

在第一部分中,我们已经说明了一个对象的属性是怎么查找的:先在对象本身上查找是否有这个属性,如果有获取它的属性;如果找不到,在它的原型上查找,如果还是找不到,继续在它的原型的原型上查找,直到找到,或者直到到达原型的尽头null。

换一个角度来说,本来想访问实例对象原型上的属性,但如果实例对象上也定义了相同的属性,这时候就会发生属性的屏蔽,即你只能访问到最近的那个属性。下例中,dog对象定义的sayHello属性会覆盖构造函数Animal的原型对象上的sayHello。

function Animal(name, species, n){
    this.name = name;
    this.species = species || 'animal';
    this.legsNum = n;
}

Animal.prototype.sayHello = function(){
    console.log('Hello, I am a ' + this.species + ', my name is ' + this.name);
}

var dog = new Animal('Belly', 'dog', 4);
dog.sayHello = function(){
    console.log('Hi, I am a dog, what about you?');
}

dog.sayHello(); // 'Hi, I am a dog, what about you?'

那么对象的属性设置因为原型链的存在,它会是怎么样的过程呢?分成几种情况:

  • 如果对象本身上有这个属性,直接对这个属性进行改写;
  • 如果对象本身没有这个属性,就在原型链中查找:
    • 如果没有找到,将属性直接添加在该对象上;
    • 如果找到,又分为3种情况:
      • 如果原型链上的该属性writable为true,直接在对象上添加该属性
      • 如果原型链上的该属性writable为false,赋值会被忽略(严格模式下报错)
      • 如果原型链上的该属性是通过setter来设置的,执行setter函数

如果想要直接在对象本身上添加属性,而不管这么多,可以用Object.defineProperty。

另外,查找原型链很耗性能,最好避免。避免的方式可以有:hasOwnProperty, Object.keys。

为什么要有prototype

最后我们从一个狭小的角度瞎聊一下为什么要有prototype这个属性。我们修改一下最开始那个例子。

function Animal(name, species, n){
    this.name = name;
    this.species = species || 'animal';
    this.legsNum = n;
    this.sayHello = function(){
        console.log('Hello, I am a ' + this.species + ', my name is ' + this.name);
    }
}

var dog = new Animal('Belly', 'dog', 4); 
var cat = new Animal('Lucy', 'cat', 4);
var chicken = new Animal('Mike', 'chicken', 2);

其实sayHello是他们通用的方法,每次实例化的时候都复制了一份。而当你想要修改sayHello方法的时候,你又需要在每个实例中一一进行修改,显然非常地不合理。所以函数对象上添加了一个prototype的属性,从这个函数生成的实例能共享prototype中定义的属性和方法。

参考资料: