深入理解 JavaScript 继承:从原型链到 call/apply 的灵活运用

在 JavaScript 这门语言中,继承是一个绕不开的话题。不同于 Java、C++ 等传统面向对象语言的"类继承",JavaScript 采用的是基于**原型(Prototype)**的继承机制。这种机制既灵活又强大,但也常常让初学者感到困惑。

本文将带你从基础出发,深入剖析 JavaScript 中的继承方式,并重点讲解 callapply 在构造函数继承中的妙用。文章结合原理、代码示例与思考,助你真正掌握 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.callapply

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 的朋友。前端路上,我们一起成长 🌱


相关推荐
dorisrv1 小时前
CSS Grid + Flexbox响应式复杂布局实现
前端
前端灵派派1 小时前
openlayer选择移动图标
前端
禅思院1 小时前
vite项目hmr热更新问题
前端·vue.js·架构
dorisrv1 小时前
TRAE SOLO 正式版:AI全链路开发的新范式 🚀
前端·trae
小明记账簿_微信小程序1 小时前
antd v3 select自定义下拉框内容失去焦点时会关闭下拉框
前端
前端老宋Running1 小时前
告别“祖传”defineProperty!Vue 3 靠 Proxy 练就了什么“神功”?
前端·vue.js·面试
码途进化论1 小时前
前端Docker多平台构建自动化实践
前端·javascript·后端
dorisrv1 小时前
React轻量级状态管理方案(useReducer + Context API)
前端
qq_316837751 小时前
uniapp 缓存请求文件时 判断是否有文件缓存 并下载和使用
前端·缓存·uni-app