万字解析《JS 语言精粹》之第五章:继承 5 大核心精髓(JS 原型核心)

我敢说,80% 的前端同学刚学 JS 继承的时候,都是拿着 Java 的思路往里套,我当年也一样。

当初从 Java 转写前端,我天真地以为继承就是子类 extends 父类那套逻辑。直到第一次写构造函数漏写了new,把 window 上的全局变量给改了,对着页面 bug 排查了一下午,才彻底意识到:JS 的继承,内核和类语言完全是两码事。

后来翻《JavaScript 语言精粹》的继承章节,越看越觉得有意思:JS 压根就没打算走 "类" 的老路,基于原型的特性玩出了好几种代码复用的模式,灵活度比类继承高得多。很多人天天写 ES6 的 class,却连底层原型继承的逻辑都没捋顺,面试一考手写继承就卡壳。

今天我把书中的 5 种继承模式结合自己这些年的实操踩坑整理出来,每种都配可运行的精简代码、高频踩坑点和面试常考方向,看完不仅能搞定手写继承的面试题,日常开发选技术方案也能心里有底。

先聊透:JS 的继承和 Java 不是一回事

在 Java 这类基于类的语言里,继承干了两件事:一是代码复用,子类复用父类逻辑,减少重复代码;二是类型系统规范,有编译期检查,不用写强制类型转换。

但 JS 是弱类型语言,根本不存在类型转换这回事。对 JS 的对象来说,重要的是它能做什么,而不是它从哪个类继承来的。所以 JS 的继承,核心目标只有一个:代码复用

而且 JS 走的是 "原型继承" 的路子 ------ 对象直接从另一个对象继承属性,不需要中间夹一个 "类" 的概念。所谓的 class,本质上只是给原型套了层大家眼熟的语法糖而已。

1. 伪类继承:最眼熟的写法,坑也最多

所谓 "伪类",就是 JS 为了迎合大家写类的习惯,搞出来的一套 "构造函数 + prototype" 的模拟写法。看着像 Java 的类,实则底层还是原型。

核心逻辑

每个函数创建的时候,都会自带一个prototype对象,里面有个constructor指回函数本身。当我们用new调用这个函数时,会生成一个新对象,这个对象会继承构造函数prototype上的所有属性。

想实现继承,就把子类的prototype替换成父类的实例,这样子类实例就能顺着原型链拿到父类的方法。

javascript 复制代码
// 父类构造函数
function Mammal(name) {
  this.name = name;
}

Mammal.prototype.get_name = function () {
  return this.name;
};

Mammal.prototype.says = function () {
  return this.saying || '';
};

// 子类构造函数
function Cat(name) {
  this.name = name;
  this.saying = 'meow';
}

// 核心:子类原型替换为父类实例,打通原型链
Cat.prototype = new Mammal();

// 给子类扩展自有方法
Cat.prototype.purr = function (n) {
  let s = '';
  for (let i = 0; i < n; i++) {
    s += s ? '-r' : 'r';
  }
  return s;
};

Cat.prototype.get_name = function () {
  return `${this.says()} ${this.name} ${this.says()}`;
};

// 测试
const myCat = new Cat('Henrietta');
console.log(myCat.says());     // 'meow'
console.log(myCat.purr(5));    // 'r-r-r-r-r'
console.log(myCat.get_name()); // 'meow Henrietta meow'

为了写起来更优雅,还可以封装一个inherits方法支持链式调用,省去每次手动赋值原型的麻烦:

javascript 复制代码
Function.prototype.inherits = function (Parent) {
  this.prototype = new Parent();
  return this;
};

// 链式写法清爽很多
const Cat = function (name) {
  this.name = name;
  this.saying = 'meow';
}.inherits(Mammal)

踩坑合集(当年我全踩过)

  1. 漏写 new 直接污染全局 这是最经典的坑:不用 new 调用构造函数,this会直接绑定到全局对象(浏览器里是 window),相当于悄咪咪修改全局变量。既没有编译警告也没有运行时报错,排查起来巨酸爽。行业约定构造函数首字母大写,就是为了靠肉眼发现漏写 new 的情况,但这只是人为规范,不是语法约束。
  2. 所有属性全公开,毫无隐私可言不管属性还是方法,全挂在 this 和原型上,外部随便改随便访问。想做私有属性只能靠下划线命名 "自欺欺人",懂的都懂。
  3. 没法优雅调用父类方法子类重写父类方法后,想再调用父类原方法,得手动去父类原型上 call,写起来又丑又绕。
  4. 容易诱导写出过度复杂的继承层级看着像类,很多人就照着 Java 的思路写三四层继承链,最后原型链绕得自己都看不懂,完全违背了 JS 的设计初衷。

面试高频考点:new 操作符的执行过程、原型链的指向规则、构造函数继承的缺点

2. 对象说明符:一个被忽略的传参优化技巧

这个不算严格的继承模式,但属于构造函数的实用优化技巧,日常开发百分百用得到。

核心逻辑

如果构造函数要传的参数特别多,挨个传不仅要记顺序,漏传、错传概率极高。不如直接传一个对象作为 "配置说明符",参数名和值一一对应,顺序随便排,还能方便地设默认值。

javascript 复制代码
// 糟糕的写法:参数多了根本记不住顺序
// const myObject = maker(first, last, city, state, age);

// 舒服的写法:传对象,可读性拉满
const myObject = maker({
  first: '张',
  last: '三',
  city: '北京',
  state: '北京',
  age: 25
});

实用场景

  • 封装组件、工具类的时候,配置项超过 3 个基本都会用这种写法
  • 和 JSON 数据无缝配合:后端返回的 JSON 可以直接丢进构造函数生成带方法的对象,不用挨个字段赋值

说句实在的,现在大家写代码基本都默认这么干了,但很多人不知道这其实是继承章节里提的经典最佳实践。

3. 原型式继承:回归本质,对象直接继承对象

如果抛开 "类" 的执念,JS 最原生的继承玩法其实特别简单:拿一个现成的对象当原型,生成新对象,再按需修改差异部分。这就是原型式继承,也叫差异化继承。

核心逻辑

不用写构造函数,不用折腾 prototype,先做一个能用的基础对象,然后基于这个对象生成新实例,再给新实例加自己的属性和方法就行。Object.create就是干这个事的。

javascript 复制代码
// 先做一个基础对象
const myMammal = {
  name: 'Herb the Mammal',
  get_name() {
    return this.name;
  },
  says() {
    return this.saying || '';
  }
};

// 基于基础对象生成新对象
const myCat = Object.create(myMammal);
// 只修改差异化的部分,其他全继承基础对象
myCat.name = 'Henrietta';
myCat.saying = 'meow';
myCat.purr = function (n) {
  let s = '';
  for (let i = 0; i < n; i++) {
    s += s ? '-r' : 'r';
  }
  return s;
};
myCat.get_name = function () {
  return `${this.says()} ${this.name} ${this.says()}`;
};

适用场景

这种模式特别轻量,适合不需要复杂类结构的场景。比如解析嵌套作用域的时候,内层作用域继承外层作用域的变量,用原型式继承写起来非常丝滑:

javascript 复制代码
function block() {
  const oldScope = scope;
  // 新作用域继承老作用域的所有属性
  scope = Object.create(scope);
  
  advance('{');
  parse(scope);
  advance('}');
  
  // 退出作用域,恢复原环境
  scope = oldScope;
}

认知刷新点:很多人觉得 "继承必须有类",其实在 JS 里,对象直接继承对象才是最原汁原味的玩法。很多简单的复用场景,根本没必要硬写一个类出来。

面试高频考点:Object.create 的实现原理、原型式继承和原型链继承的区别

4. 函数化模式:真正实现私有属性的继承方案

前面几种模式都有一个通病:做不到真正的私有属性。而函数化模式利用闭包,完美解决了这个问题,也是我个人觉得最巧妙的一种模式。

核心逻辑

用一个工厂函数来生成对象,全程不用 new,依靠闭包持有私有变量,只有特权方法能访问内部状态。整个流程分四步:

  1. 创建一个新对象
  2. 定义私有变量和内部函数(闭包持有,外部访问不到)
  3. 给新对象加特权方法,能访问私有变量
  4. 返回这个对象
javascript 复制代码
// 父级工厂函数
function mammal(spec) {
  // spec里的参数全靠闭包持有,外部直接访问不到
  const that = {};
  
  // 特权方法:唯一能访问内部状态的入口
  that.get_name = function () {
    return spec.name;
  };
  that.says = function () {
    return spec.saying || '';
  };
  
  return that;
}

// 子级工厂函数
function cat(spec) {
  spec.saying = spec.saying || 'meow';
  // 直接调用父级工厂生成基础对象,不用搞原型链那套
  const that = mammal(spec);
  
  // 扩展子类自有方法
  that.purr = function (n) {
    let s = '';
    for (let i = 0; i < n; i++) {
      s += s ? '-r' : 'r';
    }
    return s;
  };
  that.get_name = function () {
    return `${that.says()} ${spec.name} ${that.says()}`;
  };
  
  return that;
}

// 测试,全程不用new
const myCat = cat({ name: 'Henrietta' });
console.log(myCat.get_name()); // 'meow Henrietta meow'
// 直接访问name根本访问不到,真正的私有
console.log(myCat.name); // undefined

如果想调用父类被重写的方法,还可以封装一个superior方法,优雅实现 super 的效果:

javascript 复制代码
Object.prototype.superior = function (name) {
  const that = this;
  const method = that[name];
  return function () {
    return method.apply(that, arguments);
  };
};

// 子类调用父类方法
function coolcat(spec) {
  const that = cat(spec);
  const super_get_name = that.superior('get_name');
  
  that.get_name = function () {
    return `like ${super_get_name()} baby`;
  };
  return that;
}

const myCoolCat = coolcat({ name: 'Bix' });
console.log(myCoolCat.get_name()); // 'like meow Bix meow baby'

模式优势

  • 真正的私有性:内部状态完全靠闭包持有,外部无法直接修改,对象是 "防伪" 的
  • 不用 new,杜绝 new 的坑:工厂函数调用方式和普通函数没区别,不会污染全局
  • 灵活度高:继承逻辑就是普通函数调用,想怎么组合就怎么组合

个人感悟:当初为了实现 JS 私有属性,又是 Symbol 又是 WeakMap 绕了半天,后来才发现函数化模式早就把闭包私有玩明白了。这种模式写出来的代码安全性很高,适合做 SDK、底层库这类对封装性要求高的场景。

面试高频考点:JS 实现私有属性的方式、闭包在继承中的应用、特权方法的概念

5. 部件模式:组合优于继承,给对象 "加装" 能力

继承不是代码复用的唯一答案。很多时候,我们不需要 "是一个" 的继承关系,只需要给对象 "加装" 一些能力就行,这就是部件模式,也叫 Mixin 模式。

核心逻辑

写一个个独立的功能部件函数,接收一个对象,给这个对象添加上对应的方法,然后返回原对象。需要什么能力就加什么能力,不用搞复杂的继承谱系。

比如我们做一个事件处理的部件,能给任意对象加上onfire事件方法:

javascript 复制代码
function eventuality(that) {
  const registry = {}; // 私有事件注册表
  
  // 触发事件
  that.fire = function (event) {
    const type = typeof event === 'string' ? event : event.type;
    if (!registry[type]) return this;
    
    registry[type].forEach(handler => {
      const func = typeof handler.method === 'string' 
        ? this[handler.method] 
        : handler.method;
      func.apply(this, handler.parameters || [event]);
    });
    return this;
  };
  
  // 注册事件
  that.on = function (type, method, parameters) {
    const handler = { method, parameters };
    registry[type] ? registry[type].push(handler) : registry[type] = [handler];
    return this;
  };
  
  return that;
}

用的时候直接给对象 "装上" 就行:

javascript 复制代码
// 给普通对象加事件能力
const myObj = eventuality({});
myObj.on('say', () => console.log('hello'));
myObj.fire('say'); // 输出 hello

也可以在构造函数内部调用,给生成的实例统一加装能力。

设计思路:这就是 "组合优于继承" 的典型实践。JS 的弱类型在这里反而是优势 ------ 不用纠结类型继承关系,对象有对应的方法就能用,需要什么能力拼什么能力,比一层层继承灵活太多。

日常应用:Vue 的 Mixin、很多工具库的能力混入、TS 里的接口合并,本质都是这个思路。


最后复盘:这么多模式,该怎么选?

聊了五种模式,最后给大家捋捋,方便按需取用:

  1. 伪类继承:适合团队习惯类写法、用 ES6 class 的场景,知道底层是原型就行,注意规避 new 的坑。
  2. 对象说明符:通用优化技巧,参数多的时候无脑用,大幅提升代码可读性。
  3. 原型式继承 :轻量对象复用首选,简单直接,Object.create随手就能写。
  4. 函数化模式:需要严格私有属性、高安全性的场景优先选,闭包实现真私有。
  5. 部件模式:功能复用优先用组合,别硬套继承,复杂场景比多层继承好维护得多。

其实学 JS 的继承,最忌讳的就是抱着类语言的思维不放。JS 的核心是原型和对象,继承的本质就是代码复用,哪种方式能优雅解决问题,就是好方式。现在大家天天写的 class,本质就是伪类模式的语法糖,把这些底层模式搞懂了,再去看寄生组合继承、class 继承原理这些面试题,一眼就能看透本质。

接下来如果想深入,可以自己手写一遍寄生组合继承,再去看看 ES6 class 的 babel 编译结果,把今天讲的知识点串起来,JS 原型这块基本就通透了。

相关推荐
时光足迹1 小时前
极光推送全攻略(上):被iOS证书折磨了三天,我写了一份前端也能看懂的避坑指南
前端·ios·uni-app
DyLatte1 小时前
AI 时代,最危险的不是被替代,而是努力不沉淀
前端·后端·程序员
mCell2 小时前
【锐评】桌面端技术营销:别拿跑分当工程判断
前端·rust·electron
柒和远方2 小时前
从一次工程审查看 AI 学习产品的边界兜底:RAG 资料链路一致性实战
前端·后端·架构
疯狂的魔鬼2 小时前
一个"懂分寸"的文本省略组件是怎样炼成的
前端·vue.js·设计
angerdream2 小时前
手把手编写儿童手机远程监控App之vue3 AI Gent
前端
李明卫杭州2 小时前
CSS BFC 完全指南:从原理到实战,彻底搞懂这个"结界"
前端
裕波2 小时前
AI 正在重写应用开发。Vue 与 Vite,给出新的答案。
javascript·vue.js
Momo__2 小时前
MDN MCP Server——Mozilla 把 Web 文档接进 AI Agent,从此 LLM 不再瞎编 API
前端·ai编程·mcp