继承
Javascript中继承都基于两种方式:
1.通过原型链继承,通过修改子类原型的指向,使得子类实例通过原型链查找,实现继承父类的属性和方法。这种方式可以让所有实例共享原型链上的方法和属性。但当某个实例修改引用类型的属性时,其他实例也会受影响(引用地址相同)。
2.借用构造函数,通过apply、call方法在子类中调用父类构造函数,子类实例继承了父类构造函数的实例属性和方法。这个方式继承的属性和方法相互独立,但实例方法重复创建,没有共享。
- 原型链继承
原理:将父类的实例对象作为子类的原型对象。
原型链继承的实质是重写子类的原型对象,将父类的实例作为子类的新原型,于是子类新原型就继承了父类所有的属性和方法。
function Parent() {}; // 父类
function Child() {}; // 子类
var c1 = new Child(); // 继承前子类的实例
Child.prototype = new Parent();//将子类的原型重写为父类的实例
var p1 = new Child(); // 继承后子类的实例
// 没有修改Child.constructor时
console.log(c1.__proto__.constructor.prototype === p1.__proto__); // true
console.log(p1.__proto__.__proto__.constructor === Parent); // true
原型链继承.png
将父类的实例对象作为子类的原型对象,也就是将原来存在父类实例中的属性和方法,继承给了子类的原型对象。
当读取某个子类实例对象的属性时:1.首先在子类实例中查找;2.如果没找到,就到子类的原型(也就是父类的实例)中查找;3.如果没找到,就在父类的原型中查找;4.如果没找到,就到Object.prototype中查找。
function Parent () {
// 父类的实例属性
this.name = 'Parent'; //基本类型的实例属性,实例之间相互独立
this.age = [30, 35];// 引用类型的实例属性,所有实例共享
}
// 父类的原型方法
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {}
// 将父类的实例对象作为子类的原型对象
Child.prototype = new Parent();
// 子类原型的继承了父类实例的所有属性和方法
var p1 = new Child();
var p2 = new Child();
p1.getName(); // Parent, 子类继承了父类的原型方法
console.log(p1.age); // [30, 35],子类继承了父类的实例属性
// 修改子类实例的属性
p1.name = 'Tom'; // 修改基本类型的属性
p1.age.push(40); // 修改引用类型的属性
// 基本类型属性值相互独立
p1.getName(); // Tom
p2.getName(); // Parent
// 引用类型的属性被所有实例共享,修改某一实例的值会影响其他实例
console.log(p1.age); // [30, 35, 40]
console.log(p2.age); // [30, 35, 40]
子类覆盖父类中某个方法或添加父类中不存在的某个方法时,需要写在替换原型语句的后面。
并且不能使用字面量(相当于Object的实例)的方法创建子类原型方法,否则会覆盖之前的原型。
原型链继承的缺点:
1.使用原型链继承时,父类的实例成为子类的原型,父类的实例属性就变成了原型属性。而引用类型值的原型属性会被所有实例共享。
2.创建子类实例时,不能向父类构造函数传递参数。
- 借用构造函数继承
原理:在子类构造函数中,调用父类构造函数。(没有用到原型)
使用call()或apply()方法可以在创建子类实例时,调用执行父类构造函数,这样子类实例就会执行父类构造函数定义的初始化代码。
创建子类实例时,可以向父类传递参数。并且子类实例不会共享父类引用类型属性,因为继承的都是父类的实例属性和方法,不在原型链上。
function Parent(name) {
this.name = name;
this.age = [30, 35];
}
function Child(name) {
// 调用父类的构造函数
// Parent.call(this, name);
Parent.apply(this, arguments);
}
var p1 = new Child('Leo');
var p2 = new Child('Tom');
console.log(p1.name); // Leo
console.log(p1.age); // [30, 35]
p1.age.push(40);
console.log(p1.age); // [30, 35, 40]
console.log(p2.age); // [30, 35]不会共享父类引用类型属性
借用构造函数继承的缺点:
1.只能继承父类的实例属性和方法,不能继承原型属性和方法(没有使用原型)。
2.方法都在定义在构造函数中,每次创建实例都会创建一遍方法,无法实现函数复用,影响性能。
- 组合继承
原理:通过原型链实现原型属性和方法的继承,通过借用构造函数来实现实例属性和方法的继承。
一般在构造函数上定义实例属性,在原型上定义方法,通过组合继承既满足方法的复用,又保证实例有自己的属性。
function Parent(name) {
this.name = name;
this.colors = ['blue', 'red'];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child(name, age) {
// 调用父类的构造函数
Parent.call(this, name); // 第二调用Parent()
this.age = age;
}
// 原型链继承
Child.prototype = new Parent();// 第一调用Parent()
Child.prototype.constructor = Child;
var p1 = new Child('Leo', 26);
var p2 = new Child('Tom', 20)
p1.colors.push('green');
p1.getName(); // Leo
console.log(p1.age); // 26
console.log(p1.colors); // ["blue", "red", "green"]
console.log(p2.colors); // ["blue", "red"]
组合继承的缺点:
调用两次父类构造函数。第一次是在重写子类原型的时,将子类的原型指向父类实例,此时子类原型会继承父类所有的属性和方法(包括实例属性和方法及原型属性和方法)。第二次是在子类调用构造函数创建实例时,在子类构造函数中调用父类构造函数,此时新创建的子类实例对象的实例属性和方法(直接继承来自父类构造函数的实例属性和方法)会覆盖子类原型中的同名实例属性和方法。
- 原型式继承
原理:使用Object.create()方法,将传入的对象作为空构造函数的原型,并返回这个空构造函数的实例。
这种方法不需要创建构造函数,只需传入一个对象,就能从传入的对象衍生出新的对象。本质上还是通过原型链,实例的原型指向传入的对象。
Object.create()方法接受一个对象为参数,必须传一个对象否则报错,返回一个新的对象,传入的对象作为新对象的原型。
该方法还可接受第二个参数,该参数是属性描述对象,它所描述的对象属性,会添加到新对象。
Object.create()方法生成的对象,继承了它原型对象的构造函数。
// 模拟Object.create()
function create(obj) {
function F() {}
F.prototype = obj;
return new F();
}
使用Object.create()实现继承。
function Parent(name) {
this.name = name;
this.colors = ['blue', 'red'];
this.getName = function () {
console.log(this.name);
}
}
Parent.prototype.getColor = function () {
console.log(this.colors);
}
var p1 = new Parent('Leo');
// 使用Object.create()方法,需传入一个对象
var c1 = Object.create(p1);
var c2 = Object.create(p1);
c1.colors.push('green');
c1.getName();// Leo
c1.getColor(); // ["blue", "red", "green"]
c2.getColor(); // ["blue", "red", "green"]
原型式继承缺点:
传入的对象(原型)的引用类型属性,会被所有创建出来的实例共享。
- 寄生式继承
原理:创建一个仅用于封装继承过程的函数,在该函数内部创建一个新对象,再增强对象,最后返回这个新对象。
寄生继承的思路跟工厂模式差不多,就是调用一个仅用于封装继承过程的函数。
寄生继承不用实例化父类,直接实例化一个临时副本实现了原型链继承。
在该函数内部创建新对象的方法很多:可以使用Object.create(),new或者使用字面量创建。
function createObj(obj) {
// 创建新对象()
var clone = Object.create(obj);
// 增强这个对象
clone.sayHi = function() {
console.log('Hi,'+name);
}
// 返回这个对象
return clone;
}
寄生式继承缺点:
无法实现函数复用(封装在一个函数内,没有用到原型,和构造函数模式一样)。
- 寄生组合式继承
原理:组合继承的改进版,还是使用原型链来继承原型属性和方法,借用构造函数来继承实例属性和方法。但是,不将子类原型指向父类实例,而是创建一个空构造函数,将父类的原型指向这个空构造函数的原型,将子类的原型指向这个空构造函数的实例。从而实现子类原型间接继承父类原型。
// 1.模拟Object.create()
function object(o) {
function F() {}; // 创建空构造函数
F.prototype = o; // 将空构造函数的原型指向传入的对象
return new F(); // 返回构造函数的实例对象,该实例对象继承了传入对象的属性和方法
}
// 封装继承函数
function extend(Child, Parent) {
// 传入父类的原型,返回的实例obj继承了父类的原型属性和方法
var obj = object(Parent.prototype);
// 子类的原型指向obj,子类的原型间接继承了父类的原型属性和方法
Child.prototype = obj;
// 修正子类新原型(即obj对象)的constructor,使其指向子类构造函数
obj.constructor = Child;
}
// 2.封装成一个函数
function extend(Child, Parent) {
var F = function () {};
// 空构造函数的原型指向父类的原型,相当于复制了一份父类原型
F.prototype = Parent.prototype;
// 子类的原型指向空构造函数的实例,相当于子类原型间接继承了父类原型
Child.prototype = new F();
// 修正子类原型constructor指向
Child.prototype.constructor = Child;
}
// 3.直接使用Object.create()方法
function extend(Child, Parent) {
var obj = Object.create(Parent.prototype);
Child.prototype = obj;
obj.constructor = Child;
}
寄生组合继承.png
function Parent (name) {
this.name = name;
this.colors = ['blue', 'red'];
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child (name, age) {
// 通过借用构造函数继承父类的实例属性和方法
Parent.call(this, name);
this.school = age; // 添加新的子类属性
}
// 声明继承函数
function extend(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
// 寄生式继承父类原型属性和方法
extend(Child, Parent);
var p1 = new Child('Leo', 26);
var p2 = new Child('Tom', 20);
p1.colors.push('green');
p1.getName(); // Leo
console.log(p1.colors); //["blue", "red", "green"]
console.log(p2.colors); //["blue", "red"]
参考资料:
《JavaScript高级程序设计》
《JavaScript 标准参考教程》
网友评论