在前端领域,有一个永远绕不过去的概念——原型(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 关键字调用它时,发生了什么?
- 创建:内存中创建了一个新的空对象。
- 绑定:函数体内的
this指向了这个新对象。 - 执行:执行构造函数代码,给这个新对象添加属性(如
color,name)。 - 返回:返回这个新对象(即实例)。
这就好比工厂的流水线,模具(构造函数)一压,一辆崭新的车(实例)就诞生了。
二、 拒绝浪费:引入 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 也就是那个著名的三角关系:
prototype:是函数特有的属性。它指向一个对象(原型对象),用于存储共享属性。__proto__:是对象(实例)特有的属性。它指向创建该对象的构造函数的原型。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 引擎会怎么做?
- 搜自身:先看
gg实例本身有没有species属性?没有。 - 搜原型:顺着
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,引擎会怎么找?
-
找
gg自身 -> 无。 -
找
Person.prototype-> 无。 -
Person.prototype也是个对象,它也有__proto__。它的原型是谁?- 所有普通对象默认都是
new Object()生成的。 - 所以指向
Object.prototype。
- 所有普通对象默认都是
-
找
Object.prototype-> 无。 -
再往上?
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 原型的美感所在。