面试官问我原型链,我反手给他 new 了一辆小米 SU7

11 阅读6分钟

在前端领域,有一个永远绕不过去的概念——原型(prototype)
不论你在写 Vue、React、Node,甚至是读某个库的源码,原型相关的实现都随处可见。但很多人学得云里雾里:什么是构造函数?什么是 prototype?为什么实例有 __proto__?它和 class 有什么关系?

没关系,今天我们通过一个轻松的例子——如何 new 一辆小米 SU7——从根上把这些问题讲透,彻底聊聊前端面试中“最熟悉的陌生人”——原型(Prototype)与原型链

一、 造车的起步:构造函数(Constructor)

假设我们要用代码来模拟小米汽车的生产过程。每一辆下线的 SU7 都是独一无二的实体(实例),但它们又有着相同的出厂标准。

在 ES5 时代,我们会这样写:

JavaScript

// 1.js - 构造函数
function Car(color) {
    // 构造函数内部的 this,指向即将被 new 出来的新实例对象
    this.name = 'su7';
    this.color = color;
    this.height = 1.4;
    this.weight = 1.5;
    this.long = 4800;
}

const car1 = new Car('霞光紫');
const car2 = new Car('海湾蓝');

console.log(car1.color); // 霞光紫
console.log(car2.color); // 海湾蓝

1.1 new 的魔法

这里的 Car 就是构造函数。当我们使用 new 关键字调用它时,发生了什么?

  1. 创建:内存中创建了一个新的空对象。
  2. 绑定:函数体内的 this 指向了这个新对象。
  3. 执行:执行构造函数代码,给这个新对象添加属性(如 color, name)。
  4. 返回:返回这个新对象(即实例)。

这就好比工厂的流水线,模具(构造函数)一压,一辆崭新的车(实例)就诞生了。

二、 拒绝浪费:引入 Prototype(原型)

如果仅仅只有构造函数,会有一个巨大的性能浪费。

假设每辆车都需要一个 drive(驾驶)功能。如果在构造函数里写 this.drive = function() {...},那么生产 10 万辆车,内存里就会创建 10 万个一模一样的 drive 函数。这显然是不合理的——车漆颜色可以是私有的,但驾驶系统应该是全系共享的。

这时候,Prototype(原型) 登场了。

JavaScript

// 共享的方法和属性,挂载到原型上
Car.prototype = {
    drive() {
        console.log('启动,下赛道!');
    },
    brand: 'Xiaomi', // 所有车都贴小米标
    system: 'HyperOS'
}

car1.drive(); // 启动,下赛道!
car2.drive(); // 启动,下赛道!

核心概念:

  • 构造函数(Constructor) :负责配置实例私有的属性(如颜色、车架号)。
  • 原型对象(Prototype) :负责配置所有实例公共的方法和属性(如驾驶功能、车机系统)。

这种设计非常巧妙。它不是把功能复制给每一个实例,而是告诉实例:“如果你自己没有这个功能,就去我的‘公共库’(原型)里找。”

三、 哲学思考:原型的本质

这里我们稍微拔高一点,聊聊编程哲学。

传统的面向对象语言(如 Java, C++)是基于类(Class) 的。它们的关系是血缘继承,像铸造模具一样,定义好了模板,实例化就是复制模板。

而 JavaScript 是基于原型(Prototype) 的。这更像是一种委托关系

一个很有趣的比喻:“孔子是韩国的”。

这其实在调侃一种归属感。在 JS 里,属性的归属是动态查找的。

  • Class 派:我是你爸爸,我的基因直接刻在你身体里。
  • Prototype 派:我不是你爸爸,但我可以是你师傅。你身上没有的技能,可以来找我借。

JS 的这种设计更灵动。实例和原型之间不是复制关系,而是链接关系

四、 寻根问底:原型链与 __proto__

既然说是“链接”,那这条线到底在哪里?

我们在控制台打印一下实例:

JavaScript

console.log(car1);

你会发现对象里有一个隐藏属性:[[Prototype]](在浏览器中通常显示为 __proto__)。

4.1 也就是那个著名的三角关系:

  1. prototype:是函数特有的属性。它指向一个对象(原型对象),用于存储共享属性。
  2. __proto__ :是对象(实例)特有的属性。它指向创建该对象的构造函数的原型。
  3. constructor:原型对象上默认有一个 constructor 属性,指回构造函数本身。

用代码验证一下(参考笔记 3.html & 4.html):

JavaScript

function Person(name, age) {
    this.name = name;
}
Person.prototype.species = '人类';

const gg = new Person('GGbond');

// 1. 实例的 __proto__ 指向 构造函数的 prototype
console.log(gg.__proto__ === Person.prototype); // true

// 2. 原型对象的 constructor 指回 构造函数
console.log(Person.prototype.constructor === Person); // true

// 3. 顺藤摸瓜:Object.getPrototypeOf 是获取原型的标准方法
console.log(Object.getPrototypeOf(gg) === Person.prototype); // true

4.2 查找规则:就近原则

当我们访问 gg.species 时,JS 引擎会怎么做?

  1. 搜自身:先看 gg 实例本身有没有 species 属性?没有。
  2. 搜原型:顺着 gg.__proto__ 找到 Person.prototype,这里有吗?有!输出“人类”。

如果我也设置了同名属性呢?(参考笔记 5.html)

JavaScript

const xx = new Person('小呆呆');
xx.species = '是头猪'; // 实例上设置自有属性

console.log(xx.species); // 输出 '是头猪'
// 这叫“属性遮蔽(Shadowing)”。我自己有了,就不麻烦原型了。

// 但原型被修改了吗?没有。
console.log(xx.__proto__.species); // 依然是 '人类'

4.3 链条的终点

如果我们请求一个根本不存在的属性 gg.fly,引擎会怎么找?

  1. gg 自身 -> 无。

  2. Person.prototype -> 无。

  3. Person.prototype 也是个对象,它也有 __proto__。它的原型是谁?

    • 所有普通对象默认都是 new Object() 生成的。
    • 所以指向 Object.prototype
  4. Object.prototype -> 无。

  5. 再往上?Object.prototype.__proto__ 是什么?

    • null

null 表示“这里没有对象了”,查找停止,返回 undefined

这就是原型链: 一条由 __proto__ 串联起来的寻宝路径,终点是 null

五、 继承的演变

理解了原型链,继承就很好理解了。所谓的继承,无非就是让子类的原型,指向父类的实例(或原型)。

JavaScript

// 参考 7.html 的逻辑
function Animal() {}
Animal.prototype.species = '动物';

function Person() {}

// 关键一步:Person 的原型变成了 Animal 的实例
// 这样 Person 的实例就能顺着链条找到 Animal.prototype
Person.prototype = new Animal(); 

var qq = new Person();
console.log(qq.species); // '动物'

当然,现在 ES6 的 class extends 语法糖让这一切变得更简单,但作为资深工程师,你必须明白:ES6 class 的底层,依然是这套原型链机制。

总结

回到最开始的小米 SU7。

  • 构造函数是那个高科技工厂,负责 new 出具体的车。
  • 实例是你买回家的那辆具体的车(拥有独一无二的车架号)。
  • 原型是这辆车背后的设计图纸和软件系统,所有车共享。
  • 原型链是当车机系统在一个功能里找不到代码时,它会去底层系统找,去内核找,直到找不到为止。

JavaScript 的世界里,没有复杂的血缘阶级,只有简单直接的链接与委托。这就是 JS 原型的美感所在。