文章

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

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

一句话概括

原型链是 JavaScript 对象之间共享属性的机制——每个对象都有一个隐藏的内部属性 [[Prototype]],指向它的”模板对象”。访问 obj.something 时,引擎先在自身找,找不到就沿着 [[Prototype]] 一路往上查,直到 null。整个 JS 的”继承”都建立在这条链上。

1
2
3
4
const arr = [1, 2, 3];
console.log(arr.map === Array.prototype.map);         // true —— 沿链找到
console.log(arr.toString === Object.prototype.toString); // true —— 继续往上
// arr → Array.prototype → Object.prototype → null

核心知识点 ①:__proto__ vs prototype

这是原型面试中被问烂了的问题,区别一句话:

  • prototype函数才有的属性,指向该函数 new 出来的实例的原型模板对象
  • __proto__(即 [[Prototype]])— 所有对象都有,指向创建它的构造函数的 prototype
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Dog(name) {
  this.name = name;
}
Dog.prototype.bark = function() {
  console.log(`${this.name}: 汪汪!`);
};

const wangcai = new Dog('旺财');

console.log(typeof Dog.prototype);            // 'object' — 函数才有
console.log(wangcai.__proto__ === Dog.prototype);  // true

// 函数自己也是对象,也有 __proto__
console.log(Dog.__proto__ === Function.prototype);  // true

核心知识点 ②:原型链查找规则

访问 obj.prop 时,查找路径:

1
obj 自身 → obj.__proto__ → obj.__proto__.__proto__ → ... → null → undefined
1
2
3
4
5
6
7
8
9
const obj = { a: 1 };
Object.prototype.x = '来自顶层';

console.log(obj.a);  // 1      — 自身有
console.log(obj.x);  // '来自顶层' — 沿着链到 Object.prototype
console.log(obj.z);  // undefined — 到 null 都没找到

// 终点永远是 null(面试送分题)
console.log(Object.prototype.__proto__);  // null

核心知识点 ③:new 操作符的四步手写

new 是原型链的入口,面试高频手写题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function myNew(Constructor, ...args) {
  const obj = Object.create(Constructor.prototype);  // 1️⃣ 创建对象 + 链接原型
  const result = Constructor.apply(obj, args);        // 2️⃣ 绑 this 执行
  return result instanceof Object ? result : obj;     // 3️⃣ 返回结果
}

function Person(name) {
  this.name = name;
}
Person.prototype.greet = function() {
  return `Hello, ${this.name}`;
};

const p = myNew(Person, 'Alice');
console.log(p.__proto__ === Person.prototype);  // true
console.log(p.greet());                         // 'Hello, Alice'

加分说辞new 本质就是在普通函数调用前插了一步 Object.create(constructor.prototype)Reflect.construct 是标准化实现。

核心知识点 ④:属性屏蔽(Property Shadowing)

读属性沿原型链往上找,写属性只写到自身,不修改原型:

1
2
3
4
5
6
7
8
9
10
11
function Parent() {}
Parent.prototype.value = 100;

const child = new Parent();
console.log(child.value);             // 100 —— 原型上的

child.value = 200;                    // 写到自身,原型不变
console.log(child.__proto__.value);   // 100 —— 原型上的纹丝不动

delete child.value;                   // 删掉自身后
console.log(child.value);             // 100 —— 原型上的又露出来了

陷阱:原型上是引用类型时,改内部会波及所有实例

1
2
3
4
Parent.prototype.items = ['a', 'b'];
const c1 = new Parent(), c2 = new Parent();
c1.items.push('c');          // push 是"读"到数组引用再改内部
console.log(c2.items);       // ['a', 'b', 'c'] —— c2 也受影响了!

核心知识点 ⑤:instanceof 原理

instanceof 检查的是:右边函数的 prototype 是否在左边对象的原型链上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function myInstanceof(obj, Constructor) {
  let proto = Object.getPrototypeOf(obj);
  while (proto) {
    if (proto === Constructor.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
  return false;
}

const arr = [];
console.log(myInstanceof(arr, Array));    // true
console.log(myInstanceof(arr, Object));   // true —— Array.prototype 上层是 Object.prototype
console.log(myInstanceof(arr, RegExp));   // false
console.log(myInstanceof(42, Number));    // false —— 原始值没有原型链

class 只是语法糖

ES6 的 class 长得像 Java,但底层完全是原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal {
  constructor(type) { this.type = type; }
  eat() { return `${this.type} is eating`; }
}

// 等价于:
function Animal(type) { this.type = type; }
Animal.prototype.eat = function() { return `${this.type} is eating`; };

// extends 就是寄生组合继承:
class Dog extends Animal {
  constructor(name) { super('dog'); this.name = name; }
}
console.log(Dog.prototype.__proto__ === Animal.prototype);  // true

// 静态方法挂在函数对象自身上
class Utils { static log(msg) { console.log(msg); } }
// 等价于 Utils.log = function(msg) { console.log(msg); };

其实你每天都在用

  • 数组上调 .map() .filter() — 方法全在 Array.prototype 上,所有实例共享同一份
  • obj.constructor — 不在自身,是沿原型链从 prototype.constructor 找到的
  • class MyComponent extends React.Componentextends 背后就是 Object.create(Parent.prototype)
  • Object.create(null) 做纯净字典 — 切断原型链,防止 "toString" 等 key 冲突
  • Chrome DevTools 展开对象看到的 [[Prototype]] — 就是可视化的原型链

常见误解

❌ 误区一:”prototype 是所有对象都有的属性”

只有函数才有 prototypeconst obj = {} 身上只有 __proto__Dog.prototype 是 Dog 作为”构造函数模板”的一面,Dog.__proto__ 是 Dog 作为”函数对象实例”的一面——两码事。

❌ 误区二:”给实例的原型属性赋值会改到原型”

不会。读沿链往上找,写只写自身(属性屏蔽)。只有修改引用类型内部(如 obj.arr.push(1)push 本质是”读引用改内容”)才会间接影响原型。

❌ 误区三:”class 是全新的继承机制,跟 prototype 没关系”

class A extends B 编译成 ES5 就是 A.prototype = Object.create(B.prototype) + A.__proto__ = B。面试官让你 ES5 实现 class,做不出就尴尬了。

❌ 误区四:”Object.create(null) 创建的对象可以用 instanceof

不能。Object.create(null)__proto__null,不连任何原型,对它instanceof Object 返回 false,调 .toString() 直接报错。这就是它适合做纯字典的原因。

一句话总结

原型链 = 属性查找路径:自身 → __proto__ → ... → nullprototype 是函数定义模板的一面,__proto__ 是实例引用模板的一面。new 把实例挂上链,class 只是语法糖。面试记住四张牌:两个属性、一条链、一个 new、一个 instanceof。

本文由作者按照 CC BY 4.0 进行授权