Javascript原型链prototype

简单说下JavaScript中原型链继承关系。

前言

最近喜欢利用业余时间,逛一些社区论坛,一是主要多看多学习,看下大家的问题和答案,二呢,就是碰巧遇上自己能答的简单问题,就来一发个人见解,这样两全其美的方式,何乐而不为呢?不过前段时间遇到一个JavaScript对象原型链的问题,就厚着脸答了一波,从其他人的答案里,也认识到自己的不足,遂写下该篇文章。

构造函数原型prototype

我们都知道,JavaScript语言中是没有类的概念的,于是伟大的前人们就利用JavaScript的语言特性和类的特点,通过”曲线救国”的方式,实现了JavaScript中语言特有的类。在ES3时代,我们通过以下方式来模仿一个简单的类。

1
2
3
4
5
6
7
function Animal(name) {
this.name = name;
}
Animal.prototype.hello = function() {
console.log('say hello!');
}

接下来,我们就可以通过new关键字来实例化一个Animal对象。

1
2
var animal1 = new Animal('旺财');
var animal2 = new Animal('小强');

上面我们借助JavaScript特有的构造函数原型对象Prototype来实现两个对象之间共有的方法,这样小强和旺财两个实例的say方法均指向构造函数prototype中的say方法,而且一直保持同步。例子中的Animal类实际上就是一个普通的函数,我们用首字母大写来约定这个函数作为构造函数,即通过new的方式来调用。
每个构造函数都有个prototype属性,该属性指向一个对象,该对象初始时只有一个constructor属性,指向构造函数本身,即Animal.prototype.constructor === Animal(这样也就说明constructor会有namelength属性)。我们在通过new新建一个实例对象时,新创建的对象会有一个[[Prototype]]属性,该属性指向构造函数的prototype属性对象。

原型对象[[Prototype]]

JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。JavaScript中通过结合以上两种对象的特点来实现面向对象语言中类特有的继承特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Bird(color) {
Animal.call(this, 'bird');
this.color = color;
}
function Fn() {}
Fn.prototype = Animal.prototype;
Bird.prototype = new Fn();
Bird.prototype.constructor = Bird;
Bird.prototype.sing = function() {
console.log('world~~')
};

在一些高级浏览器中,例如Chrome中,我们是可以通过__proto__来访问一个对象的原型属性[[Prototype]]。当多个对象的原型属性通过上面那种方式实现继承时,就牵涉到JavaScript中另一个概念,原型链。

原型链查找

当我们实例化一个bird对象并且调用bird.say()方法时,JavaScript引擎首先会查找该对象内部是否还有say方法,如果没找到,就会查找bird的原型属性[[Ptototype]],即Bird.prototype,如果还没找到,会继续查找Bird.prototype的原型属性,即Animal.prototype,找到就直接返回调用,否则继续查找,直到找到Object.prototype.__proto__null为止,此时返回undefined。所以该原型链查找过程如下:
bird.__proto__ -> Bird.prototype.__proto__ -> Animal.prototype.__proto__ -> Object.prototype.__proto__ -> null

特例Function

上面的结论一切都正确,不过凡事总有例外。这个在JavaScript语言中也不例外。而这个例外就是我们的Function构造函数。在JavaScript中,函数也是对象。即一个函数首先是一个对象,其次才是一个函数。既然我们的Function构造函数是一个对象,那这个对象的构造函数又是谁呢?换言之,Function.__proto__指向谁?
Function.__proto__ === Function.prototype,这就是我们说的Function的特殊之处,即Function对象是Function构造函数的一个实例,而Function.prototype对象上存在一些函数的基础方法,例如callapply等方法。我们可以通过ES5的Object.getOwnPropertyDescriptors(Function.prototype)查看。

ES5

ES5中新增了一些方法和属性,来加强对象的功能。包括但不限于:

1
2
3
4
5
6
Object.create(o) // 类似我们上面模拟的通过空函数实现继承的方式
Object.getPrototypeOf(obj) // 获取一个对象的原型链
obj.propertyIsEnumerable(..) // 检查给定的属性是否直接存在于对象中(不是在圆形脸上)且满足enumerable: true
Object.keys() // 返回一个数组,包含所有对象直接包含的可枚举属性
Object.getOwnPropertyNames(Obj) // 返回一个数组,包含对象直接包含的所有属性,无论他们是否可枚举
Obj.hasOwnProperty(str) // 对象是否直接包含str属性

而我们在对对象的某个属性赋值时obj.foo = 'bar'h会出现以下三种情况:

  1. 如果在[[Prototype]]链上存在名为foo的普通数据访问属性,并且没有被标记为只读(writable: false),那就会直接在obj中添加一个名为foo的新属性,它会屏蔽原型链上的同名属性。
  2. 同上,如果foo被标记为只读,那么无法修改已有属性或者在obj上创建屏蔽属性,如果运行在严格模式下,代码会抛出一个错误。否则,这条赋值语句会被忽略。
  3. 如果在[[Prototype]]链上存在foo并且它是一个setter,那就一定会调用这个setter,foo不会被添加到(或者说屏蔽于)obj,也不会重新定义这个setter。

如果我们希望在第二种和第三种情况下也屏蔽foo,那就不能使用=操作符来赋值,而是使用Object.defineProperty()来向obj对象foo属性。

ES6

ES6中新加了classextends等语法糖,用来保持和Java等语言保持一致,具体的可自行了解,这里就不在赘述。

最后

最后,我们通过网上一位大牛的一张图片来形象地说明JavaScript中各个对象与构造函数和原型链的关系。
JavaScript原型链

ps: 本文部分内容摘自《你不知道的JavaScript》(上卷)