JavaScript 继承的七种姿势:从“原型链”到“class”的进化史

昨天我们聊了原型链,知道了JS对象之间是怎么"攀亲戚"的。今天咱们来聊聊继承------也就是怎么让一个对象"认祖归宗",继承另一个对象的属性和方法。从最原始的手动操作,到ES6优雅的class语法,这中间有好几种姿势,每种都有自己的脾气。今天一次性给你盘清楚。

前言

继承在JS里就像"房产继承"------你想把老爹的房子传给孩子,但又不想直接把房子拆了重新盖。不同的继承方式,就像是不同的"过户"手段,有的简单粗暴,有的精细巧妙,有的会留下后遗症。

今天我们就来盘点JS里实现继承的七种方式,从最基础的到最完善的,让你在面试官问"JS继承有哪些方式"时,能从容应对,还能说出各自的优缺点。

一、原型链继承:最原始的"血脉相连"

这是JS里最基础的继承方式,核心就是让子类的原型指向父类的实例。

js 复制代码
function Animal() {
  this.name = '动物';
  this.colors = ['黑色', '白色'];
}
Animal.prototype.eat = function() {
  console.log('吃东西');
};

function Dog() {}
// 关键一步:让Dog的原型指向Animal的实例
Dog.prototype = new Animal();

const dog1 = new Dog();
const dog2 = new Dog();

dog1.colors.push('金色');
console.log(dog2.colors); // ['黑色', '白色', '金色'] ------ 哎呀,被改了!

优点:实现了方法的继承,写起来简单。

缺点

  • 引用类型的属性会被所有实例共享,一个改了大家都改。
  • 无法向父类构造函数传参。
  • 子类实例的构造函数被"篡改"成了Animal。

这种继承就像家族企业,祖宗留下的财产(比如房产证)大家共用,一个孙子把房子卖了,其他孙子都没了。

二、构造函数继承:借鸡生蛋

为了解决引用共享和传参问题,诞生了"借用构造函数"的方式,在子类构造函数里调用父类构造函数。

js 复制代码
function Animal(name) {
  this.name = name;
  this.colors = ['黑色', '白色'];
}
Animal.prototype.eat = function() {
  console.log('吃东西');
};

function Dog(name) {
  Animal.call(this, name); // 借调父类构造函数
}

const dog1 = new Dog('旺财');
const dog2 = new Dog('来福');

dog1.colors.push('金色');
console.log(dog1.colors); // ['黑色', '白色', '金色']
console.log(dog2.colors); // ['黑色', '白色'] ------ 没被影响!
console.log(dog1.eat); // undefined ------ 父类原型上的方法没继承到

优点

  • 解决了引用共享问题,每个实例有自己的属性副本。
  • 可以向父类传递参数。

缺点

  • 只能继承父类实例属性,继承不到父类原型上的方法。
  • 方法都在构造函数里定义,每次创建实例都会创建新方法,浪费内存。

这就像"借钱不借地",你把老爹的现金拿来了,但老爹的祖宅(原型上的方法)没拿到。

三、组合继承:取长补短

把原型链继承和构造函数继承结合起来,各取所长。

js 复制代码
function Animal(name) {
  this.name = name;
  this.colors = ['黑色', '白色'];
}
Animal.prototype.eat = function() {
  console.log('吃东西');
};

function Dog(name) {
  Animal.call(this, name); // 第二次调用父类
}
Dog.prototype = new Animal(); // 第一次调用父类
Dog.prototype.constructor = Dog; // 修正构造函数指向

const dog1 = new Dog('旺财');
const dog2 = new Dog('来福');

dog1.colors.push('金色');
console.log(dog1.colors); // ['黑色', '白色', '金色']
console.log(dog2.colors); // ['黑色', '白色']
dog1.eat(); // 吃东西 ------ 方法也继承到了

优点

  • 解决了引用共享问题。
  • 可以向父类传参。
  • 继承了父类原型上的方法。

缺点

  • 父类构造函数被调用了两次,造成了一定的性能浪费和属性冗余(实例上有,原型上也有)。

组合继承是JS里最常用的继承方式,虽然有小瑕疵,但足够好用。ES6的class本质上就是它的语法糖。

四、原型式继承:Object.create的雏形

这种方式不涉及构造函数,直接通过一个对象创建另一个对象。

js 复制代码
const animal = {
  name: '动物',
  colors: ['黑色', '白色'],
  eat() {
    console.log('吃东西');
  }
};

function createObject(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

const dog1 = createObject(animal);
const dog2 = createObject(animal);

dog1.colors.push('金色');
console.log(dog2.colors); // ['黑色', '白色', '金色'] ------ 又被共享了

ES5提供了Object.create()方法,就是干这个的。

js 复制代码
const dog1 = Object.create(animal);
const dog2 = Object.create(animal);

优点:不需要构造函数,直接基于已有对象创建新对象。

缺点:引用类型属性还是会被共享。

这种继承就像"克隆人",克隆体共享同一个原型,一个改了大家都改。

五、寄生式继承:给继承加个包装

在原型式继承的基础上,给新对象添加方法。

js 复制代码
function createDog(original) {
  const clone = Object.create(original);
  clone.bark = function() {
    console.log('汪汪汪');
  };
  return clone;
}

const animal = { name: '动物', eat() { console.log('吃东西'); } };
const dog = createDog(animal);
dog.bark(); // 汪汪汪

优点:可以在不修改原始对象的情况下添加新功能。

缺点:和原型式继承一样,引用共享问题依然存在;而且方法每次创建都会重新生成,没法复用。

六、寄生组合继承:最完美的姿势

组合继承的缺点是调用了两次父类构造函数。寄生组合继承解决了这个问题,它被认为是JS继承的"最佳实践"。

js 复制代码
function Animal(name) {
  this.name = name;
  this.colors = ['黑色', '白色'];
}
Animal.prototype.eat = function() {
  console.log('吃东西');
};

function Dog(name) {
  Animal.call(this, name); // 只调用一次父类
}

// 核心:用Object.create代替new Animal()
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
  console.log('汪汪汪');
};

const dog1 = new Dog('旺财');
const dog2 = new Dog('来福');

dog1.colors.push('金色');
console.log(dog2.colors); // ['黑色', '白色'] ------ 不受影响
dog1.eat(); // 吃东西
dog1.bark(); // 汪汪汪

优点

  • 父类构造函数只调用一次。
  • 原型链干干净净,没有冗余属性。
  • 既有自己的属性副本,又继承了原型方法。

寄生组合继承是目前最理想的继承实现方式,也是ES6 class 背后做的事情。

七、ES6 Class:语法糖的终极形态

ES6引入了class关键字,让继承写起来像其他语言一样优雅。

js 复制代码
class Animal {
  constructor(name) {
    this.name = name;
    this.colors = ['黑色', '白色'];
  }
  eat() {
    console.log('吃东西');
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name); // 调用父类构造函数
    this.breed = breed;
  }
  bark() {
    console.log('汪汪汪');
  }
}

const dog1 = new Dog('旺财', '土狗');
const dog2 = new Dog('来福', '金毛');

dog1.colors.push('金色');
console.log(dog2.colors); // ['黑色', '白色'] ------ 完美
dog1.eat(); // 吃东西
dog1.bark(); // 汪汪汪

优点

  • 语法清晰,易读易写。
  • 底层就是寄生组合继承,性能好。
  • 支持super关键字方便调用父类方法。

缺点

  • 本质还是原型那一套,但语法糖已经够甜了。

八、各种继承方式对比总结

继承方式 优点 缺点 适用场景
原型链继承 简单 引用共享、不能传参 基本不用
构造函数继承 解决引用共享、能传参 不能继承原型方法 基本不用
组合继承 两者优点都有 调用两次父类 以前常用
原型式继承 基于已有对象创建 引用共享 简单对象复用
寄生式继承 可添加新功能 引用共享 临时增强对象
寄生组合继承 完美 写法稍复杂 ES6之前的首选
ES6 class 语法优雅、标准 需要转译(老环境) 现代开发首选

九、实际开发中怎么选?

无脑选ES6 class 。除非你还要兼容IE这种古董,否则直接用classextends就完事了。不仅代码量少,而且不容易踩坑。

如果你好奇class底层干了啥,或者要写一些高阶的继承场景(比如混入multiple inheritance),那寄生组合继承的手写实现还是值得掌握的。

js 复制代码
function inherit(child, parent) {
  child.prototype = Object.create(parent.prototype);
  child.prototype.constructor = child;
}

这个工具函数,就是寄生组合继承的核心。

十、总结:从"手动挡"到"自动挡"

JS的继承演进史,其实就是一部从"手动挡"到"自动挡"的发展史:

  • 原型链继承是手动挡,操作复杂容易出事。
  • 组合继承是自动挡,但油耗(性能)稍高。
  • 寄生组合继承是CVT,平顺又高效。
  • ES6 class是智能驾驶,你只管踩油门,剩下的交给它。

无论哪种方式,底层都是原型链那一套。掌握了原型,继承的七种姿势不过是排列组合。以后面试官再问"JS继承有哪些方式",你可以从容地把这七种娓娓道来,顺便告诉他:"但实际开发,我选ES6 class。"

明天我们将进入JavaScript的另一个核心领域------异步编程,从回调地狱到Promise,再到async/await,带你彻底理清JS的异步世界。

如果你觉得今天的文章对你有帮助,点个赞让更多人看到。有疑问评论区见,我们明天见!

相关推荐
穷鱼子酱2 小时前
ElSelect二次封装组件-实现分页(下拉加载、缓存)、回显
前端
科科睡不着2 小时前
拆解iOS实况照片📷 - 附React web实现
前端
前端老兵AI2 小时前
Electron 桌面应用开发入门:前端工程师的跨平台利器
前端·electron
胖子不胖2 小时前
浅析cubic-bezier
前端
reasonsummer2 小时前
【办公类-133-02】20260319_学区化展示PPT_02_python(图片合并文件夹、提取同名图片归类文件夹、图片编号、图片GIF)
前端·数据库·powerpoint
胡耀超2 小时前
Web Crawling 网络爬虫全景:技术体系、反爬对抗与全链路成本分析
前端·爬虫·python·网络爬虫·数据采集·逆向工程·反爬虫
阿明的小蝴蝶2 小时前
记一次Gradle环境的编译问题与解决
android·前端·gradle
Ruihong2 小时前
【VuReact】轻松实现 Vue 到 React 路由适配
前端·react.js
山_雨2 小时前
startViewTransition
前端