原型与原型链:JavaScript 对象“继承”背后的底层规则
在 JavaScript 里,几乎所有“对象能力”都绕不开两件事:
- 原型(prototype):用来“复用属性/方法”的一套机制
- 原型链(prototype chain):对象查找属性时一路向上的“查字典路径”
很多知识点都会在这里汇合,比如:
new到底做了什么?instanceof为什么本质是“查原型链”?- 为什么给构造函数的
prototype加方法,所有实例都能用? class/extends语法糖背后到底改了哪些原型?
这篇文档目标是:用最少的抽象,把原型与原型链讲到你能画图、能写代码、能回答面试题。
一、先把 4 个“长得像但不一样”的概念分清
建议先背一遍这张表,后面所有图和代码都会围绕它展开。
| 名称 | 你在哪里见到它 | 它到底是什么 | 常见用途 |
|---|---|---|---|
F.prototype | 函数(构造函数)上 | 一个对象,默认带 constructor | new F() 创建实例时,决定实例的原型 |
obj.__proto__ | 任意对象上(历史属性) | 访问器,读写 [[Prototype]] | 调试时查看原型;不推荐在业务代码用 |
obj[[Prototype]] | 规范里的写法(看不到) | 内部槽,指向另一个对象或 null | 决定原型链;属性查找会沿它向上 |
obj.constructor | 通常能访问到 | 来自原型上的一个属性引用 | 可能被改写,不建议当“可靠类型判断” |
更推荐的“标准 API”是:
Object.getPrototypeOf(obj):读[[Prototype]]Object.setPrototypeOf(obj, proto):写[[Prototype]](谨慎)Object.create(proto):创建一个指定原型的对象
二、原型链是什么:对象“找属性”的路线图
当你访问 obj.someKey 时,JS 引擎大致按这个顺序找(简化版):
- 先在 对象自身(own properties) 找
- 找不到就去
obj[[Prototype]]指向的对象找 - 还找不到就继续沿着
[[Prototype]]往上 - 一直找到
null(原型链终点)仍没有:返回undefined
用一张图记住它:
2.1 “自己有”和“链上有”:hasOwnProperty vs in
很多题就卡在这:
const obj = {a: 1};
console.log("a" in obj); // true(自己有)
console.log(obj.hasOwnProperty("a")); // true
console.log("toString" in obj); // true(来自 Object.prototype)
console.log(obj.hasOwnProperty("toString")); // false
在现代项目里,判断“是否为自身属性”更推荐:
Object.hasOwn(obj, "a"); // ES2022+
例如 Object.create(null) 创建的“纯字典对象”没有原型,也就没有 hasOwnProperty 方法:
const dict = Object.create(null);
// dict.hasOwnProperty("x"); // TypeError
Object.hasOwn(dict, "x"); // ✅
2.2 属性遮蔽(shadowing):同名属性会“就近覆盖”
原型链查找有个很重要的“就近原则”:离我最近的属性先生效。
function Foo() {}
Foo.prototype.x = 1;
const a = new Foo();
console.log(a.x); // 1(来自 Foo.prototype)
a.x = 2;
console.log(a.x); // 2(自己的属性把原型上的 x “遮住了”)
delete a.x;
console.log(a.x); // 1(删掉自己的 x,又能看到原型上的 x)
这也是为什么“给实例加同名属性”不会影响其他实例:你只是给当前对象加了一个 own property。
三、new 做了什么:把“原型”接到实例上
你写:
function Foo(name) {
this.name = name;
}
Foo.prototype.sayHi = function () {
return `Hi, ${this.name}`;
};
const a = new Foo("Alice");
JS 引擎在背后大概做了这些事(非常接近面试标准答案):
- 创建一个新对象:
obj = {} - 把新对象的原型指向构造函数的原型:
obj[[Prototype]] = Foo.prototype - 用
obj作为this调用构造函数:Foo.call(obj, "Alice") - 如果构造函数显式返回了一个对象,则返回它;否则返回
obj
图解:
对应关系可以用几行代码验证:
console.log(Object.getPrototypeOf(a) === Foo.prototype); // true
console.log(a.__proto__ === Foo.prototype); // true(不推荐,但便于理解)
console.log(Foo.prototype.constructor === Foo); // true(默认情况)
new Foo() 的关键就是:把“实例的原型”连接到 Foo.prototype,于是实例就能沿原型链“借用”上面的能力。
四、函数也是对象:别把 Foo.prototype 和 Foo.__proto__ 搞混
新手最容易混淆的一点是:函数本身也是对象,所以它也有自己的 [[Prototype]](也就是你常看到的 __proto__)。
于是就出现了“两条链”:
- 给实例用的链:
实例[[Prototype]] -> Foo.prototype -> ... - 给函数对象 Foo 自己用的链:
Foo[[Prototype]] -> Function.prototype -> ...
用图画出来更直观:
几行代码验证一下(建议自己在控制台敲一遍):
function Foo() {}
const a = new Foo();
console.log(Object.getPrototypeOf(a) === Foo.prototype); // true(实例链)
console.log(Object.getPrototypeOf(Foo) === Function.prototype); // true(函数对象链)
console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype); // true
console.log(Object.getPrototypeOf(Object.prototype) === null); // true(终点)
Foo.prototype 是“给 a 这种实例用的”;Foo.__proto__(Foo[[Prototype]])是“Foo 这个函数对象自己用的”。
五、prototype 到底放什么:方法放原型上 vs 放实例上
5.1 放实例上:每个对象一份(更耗内存)
function Foo() {
this.say = function () {
return "hi";
};
}
const a = new Foo();
const b = new Foo();
console.log(a.say === b.say); // false(两个不同函数)
5.2 放原型上:所有实例共享一份(更常见)
function Foo() {}
Foo.prototype.say = function () {
return "hi";
};
const a = new Foo();
const b = new Foo();
console.log(a.say === b.say); // true(同一个函数)
结论非常实用:
- 实例属性:每个实例独有的数据(例如
name、id) - 原型属性/方法:所有实例共享的能力(例如
sayHi、toJSON)
六、原型链与“继承”:ES5 写法到 ES6 class
6.1 ES5:最推荐的继承写法(寄生组合继承)
核心目标是两件事:
- 子类实例能用父类原型方法(连接
Sub.prototype -> Super.prototype) - 子类构造函数能初始化父类的实例属性(
Super.call(this, ...))
function Super(name) {
this.name = name;
}
Super.prototype.sayName = function () {
return this.name;
};
function Sub(name, age) {
Super.call(this, name); // 关键:继承“实例属性”
this.age = age;
}
// 关键:继承“原型方法”,而不是 new Super()
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.sayAge = function () {
return this.age;
};
const s = new Sub("Alice", 18);
console.log(s.sayName()); // "Alice"
console.log(s.sayAge()); // 18
6.2 ES6:class extends 是语法糖,但原型仍然在
class Super {
constructor(name) {
this.name = name;
}
sayName() {
return this.name;
}
}
class Sub extends Super {
constructor(name, age) {
super(name); // 必须先 super()
this.age = age;
}
sayAge() {
return this.age;
}
}
你可以用两条“黄金判断”验证它本质还是原型链:
console.log(Object.getPrototypeOf(Sub) === Super); // true(类本身的原型链)
console.log(Object.getPrototypeOf(Sub.prototype) === Super.prototype); // true(实例的原型链)
extends 做了两条链:
Sub.__proto__ -> SuperSub.prototype.__proto__ -> Super.prototype
七、几个高频坑:面试/实战都常见
7.1 改了 prototype,constructor 往往就“不对了”
function Foo() {}
Foo.prototype = {
say() {
return "hi";
},
};
const a = new Foo();
console.log(a.constructor === Foo); // false(constructor 丢了)
修复方式通常是补回去:
Foo.prototype.constructor = Foo;
更稳妥的做法是用 Object.defineProperty 把 constructor 设为不可枚举(贴近默认行为),但日常业务一般不需要做到这么细。
7.2 箭头函数/方法简写/类方法没有 prototype,也不能 new
const Foo = () => {};
console.log(Foo.prototype); // undefined
// new Foo(); // TypeError: Foo is not a constructor
const obj = {
m() {},
};
console.log(obj.m.prototype); // undefined
// new obj.m(); // TypeError: obj.m is not a constructor
7.3 Object.create(null) 是“干净字典”,但少了很多方法
它没有 Object.prototype,所以:
- 没有
toString、hasOwnProperty…… ("x" in dict)仍然可用(in是语法,不靠原型方法)
适合的场景:做 Map/字典,避免原型污染带来的冲突。
7.4 不要随便改内置原型(尤其是 Object.prototype)
Object.prototype.hack = 123;
这样做会让所有对象都“多出一个属性”,会带来:
for...in、in判断被影响- 第三方库行为异常
- 安全风险(原型污染相关问题)
现代项目更推荐:
- 用工具函数(例如
lodash) - 用更明确的封装(例如
class、组合函数)
八、面试高频问答(建议背到能画图+能讲清楚)
1)prototype 和 __proto__ 的区别是什么?
prototype在函数上,主要用于new:决定实例的原型。__proto__在对象上,是访问[[Prototype]]的历史方式(不推荐业务使用)。
2)访问 obj.key 时,JS 会怎么找?
先找自身属性;找不到就沿 Object.getPrototypeOf(obj) 一路向上;到 null 还没有就返回 undefined。
3)instanceof 的本质是什么?
判断右侧 Constructor.prototype 是否出现在左侧对象的原型链上。
4)new 做了哪几步?
创建新对象 → 连接原型(指向 Constructor.prototype)→ 以新对象为 this 执行构造函数 → 决定返回值(默认返回新对象)。
5)为什么把方法放在 Foo.prototype 上更省内存?
因为所有实例共享同一个函数引用,而不是每个实例都创建一份。
6)in 和 hasOwnProperty(或 Object.hasOwn)的区别?
in:自己有或原型链上有都算。hasOwn:只看自己有没有(不查原型链)。
7)Object.create(null) 有什么特点?
创建一个没有原型的对象,适合做“纯字典”,但也缺少很多 Object.prototype 上的方法。
8)class/extends 和原型链是什么关系?
class 是语法糖;extends 主要做了两条链:Sub.__proto__ -> Super 和 Sub.prototype.__proto__ -> Super.prototype。
9)Foo.prototype 和 Foo.__proto__ 分别指向什么?
Foo.prototype:给new Foo()创建出来的实例用的(实例的[[Prototype]]会指向它)。Foo.__proto__:Foo这个函数对象自己的原型链入口,通常是Function.prototype。
10)什么是“属性遮蔽(shadowing)”?怎么判断属性来自哪里?
- 当实例上有同名属性时,会“遮住”原型上的同名属性(就近原则)。
- 判断是否为自身属性:
Object.hasOwn(obj, key);如果是false但能访问到值,通常来自原型链。
11)for...in 为什么可能遍历到原型链上的属性?
因为 for...in 会枚举对象自身 + 原型链上所有“可枚举(enumerable)”的属性。
只想遍历自身属性时,优先用 Object.keys(obj) / Object.entries(obj)。
12)为什么不建议频繁改 __proto__ / Object.setPrototypeOf?
它会改变对象的“形状”(影响引擎优化),在热路径里可能造成明显性能损失。更推荐:
- 初始化时就用
Object.create(proto)/class设计好原型关系 - 或者用组合/委托替代复杂继承