一句话概括
JavaScript 中每个对象都有一个隐式原型 __proto__,每个函数都有一个显式原型 prototype,对象通过 __proto__ 链接到其构造函数的 prototype,形成一条向上查找属性的原型链,直到 null 为止。
背景
JavaScript 是一门基于原型的面向对象语言,不同于 Java/C++ 的类继承体系。在 ES6 class 语法出现之前,所有的继承与复用都依赖原型链来实现。即便是 ES6 的 class,底层也只是原型链的语法糖。
理解原型链是掌握 JavaScript 继承、instanceof 判断、属性查找等核心机制的基础,也是前端面试的必考点。
概念与定义
1. prototype(显式原型)
prototype 是函数独有的属性,指向一个对象(原型对象)。当函数作为构造函数使用时,通过 new 创建的实例对象的 __proto__ 会指向该函数的 prototype。
1
2
3
4
5
6
| function Person(name) {
this.name = name;
}
console.log(typeof Person.prototype); // "object"
console.log(Person.prototype.constructor === Person); // true
|
2. __proto__(隐式原型)
__proto__ 是每个对象都有的属性(包括函数对象),指向创建该对象的构造函数的 prototype。
注意:__proto__ 是非标准属性,标准写法是 Object.getPrototypeOf(obj)。
1
2
3
| const p = new Person('Alice');
console.log(p.__proto__ === Person.prototype); // true
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
|
3. 原型链
当访问一个对象的属性时,JavaScript 引擎会:
- 先在对象自身查找
- 找不到则沿
__proto__ 向上查找 - 直到找到该属性或到达
null(原型链终点)
最小示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
return `${this.name} makes a sound.`;
};
const dog = new Animal('Dog');
// 自身属性
console.log(dog.name); // "Dog"
// 原型链上的属性
console.log(dog.speak()); // "Dog makes a sound."
// 原型链关系
console.log(dog.__proto__ === Animal.prototype); // true
console.log(Animal.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__); // null
|
原型链结构:
1
2
3
4
| dog
└── __proto__ → Animal.prototype
└── __proto__ → Object.prototype
└── __proto__ → null
|
核心知识点拆解
1. prototype vs __proto__ 对比
| 属性 | 归属 | 指向 | 标准性 |
|---|
prototype | 函数对象 | 原型对象(含 constructor) | 标准属性 |
__proto__ | 所有对象 | 构造函数的 prototype | 非标准(ES6 规范化) |
1
2
3
4
5
6
| // 函数既有 prototype,也有 __proto__
function Foo() {}
console.log(Foo.prototype); // Foo 的原型对象
console.log(Foo.__proto__); // Function.prototype
console.log(Foo.__proto__ === Function.prototype); // true
|
2. constructor 属性
原型对象上默认有一个 constructor 属性,指回构造函数本身:
1
2
3
4
5
| function Cat() {}
console.log(Cat.prototype.constructor === Cat); // true
const c = new Cat();
console.log(c.constructor === Cat); // true(通过原型链找到)
|
⚠️ 重写 prototype 时会丢失 constructor,需手动补回:
1
2
3
4
| Cat.prototype = {
constructor: Cat, // 手动补回
meow() { return 'Meow!'; }
};
|
3. 原型链查找规则
1
2
3
4
5
6
7
8
9
10
11
12
| function Base() {
this.own = 'own property';
}
Base.prototype.shared = 'prototype property';
const obj = new Base();
// 查找顺序:自身 → 原型 → Object.prototype → null
console.log(obj.own); // "own property"(自身)
console.log(obj.shared); // "prototype property"(原型)
console.log(obj.toString); // [Function: toString](Object.prototype)
console.log(obj.notExist); // undefined(未找到)
|
4. hasOwnProperty 与 in 操作符
1
2
3
4
5
6
7
8
9
| const obj = new Base();
// hasOwnProperty:只检查自身属性
console.log(obj.hasOwnProperty('own')); // true
console.log(obj.hasOwnProperty('shared')); // false
// in 操作符:检查自身 + 原型链
console.log('own' in obj); // true
console.log('shared' in obj); // true
|
5. Object.create() 与原型链
Object.create(proto) 创建一个以 proto 为原型的新对象:
1
2
3
4
5
6
7
8
9
10
11
| const animal = {
speak() {
return `${this.name} speaks.`;
}
};
const dog = Object.create(animal);
dog.name = 'Rex';
console.log(dog.speak()); // "Rex speaks."
console.log(Object.getPrototypeOf(dog) === animal); // true
|
6. instanceof 的原理
instanceof 沿着左侧对象的原型链查找,看是否能找到右侧构造函数的 prototype:
1
2
3
4
5
6
7
8
| function A() {}
function B() {}
B.prototype = Object.create(A.prototype);
const b = new B();
console.log(b instanceof B); // true
console.log(b instanceof A); // true(原型链上能找到 A.prototype)
console.log(b instanceof Object); // true
|
实战案例
案例一:原型方法共享
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| function User(name, age) {
this.name = name;
this.age = age;
}
// 方法定义在原型上,所有实例共享,节省内存
User.prototype.greet = function () {
return `Hi, I'm ${this.name}, ${this.age} years old.`;
};
User.prototype.toString = function () {
return `[User: ${this.name}]`;
};
const u1 = new User('Alice', 25);
const u2 = new User('Bob', 30);
console.log(u1.greet()); // "Hi, I'm Alice, 25 years old."
console.log(u2.greet()); // "Hi, I'm Bob, 30 years old."
// 方法是共享的,不是各自拷贝
console.log(u1.greet === u2.greet); // true
|
案例二:原型链继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| function Shape(color) {
this.color = color;
}
Shape.prototype.getColor = function () {
return this.color;
};
function Circle(color, radius) {
Shape.call(this, color); // 继承实例属性
this.radius = radius;
}
// 继承原型方法
Circle.prototype = Object.create(Shape.prototype);
Circle.prototype.constructor = Circle;
Circle.prototype.getArea = function () {
return Math.PI * this.radius ** 2;
};
const c = new Circle('red', 5);
console.log(c.getColor()); // "red"(来自 Shape.prototype)
console.log(c.getArea()); // 78.53...
console.log(c instanceof Circle); // true
console.log(c instanceof Shape); // true
|
案例三:属性遮蔽(Shadowing)
1
2
3
4
5
6
7
8
9
10
11
12
13
| function Vehicle() {}
Vehicle.prototype.speed = 100;
const car = new Vehicle();
console.log(car.speed); // 100(来自原型)
// 在实例上设置同名属性,遮蔽原型属性
car.speed = 200;
console.log(car.speed); // 200(来自自身)
console.log(Vehicle.prototype.speed); // 100(原型未变)
delete car.speed;
console.log(car.speed); // 100(删除自身属性后,重新访问原型)
|
底层原理
V8 引擎中的原型链实现
在 V8 中,每个 JavaScript 对象都有一个隐藏的 Map(Hidden Class),其中记录了对象的结构信息,包括原型指针。
1
2
3
4
5
6
| 对象内存布局(简化):
┌─────────────────────────────┐
│ Map(Hidden Class) │ ← 包含原型指针
│ Properties(属性存储) │
│ Elements(数组元素) │
└─────────────────────────────┘
|
属性查找过程(简化):
- 检查对象自身的
Properties - 未找到 → 读取
Map 中的原型指针 - 跳转到原型对象,重复步骤 1
- 直到原型为
null,返回 undefined
性能优化:内联缓存(Inline Cache)
V8 会缓存属性查找路径,避免每次都遍历原型链。当对象结构(Hidden Class)不变时,属性访问可以直接命中缓存,达到接近 C++ 的访问速度。
Object.prototype 是原型链的终点
1
| console.log(Object.prototype.__proto__); // null
|
所有普通对象的原型链最终都指向 Object.prototype,它提供了 toString、hasOwnProperty、valueOf 等基础方法。
高频面试题解析
Q1:__proto__ 和 prototype 有什么区别?
答:
prototype 是函数的属性,指向该函数作为构造函数时创建的实例的原型对象__proto__ 是所有对象的属性,指向创建该对象的构造函数的 prototype- 关系:
new Foo() 创建的实例 obj.__proto__ === Foo.prototype
Q2:如何判断属性是自身属性还是原型属性?
1
2
3
4
| const obj = new Foo();
obj.hasOwnProperty('prop'); // 自身属性返回 true
Object.getOwnPropertyNames(obj); // 返回所有自身属性名(含不可枚举)
for...in 循环 // 遍历自身 + 原型链上的可枚举属性
|
Q3:instanceof 的原理是什么?能手写吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| function myInstanceof(left, right) {
// 获取右侧构造函数的 prototype
const prototype = right.prototype;
// 获取左侧对象的原型
let proto = Object.getPrototypeOf(left);
while (proto !== null) {
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
console.log(myInstanceof([], Array)); // true
console.log(myInstanceof([], Object)); // true
console.log(myInstanceof([], String)); // false
|
Q4:原型链的终点是什么?
Object.prototype.__proto__ === null,null 是原型链的终点。访问不存在的属性时,查找到 null 后返回 undefined。
Q5:Object.create(null) 创建的对象有什么特点?
1
2
3
| const obj = Object.create(null);
console.log(obj.__proto__); // undefined(没有原型链)
console.log(obj.toString); // undefined(没有继承 Object.prototype)
|
这种对象没有任何原型,常用于创建纯净的字典对象(如 Map 的替代品),避免原型属性污染。
Q6:修改原型会影响已创建的实例吗?
1
2
3
4
5
6
7
8
9
10
11
12
13
| function Foo() {}
const f = new Foo();
// 在原型上添加方法 → 已有实例立即可用
Foo.prototype.newMethod = function () { return 'new!'; };
console.log(f.newMethod()); // "new!"
// 替换整个 prototype → 已有实例不受影响(仍指向旧原型)
Foo.prototype = { replaced: true };
console.log(f.newMethod()); // "new!"(f.__proto__ 仍指向旧原型)
const f2 = new Foo();
console.log(f2.replaced); // true(新实例指向新原型)
|
总结与扩展
核心要点
| 概念 | 要点 |
|---|
prototype | 函数专属,是实例的原型模板 |
__proto__ | 对象专属,指向构造函数的 prototype |
| 原型链 | 通过 __proto__ 串联,终点为 null |
| 属性查找 | 自身 → 原型链逐级向上 → null |
constructor | 原型对象上指回构造函数的属性 |
原型链关系图
1
2
3
4
5
6
7
8
9
| 实例对象 (instance)
│
└── __proto__ ──→ 构造函数.prototype
│
├── constructor ──→ 构造函数
│
└── __proto__ ──→ Object.prototype
│
└── __proto__ ──→ null
|
扩展阅读
- ES6 class 与原型链:
class 是语法糖,extends 本质是设置原型链 - 寄生组合继承:目前最优的原型继承方案,ES6 class 的底层实现参考
- Proxy 与原型:Proxy 可以拦截原型链上的属性访问
- WeakRef 与原型:弱引用对象的原型链行为
下一步学习
- 第2周周三:JavaScript 继承的六种实现方式(深入原型链继承的实践)
- 第2周周四:new 运算符的执行原理
- 第2周周五:手写 call / apply / bind