文章

原型对象与原型链机制深度解析

深入剖析 JavaScript 原型对象与原型链机制,理解 __proto__ 与 prototype 的本质区别及原型链查找规则。

原型对象与原型链机制深度解析

一句话概括

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 引擎会:

  1. 先在对象自身查找
  2. 找不到则沿 __proto__ 向上查找
  3. 直到找到该属性或到达 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. hasOwnPropertyin 操作符

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(数组元素)        │
└─────────────────────────────┘

属性查找过程(简化):

  1. 检查对象自身的 Properties
  2. 未找到 → 读取 Map 中的原型指针
  3. 跳转到原型对象,重复步骤 1
  4. 直到原型为 null,返回 undefined

性能优化:内联缓存(Inline Cache)

V8 会缓存属性查找路径,避免每次都遍历原型链。当对象结构(Hidden Class)不变时,属性访问可以直接命中缓存,达到接近 C++ 的访问速度。

Object.prototype 是原型链的终点

1
console.log(Object.prototype.__proto__); // null

所有普通对象的原型链最终都指向 Object.prototype,它提供了 toStringhasOwnPropertyvalueOf 等基础方法。


高频面试题解析

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__ === nullnull 是原型链的终点。访问不存在的属性时,查找到 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
本文由作者按照 CC BY 4.0 进行授权