前言
作者最近在看神三元大佬的JS灵魂之问系列同中的“JS如何实现继承这部分”时,感触很深,又结合了一下《你不知道的JavaScript》,准备借这篇文章对这部分内容进行总结输出,一方面可以巩固自己的记忆,另一方面可以给部分想要了解这个知识的jym进行参考。
这篇文章会参考部分神三元大佬的内容,但是在阅读过《你不知道的JavaScrip》和问了很多次DeepSeek后,加入了很多自己的解读而且还会拓展一些知识,会讲的很细,保证大家能够理解!
继承特点是什么
首先我们先思考一下,继承的特点是什么
继承的核心特点可以概括为:
- 代码复用:子类能够继承父类的属性和方法,避免重复编写相同代码
- 隔离性:子类是一个独立的实例,在子类上的操作不会影响父类
- 多态支持:子类可以重写父类方法,实现不同行为
- 扩展能力:子类可以在继承基础上添加新属性和方法
我们接下来实现时则需要思考这些点。
es5 如何实现继承的
在ECMAScript6之前,JavaScript中的继承可谓是非常的繁琐的,有各种各样的继承:
1. call/apply继承
实现原理:在子类构造函数中用call/apply调用父类构造函数
function Parent(name) {
this.name = name;
this.getName = function() {
return this.name;
}
}
function Child(name) {
Parent.call(this, name); // 核心代码
this.type = 'child';
}
let child = new Child('Tom');
console.log(child.name); // Tom
但这样子做会出现一个问题,子类无法继承父类的方法:
Parent.prototype.sayHi = function(){
console.log("hi!");
}
Child.sayHi() //报错:child.sayHi is not a function
2.原型链继承
实现原理:将子类的原型对象指向父类的实例
function Parent() {
this.name = 'parent';
this.play = [1, 2, 3];
}
Parent.prototype.sayHi = function(){
console.log("hi")
}
function Child() {
this.type = 'child';
}
Child.prototype = new Parent();
var s1 = new Child();
s1.sayHi() //“hi” 能够继承父类的方法
这样子虽然解决了不能继承方法的缺点,但是还是会出现问题:
var p = new Child(); //父类实例
var s1 = new Child(); //子类1实例
var s2 = new Child(); //子类2实例
s1.play.push(4); //操作子类1的属性
// 影响了父类属性
console.log(p.play); //[1,2,3,4]
console.log(s1.play) //[1,2,3,4]
console.log(s2.play) //[1,2,3,4]
这不符合继承的隔离性:子类是一个独立的实例,在子类上的操作不会影响父类
3.组合继承
实现原理:结合原型链继承和构造函数继承
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
}
function Child(name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
}
var child1 = new Child('Tom', 20);
child1.colors.push('green');
console.log(child1.colors); // ["red","blue","green"]
child1.sayName(); // Tom
child1.sayAge(); // 20
var child2 = new Child('Jack', 18);
console.log(child2.colors); // ["red","blue"]
可以看到,组合式继承可以完美地解决上述问题,实现了继承所有特性
- 代码复用:子类能够继承父类的属性和方法,避免重复编写相同代码
- 隔离性:子类是一个独立的实例,在子类上的操作不会影响父类
- 多态支持:子类可以重写父类方法,实现不同行为
- 扩展能力:子类可以在继承基础上添加新属性和方法
可是它的实现仍然存在一个问题:你会发现这里调用了两次父类构造函数(Parent.call(this) + new Parent())
- 第一次:
Parent.call(this)(在子类构造函数里) - 第二次:
Child.prototype = new Parent()(设置子类原型时) - 这会导致 子类原型上多出一份冗余的父类实例属性(虽然实例属性会覆盖原型上的同名属性,但仍然浪费内存)。
4.组合继承改进方案(最推荐)
实现原理:使用 Object.create() 替代 new Parent(),避免重复调用父类构造函数:
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 只在这里调用一次 Parent
this.age = age;
}
// 关键改进:用 Object.create 建立原型链,避免 new Parent()
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
console.log(this.age);
};
const child1 = new Child('Tom', 20);
child1.colors.push('green');
console.log(child1.colors); // ["red", "blue", "green"]
child1.sayName(); // "Tom"
child1.sayAge(); // 20
const child2 = new Child('Jack', 18);
console.log(child2.colors); // ["red", "blue"]
在改进之后,
- 不再调用
new Parent(),而是直接让Child.prototype继承Parent.prototype,避免了冗余的父类实例属性。
通常来说,这一版的继承方案就是我们最推荐的ES5中的继承方案!
说说上述 Object.create(Parent.prototype) 的作用
Object.create() 方法创建一个新对象,并使用传入的对象作为这个新对象的原型(__proto__)。
Child.prototype = Object.create(Parent.prototype);
这行代码做了以下事情:
- 创建一个空对象:
let newObj = {} - 设置这个空对象的原型:将这个空对象的
__proto__指向Parent.prototype。即newObj.__proto__ = Parent.prototype。 - 将这个新对象赋值给
Child.prototype:现在,Child.prototype是一个空对象,但它的原型是Parent.prototype。
这样做的巨大好处:
- 实现了原型链继承:现在,任何通过
new Child()创建的实例,它的原型链是:
child1->Child.prototype(一个空对象) ->Parent.prototype->Object.prototype->null
因此,child1可以顺利找到并调用Parent.prototype.sayName方法。 - 避免了执行父构造函数:
Object.create()不会调用Parent构造函数,所以Child.prototype这个对象本身是干净的,没有多余的name和colors属性。父构造函数只在Child构造函数内部通过Parent.call(this, name)被调用,用于初始化实例自身的属性。
es6是如何实现继承的
ES6发布之后,JavaScript规范中多了一个"语法糖"——class关键字,它很大程度上模仿了面向过程语言的规范,其中就包括了类继承要用到的关键字extends1!
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log('Parent name:', this.name);
}
static staticMethod() {
console.log('static method');
}
}
class Child extends Parent {
constructor(name, age) {
super(name); // 调用父类的constructor
this.age = age;
}
sayAge() {
console.log('Child age:', this.age);
}
}
const child = new Child('Tom', 18);
child.sayName(); // Parent name: Tom
child.sayAge(); // Child age: 18
Child.staticMethod(); // static method
es6的继承源码解读
ES6的class语法本质上是语法糖,其底层仍然是基于原型链实现的。我们可以通过Babel等工具查看其转译后的ES5代码:
"use strict";
function _typeof(obj) { /*...*/ }
// 关键:继承部分
// 实现类继承的辅助函数
function _inherits(subClass, superClass) {
// 类型检查:确保父类是函数或null
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
// 创建子类原型对象,继承父类原型方法
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass, // 重置构造函数指向子类
writable: true,
configurable: true
}
});
// 设置子类的__proto__继承父类(用于静态方法继承)
if (superClass) _setPrototypeOf(subClass, superClass);
}
// 设置对象原型的兼容性写法
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p; // 兼容旧环境
return o;
};
return _setPrototypeOf(o, p);
}
// 创建父类构造函数的引用(用于super调用)
function _createSuper(Derived) { /*...*/ }
// 父类构造函数
var Parent = function Parent(name) {
_classCallCheck(this, Parent); // 类型检查
this.name = name;
};
// 父类原型方法
Parent.prototype.sayName = function sayName() {
console.log('Parent name:', this.name);
};
// 父类静态方法
Parent.staticMethod = function staticMethod() {
console.log('static method');
};
// 子类定义
var Child = /*#__PURE__*/function (_Parent) {
_inherits(Child, _Parent); // 继承父类
var _super = _createSuper(Child); // 创建super引用
function Child(name, age) {
_classCallCheck(this, Child);
// 调用父类构造函数
_super.call(this, name);
this.age = age;
}
return Child;
}(Parent);
这里的代码可能一些基础不太好的朋友们看起来比较吃力,建议大家可以去用ai软件解读一下这里面的每个部分,实际上就是一些api的调用规则,作者也是一边叫ai解读一边理解的!
这里由于篇幅有限,作者就给出里面最关键的两段代码深度解读一下:
一、_inherits实现继承中的原型更改
function _inherits(subClass, superClass) {
// 创建子类原型对象(核心继承逻辑)
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
// 设置静态方法继承(关键扩展点)
if (superClass) _setPrototypeOf(subClass, superClass);
}
这里的_inherits函数的目的就是为了使用原型链的方式继承(这里就是我们上面提到的4.组合继承):
首先接受两个参数subClass表示子类, superClass表示父类
Object.create()中:
-
第一个参数: 使用&&表达式,解决当superClass传入为空时,属性
superClass.prototype报错:不能读取空对象的prototype的情况。 -
第二个参数:属性描述符,目的是用于修正constructor指向
-
效果:
subClass.prototype = Object.create(...)创建 subClass.prototype 对象,该对象原型链指向父类原型,同时保证 constructor 正确指向子类
二、子类定义
var Child = /*#__PURE__*/function (_Parent) {
_inherits(Child, _Parent); // 继承父类
var _super = _createSuper(Child); // 创建super引用
function Child(name, age) {
_classCallCheck(this, Child);
// 调用父类构造函数
_super.call(this, name);
this.age = age;
}
return Child;
}(Parent);
-
开始的
/*#__PURE__*/注释,这有助于打包工具进行tree-shaking -
该函数第一行 使用了
一、_inherits实现继承中的原型更改的方法,通过更改原型链来继承父类, -
var _super = _createSuper(Child);的部分是实现了super的定义 -
function Child(name, age)这部分是实现子类构造函数的定义,方便我们创建实例的时候用
经典问题解析
为什么ES6继承必须先调用super?
ES6的继承机制与ES5完全不同,ES6是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。如果不调用super方法,子类就得不到this对象。
ES6的class是真正的类吗?
ES6的class本质上仍然是基于原型的继承,只是语法糖。JavaScript中没有真正的类,class语法只是让原型继承的写法更加清晰、更像面向对象编程的语法而已。
什么时候可以省略constructor?
如果子类没有显式定义constructor方法,这个方法会被默认添加。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。
总结
JavaScript的继承机制经历了一个从复杂到简洁的演进过程:
- ES5时代:我们需要通过原型链、构造函数、组合继承等多种方式手动实现继承,最完美的方式是组合式继承
- ES6时代:通过class和extends关键字简化了继承的实现,但其本质仍然是基于原型的继承
理解JavaScript继承的关键在于深入理解原型链机制。ES6的class语法虽然让代码更加简洁易读,但背后仍然是基于原型的继承模型。掌握这些底层原理,对于编写高质量的JavaScript代码至关重要。
在实际开发中,建议:
- 现代项目优先使用ES6的class和extends
- 需要兼容老旧环境时,可以使用寄生组合式继承
- 理解底层原理,避免因误解class机制而导致的问题
JavaScript的继承机制体现了这门语言的灵活性和强大功能,虽然它的实现方式与传统的面向对象语言有所不同,但通过深入理解原型链,我们可以充分利用这些特性构建出优雅高效的代码结构。