在 JavaScript 这门语言中,继承是一个绕不开的话题。不同于 Java、C++ 等传统面向对象语言的"类继承",JavaScript 采用的是基于**原型(Prototype)**的继承机制。这种机制既灵活又强大,但也常常让初学者感到困惑。
本文将带你从基础出发,深入剖析 JavaScript 中的继承方式,并重点讲解 call 和 apply 在构造函数继承中的妙用。文章结合原理、代码示例与思考,助你真正掌握 JS 继承的本质。
一、原型继承:JS 的根基
JavaScript 中每个函数都有一个 prototype 属性,每个对象都有一个内部属性 [[Prototype]](可通过 __proto__ 访问),指向其构造函数的 prototype 对象。
javascript
function Animal(name) {
this.name = name;
}
Animal.prototype.say = function() {
console.log(`${this.name} is an animal.`);
};
const dog = new Animal('Dog');
dog.say(); // Dog is an animal.
这就是最经典的原型链继承 。子类实例通过 __proto__ 指向父类的 prototype,从而实现方法共享。
但原型继承有一个明显问题:无法在不创建父类实例的情况下,向父类构造函数传递参数 。这引出了我们接下来要讲的------构造函数继承。
二、构造函数继承:借助 call / apply
构造函数继承的核心思想是:在子类构造函数中,调用父类构造函数,并将 this 指向子类实例。
这就需要用到 Function.prototype.call 或 apply:
javascript
function Animal(name) {
this.name = name;
}
function Dog(name, breed) {
// 关键:使用 call 将 Animal 的 this 指向当前 Dog 实例
Animal.call(this, name);
this.breed = breed;
}
const myDog = new Dog('Buddy', 'Golden Retriever');
console.log(myDog.name); // Buddy
console.log(myDog.breed); // Golden Retriever
✨ call vs apply:区别在哪?
call(thisArg, arg1, arg2, ...):逐个传参apply(thisArg, [arg1, arg2, ...]):以数组形式传参
两者都能指定函数执行时的 this 指向,且立即执行函数 。在继承场景中,call 更常用,因为参数通常是已知的。
📌 关键点 :
call/apply并不会建立原型链!它们只是"借用"了父类构造函数来初始化子类实例的属性。
三、组合继承:原型 + 构造函数
既然原型继承能共享方法,构造函数继承能传参,那能不能两者结合?
当然可以!这就是经典的 组合继承(Combination Inheritance) :
javascript
function Animal(name) {
this.name = name;
}
Animal.prototype.say = function() {
console.log(`${this.name} says hello!`);
};
function Dog(name, breed) {
Animal.call(this, name); // 构造函数继承:传参 + 初始化属性
this.breed = breed;
}
Dog.prototype = new Animal(); // 原型继承:共享方法
Dog.prototype.constructor = Dog; // 修正 constructor 指向
const dog = new Dog('Max', 'Husky');
dog.say(); // Max says hello!
这种方式几乎完美,但有一个小瑕疵:父类构造函数被调用了两次 (一次在 new Animal() 设置原型,一次在 Dog 内部)。
四、更优雅的方式:寄生组合继承
为了解决重复调用问题,我们可以使用 寄生组合继承(Parasitic Combination Inheritance) ------ 也是现代框架(如 React 早期)推荐的方式。
核心思想:不通过 new Parent() 设置子类原型,而是用一个空函数作为中介。
这正是你上传的 2.html 中提到的:"利用空对象作为中介"。
javascript
function inheritPrototype(Child, Parent) {
const F = function() {}; // 空构造函数
F.prototype = Parent.prototype;
Child.prototype = new F(); // 避免调用 Parent()
Child.prototype.constructor = Child;
}
function Animal(name) {
this.name = name;
}
Animal.prototype.say = function() {
console.log(`${this.name} speaks.`);
};
function Cat(name, color) {
Animal.call(this, name);
this.color = color;
}
inheritPrototype(Cat, Animal);
const kitty = new Cat('Luna', 'white');
kitty.say(); // Luna speaks.
这种方式只调用一次父类构造函数,效率更高,是目前最推荐的继承模式。
五、ES6 Class:语法糖下的本质
ES6 引入了 class 语法,让继承看起来更"传统":
scala
class Animal {
constructor(name) {
this.name = name;
}
say() {
console.log(`${this.name} talks.`);
}
}
class Bird extends Animal {
constructor(name, wingspan) {
super(name); // 等价于 Animal.call(this, name)
this.wingspan = wingspan;
}
}
但请注意:class 本质上仍是基于原型的语法糖 。super() 底层依然是通过 call 调用父类构造函数。
理解底层机制,才能在遇到边界情况(如 this 绑定、混入 Mixin、动态继承)时游刃有余。
六、思考:为什么 JS 的继承如此特别?
JavaScript 的继承不是"复制",而是"链接"。它强调**行为委托(delegation)**而非"拥有"。
- 原型链:对象 → 原型 → 原型的原型......直到
null call/apply:临时改变上下文,实现"借用"- 组合继承:兼顾属性初始化与方法复用
这种设计赋予 JS 极大的灵活性,也带来了学习曲线。但一旦掌握,你就能写出更高效、更可维护的代码。
结语
继承不是目的,复用与扩展才是 。无论是古老的原型链,还是现代的 class,理解其背后的运行机制,才能真正驾驭 JavaScript。
💡 建议 :不要死记语法,多动手画原型链图,多调试
this指向。真正的高手,看得见"看不见的链接"。
欢迎点赞、收藏、评论交流!
如果你觉得这篇文章对你有帮助,不妨分享给正在学习 JS 的朋友。前端路上,我们一起成长 🌱