大部分面向对象的编程语言,都是通过“类”(class)来实现对象的继承。而JavaScript语言不一样,是通过“原型对象”(prototype)来实现的,本文介绍JavaScript的原型链继承。
ES6引入了class语法,基于class的继承将在后面介绍。
1 原型对象概述
1.1 构造函数的缺点
JavaScript通过构造函数生成新对象,构造函数内部可以定义对象的属性和方法。
function Person(name, age) {
this.name = name;
this.age = age;
}
var man1 = new Person('John', 25);
man1.name // "John"
man1.age // 25
上面代码中,Person函数是一个构造函数,定义了name和age属性,通过这个构造函数生成的所有实例对象都具有这两个属性。
这种方法虽然方便,但有一个缺点,即同一构造函数的多个实例之间,无法共享属性,造成系统资源的浪费。
function Person(name, age) {
this.name = name;
this.age = age;
this.work = function() {
console.log('work');
}
}
var man1 = new Person('John', 25);
var man2 = new Person('Mark', 30);
man1.work === man2.work // false
上面代码中,man1和man2是同一构造函数的两个实例,它们都具有work方法。但是每生成一个实例,就会新建一个work方法,这既没有必要,又浪费系统资源,因为所有work方法都是相同行为,完全应该共享。
这个问题的解决方法,就是JavaScript的原型对象(prototype)。
1.2 prototype属性的作用
prototype机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。这样不仅节省了内存,还体现了实例对象之间的联系。
如何指定原型对象呢?每个函数都有一个prototype属性,指向一个对象。
function f() {}
typeof f.prototype // "object"
上面代码中,函数f默认具有prototype属性,指向一个对象。
对于普通函数来说,该属性没有作用。但对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
function Person(name) {
this.name = name;
}
Person.prototype.age = 20;
var man1 = new Person('John');
var man2 = new Person('Mark');
man1.age // 20
man2.age // 20
上面代码中,构造函数Person的prototype属性,就是实例对象man1和man2的原型对象。prototype对象添加的age属性,被所有实例对象共享。
修改原型对象,变动会立刻体现在所有实例对象上。
Person.prototype.age = 30;
man1.age // 30
man2.age // 30
上面代码中,原型对象的age属性变为30,两个实例对象的age属性立刻跟着变了。这是因为实例对象自身其实没有age属性,实际读取的是原型对象的age属性。也就是说,当实例对象自身没有某个属性或方法,它会到原型对象去寻找。这就是原型对象的特殊之处。
如果实例对象自身就有某个属性或方法,它就不会去原型对象中寻找了。如下所示:
man1.age = 18;
man1.age // 18
man2.age // 30
Person.prototype.age // 30
总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。而实例对象可以视作从原型对象衍生出来的子对象。
1.3 原型链
JavaScript中所有对象都有自己的原型对象(prototype),由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”。
如果一层一层地上溯,所有对象的原型最终都是Object.prototype。那么,Object.prototype对象有没有它的原型呢?有,就是null。null没有任何属性和方法,也没有自己的原型,因此,原型链的尽头就是null。
Object.getPrototypeOf(Object.prototype) // null
上面代码表示,Object.prototype对象的原型是null,null没有任何属性,原型链至此为止。
Object.getPrototypeOf方法返回参数对象的原型。
读取对象的某个属性时,JavaScript引擎首先寻找对象自己的属性,如果找不到,就到它的原型里找,如果还是找不到,就到原型的原型去找,一直找到最顶层的Object.prototype。如果还是找不到,则返回undefined。
如果对象自身和它的原型,都定义了同名属性,那么优先读取对象自身的属性。
1.4 constructor属性
prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
function Person() {}
Person.prototype.constructor === Person // true
var p = new Person();
p.constructor == Person // true
p.constructor === Person.prototype.constructor // true
p.hasOwnProperty('constructor') // false
上面代码中,p自身没有constructor属性,其实读取的是原型链上面的Person.prototype.constructor属性。
constructor属性的作用是,可以得知某个实例对象,到底是哪个构造函数产生的。
function Person() {};
var p = new Person();
p.constructor === Person // true
p.constructor === RegExp // false
如果不确定constructor属性是什么函数,还可以通过name属性,从实例得到构造函数的名称。
function Person() {}
var p = new Person();
p.constructor.name // "Person"
2 instanceof运算符
instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例。
var p = new Person();
p instanceof Person // true
上面代码中,对象p是构造函数Person的实例,因此返回true。
它还会检查右边构造函数的原型对象(prototype),是否在左边对象的原型链上。因此,下面两种写法是等价的。
p instanceof Person
// 等同于
Person.prototype.isPrototypeOf(p)
instanceof运算符的一个用处,就是判断值的类型。
var x = [1, 2, 3];
var y = {};
x instanceof Array // true
y instanceof Object // true
注:本文适用于ES5规范,原始内容来自 JavaScript 教程,有修改。