面试官再问原型链,就把这篇文章甩给他!一图胜千言,彻底搞懂JavaScript继承的奥秘
本文正在参加「金石计划」
前言
你是否曾在面试中被"简述原型链"一问击倒?是否曾对 prototype 和 __proto__ 感到头晕目眩?别担心,这是每个前端人的必经之路。
原型链不是用来死记硬背的,它是JavaScript精妙的设计模式。理解它,你才能真正理解这门"基于原型的语言"。很多人靠死记硬背过关,但真正理解的很少——这就是我们这篇文章的价值所在。
本文将带你:
- 🎯 亲手"画出"原型链,搞清所有关键属性的关系
- 🔧 理解
new和instanceof的底层逻辑 - 💻 掌握从ES5到ES6的继承实现方案
- 🚀 看透
class语法糖背后的真相
前置知识:为什么需要原型?
在开始之前,我们先明确一个基本概念:JavaScript中的对象就是一组键值对的集合。
现在思考一个问题:如果我们有1000个Person对象,每个对象都需要一个sayHello方法,你会怎么做?
// 低效的做法:每个实例都有自己的方法副本
function Person(name) {
this.name = name;
this.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
}
const p1 = new Person('Alice');
const p2 = new Person('Bob');
console.log(p1.sayHello === p2.sayHello); // false - 两个不同的函数!
这样会创建1000个相同的函数,极度浪费内存。原型就是为了解决这个问题而生的:让多个对象共享同一个方法。
第一章:核心概念"三巨头"
要理解原型链,必须先搞清楚三个关键属性:prototype、__proto__ 和 constructor。
1. prototype(显式原型)
prototype 是函数的属性,只有函数才有。
function Person(name) {
this.name = name;
}
// 函数Person天生就有一个prototype属性
console.log(Person.prototype); // 输出一个对象
console.log(typeof Person.prototype); // "object"
// 我们可以在prototype上添加方法,这些方法将被所有实例共享
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
关键点:prototype 是构造函数的"蓝图",它定义了由该构造函数创建的所有实例会"继承"什么。
2. __proto__(隐式原型)
__proto__ 是对象的属性,几乎所有对象都有。
const p1 = new Person('Alice');
// 实例p1的__proto__指向其构造函数的prototype
console.log(p1.__proto__ === Person.prototype); // true
// 通过__proto__,p1可以访问到Person.prototype上的方法
p1.sayHello(); // "Hello, I'm Alice"
关键点:__proto__ 是实例对象的"血脉",它指向创建该对象的构造函数的 prototype。
3. constructor(构造函数)
constructor 是原型对象的属性,它指向该原型对象关联的构造函数本身。
// Person.prototype的constructor指回Person函数本身
console.log(Person.prototype.constructor === Person); // true
// 实例p1本身没有constructor,但会沿着原型链找到它
console.log(p1.constructor === Person); // true
console.log(p1.hasOwnProperty('constructor')); // false - constructor不在p1自身上
三巨头关系总结表
| 属性 | 归属 | 指向 | 说明 |
|---|---|---|---|
prototype | 函数 | 原型对象 | 构造函数的蓝图 |
__proto__ | 对象 | 原型对象 | 实例对象的"血脉" |
constructor | 原型对象 | 构造函数 | 说明"我是谁造的" |
记忆口诀:函数有prototype,对象有__proto__,原型有constructor
第二章:图解!什么是原型链?
现在来到最核心的部分:原型链到底是什么?
原型链的查找机制
当访问一个对象的属性时,JavaScript引擎会:
- 先在对象自身属性中查找
- 如果找不到,就去它的
__proto__上找 - 如果还找不到,就去
__proto__.__proto__上找 - 依此类推,直到找到属性或者到达
null
这条由 __proto__ 连接起来的链路,就是原型链!
终极原型链关系图
让我们用Mermaid图来可视化这个关系:
graph TD
A[实例 p1] -->|__proto__| B[Person.prototype];
B -->|__proto__| C[Object.prototype];
C -->|__proto__| D[null];
E[函数 Person] -->|prototype| B;
B -->|constructor| E;
F[函数 Object] -->|prototype| C;
C -->|constructor| F;
style A fill:#e1f5fe
style E fill:#f3e5f5
style F fill:#f3e5f5
style B fill:#fff3e0
style C fill:#fff3e0
图解说明:
p1.__proto__指向Person.prototypePerson.prototype.__proto__指向Object.prototypeObject.prototype.__proto__指向null(原型链的尽头)- 这就是完整的原型链!
实际查找示例
// 1. 查找自身属性
console.log(p1.name); // "Alice" - 在p1自身上找到
// 2. 查找原型上的方法
console.log(p1.sayHello); // function - 在Person.prototype上找到
// 3. 查找更上层的方法
console.log(p1.toString); // function - 在Object.prototype上找到
// 4. 查找不存在的属性
console.log(p1.neverExist); // undefined - 一直找到null也没找到
// 验证原型链
console.log(p1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
第三章:实战!手写一个new操作符
理解了原型链,我们来看看与之密切相关的 new 操作符。很多人天天用 new,却不知道它背后做了什么。
new 操作符的四步魔法
当你执行 new Person('Alice') 时,实际上发生了四件事:
- 创建新对象:创建一个空对象
- 设置原型链:将新对象的
__proto__指向构造函数的prototype - 绑定this执行:执行构造函数,将
this绑定到新对象 - 返回结果:如果构造函数返回对象则返回该对象,否则返回新对象
手写 myNew 函数
function myNew(constructorFn, ...args) {
// 1. 创建一个新对象,并将其__proto__指向构造函数的prototype
const obj = Object.create(constructorFn.prototype);
// 2. 执行构造函数,并将this绑定到新创建的对象上
const result = constructorFn.apply(obj, args);
// 3. 如果构造函数返回了一个对象,则返回该对象;否则,返回新创建的对象
return result instanceof Object ? result : obj;
}
// ---------------- 测试我们的myNew ----------------
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, I'm ${this.name}`);
};
const p2 = myNew(Person, 'Bob');
console.log(p2.name); // 'Bob'
p2.sayHello(); // 'Hello, I'm Bob'
console.log(p2.__proto__ === Person.prototype); // true
console.log(p2 instanceof Person); // true
关键点解析:
Object.create(constructorFn.prototype)一步完成了创建对象和设置原型链apply方法将构造函数的this绑定到新对象- 判断返回值确保与原生
new行为一致
第四章:instanceof 的底层原理
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
手写 myInstanceof 函数
function myInstanceof(obj, constructorFn) {
// 基本类型直接返回false
if (obj === null || typeof obj !== 'object') {
return false;
}
let proto = obj.__proto__;
while (proto) {
// 如果找到了构造函数的prototype
if (proto === constructorFn.prototype) {
return true;
}
// 沿着原型链继续向上查找
proto = proto.__proto__;
}
// 找到原型链顶端(null)还没找到,返回false
return false;
}
// ---------------- 测试 ----------------
const p1 = new Person('Alice');
console.log(myInstanceof(p1, Person)); // true
console.log(myInstanceof(p1, Object)); // true
console.log(myInstanceof(p1, Array)); // false
// 与原生instanceof对比
console.log(p1 instanceof Person); // true
console.log(p1 instanceof Object); // true
console.log(p1 instanceof Array); // false
工作原理:instanceof 就是沿着对象的 __proto__ 链向上查找,如果能找到右边构造函数的 prototype 属性,就返回 true。
第五章:现代继承的实现(从ES5到ES6)
理解了原型链,我们现在可以来看看如何在JavaScript中实现继承。
5.1 ES5时代的继承演变与"圣杯模式"
在ES6之前,开发者们探索了多种继承方式,最终"寄生组合式继承"被认为是ES5时代的最优解。
各种继承方式的比较
- 原型链继承:引用属性被所有实例共享(大问题!)
- 构造函数继承:方法无法复用(也是问题!)
- 组合继承:调用两次父类构造函数(效率问题)
圣杯模式:寄生组合式继承
// 目标:让 SubType 继承 SuperType 的属性和方法
// 父类构造函数
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
// 父类原型方法
SuperType.prototype.sayName = function() {
console.log(this.name);
};
// 子类构造函数
function SubType(name, age) {
// 1. 继承属性:调用父类构造函数,绑定子类this
SuperType.call(this, name); // 第二次(也是唯一一次)调用SuperType
this.age = age;
}
// 2. 继承方法:这是核心,建立子类与父类原型的链接
function inheritPrototype(subType, superType) {
// 创建一个以父类原型为原型的新对象,作为子类的原型
var prototype = Object.create(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象(修复constructor指向)
subType.prototype = prototype; // 指定对象
}
// 调用函数,完成继承
inheritPrototype(SubType, SuperType);
// 子类原型方法
SubType.prototype.sayAge = function() {
console.log(this.age);
};
// ---------------- 测试 ----------------
var instance1 = new SubType('Nicholas', 29); // 第一次调用SuperType
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black']
instance1.sayName(); // 'Nicholas' - 来自父类原型
instance1.sayAge(); // 29 - 来自子类原型
var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); // ['red', 'blue', 'green'] (互不影响)
instance2.sayName(); // 'Greg'
instance2.sayAge(); // 27
// 验证原型链
console.log(instance1 instanceof SubType); // true
console.log(instance1 instanceof SuperType); // true
console.log(instance1 instanceof Object); // true
寄生组合式继承的原型链
graph TD
A[instance1] -->|__proto__| B[SubType.prototype];
B -->|__proto__| C[SuperType.prototype];
C -->|__proto__| D[Object.prototype];
D -->|__proto__| E[null];
B -->|constructor| F[SubType];
C -->|constructor| G[SuperType];
style A fill:#e1f5fe
style B fill:#fff3e0
style C fill:#fff3e0
关键优势:
- 只调用一次父类构造函数(在子类构造函数中)
- 原型链纯净,没有不必要的属性
- ** instanceof 和 isPrototypeOf 正常工作**
5.2 ES6的 class 与 extends 语法糖
ES6引入了 class 关键字,让继承写起来像传统面向对象语言一样简单。但请记住:它只是语法糖!
用 ES6 class 重写继承
class SuperType {
constructor(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
sayName() {
console.log(this.name);
}
}
class SubType extends SuperType {
constructor(name, age) {
super(name); // 相当于之前的 SuperType.call(this, name)
this.age = age;
}
sayAge() {
console.log(this.age);
}
}
// ---------------- 测试 ----------------
const instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black']
instance1.sayName(); // 'Nicholas'
instance1.sayAge(); // 29
const instance2 = new SubType('Greg', 27);
console.log(instance2.colors); // ['red', 'blue', 'green']
透过语法糖看本质
让我们验证一下 class 的底层依然是原型链:
// 原型链关系与寄生组合式继承完全一致!
console.log(instance1.__proto__ === SubType.prototype); // true
console.log(SubType.prototype.__proto__ === SuperType.prototype); // true
console.log(instance1 instanceof SubType); // true
console.log(instance1 instanceof SuperType); // true
// class 中定义的方法,都在原型上,是不可枚举的
console.log(Object.keys(SubType.prototype)); // [] (ES5写法的方法是可枚举的)
console.log(Object.getOwnPropertyNames(SubType.prototype)); // ['constructor', 'sayAge']
Babel转换验证: 如果你将上面的ES6代码粘贴到 Babel REPL,会发现转换后的ES5代码核心部分正是我们手写的寄生组合式继承!
总结与升华
让我们回顾一下今天学到的核心内容:
一张图回顾核心关系
graph TD
A[实例] -->|__proto__| B[构造函数.prototype];
B -->|__proto__| C[Object.prototype];
C -->|__proto__| D[null];
E[构造函数] -->|prototype| B;
B -->|constructor| E;
style A fill:#e1f5fe
style E fill:#f3e5f5
style B fill:#fff3e0
style C fill:#fff3e0
核心思想
JavaScript通过原型链实现了基于原型的继承和共享,这是一种极其灵活的对象创建和复用机制。与传统的类式继承不同,原型继承更直接、更动态。
关键一句话:__proto__ 是对象的血脉,prototype 是函数的蓝图,它们共同编织了一张名为原型链的网,将所有对象联系在一起。
重要结论
- 每个函数都有
prototype属性(除了箭头函数和Function.prototype) - 每个对象都有
__proto__属性(除了Object.prototype) - 原型链的尽头是
null class是语法糖,底层还是原型链- 理解原型链是理解JavaScript对象系统的关键
互动引导
你现在对原型链的理解清晰了吗?欢迎在评论区:
- 分享你曾经在理解原型链时遇到的最大困惑
- 谈谈你在实际项目中运用原型链的经验
- 尝试解释这个有趣的现象:
Function.__proto__ === Function.prototype(这是JavaScript中的"鸡生蛋蛋生鸡"问题!)
或者,你可以思考一下:在ES6的 class 中,如果子类构造函数里不写 super() 调用,为什么会报错?(提示:这和ES5的继承实现机制有何不同?)
希望这篇文章能帮你彻底搞懂JavaScript原型链!如果觉得有帮助,欢迎点赞收藏,让更多小伙伴看到。 Happy Coding! 🚀