关于javascript继承首先需要了解原型,原型链,call以及new的相关知识。
简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
常见的继承方法有:
- 原型链继承
- 借用构造函数继承
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
原型链继承
基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
这里是利用原型让 Son.prototype 指向了 Father 的实例,从而继承了 Fahter 的方法和属性。
但是也存在问题:
- 最主要的问题来自包含引用类型值的原型。
例如这个例子中baseInfo是引用类型值,son1和son2通过继承,对其进行数据共享。从而相互影响。 - 在创建子类型的实例时,不能向超类型的构造函数中传递参数
function Father() {
this.greet = function fatherGreet() {
console.log('今天天气不错')
};
this.lastName = '王';
this.firstName = '爸';
this.baseInfo = {
height: 180,
weight: 70
}
this.sayName = function sayName() {
console.log(this)
console.log('我的名字叫' + this.lastName + this.firstName)
}
}
Father.prototype.fromFather = function fromFather() {
console.log('父辈会跳disco')
}
function Son(name) {
this.greet = function sonGreet() {
console.log("很高兴认识你")
};
this.firstName = name
}
Son.prototype = new Father(); //{1}
Son.prototype.fromSon = function fromSon() {
console.log('年轻人喜欢rap')
}
var son1 = new Son('儿一');
var son2 = new Son('儿二')
son1.sayName();
son1.greet();
son1.fromSon();
son1.fromFather();
console.log(son1.baseInfo);
son1.baseInfo.height = 185;
console.log(son2.baseInfo);


示例解析:
在代码{1}通过原型使 Son.prototype 指向 new Father 实例对象,从而实现继承。
当访问实例对象son1和son2上的方法或属性的时候,会遵循原型链查找的方式逐级向上查找。
例如,当调用 son1.fromFather() 时,会先在当前 son1 对象上进行查找,由图可见 son1 上并没有 sayName 方法。 继而通过 __proto__ 查找构造函数的原型,即 Son.prototype。可以看到Son.prototype 上继承了构造函数定义的方法和属性,并扩展定义了 fromSon 方法,但是没有 fromFather 方法。继而继续查找 Son.prototype.__proto__ 。由于代码 {1} 中指定了 Son.prototype 是构造函数 Father 的实例。所以 Son.prototype.__proto__ 指向 Father.prototype。在 Father.prototype 上定义了 fromFather 方法,到此为止。
同理,如果调用 son1.greet 方法,由于直接在 son1 对象上查找到该方法,继而不会继续沿着原型链进行查找。
由于 javascript 中引用类型数据是存储在堆内存中,如同所示。由于 baseInfo 是引用数据类型,所以 son1.baseInfo,son2.baseInfo 实际上都是指向堆内存中的 baseInfo。
所以它们的数据是共享了一份,因而数据操作的时候会相互影响。
借用构造函数
为解决在原型继承中包含引用类型值所带来的问题,可以借用构造函数来实现继承。基本思想是在子构造函数的内部调用超类型构造函数{1}。
借用构造函数实现继承有以下特点:
- 优点:可以在子类构造函数中向超类型构造函数传递参数 {1}
- 缺点:超类型原型中定义的方法对子类型是不可见的。即{3}中之所以调用超类型的原型上的方法会报错正是因为此原因导致的
function Father(name) {
this.greet = function fatherGreet() {
console.log('今天天气不错')
};
this.lastName = '王';
this.firstName = name;
this.baseInfo = {
height: 180,
weight: 70
}
this.sayName = function sayName() {
console.log(this)
console.log('我的名字叫' + this.lastName + this.firstName)
}
}
Father.prototype.fromFather = function fromFather() {
console.log('父辈会跳disco')
}
function Son(name) {
Father.call(this, name) // {1}
this.greet = function sonGreet() { //{2}
console.log("很高兴认识你")
};
}
Son.prototype.fromSon = function fromSon() {
console.log('年轻人喜欢rap')
}
var son1 = new Son('儿一');
var son2 = new Son('儿二');
son1.sayName(); //我的名字叫王儿一
son1.greet(); //很高兴认识你
son1.fromSon(); //年轻人喜欢rap
console.log(son1.baseInfo, son2.baseInfo);//{height: 180, weight: 70} {height: 180, weight: 70}
son1.baseInfo.height = 185;
console.log(son1.baseInfo, son2.baseInfo); //{height: 185, weight: 70} {height: 180, weight: 70}
console.log(son1.__proto__.__proto__ === Object.prototype) ; // true
son1.fromFather(); //{3} Uncaught TypeError: son1.fromFather is not a function


示例解析:
在构造函数 Son 中执行代码{1} ,借用构造函数 Father 实现对 Father 的继承。
在之后创建 Son 的示例对象的时候,会如图所示为 son1,son2 添加方法和属性,且 son1 ,son2 对象中的数据是独立存在的。 这样就解决了原型继承中包含引用类型数据带来的问题。
由于代码{2}执行在{1}之后,因而子类构造函数中定义的相同的方法会覆盖父类构造函数中相同的方法,比如此例中的 greet 方法。
超类型原型中定义的方法对子类型是不可见的。如图所示,son1, son2 的原型链上是不存在 Father.prototype。son1, son2 原型链查找顺序是:
son1 --> son1.__proto__ = Son.prototype --> Son.prototype.__proto__ = Object.prototype --> Object.prototype.__proto__ = null;
这也是为什么调用 son1.fromFather 会报错
组合继承
组合继承是结合了原型继承和借用构造函数继承两者,在{1}借用构造函数,在{2}利用原型,使两者结合实现继承。避免了两者的缺点,融合了它们的优势。是比较常见的继承方式。
但是同时也存在弊端:
- 在实现继承的过程中,调用两次超类型构造函数
- 子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性;比如下面示例中,
Son类中改写了greet方法;在调用greet方法的时候,会沿原型链查找的方式向上查找,只是优先查找到了Son类中被改写的方法。但是在整个完整的原型链中,存在多个greet方法。
function Father(name) {
this.greet = function fatherGreet() {
console.log('今天天气不错')
};
this.lastName = '王';
this.firstName = name;
this.baseInfo = {
height: 180,
weight: 70
}
this.sayName = function sayName() {
console.log(this)
console.log('我的名字叫' + this.lastName + this.firstName)
}
}
Father.prototype.fromFather = function fromFather() {
console.log('父辈会跳disco')
}
function Son(name) {
Father.call(this, name); //{1}
this.greet = function sonGreet() {
console.log("很高兴认识你")
};
}
Son.prototype = new Father(); //{2}
Son.prototype.constructor = Son; //{3}
Son.prototype.fromSon = function fromSon() {
console.log('年轻人喜欢rap')
}
var son1 = new Son('儿一');
var son2 = new Son('儿二')
son1.sayName();
son1.greet();
son1.fromSon();
console.log(son1.baseInfo, son2.baseInfo);
son1.baseInfo.height = 185;
console.log(son1.baseInfo, son2.baseInfo);
son1.fromFather();


示例解析:
组合继承是前两种继承的结合。 在 {1} 借用构造函数实现对 Father 继承(Son 构造函数将具备 Father 构造函数中的方法和属性,这样有效解决了包含引用数据类型的问题)。同时在 {2} 通过原型再次实现 Father 继承(可以通过原型链查找到 Father.prototype )。
对比该图和借用构造函数继承,红线部分 Son.prototype.__proto__ 指向 Father.prototype,正是因为 Son.prototype = new Father(); //{2}。因而 son1.fromFather() 能够正常调用。
原型式继承
实际上每次创建的过程都是创建一个新的构造函数,指定其原型对象,并创建实例对象返回的过程;
ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承
但是存在引用类型数据共享的问题
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
var Father = {
greet: function fatherGreet() {
console.log('今天天气不错')
},
lastName: '王',
firstName: '爸',
baseInfo: {
height: 180,
weight: 70
},
sayName: function () {
console.log(this)
console.log('我的名字叫' + this.lastName + this.firstName)
}
}
var son1 = object(Father)
var son2 = object(Father)
son1.sayName();
son1.greet();
console.log(son1.baseInfo, son2.baseInfo);
son1.baseInfo.height = 185;
console.log(son1.baseInfo, son2.baseInfo);
寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该
函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
在{1}调用object方法,创建一个实例对象,实例对象的__proto__指向超类型原型
在{2}为实例对象添加方法或者属性
在{3}返回实例对象
同样存在引用类型数据共享的问题
function object(o) {
function F() { } //{1}
F.prototype = o; //{2}
return new F(); //{3}
}
var Father = {
greet: function fatherGreet() {
console.log('今天天气不错')
},
baseInfo: {
height: 180,
weight: 70
}
}
function createSon(original){
var son = object(original) //{1}
son.playGame = function(){ // {2}
console.log('play gaming')
};
return son; //{3}
}
var son1 = createSon(Father)
var son2 = createSon(Father)
son1.greet();
console.log(son1.baseInfo, son2.baseInfo);
son1.baseInfo.height = 185;
console.log(son1.baseInfo, son2.baseInfo);
关于 object 函数:

寄生组合式继承
组合式继承优点很多,但是同时存在问题:
- 在实现继承的过程中,调用两次超类型构造函数
- 子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型 原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function object(o) {
function F() { }
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}
function Father(name) {
this.greet = function fatherGreet() {
console.log('今天天气不错')
};
this.lastName = '王';
this.firstName = name;
this.baseInfo = {
height: 180,
weight: 70
}
this.sayName = function () {
console.log(this)
console.log('我的名字叫' + this.lastName + this.firstName)
}
}
Father.prototype.fromFather = function () {
console.log('父辈会跳disco')
}
function Son(name) {
Father.call(this, name)
this.greet = function sonGreet() {
console.log("很高兴认识你")
};
this.firstName = name
}
inheritPrototype(Son, Father);
Son.prototype.fromSon = function () {
console.log('年轻人喜欢rap')
}
var son1 = new Son('儿一')
var son2 = new Son('儿二')
son1.sayName();
son1.greet();
console.log(son1.baseInfo, son2.baseInfo);
son1.baseInfo.height = 185;
console.log(son1.baseInfo, son2.baseInfo);
图解inheritPrototype:
