不要因为没有掌声,就放弃了最初的理想
继承本质
软件中的继承是对真实世界继承的一种抽象。在面向对象语言中,提到继承,我们先引出 类 这个概念。
类 是什么?
类 是一种自定义的数据类型,每个类可以包含一组数据类型和操作数据的方法。字符串、数字就是数据类型,当然数据类型有很多,我们可以有不同的组合方式,这样组合出来的复杂数据类型,我们就称之为类,通常我们还会向里面添加操作数据的方法。
面向对象语言中的 继承
面向对象语言中的 继承 ,本质上就是 复制 。子类将父类复制一份,那么父类上有的属性、方法,在子类上也就有了,我们就说子类 继承 了父类。
Javascript 中的 继承
很可惜,在Javascript中并没有 类 这个概念,在这里万物皆对象,所以自然不存在 类复制 这一说法。那Javascript中的 继承 是怎样做到的呢?答案是 复制对象 和 委托关联 。当然最好的实现方式是 委托关联,就是利用原型链机制来达到所谓的 继承 。简单理解就是,有一个公有的对象,它上面存有属性和操作方法,别的对象都可以去它上面拿到想拿的东西,它在这里共享,这种模式就是Javascript中的 继承 本质。
当然上面只是简单分析,下面深入原理。不过在此之前,因为本篇是讲 继承,我们顺便也讲讲对应面向对象语言另外两大特征:封装 和 多态 在Javascript中的实现方式。还是按 封装、继承、多态 的顺序讲吧,下面就一起进入丛林探险。

封装
顾名思义,封装 就是将数据密封起来,让外界访问不到,然后我们对外只提供操作数据的方法。封装 的目的在于隐藏内部实现,保护数据安全,避免错误的修改数据。
在Javascript中,我们利用 闭包 来实现数据的 封装 。举例如下:
function person(){
let obj = {
name: 'Tom',
age: 18
};
return {
get(){
return obj.name + '====' + obj.age
},
set(name, age){
obj.name = name;
obj.age = age;
}
}
}
var p = person();
p.get(); // "Tom====18"
p.set('Bob', 20);
p.get(); // "Bob====20"
上面我们无法直接访问obj中的数据,对数据的访问只能通过返回的方法来进行读取和操作。当然闭包有闭包的好处,不过也不要滥用,要及时消除闭包。
继承
这里不会讲 class、extends 这种语法糖,因为 class 的本质还是对象(万物皆对象),我们只讲模拟实现 继承 的五种方式。
1. 显示混入
function mixin( sourceObj, targetObj ) { // 混入的方法
for (var key in sourceObj) { // 只会在不存在的情况下复制
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Animal = {
eyes: 2,
eat: function() {
console.log( "Animal love eat." );
}
};
var Cat = mixin( Animal, {
tail: 1, // 猫有1条尾巴
eat: function() {
Animal.eat.call(this);
console.log( "Cat also love eat." );
}
});
需要注意的就是,这里的 复制 是 浅复制(浅拷贝),当然安全起见还是进行 深拷贝 。
2.寄生继承
显式混入模式的一种变体,它既是显式的又是隐式的。
function Animal() {
this.eyes = 2;
}
Animal.prototype.eat = function() {
console.log( "Animal love eat." );
};
//“寄生类”Cat
function Cat() { // cat 也是一种 Animal
var cat = new Animal();
cat.tail = 1;
var animalEat = cat.eat;
cat.eat = function() {
animalEat.call( this );
console.log( "Cat also love eat." );
}
return cat;
}
var myCat = new Cat();
myCat.eat(); // Animal love eat. Cat also love eat.
如你所见,首先我们 复制 了一份 Animal 父类(对象)的定义,然后 混入 Cat 子类(对象)的定义,然后再用这个复合对象构建实例。
3.隐式混入
var Animal = {
eat: function() {
this.kind = 'Animal'
}
};
Animal.eat();
Animal.kind; // "Animal"
var Cat = {
eat: function() { // 隐式把 Animal 混入 Cat
Animal.eat.call(this);
}
};
Cat.eat();
Cat.kind; // "Animal"
通过在方法调用中使用 Animal.eat.call(this),我们把 Animal 的行为 混入 到了 Cat 中。
4.原型继承
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
Foo.call( this, name );
this.label = label;
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.myLabel = function() {
return this.label;
};
var a = new Bar( "a", "obj a" );
a.myName(); // "a"
a.myLabel(); // "obj a"
典型的“原型风格”,核心部分 Bar.prototype = Object.create( Foo.prototype ),
即创建一个新的 Bar.prototype 对象并把它关联到 Foo. prototype。
5.对象关联
var Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
Bar = Object.create( Foo );
Bar.speak = function() {
console.log( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak(); // Hello, I am b1. Hello, I am b2.
我们利用 [[Prototype]] 委托,实现了三个对象之间的关联,代码简洁易懂。
多态
多种状态,指同一个实体同时具有多种形式。比如水有三种形态,液态、气态、固态。我们简单理解就是子类都由父类派生,但它们的表现形式不同。在代码中的具体体现就是子类方法的覆写,大家都懂的,这里就不做赘述了。
最后
这里注意两个概念:
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。
Tips: 千万不要按字面意思理解,将instanceof理解为谁是谁的实例,将isPrototypeOf理解为谁是谁的原型。
