JS 原型链:你以为你在 new 一个对象,其实你在召唤祖宗!
“在 JavaScript 的世界里,每个
new都是一场跨代际的召唤仪式。”
引子:当面试官问“说说原型”,他在问什么?
想象一下这个场景:
面试官(微笑):“来,聊聊 JavaScript 的原型机制吧。”
你(自信):“哦,就是prototype和__proto__啊……”
面试官(继续微笑):“那Object.getPrototypeOf和.__proto__有什么区别?为什么重写prototype要手动修constructor?instanceof底层怎么工作的?ES6 的class是不是彻底抛弃了原型?”
你:……(大脑宕机,开始怀疑人生)
别慌!这篇文章,就是你的“祖传秘籍”。我们将用最接地气的语言、最硬核的逻辑、最贴近大厂实战的视角,带你从 ES5 构造函数出发,一路打通原型链的任督二脉——不仅知道“是什么”,更要明白“为什么”和“怎么考”。
一、没有 class 的年代:JS 是如何“装”出面向对象的?
在 ES6 之前,JavaScript 并没有 class 这个关键字。但开发者早就玩出了花:用函数模拟类,用原型实现共享,用 new 创造实例。
// es5 没有类class
// JS函数是一等对象
// 首字母大写的 构造函数
function Car(color) {
// this? 指向新创建的对象
this.color = color;
}
// 共享
Car.prototype = {
drive() {
console.log('drive,下赛道');
},
name: 'su7',
height: 1.4,
weight: 1.5,
long: 4800,
}
const car1 = new Car('霞光紫');
console.log(car1, car1.name, car1.weight);
car1.drive();
const car2 = new Car('海湾蓝');
console.log(car2, car2.name, car2.weight);
🔍 本质拆解:
-
构造函数(Constructor) :只是一个普通函数,但被
new调用时,JS 引擎会做四件事:- 创建一个空对象
{}; - 将该对象的
__proto__指向Car.prototype; - 执行函数体,
this指向新对象; - 返回该对象(除非显式返回引用类型)。
- 创建一个空对象
-
prototype:每个函数天生自带的属性,它不是一个“类模板”,而是一个“共享仓库” 。所有通过new Car()创建的实例,都能“借用”这个仓库里的东西。
💡 哲学思考:
Java/C++ 的继承是“血缘关系”——你是我的孩子,所以你有我的基因。
JS 的继承是“资源共享”——你不是我的孩子,但你可以用我家的 WiFi(原型链)。
📌 面试高频问法:
“为什么要把方法放在 prototype 上,而不是构造函数里?”
答:避免重复创建函数,节省内存。每个实例的 drive 方法都指向同一个函数引用。
二、prototype vs __proto__:谁是谁的爹?
这是 JS 原型中最经典的混淆点。我们用一句话厘清:
prototype是函数的属性,__proto__是对象的属性;实例的__proto__指向其构造函数的prototype。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.speci = '人类';
const person1 = new Person('张三', 18);
console.log(person1.name, person1.speci);
const person2 = new Person('李四', 20);
console.log(person2.name, person2.speci);
console.log(person1.__proto__, '////');
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
speci: '人类',
}
var ch = new Person('陈老板', 19);
console.log(ch.__proto__ == Person.prototype); // true
console.log(Person == Person.prototype.constructor, Person.prototype.constructor); // false! constructor 被覆盖
🧪 关键结论:
ch.__proto__ === Person.prototype→ 永远成立(只要没篡改)。- 但
Person.prototype.constructor === Person→ 仅在未重写prototype时成立。
🤯 冷知识:
函数本身也是对象,所以Person.__proto__ === Function.prototype。
而Function.__proto__ === Function.prototype—— 它是自己的“祖先”。实际上,
Function是由自身构造的,因此Function.__proto__ === Function.prototype成立。这是 JavaScript 引擎启动时预置的“自举”设计,属于语言底层的特例。
三、重写 prototype 的“坑”:constructor 失踪案
你提供的代码清晰展示了这个问题:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
speci: '人类',
}
var ch = new Person('陈老板', 19);
console.log(Person == Person.prototype.constructor); // false!
🔍 为什么?
因为你用字面量 {} 覆盖了原来的 prototype 对象。而 {} 的 constructor 默认是 Object。
✅ 正确姿势(虽未在原始代码中体现,但基于其问题引申):
若坚持用字面量重写,应手动修复:
Person.prototype = {
constructor: Person,
speci: '人类',
}
📌 面试加分项:
“我注意到原始代码中Person.prototype.constructor指向了Object,这可能导致instance.constructor判断错误。实践中我会避免整体覆盖,或显式修复constructor。”
四、属性查找:遮蔽(Shadowing)与“就近原则”
function Person() {}
Person.prototype.species = '人类';
var ch = new Person('陈', 18);
ch.species = '瓦达人'; // 实例上设置属性, 不是修改原型上的属性
// 如果实例上有属性, 就不会去原型上查找
console.log(ch, ch.species, ch.__proto__);
输出结果:
ch.species→'瓦达人'(实例优先)ch.__proto__.species→'人类'(原型未变)
🧠 查找规则(ECMA 规范):
- 检查对象自身是否有该属性;
- 若无,沿
__proto__向上查找; - 直到
Object.prototype; - 若仍未找到,返回
undefined。
⚠️ 注意:
ch.toString()能调用,是因为最终查到了Object.prototype.toString。
五、原型链继承:从“祖传代码”到现代理解
var obj = new Object(); // {}
obj.species = '动物';
function Animal() {}
Animal.prototype = obj;
function Person() {}
Person.prototype = new Animal();
var ch = new Person();
console.log(ch.species, ch.__proto__, ch.toString());
🔍 分析:
ch.species→'动物',因为ch.__proto__ → Person.prototype → Animal.prototype (= obj)ch.toString()可用,因为obj.__proto__ === Object.prototype
❌ 经典问题暴露:
- 无法向
Animal传参; - 若
obj包含引用类型(如数组),所有Person实例将共享它。
六、终极拷问:class 是不是干掉了原型?
// class Person {
// constructor(name, age) {
// this.name = name;
// this.age = age;
// }
// sayHi() {
// console.log(`你好,我是${this.name}`);
// }
// }
// es5 没有class 抽象一个类的模板(属性 方法)
并用 ES5 实现:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
speci: '人类',
sayHello: function() {
console.log(`你好,我是${this.name}`);
}
}
const ch = new Person('陈', 18);
console.log(ch.__proto__ == Person.prototype); // true
console.log(Person.prototype.constructor == Person); // false(因重写)
console.log(Object.getPrototypeOf(ch) == Person.prototype); // true
✅ 结论:
class只是语法糖,底层仍是构造函数 +prototype;Object.getPrototypeOf(ch)是 ECMAScript 标准方法;ch.__proto__是早期浏览器实现的非标准属性,虽已被纳入 Annex B(兼容性附录),但在严格模式或某些环境中应优先使用标准 API。”
七、大厂高频原型面试题精析(基于你的代码)
Q1:如何判断 ch 是否由 Person 构造?
// 你的代码已验证:
console.log(ch.__proto__ == Person.prototype); // true
console.log(Object.getPrototypeOf(ch) == Person.prototype); // true
Q2:为什么 Person.prototype.constructor !== Person?
→ 因为你用 {} 重写了 prototype,而 {} 的 constructor 是 Object。
Q3:ch.toString() 为何能调用?
console.log(ch.__proto__.__proto__); // Object.prototype
→ 原型链最终指向 Object.prototype,其中定义了 toString。
八、结语:原型链,是 JS 的“道”
JavaScript 的原型机制,表面是技术细节,内核是设计哲学:
- 没有强制封装,只有灵活委托;
- 没有血缘继承,只有能力复用;
- 没有静态模板,只有动态链接。
当你理解了这一点,你就不再“背”原型,而是“用”原型。无论是写框架、搞 polyfill,还是应对大厂面试,你都能游刃有余。
最后送你一句:
“在 JS 的世界里,万物皆可原型,唯独你的简历不能‘原型污染’——得靠真本事!”