《JS 原型链:你以为你在 new 一个对象,其实你在召唤祖宗!》

49 阅读5分钟

JS 原型链:你以为你在 new 一个对象,其实你在召唤祖宗!

“在 JavaScript 的世界里,每个 new 都是一场跨代际的召唤仪式。”


引子:当面试官问“说说原型”,他在问什么?

想象一下这个场景:

面试官(微笑):“来,聊聊 JavaScript 的原型机制吧。”
你(自信):“哦,就是 prototype__proto__ 啊……”
面试官(继续微笑):“那 Object.getPrototypeOf.__proto__ 有什么区别?为什么重写 prototype 要手动修 constructorinstanceof 底层怎么工作的?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 引擎会做四件事:

    1. 创建一个空对象 {}
    2. 将该对象的 __proto__ 指向 Car.prototype
    3. 执行函数体,this 指向新对象;
    4. 返回该对象(除非显式返回引用类型)。
  • 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 规范):

  1. 检查对象自身是否有该属性;
  2. 若无,沿 __proto__ 向上查找;
  3. 直到 Object.prototype
  4. 若仍未找到,返回 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,而 {}constructorObject

Q3:ch.toString() 为何能调用?

console.log(ch.__proto__.__proto__); // Object.prototype

→ 原型链最终指向 Object.prototype,其中定义了 toString


八、结语:原型链,是 JS 的“道”

JavaScript 的原型机制,表面是技术细节,内核是设计哲学:

  • 没有强制封装,只有灵活委托;
  • 没有血缘继承,只有能力复用;
  • 没有静态模板,只有动态链接。

当你理解了这一点,你就不再“背”原型,而是“用”原型。无论是写框架、搞 polyfill,还是应对大厂面试,你都能游刃有余。

最后送你一句:
“在 JS 的世界里,万物皆可原型,唯独你的简历不能‘原型污染’——得靠真本事!”