在上一篇文章中,我们主要介绍了 JavaScript 中原型对象的概念。这篇文章我们来聊一聊 JavaScript 中的继承。
一、继承的基本概念
相对于 JavaScript 来说,在其他一些面向对象的编程语言中,继承主要指的是父类和子类的一些关系。而在 JavaScript 中,继承主要是基于原型链来实现的。更简单地说,在 JavaScript 中,某个对象可以访问到另一个对象中的属性和方法,我们就可以认为它们之间存在继承关系。
通过上篇讲到的原型对象的知识来举个例子:
function Fruit() {
// code...
}
var apple = new Fruit();
apple.__proto__ === Fruit.prototype;
此时,实例 apple
不仅可以访问到自身的属性和方法,同时也可以访问到 Fruit.prototype
中的属性和方法,所以我们说 apple
继承自 Fruit.prototype
。
二、 JavaScript 中主流的继承方式
1、基于原型链实现继承
要了解基于原型链的继承,先要搞清楚什么是原型链。
原型对象就是某个构造函数的 prototype
属性所指向的那个对象,也就是构造函数的实例的 __proto__
属性所指向的那个对象。而原型对象上其实也有一个 __proto__
属性指向另外一个对象。听起来比较绕?那么我们用控制台来打印一下试试。

通过上图我们发现,在构造函数的原型对象 Fruit.prototype
上有一个 __proto__
属性指向另一个对象'A',同时在 Fruit.prototype.__proto__
这个对象'A'上,依然有一个 __proto__
属性,指向对象'B'......
像这样,已知一个对象,通过这个对象上的 __proto__
属性找到构造函数的原型对象,再通过原型对象上的 __proto__
属性找到这个原型的构造函数的原型,最终找到某个不含有 __proto__
属性的对象终止(原型链的顶端)的链式结构,我们称之为原型链(说的比较绕,其实自己理解了就好)。
搞清楚了原型链,我们就来说说基于原型链的继承。
基于原型链的继承,实际上就是在原型对象中扩展方法。实现方式我们也可以再分成两种:(1) 扩展原型对象 和 (2) 替换原型对象。
(1) 扩展原型对象
function Person() {}
var p1 = new Person();
当一个函数创建好之后,就会有一个默认的原型对象。在给这个原型对象添加属性和方法时,就用到了扩展原型对象实现继承。
举例来说:
Person.prototype.run = function() {
console.log("I'm running");
};
console.log(p1.run);
此时,p1
是可以访问到 run
方法的,我们就说 p1
继承自 Person.prototype
。
(2) 替换原型对象
扩展原型对象的方法虽然很好,但是它也有一些弊端。比如我们要给原型对象中扩展多个属性和方法时,就会出现以下情形:
Person.prototype.run = function() {
console.log("I'm running");
};
Person.prototype.say = function() {
console.log("I'm saying");
};
Person.prototype.sing = function() {
console.log("I'm singing");
};
Person.prototype.walk = function() {
console.log("I'm walking");
};
此时,我们发现使用扩展原型对象的方式又会出现一些重复的代码。而当出现重复代码的时,作为程序猿的我们自然会想到将这些重复封装起来。所以,替换原型对象实现继承的方式就出现了。
function Person() {}
// 替换原型对象
Person.prototype = {
constructor: Person, // 重点
run: function() {},
say: function() {},
sing: function() {},
walk: function() {}
};
// 实例可以访问
var p1 = new Person();
p1.run;
p1.say;
...
用图来表示一下这个过程。

实际上在 Person
函数创建好以后,会自动创建一个 Person.prototype (old)
。而当我们新创建一个 Person.prototype (new)
对象,并且把其中的 constructor
属性值设为 Person
后,Person
函数中的 prototype
属性就会指向我们新创建的这个 Person.prototype (new)
对象。
最后,我们通过一个比较经典的面试题再来理解一下其中的过程:
function Person() {}
Person.prototype.run = function() {
// code...
};
var p1 = new Person(); // p1.__proto__ 指向默认的原型对象
Person.prototype = {
constructor: Person,
say: function() {
// code...
}
};
var p2 = new Person(); // p2.__proto__ 指向新的原型对象
console.log(typeof p1.say); // undefined
console.log(typeof p2.say); // "function"
2. 混入继承(又称拷贝继承)
在日常工作中,经常遇到给一个函数传递多个参数的情况。比如说,我们需要得到用户的详细地址信息。
function getAddress(country, name, city, street, code, province, tel) {
// code ...
}
// 当传递参数时,我们需要非常小心
getAddress('China', 'zs', 'Beijing', 'xxx', '102611', 'Beijing', '13888889999');
在上面的例子中,由于参数非常多,我们在传递参数时就必须非常小心,一旦传错,整个信息就会错乱。于是我们找到了一个很好的解决办法,可以把这些参数当成一个对象来传递。
function getAddress(obj) {
this.country = obj.country;
this.name = obj.name;
this.city = obj.city;
this.street = obj.street;
this.code = obj.code;
this.province = obj.province;
this.tel = obj.tel;
}
getAddress({
street: 'xxx',
country: 'China',
name: 'zs',
city: 'Beijing',
code: '102611',
tel: '13888889999',
province: 'Beijing',
});
此时我们发现,当函数的参数是一个对象时,出错的几率大大降低,因为我们的参数可以调整顺序。对应的变量接收对应的参数。但是我们又发现,函数内部这一大坨东西依然很恶心,如果信息再多点,那岂不是......
这个时候,混入继承出现了。用代码表示就是:
function getAddress(obj) {
for (var key in obj) { // key 保存了 obj 中每一个属性的属性名
// 获取指定属性的值
this[key] = obj[key]; // this["street"] = obj["street"]
}
}
getAddress({
street: 'xxx',
country: 'China',
name: 'zs',
city: 'Beijing',
code: '102611',
tel: '13888889999',
province: 'Beijing',
});
这样下来,代码是不是简化了很多?
有的同学可能会问了,这仅仅是传递参数的情况,那我如何把一个对象中的属性和方法拷贝到另一个对象中呢?其实封装一下就好。
function mixin(target, source) {
for (var key in source) {
target[key] = source[key];
}
return target;
}
var obj1 = { name: 'zs', age: 18 };
var obj2 = {};
mixin(obj2, obj1);
简单来说,就是利用 for...in
循环,将源对象中的属性和方法拷贝到目标对象中,从而实现继承。
以上,就是关于混入继承,也称拷贝继承的实现方式。
3. 原型式继承(也叫经典继承)
混入继承很牛x,但是它也有一些问题,比如说:
var obj3 = { name:'ls', age: 18 };
var obj4 = { name: 'ww', gender: '女' };
mixin(obj4, obj3);
// obj4 = { name: "ls", gender: "女", age: 18 };
我们想要让 obj4
继承 obj3
的年龄,但是 obj4
的 name
也被覆盖了。有时候,我们仅仅想继承自身没有的属性,而保留自身已有的属性。这个时候,混入继承是做不到了,但原型式继承却可以。
原型式继承的大致思路就是让 obj3
和 obj4
之间产生继承关系,比如:
// 让 obj4 继承自 obj3
// obj4.__proto__ == obj3;
但是在这里我们不能直接使用 obj4.__proto__ == obj3
,因为 __proto__
属性时非标准属性,有浏览器兼容问题。此时,我们可以想到之前提到的继承方式:
function Person() {}
var p1 = new Person();
p1.__proto__ === Person.prototype;
为了实现上面的关系,我们可以进行相关转换,也就是通过某个构造函数,创建一个实例 obj4
,然后让构造函数的原型对象指向 obj3
。下面我们用代码来实现一下:
var obj3 = { name:'ls', age: 18 };
function F() {}
F.prototype = obj3; // 让 F 的实例可以访问到 obj3 的属性和方法
var obj4 = new F();
obj4.name = 'ww';
obj4.gender = '女';
console.log(obj4.name); // obj4 有自己的 name,打印 'ww'
console.log(obj4.gender); // obj4 有自己的 gender,打印 '女'
console.log(obj4.age); // obj4 没有自己的 age,访问 obj3 中的 age,打印 18
最后我们来总结一下原型式继承的功能:创建一个新的对象,让新的对象可以继承自指定的对象,从而新的对象可以访问到自己的属性和方法,也可以访问到指定对象的属性和方法。
其实,除了上面三种继承方法,还有比较经典的借用构造函数实现继承的方式,我会把它放在之后的文章中讲解。而基于原型链的继承,可能是我们会比较常用的继承方式,混入继承与经典继承,大家多理解就好,也许哪天面试就遇到了呢?
我说的不一定都对,你一定要自己试试。