本篇文章是基于《 JavaScript 高级程序设计》和《 ECMAScript 6 入门教程》,对 JavaScript 继承实现的七种方式(6+1)做了一个详细的整理,有需要的朋友可以参考一下,希望对你有所帮助,如果有不对的地方欢迎指正。
前言
继承是面向对象的三大基本特征之一,它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。大部分的面向对象语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。在 JavaScript 中,由于函数没有签名,因此无法实现接口继承,只支持实现继承,而其实现继承主要是依靠原型链来实现的。
01、原型链继承
代码示例:
function User() {
this.name = "用户名";
this.auth = [1];
}
User.prototype.getUserName = function () {
return this.name;
};
function Seller(sellerName, age) {
this.sellerName = sellerName;
this.age = age;
}
Seller.prototype.getSellerAge = function () {
return this.age;
};
Seller.prototype = new User("用户名");
Seller.prototype.getSellerName = function () {
return this.sellerName;
};
var seller_1 = new Seller();
var seller_2 = new Seller();
console.log(seller_1.getUserName()); // 用户名
console.log(seller_1.getSellerName()); // 张三
console.log(seller_1.getSellerAge()); // Uncaught TypeError: seller_1.getSellerAge is not a function
console.log(seller_2.getUserName()); // 用户名
console.log(seller_2.getSellerName()); // 李四
console.log(seller_2.getSellerAge()); // Uncaught TypeError: seller_2.getSellerAge is not a function
console.log(seller_1.auth); // [1]
console.log(seller_2.auth); // [1]
seller_1.auth.push(2, 3, 4);
console.log(seller_1.auth); // [1, 2, 3, 4]
console.log(seller_2.auth); // [1, 2, 3, 4]
上面的例子中,实现继承的核心语句就是 Seller.prototype = new User(),我们没有使用 Seller 默认提供的原型,而是将它的原型替换为 User 的实例,因此,新原型具有作为 User 实例所拥有的全部实现和方法,并且其内部还有一个指针,指向 User 的原型。
存在的问题:
- 在创建子类的实例时,不能向父类的构造函数中传递参数,也就是没有办法在不影响所有对象实例的情况下,给父类的构造函数传递参数。
- 通过原型链实现继承时,原型实际上会变成父类的实例,而父类的实例属性也会变成原型属性,如果该属性为引用类型时,所有子类的实例都会共享该属性,一个实例修改了该属性,其它实例也会发生变化。也就是上面例子中,在修改 seller_1 的 auth 属性后,seller_2 的 auth 属性也会跟随改变。
- 如果子类的原型对象上原本有属性和方法,在执行 Seller.prototype = new User() 后,之前的属性和方法将会丢失,这也就是上面例子中第 24 行和第 27 行报错的原因。
02、借用构造函数继承(伪造对象或经典继承)
为了解决原型链继承中包含引用类型值会被所有实例共享以及创建子类实例时无法向父类的构造函数传递参数这些问题,我们可以使用构造函数这种技术来实现继承。
代码示例:
function User(userName) {
this.name = userName;
this.auth = [1];
}
User.prototype.getName = function () {
return this.name;
};
function Seller(sellerName, age) {
User.call(this, sellerName); // 也可以使用 apply
this.age = age;
}
var seller_1 = new Seller("张三", 20);
var seller_2 = new Seller("李四", 25);
console.log(seller_1.name, seller_1.age); // 张三 20
console.log(seller_2.name, seller_2.age); // 李四 25
console.log(seller_1.auth); // [1]
console.log(seller_2.auth); // [1]
seller_1.auth.push(2, 3, 4);
console.log(seller_1.auth); // [1, 2, 3, 4]
console.log(seller_2.auth); // [1]
console.log(seller_2.getName()); // Uncaught TypeError: seller_2.getName is not a function
在上面的例子中,实现继承的核心语句就是 User.call(this, sellerName),即在子类构造函数的内部调用父类构造函数。通过使用 call() 或者 apply() 方法,在创建 Seller 实例时,调用 User 构造函数,这样一来,就会在新 Seller 对象上执行 User() 函数中定义的所有对象初始化代码。结果,Seller 的每个实例就都会具有自己的 name 和 auth 属性的副本了。
在调用 User() 函数时,我们还将 sellerName 作为一个参数传递进去,并赋值给 name 属性,因为在 Seller 构造函数内部调用 User 构造函数时,实际上是为 Seller 的实例设置了name 属性。所以为了确保 User 构造函数不会重写 Seller 的属性,可以在调用 User 构造函数后,再添加应该在 Seller 中定义的属性。
优势:
- 解决了原型链继承中引用类型在多个实例中共享的问题。
- 可以在子类构造函数中向父类构造函数传递参数。
存在的问题:
- 不能继承父类原型对象的属性和方法,这也就是上面例子中第 21 行报错的原因。
- 定义父类方法时,只能在构造函数中定义,这样在子类实例化时每次都会被重新定义,不能实现函数的复用。
03、组合继承(组合原型链继承和借用构造函数继承)
组合继承,有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
代码示例:
function User(userName) {
this.name = userName;
this.auth = [1];
console.log("run User"); // 检查 User 调用次数
}
User.prototype.getName = function () {
return this.name;
};
function Seller(sellerName, age) {
User.call(this, sellerName); // 也可以使用 apply
this.age = age;
}
Seller.prototype = new User();
Seller.prototype.getAge = function () {
return this.age;
};
var seller_1 = new Seller("张三", 20);
var seller_2 = new Seller("李四", 25);
console.log(seller_1);
console.log(seller_2);
console.log(seller_1.name, seller_1.age); // 张三 20
console.log(seller_2.name, seller_2.age); // 李四 25
console.log(seller_1.auth); // [1]
console.log(seller_2.auth); // [1]
seller_1.auth.push(2, 3, 4);
console.log(seller_1.auth); // [1, 2, 3, 4]
console.log(seller_2.auth); // [1]
console.log(seller_1.getName()); // 张三
console.log(seller_2.getName()); // 李四
console.log(seller_1.getAge()); // 20
console.log(seller_2.getAge()); // 25
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。
存在的问题:
重复调用父类构造函数,一次是在创建子类原型的时候,另一次是在子类构造函数内部,子类最终会包含父类对象的全部实例属性,但是需要在调用子类型构造函数时重写这些属性。这样就造成子类继承父类的属性,一组在子类实例上,一组在子类原型上。

如上图所示,我们创建了 seller_1 和 seller_2 两个 Seller 的实例,但是 User 函数却执行了三次,多出来的一次就是在执行 Seller.prototype = new User() 设置 Seller 原型时执行的。在下面打印的两个实例中,可以看到,在实例和原型(proto)中都存在 name 和 auth 属性。
04、原型式继承
原型式继承并没有使用严格意义上的构造函数。而是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
首先,我们创建一个 object 函数,在 object() 函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object() 对传入其中的对象执行了一次浅复制。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
var user = {
name: "用户名",
auth: [1],
};
var seller = object(user);
seller.name = "张三";
seller.auth.push(2, 3, 4);
var buyer = object(user);
console.log(user.auth); // [1, 2, 3, 4]
console.log(seller.auth); // [1, 2, 3, 4]
console.log(buyer.auth); // [1, 2, 3, 4]
在上面这个例子中,我们将 user 对象作为基础,传入到 object 函数中,object 函数返回一个新对象,然后再根据具体需求对这个新对象加以修改。这个新对象将 user 作为原型,所以它的原型中就包含一个基本类型值属性 name 和一个引用类型值属性 auth。这意味着 user.auth 不仅属于 user 所有,而且也会被 seller 以及 buyer 共享。实际上,这就相当于又创建了 user 对象的两个副本。
ECMAScript5 新增了 Object.create() 方法规范化了原型式继承。
var user = {
name: "用户名",
auth: [1],
};
var seller = Object.create(user);
seller.name = "张三";
seller.auth.push(2, 3, 4);
var buyer = Object.create(user);
console.log(user.auth); // [1, 2, 3, 4]
console.log(seller.auth); // [1, 2, 3, 4]
console.log(buyer.auth); // [1, 2, 3, 4]
存在的问题:
引用类型值的属性始终都会共享相应的值,容易被篡改。
05、寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,通过创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。
function createSeller(original) {
var clone = Object.create(original); // 通过调用函数创建一个新对象
clone.print = function() {
// 以某种方式来增强这个对象
console.log("Hello World!!!");
};
return clone; // 返回这个对象
}
var user = {
name: "用户名",
auth: [1]
};
var seller_1 = createSeller(user);
var seller_2 = createSeller(user);
seller_1.print(); // "Hello World!!!"
seller_1.print(); // "Hello World!!!"
seller_1.auth.push(2, 3, 4);
console.log(seller_1.auth); // [1, 2, 3, 4]
console.log(seller_2.auth); // [1, 2, 3, 4]
在上面的例子中,createSeller() 函数接收了一个参数,也就是将要作为新对象基础的对象。然后把这个对象传递给 Object.create(),生成一个新对象赋值给 clone 。再为 clone 添加一个新方法 print() ,最后返回 clone 对象。通过调用 createSeller() 函数生成 seller_1 和 seller_2 两个新对象,它们不仅具有 user 的所有属性和方法,而且还有自己的 print() 方法。
存在的问题:
-
引用类型值的属性会共享的问题依然存在;
-
函数不能复用。
06、寄生组合式继承
寄生式继承是与原型式继承紧密相关的一种思路,通过创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象。
前面说到,组合继承是 JavaScript 中最常用的继承模式,不过,它最大的问题就是会调用两次父类构造函数:一次是在创建子类原型的时候,另一次是在子类构造 函数内部。现在我们来看一下这个问题的终极解决方案,寄生组合式继承。
所谓寄生组合式继承,就是通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再 将结果指定给子类型的原型。下面我们来看一下完整的例子。
function inheritPrototype(child, parent) {
var prototype = Object.create(parent.prototype); // 创建对象
prototype.constructor = child; // 增强对象
child.prototype = prototype; // 指定对象
}
function User(name) {
this.name = name;
this.auth = [1];
console.log("run User"); // 检查 User 调用次数
}
User.prototype.getName = function() {
return this.name;
};
function Seller(name, age) {
User.call(this, name);
this.age = age;
}
inheritPrototype(Seller, User);
Seller.prototype.getAge = function() {
return this.age;
};
var seller_1 = new Seller("张三", 20);
var seller_2 = new Seller("李四", 30);
console.log(seller_1.getName()); // "张三"
console.log(seller_2.getName()); // "李四"
console.log(seller_1.getAge()); // 20
console.log(seller_2.getAge()); // 30
console.log(seller_1.auth); // [1]
console.log(seller_2.auth); // [1]
seller_1.auth.push(2, 3, 4);
console.log(seller_1.auth); // [1, 2, 3, 4]
console.log(seller_2.auth); // [1]
在上面的例子中,我们通过的 inheritPrototype() 函数实现了寄生组合式继承的最简单形式。这个函数接收两个参数:子类构造函数和父类构造函数。 在函数内部,第一步是创建父类原型的一个副本。第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor属性。最后一步,将新创建的对象(即副本)赋值给子类的原型。

如上图所示,我们创建了 seller_1 和 seller_2 两个实例,User 构造函数执行了两次,没有像原来那样执行多次,而且在 Seller.prototype 上面也没有创建多余的属性,与此同时,原型链仍然保持不变。完美的解决了组合继承的不足。
07、ES6中Class实现继承
在 ES6 中通过与 class 和 extends 关键字来定义类和继承类,这一点与传统的面向对象语言很相似,比起以上几种方式,要清晰方便很多。
class User {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
class Seller extends User {
constructor(name, age, earning) {
super(name, age);
this.earning = earning;
}
}
let seller = new Seller("张三", 30, "300.00");
console.log(seller);
在使用 ES6 实现继承时,有几点问题需要注意:
-
子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。
class User { constructor(name, age) { this.name = name; this.age = age; } } class Seller extends User { constructor(name, age, earning) { } } let seller = new Seller("张三", 30, "300.00"); // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor at new Seller上面代码中,Seller 继承了父类 User,但是它的构造函数没有调用 super 方法,导致新建实例时报错。这是因为 ES5 和 ES6 的继承机制有一个实质性的区别:ES5 的继承,是先创造子类的实例对象 this,然后再将父类的方法添加到 this 上面;ES6 的继承,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),然后再用子类的构造函数修改 this。
-
在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。
class User { constructor(name, age) { this.name = name; this.age = age; } } class Seller extends User { constructor(name, age, earning) { this.earning = earning; // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor at new Seller super(name, age); this.earning = earning; // 正确 } } let seller = new Seller("张三", 30, "300.00");上面代码中,子类的 constructor 方法没有调用 super 之前,就使用 this 关键字,结果报错,而放在super方法之后就是正确的。
小结
在 ES5 中最理想的继承方式应该就是寄生组合继承了,不过还要根据具体场景灵活运用。前端技术在飞速发展中, ES6 中通过与 class 和 extends 来实现类和继承类也越来越广泛。希望大家都能够在自己的项目中合理的运用继承哦。动动可爱的小手,关注一下公众号哦,会不定期推送新文章哦😊😊😊
