JavaScript 中原型链的概念很难理解,它到底是什么,有什么作用,对 JavaScript 又有什么意义
核心答案
原型链(Prototype Chain)是 JavaScript 实现对象之间继承的核心机制。它的本质是一条由 __proto__ 指针串联起来的对象链条。
它是什么: 每个 JavaScript 对象内部都有一个隐藏属性 [[Prototype]](可通过 __proto__ 或 Object.getPrototypeOf() 访问),它指向另一个对象(即该对象的原型)。原型本身也是对象,也有自己的 [[Prototype]],如此层层向上,直到某个对象的原型为 null,这条完整的链路就是原型链。
它有什么作用: 当你访问一个对象的属性或方法时,如果对象自身没有,JS 引擎会沿着原型链逐级向上查找,直到找到该属性或到达链顶端 null。这就是 JavaScript 实现属性继承和方法共享的方式。
对 JavaScript 的意义: JavaScript 没有传统的"类"概念(ES6 的 class 只是语法糖),它是一门基于原型(Prototype-based) 的面向对象语言。原型链是整个对象系统的地基,instanceof 判断、属性查找、方法继承、Object.create() 等核心能力全部建立在原型链之上。
深入解析
三个关键概念的关系
要理解原型链,必须先厘清三个概念之间的关系:
graph LR
A["Person<br/>(构造函数)"] -->|"prototype"| B["Person.prototype<br/>(原型对象)"]
B -->|"constructor"| A
C["person1<br/>(实例对象)"] -->|"__proto__"| B
用一张完整的关系图来看:
graph TD
Person["🔧 Person<br/>构造函数"]
PersonProto["📦 Person.prototype<br/>原型对象<br/>{ constructor, greet, ... }"]
ObjectProto["📦 Object.prototype<br/>{ toString, valueOf, ... }"]
Null["🚫 null<br/>链顶端"]
Instance["🟢 person1 实例<br/>{ name: 'Tom' }"]
Person -->|"prototype"| PersonProto
PersonProto -.->|"constructor"| Person
Instance -->|"__proto__"| PersonProto
PersonProto -->|"__proto__"| ObjectProto
ObjectProto -->|"__proto__"| Null
三条核心规则:
- 每个函数都有一个
prototype属性,指向它的原型对象 - 每个对象都有一个
__proto__(即[[Prototype]]),指向创建它的构造函数的prototype - 原型对象的
constructor属性指回构造函数本身
属性查找机制
当你执行 person1.toString() 时,JS 引擎的查找过程如下:
person1 自身有 toString 吗? → 没有
↓
person1.__proto__(即 Person.prototype)有吗? → 没有
↓
Person.prototype.__proto__(即 Object.prototype)有吗? → 找到了!执行它
↓
如果还没有,Object.prototype.__proto__ === null → 返回 undefined
这就是原型链的查找过程——从实例出发,沿着 __proto__ 逐级向上,直到 null。
为什么需要原型链?——方法共享
如果没有原型链,每创建一个实例,方法都会被复制一份,这是巨大的内存浪费:
// 不用原型 —— 每个实例各持有一份 sayHi 函数
function Person(name) {
this.name = name;
this.sayHi = function() { // 每次 new 都创建新的函数对象
console.log(`Hi, I'm ${this.name}`);
};
}
const a = new Person('A');
const b = new Person('B');
console.log(a.sayHi === b.sayHi); // false —— 两份不同的函数,浪费内存
// 用原型 —— 所有实例共享同一份 sayHi
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
console.log(`Hi, I'm ${this.name}`);
};
const a = new Person('A');
const b = new Person('B');
console.log(a.sayHi === b.sayHi); // true —— 同一份函数,节省内存
这就是原型链最实际的作用:让所有实例共享原型上的方法,避免重复创建。
new 操作符与原型链的建立
new Person('Tom') 背后发生了四件事:
function myNew(Constructor, ...args) {
// 1. 创建一个空对象,将其 __proto__ 指向构造函数的 prototype
const obj = Object.create(Constructor.prototype);
// 2. 将构造函数的 this 绑定到新对象,执行构造函数
const result = Constructor.apply(obj, args);
// 3. 如果构造函数返回了一个对象,则使用该对象;否则返回新创建的对象
return (result !== null && typeof result === 'object') ? result : obj;
}
关键在第 1 步:Object.create(Constructor.prototype) 把新对象的 __proto__ 指向了 Constructor.prototype,原型链在这一刻被建立。
原型链的终点
所有原型链最终都汇聚到同一个终点:
Object.prototype.__proto__ === null // true —— 这就是链的尽头
// 任何对象最终都能沿链到达 Object.prototype
[].__proto__.__proto__ === Object.prototype // true(Array → Object)
(function(){}).__proto__.__proto__ === Object.prototype // true(Function → Object)
特殊情况: Object.create(null) 创建的对象没有原型,它是一个"纯净"的字典对象:
const dict = Object.create(null);
dict.__proto__ // undefined
dict.toString // undefined —— 连 Object.prototype 上的方法都没有
instanceof 的本质就是原型链检测
a instanceof B 的底层逻辑就是:沿着 a 的原型链向上查找,看能不能找到 B.prototype。
function Person() {}
const p = new Person();
p instanceof Person; // true → p.__proto__ === Person.prototype ✓
p instanceof Object; // true → p.__proto__.__proto__ === Object.prototype ✓
// 手动实现 instanceof
function myInstanceof(obj, Constructor) {
let proto = Object.getPrototypeOf(obj);
while (proto !== null) {
if (proto === Constructor.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
ES6 class 只是原型链的语法糖
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof!');
}
}
// 本质上等同于:
// Dog.prototype.__proto__ === Animal.prototype
// 原型链:dog → Dog.prototype → Animal.prototype → Object.prototype → null
const dog = new Dog('Rex');
dog.bark(); // 自身原型上找到
dog.speak(); // 沿链到 Animal.prototype 找到
dog.toString(); // 沿链到 Object.prototype 找到
常见误区
- 误区一:
prototype和__proto__是同一个东西。 不是。prototype是函数的属性,指向原型对象;__proto__是所有对象的属性,指向其构造函数的prototype。普通对象没有prototype属性。 - 误区二:修改原型是安全的。 直接替换整个
prototype对象会断开已有实例的关联,且会丢失constructor指向:
function Foo() {}
const old = new Foo();
Foo.prototype = { newMethod() {} }; // 替换整个原型
const newer = new Foo();
old.newMethod; // undefined —— old 的 __proto__ 还指向旧的原型
newer.newMethod; // function —— newer 指向新的原型
newer.constructor === Foo; // false —— constructor 丢失了
- 误区三:
for...in只遍历自身属性。 错,for...in会遍历原型链上所有可枚举属性,所以通常需要配合hasOwnProperty使用:
function Person(name) { this.name = name; }
Person.prototype.type = 'human';
const p = new Person('Tom');
for (let key in p) {
console.log(key); // 'name', 'type' —— type 来自原型
}
// 只获取自身属性
for (let key in p) {
if (p.hasOwnProperty(key)) {
console.log(key); // 只有 'name'
}
}
// 更推荐用 Object.keys(),天然只返回自身可枚举属性
Object.keys(p); // ['name']
- 误区四:箭头函数也有
prototype。 没有。箭头函数不能作为构造函数,没有prototype属性,不能用new调用。
代码示例
完整的原型链验证
function Person(name) {
this.name = name;
}
Person.prototype.greet = function() {
return `Hello, I'm ${this.name}`;
};
const tom = new Person('Tom');
// 实例的 __proto__ 指向构造函数的 prototype
console.log(tom.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(tom) === Person.prototype); // true(推荐写法)
// 原型对象的 constructor 指回构造函数
console.log(Person.prototype.constructor === Person); // true
// 原型链向上追溯
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true
// 属性查找顺序验证
console.log(tom.greet()); // "Hello, I'm Tom" → 来自 Person.prototype
console.log(tom.toString()); // "[object Object]" → 来自 Object.prototype
console.log(tom.foo); // undefined → 整条链都没有
基于原型链的继承
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function() {
console.log(`${this.name} is eating`);
};
function Dog(name, breed) {
Animal.call(this, name); // 继承实例属性
this.breed = breed;
}
// 继承原型方法:将 Dog.prototype 的 __proto__ 指向 Animal.prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修复 constructor 指向
Dog.prototype.bark = function() {
console.log('Woof!');
};
const rex = new Dog('Rex', 'Labrador');
// 原型链:rex → Dog.prototype → Animal.prototype → Object.prototype → null
rex.bark(); // "Woof!" → Dog.prototype
rex.eat(); // "Rex is eating" → Animal.prototype
rex.toString(); // "[object Object]" → Object.prototype
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true
console.log(rex instanceof Object); // true
属性遮蔽(Shadowing)
function Person() {}
Person.prototype.x = 10;
const p = new Person();
console.log(p.x); // 10 —— 来自原型
p.x = 20; // 在实例自身上创建属性 x
console.log(p.x); // 20 —— 自身属性遮蔽了原型属性
console.log(p.__proto__.x); // 10 —— 原型上的 x 没被修改
delete p.x; // 删除自身属性
console.log(p.x); // 10 —— 原型属性重新"露出来"
面试技巧
面试官可能的追问方向:
-
"
prototype和__proto__有什么区别?" →prototype是函数独有的属性,__proto__是所有对象都有的。实例.__proto__ === 构造函数.prototype。 -
"手写
instanceof" → 本质就是沿着原型链查找(见上文代码)。 -
"手写
new操作符" → 要体现Object.create建立原型链这一步。 -
"ES6 class 和原型链什么关系?" → class 是语法糖,
extends本质是设置子类.prototype.__proto__ === 父类.prototype。 -
"如何创建一个没有原型的对象?" →
Object.create(null),常用于创建纯净的 map/字典。 -
"原型链有性能问题吗?" → 链太长会影响属性查找速度。现代引擎有隐藏类(Hidden Class)和内联缓存(Inline Cache)优化,但仍应避免过深的继承层次。
如何展示深度:
- 不要只背概念,用"查找机制"串联起
__proto__、prototype、constructor三者的关系 - 能画出完整的原型链指向图(面试中可以在白板上画)
- 提到
Object.create(null)的实际用途(Vue 2 源码中大量使用) - 提到
for...in会遍历原型链属性这一实际踩坑点 - 提到 ES6 class 是语法糖,说明你理解底层机制而非只会用新语法
一句话总结
原型链就是对象通过 __proto__ 层层链接到 null 的一条查找链路,JavaScript 的属性继承、方法共享、instanceof 判断全部建立在它之上——理解原型链,就理解了 JS 对象系统的根基。