原稿:语雀 · JS继承 · 原目录:JavaScript › JS继承
红宝书第八章写的真的很不错,第四版更新了ES6的继承,这里总结下✍
ES5
在ES5中没有正式的class,所有的类都是用于创建特定类型对象的构造函数,同时有以下事实 2. JS中每个函数都具有一个prototype属性,该属性是一个指向函数原型对象的引用 4. 原型对象会获得constructor属性,该属性是指回函数的引用 6. 每个被构造函数创建的实例有一个属性__proto__,该属性是指向对应原型对象的引用
上面的构造函数和原型构成了JS继承的基础
原型链继承
基本思想:创建父类实例作为子类的原型对象,该原型对象的引用变量和方法可被子类实例共享,从而实现继承
代码说明
// 父类声明
function Vehicle(){
this.superName = "Vehicle";
this.colors = ["red","blue"];
}
Vehicle.prototype.getSuperName = function(){
return this.superName;
}
// 子类声明
function Bus(){
this.subName = "Bus";
}
// 继承
Bus.prototype = new Vehicle();
// 新建方法
Bus.prototype.getSubName = function(){
return this.subName;
}
// 子类实例
let bus1 = new Bus();
console.log(bus1.getSuperName() + '-' + bus1.getSubName()); // Vehicle-Bus
优点:父类原型上的方法可以被子类实例共享,做到函数复用
局限: 2. 共享原型对象使得对不同引用值的修改也被共享:因为JS是值传递,对于原始值,每个实例拷贝一份,各自独立;但对于引用值,每个实例拷贝了不同的引用,但都指向内存中一个对象,对象上的更改通过每个引用都能看到(见下方举例) 4. 子类型实例化时不能给父类型构造函数传参
let bus1 = new Bus();
let bus2 = new Bus();
console.log(bus1.superName, bus2.getSuperName());// Vehicle Vehicle
console.log(bus1.colors);// ['red', 'blue']
bus2.superName = "changed";
bus2.colors.push("green");
console.log(bus1.superName, bus2.getSuperName());// Vehicle changed
console.log(bus1.colors);// ["red", "blue", "green"]
盗用构造函数
基本思想:在子类构造函数中调用父类构造函数,使父类构造函数this被替换为子类实例,以此解决引用值共享与向父类传参的问题
代码说明
// 父类声明
function Vehicle(newColor){
this.superName = "Vehicle";
this.colors = ["red","blue"];
this.colors.push(newColor);
}
Vehicle.prototype.getSuperName = function(){
return this.superName;
}
// 子类声明
function Bus(newColor){
// 继承
Vehicle.call(this, newColor)
this.subName = "Bus";
}
Bus.prototype.getSubName = function(){
return this.subName;
}
// 子类实例
let bus1 = new Bus("bus1's color");
let bus2 = new Bus("bus2's color");
console.log(bus1);
console.log(bus1.colors);// ["red", "blue", "bus1's color"]
console.log(bus2.colors);// ["red", "blue", "bus2's color"]
// 无法访问父类原型
console.log(bus1.getSuperName());// Uncaught TypeError
优点:可以向父类构造函数传参,且子类实例持有独立的引用值
局限:子类实例无法访问父类原型,因此必须在父类构造函数中通过this定义要继承的方法,再通过子类实例调用,不能重用函数
组合继承
基本思想:使用原型链继承父类上的可共享的属性和方法,通过盗用构造函数继承不希望共享的父类属性
代码说明
// 父类声明
function Vehicle(newColor){
this.superName = "Vehicle";
this.colors = ["red","blue"];
this.colors.push(newColor);
}
Vehicle.prototype.getSuperName = function(){
return this.superName;
}
// 子类声明
function Bus(newColor){
// 继承
Vehicle.call(this, newColor)
this.subName = "Bus";
}
// 继承
Bus.prototype = new Vehicle();
// 新建方法
Bus.prototype.getSubName = function(){
return this.subName;
}
// 子类实例
let bus1 = new Bus("bus1's color");
let bus2 = new Bus("bus2's color");
console.log(bus1);
console.log(bus1.colors);// ["red", "blue", "bus1's color"]
console.log(bus2.colors);// ["red", "blue", "bus2's color"]
console.log(bus1.getSuperName());// Vehicle
优点:继承父类的同时既能够做到函数复用,又能实现引用值的独立性
局限:父类构造函数总是会被调用两次,一次子类原型赋值时调用,一次在子类构造函数中调用,因此会在实例和子类原型上各创建一组父类实例属性,产生冗余
原型式继承
基本思想:原型式继承无需自定义类型,可以根据已有的实例创建子类实例,但子类实例间共享信息。其中引入了一个临时构造函数object(),它接受一个对象并返回以此为原型的实例
代码说明
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
// 父类实例
let bus = {
name: "bus",
colors: ["red","blue"]
};
// 子类实例
let bus1 = object(bus);
bus1.colors.push("green");
// 子类实例
let bus2 = object(bus);
bus2.colors.push("black");
console.log(bus1.colors); //["red", "blue", "green", "black"]
console.log(bus2.colors); //["red", "blue", "green", "black"]
优点:无需创建自定义类型
局限:子类实例之间会共享引用值,且如果要为子类实例添加函数,无法复用
寄生式继承
基本思想:与原型式继承类似,利用一个仅用于封装继承过程的函数,在函数内部以某种方式增强对象,最后再返回对象
代码说明
function createAnother(original) {
// 通过调用函数创建一个新对象
let clone = object(original);
// 以某种方式增强这个对象
clone.speedup = function () {
console.log("speed up!");
};
return clone; // 返回这个对象
}
let bus = {
name: "bus",
colors: ["red", "blue"],
};
let bus1 = createAnother(bus);
bus1.speedup(); // speed up!
优点:无需创建自定义类型
局限:寄生过程中无法做到函数复用
寄生式组合继承
基本思想:组合式继承存在两次调用父类构造函数的问题。寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。混合式原型链不必为了指定子类原型而调用父类构造函数,只需要一个父类原型副本替换父类实例的角色。本质上,就是使用寄生式继承来继承父类原型,然后再将结果指定给子类型的原型
代码说明
核心是inheritPrototype(subType, superType) 2. 通过临时构造函数创建父类原型副本 4. 让返回的副本constructor指向子类,否则会指向父类 6. 让子类的prototype指向父类原型副本
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象,创建父类原型副本
prototype.constructor = subType; // 增强对象,解决由于重写原型导致constructor失配问题
subType.prototype = prototype; // 赋值对象,代替之前的子类原型赋值
}
// 父类声明
function Vehicle(newColor){
this.superName = "Vehicle";
this.colors = ["red","blue"];
this.colors.push(newColor);
}
Vehicle.prototype.getSuperName = function(){
return this.superName;
}
// 子类声明
function Bus(newColor){
Vehicle.call(this, newColor)
this.subName = "Bus";
}
inheritPrototype(Bus, Vehicle)
// 新建方法
Bus.prototype.getSubName = function(){
return this.subName;
}
// 子类实例
let bus1 = new Bus("bus1's color");
let bus2 = new Bus("bus2's color");
console.log(bus1);
console.log(bus1.colors);// ["red", "blue", "bus1's color"]
console.log(bus2.colors);// ["red", "blue", "bus2's color"]
console.log(bus1.getSuperName());// Vehicle
对比组合继承 2. 组合继承子类原型是父类实例;寄生式组合继承子类原型是寄生后父类原型副本,避免在子类原型上创建冗余属性 4. 组合继承中子类原型是父类实例,访问子类原型constructor会沿原型链找到父类构造函数;寄生式组合继承在inheritPrototype中直接修改子类原型constructor指向子类构造函数
小结
ES6
类
ES6原生支持了class关键字,类实际就是函数,但是类语法明确了存在于实例、原型、类上的成员
- 实例成员:在constructor()通过this为实例添加的属性,每个实例都有唯一成员对象,所有实例不会通过this定义的属性。
- 原型成员:为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法
- 类成员:静态方法使用static关键字,其中的this引用类自身,通常用于执行不特定于实例的操作,不要求存在类实例
继承
- ES6使用extends关键字继承,可以继承类或者普通的构造函数,其背后依旧基于“寄生式组合继承”
- ES6继承要求子类必须在constructor()中调用super()后才能使用this。因为本质上讲,ES5的继承是先创建子类对象,将之替换为父类的this;然而ES6的继承没有创建子类实例而是继承父类的this对象,然后对其使用this进行加工
// 父类声明
class Vehicle {
// 类构造函数,其成员不可共享
constructor(newColor) {
this.superName = "Vehicle";
this.colors = ["red", "blue"];
this.colors.push(newColor);
}
// 原型方法,可共享
getSuperName() {
return this.superName;
};
static locate() {
console.log("class Vehicle");
};
}
// 子类声明
class Bus extends Vehicle {
constructor(newColor) {
super(newColor);
this.subName = "Bus";
}
getSubName() {
return this.subName;
};
static locate() {
console.log("class Bus");
};
}
// 子类实例
let bus1 = new Bus("bus1's color");
let bus2 = new Bus("bus2's color");
console.log(bus1);
console.log(bus1.colors); // ["red", "blue", "bus1's color"]
console.log(bus2.colors); // ["red", "blue", "bus2's color"]
console.log(bus1.getSuperName() + "-" + bus1.getSubName()); // Vehicle-Bus
console.log(Bus.locate()); // class Bus
小结
ES6继承与ES5继承对比 2. ES5的继承是先创建子类对象,将之替换为父类的this
ES6继承父类的this对象,然后在子类对其使用this进行加工 2. ES5原型链中子类原型是父类实例,子类构造函数和父类构造函数无直接关系,因此需要使用call()
ES6具有双重继承关系:除了原型对象间继承,子类本身是父类构造的实例
下面对比ES6继承和ES5寄生式组合继承:


参考链接:
详解ES5和ES6的继承 - Annikaa - 博客园 ES5继承 构造函数、原型和实例的关系:每一个构造函数都有一个原型对象,每一个原型对象都有一个指向构造函数的指针,而每一个实例都包含一个指向原型对象的内部指针, 原型链实现继承 基本思想:利用原型让一 博客园ES6的继承 ES5的继承的实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。另一个需要注意的地方是,在子类的构造函数中,只有调用super之后才可以使用this关键字,… SegmentFault