【每日一面】深入理解 JavaScript 原型链
原型链是靠 __proto__ 把实例、原型与构造函数串起来的一条查找链:属性沿它一路查到 null,把 prototype 与 constructor 的关系理顺,class extends 的本质也就通了。
基础问答
Q1:什么是原型和原型链?
答: JavaScript 中每个对象都有一个内部引用 [[Prototype]](大多数环境通过 __proto__ 访问),指向它的原型对象。当访问对象的属性或方法时,如果自身没有,就会沿着 [[Prototype]] 一路向上查找,直到找到或到达 null。这条由原型引用串联起来的链,就是原型链。
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function () {
console.log(`Hi, I'm ${this.name}`);
};
const p = new Person('Alice');
p.sayHi(); // 自身没有 sayHi,沿原型链在 Person.prototype 上找到
p.toString(); // Person.prototype 也没有,继续向上在 Object.prototype 上找到Q2:__proto__ 和 prototype 是什么关系?
答:
-
prototype是函数独有的属性,指向以该函数作为构造函数创建出来的实例的原型对象; -
__proto__是所有对象都有的属性(即[[Prototype]]的访问器),指向创建它的构造函数的prototype; -
可以简单的认为
实例.__proto__ === 构造函数.prototype。
扩展延伸
概念
理解原型链前,先分清四个核心概念:
| 概念 | 归属 | 含义 |
|---|---|---|
prototype | 仅函数 | 函数的原型对象,供所有实例共享的属性/方法挂载于此 |
__proto__([[Prototype]]) | 所有对象 | 指向自身原型,是原型链的链条 |
constructor | 所有对象 | 指向创建该对象的构造函数 |
| 构造函数 | 函数 | 用 new 调用并初始化实例的函数 |
两个易混易错点:
-
__proto__是历史产物(一开始是浏览器的实现,后被标准化为附录),ES6 推荐用Object.getPrototypeOf()/Object.setPrototypeOf()/Object.create()操作原型; -
constructor可以被改写、不可靠,判断类型更推荐instanceof或Object.prototype.toString.call()。
核心机制
属性查找
访问 obj.x 时,引擎沿原型链逐级向上查找:
flowchart TD
A[访问 obj.x] --> B{obj 自身有 x ?}
B -->|有| C[返回自身属性]
B -->|没有| D{obj.__proto__ 上有 x ?}
D -->|有| E[返回原型上的属性]
D -->|没有| F[继续沿原型链向上查找]
F --> G{到达 Object.prototype ?}
G -->|有 x| E
G -->|仍没有| H[原型链终点为 null<br/>返回 undefined]
原型链
实例、原型对象、构造函数三者构成一个三角关系,并且函数本身也是对象,也有自己的原型链:
flowchart LR
P[实例 p] -->|__proto__| PP[Person.prototype]
PP -->|__proto__| OP[Object.prototype]
OP -->|__proto__| N([null 原型链终点])
PP -.->|constructor| F["function Person()"]
OP -.->|constructor| OF["function Object()"]
F -->|__proto__| FP[Function.prototype]
OF -->|__proto__| FP
FF["function Function"] -->|__proto__| FP
FP -->|__proto__| OP
两个特殊的节点:
-
所有函数(包括
Object、Function自身)都是Function的实例,即Object.__proto__ === Function.prototype; -
Object.prototype.__proto__ === null,这是整条原型链的终点。
代码示例
验证原型链的关键等式
function Person(name) { this.name = name; }
const p = new Person('Alice');
// 三角关系
p.__proto__ === Person.prototype; // true
Person.prototype.constructor === Person; // true
// 函数也是对象,也有原型链
Person.__proto__ === Function.prototype; // true
Object.__proto__ === Function.prototype; // true
Function.__proto__ === Function.prototype; // true(Function 是自身的实例)
// 原型链终点
Function.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__ === null; // true
// instanceof 的本质:沿 p 的原型链查找 Person.prototype
p instanceof Person; // true
p instanceof Object; // true寄生组合式继承(ES5 继承的最优解)
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function () {
console.log(`Hi, I'm ${this.name}`);
};
function Student(name, grade) {
Person.call(this, name); // 第一步:借用构造函数,继承实例属性
this.grade = grade;
}
// 第二步:寄生式继承,以 Person.prototype 为原型创建干净的原型对象
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student; // 第三步:修复 constructor 指向
Student.prototype.study = function () {
console.log(`${this.name} is studying`);
};
const s = new Student('Bob', 3);
s.sayHi(); // Hi, I'm Bob ← 来自 Person.prototype
s.study(); // Bob is studyingES6 的 class Student extends Person 本质上就是这套寄生组合式继承的语法糖。
对比
prototype 与 __proto__ 对比
| 维度 | prototype | __proto__ |
|---|---|---|
| 归属 | 只有函数拥有 | 所有对象都拥有 |
| 作用 | 作为实例的原型模板 | 实例访问原型链的入口 |
| 谁指向谁 | 构造函数.prototype 是被指向方 | 实例.__proto__ 是指向方 |
| 规范性 | ES 规范属性 | 历史属性(ES6 建议用 Object.getPrototypeOf) |
继承方式对比
| 继承方式 | 实例属性独立 | 原型方法复用 | 主要缺点 |
|---|---|---|---|
| 原型链继承 | 否 | 是 | 父类引用类型属性被所有实例共享,互相污染 |
| 构造函数继承 | 是 | 否 | 无法访问父类原型方法,无法复用 |
| 组合继承 | 是 | 是 | 父类构造函数被调用两次,性能浪费 |
| 寄生组合继承 | 是 | 是 | ES5 最优解,仅写法稍繁琐 |
| class extends | 是 | 是 | 语法糖,本质是寄生组合式继承 |
如何决策
如果被问到该如何实现继承,怎么判断选择哪种方案,这里也提供一个决策标准,但是实际开发中,我们基本都考虑的是 class ,走面向对象风格,因为比较容易阅读,并且引擎也做出了很多优化,大多数情况下不需要真这样决策。
flowchart TD
A[需要实现继承] --> B{环境支持 ES6+ ?}
B -->|是| C[class / extends<br/>本质是寄生组合式继承]
B -->|否| D{需要继承父类实例属性?}
D -->|否| E[直接原型链继承]
D -->|是| F[构造函数借用<br/>Person.call this]
F --> G{需要复用父类原型方法?}
G -->|否| H[仅构造函数继承]
G -->|是| I[寄生组合式继承<br/>Object.create 组合两者]
C --> J[注意:子类构造函数<br/>必须先调用 super]
I --> K[记得修复 constructor 指向]
面试追问
追问 1:new 操作符具体做了什么?能手写一个吗?
new 共做 4 件事:创建空对象 → 链接原型 → 绑定 this 执行构造函数 → 根据返回值决定结果。
flowchart TD
A["new Person('Alice')"] --> B[创建空对象 obj]
B --> C["obj.__proto__ = Person.prototype<br/>链接原型链"]
C --> D[以 obj 为 this 执行构造函数<br/>初始化实例属性]
D --> E{构造函数显式返回了对象?}
E -->|是| F[返回该对象<br/>覆盖默认结果]
E -->|否| G[返回 obj]
实现代码:
function myNew(Constructor, ...args) {
// 1. 创建空对象并链接原型
const obj = Object.create(Constructor.prototype);
// 2. 绑定 this 执行构造函数
const result = Constructor.apply(obj, args);
// 3. 构造函数若返回对象则使用它,否则返回新对象
return result instanceof Object ? result : obj;
}追问 2:分析以下代码的输出,并解释原因
function F() {}
const f = new F();
console.log(f.__proto__ === F.prototype);
console.log(F.__proto__ === Function.prototype);
console.log(Function.__proto__ === Function.prototype);
console.log(Object.__proto__ === Function.prototype);
console.log(Function.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__);依次输出:true, true, true, true, true, null。
关键在于理解:所有函数都是 Function 的实例,包括 Object 和 Function 自身(Function.__proto__ === Function.prototype 是著名的鸡生蛋现象);而 Function.prototype 作为对象,它的原型是 Object.prototype;原型链最终在 Object.prototype.__proto__ === null 处终止。
追问 3:instanceof 的原理是什么?手写实现一个
instanceof 的原理是:沿左侧对象的原型链逐级查找,看能否找到右侧构造函数的 prototype。
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left); // 取 left 的原型
const prototype = right.prototype;
while (true) {
if (proto === null) return false; // 走到原型链终点
if (proto === prototype) return true; // 找到了
proto = Object.getPrototypeOf(proto); // 继续向上
}
}
myInstanceof(new Person('A'), Person); // true
myInstanceof(123, Number); // true(包装对象语义)与 typeof 的区别: typeof 返回基本类型字符串,但 typeof null === 'object'、无法区分具体引用类型;instanceof 能判断引用类型的继承关系,但对基本类型需依赖包装对象,且跨 iframe 场景会失效。
追问 4:为什么说组合继承调用了两次父类构造函数?如何证明?
组合继承 = 构造函数继承 + 原型链继承:
function Student(name, grade) {
Person.call(this, name); // 第二次调用:实例属性覆盖
this.grade = grade;
}
Student.prototype = new Person(); // 第一次调用:name 挂到了 Student.prototype 上证明代码:
const s = new Student('Bob', 3);
s.name; // 'Bob',来自实例属性(第二次调用的结果)
delete s.name;
s.name; // 仍然是 'Bob'!说明 Student.prototype 上还残留一份 name
// 这就是第一次调用产生的冗余属性寄生组合式继承用 Object.create(Person.prototype) 替代 new Person(),只继承原型、不执行构造函数,从而消除了这次多余调用。
追问 5:ES6 class 的 extends 相比 ES5 继承,有哪些本质区别和增强?
extends 的核心机制与寄生组合式继承一致,但语义更严格:
class Person {
static type = 'human'; // 静态属性
constructor(name) { this.name = name; }
sayHi() { console.log(this.name); }
}
class Student extends Person {
constructor(name, grade) {
super(name); // 必须先调用 super,否则 this 不可用(TDZ)
this.grade = grade;
}
}本质区别:
-
静态属性/方法也被继承:ES5 中
Student.__proto__指向Function.prototype,而 class 中Student.__proto__ === Person,所以Student.type可用; -
必须
new调用:class 不能像普通函数那样直接执行,否则报错; -
方法不可枚举:class 原型上的方法默认不可枚举,ES5 直接赋值的方式是可枚举的;
-
不存在变量提升:class 声明有暂时性死区(TDZ);
-
内部严格模式:class 声明体默认运行在严格模式下。
小结
| 要点 | 内容 |
|---|---|
| 等价关系 | 实例.__proto__ === 构造函数.prototype |
| 查找流程 | 沿 [[Prototype]] 逐级向上,终点为 Object.prototype.__proto__ === null |
| 特殊类型 | 所有函数都是 Function 的实例;Function.__proto__ === Function.prototype |
| 继承最优解 | ES5 寄生组合式继承;ES6 class extends 是其语法糖 |
| 经典考点 | 手写 new、手写 instanceof、组合继承的两次调用问题 |
关注本系列,每天进步一点点,面试不再慌。