原型对象与原型链机制深度解析
一句话概括
原型链是 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.Component—extends背后就是Object.create(Parent.prototype)Object.create(null)做纯净字典 — 切断原型链,防止"toString"等 key 冲突- Chrome DevTools 展开对象看到的
[[Prototype]]— 就是可视化的原型链
常见误解
❌ 误区一:”prototype 是所有对象都有的属性”
只有函数才有 prototype。const 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__ → ... → null。prototype是函数定义模板的一面,__proto__是实例引用模板的一面。new把实例挂上链,class只是语法糖。面试记住四张牌:两个属性、一条链、一个 new、一个 instanceof。