跳到主要内容

原型与原型链:JavaScript 对象“继承”背后的底层规则

在 JavaScript 里,几乎所有“对象能力”都绕不开两件事:

  • 原型(prototype):用来“复用属性/方法”的一套机制
  • 原型链(prototype chain):对象查找属性时一路向上的“查字典路径”

很多知识点都会在这里汇合,比如:

  • new 到底做了什么?
  • instanceof 为什么本质是“查原型链”?
  • 为什么给构造函数的 prototype 加方法,所有实例都能用?
  • class/extends 语法糖背后到底改了哪些原型?

这篇文档目标是:用最少的抽象,把原型与原型链讲到你能画图、能写代码、能回答面试题。


一、先把 4 个“长得像但不一样”的概念分清

建议先背一遍这张表,后面所有图和代码都会围绕它展开。

名称你在哪里见到它它到底是什么常见用途
F.prototype函数(构造函数)上一个对象,默认带 constructornew 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 引擎大致按这个顺序找(简化版):

  1. 先在 对象自身(own properties)
  2. 找不到就去 obj[[Prototype]] 指向的对象找
  3. 还找不到就继续沿着 [[Prototype]] 往上
  4. 一直找到 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+
注意:有些对象没有 hasOwnProperty

例如 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 引擎在背后大概做了这些事(非常接近面试标准答案):

  1. 创建一个新对象:obj = {}
  2. 把新对象的原型指向构造函数的原型:obj[[Prototype]] = Foo.prototype
  3. obj 作为 this 调用构造函数:Foo.call(obj, "Alice")
  4. 如果构造函数显式返回了一个对象,则返回它;否则返回 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.prototypeFoo.__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(同一个函数)

结论非常实用:

  • 实例属性:每个实例独有的数据(例如 nameid
  • 原型属性/方法:所有实例共享的能力(例如 sayHitoJSON

六、原型链与“继承”:ES5 写法到 ES6 class

6.1 ES5:最推荐的继承写法(寄生组合继承)

核心目标是两件事:

  1. 子类实例能用父类原型方法(连接 Sub.prototype -> Super.prototype
  2. 子类构造函数能初始化父类的实例属性(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__ -> Super
  • Sub.prototype.__proto__ -> Super.prototype

七、几个高频坑:面试/实战都常见

7.1 改了 prototypeconstructor 往往就“不对了”

function Foo() {}

Foo.prototype = {
say() {
return "hi";
},
};

const a = new Foo();
console.log(a.constructor === Foo); // false(constructor 丢了)

修复方式通常是补回去:

Foo.prototype.constructor = Foo;

更稳妥的做法是用 Object.definePropertyconstructor 设为不可枚举(贴近默认行为),但日常业务一般不需要做到这么细。

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,所以:

  • 没有 toStringhasOwnProperty……
  • ("x" in dict) 仍然可用(in 是语法,不靠原型方法)

适合的场景:做 Map/字典,避免原型污染带来的冲突。

7.4 不要随便改内置原型(尤其是 Object.prototype

Object.prototype.hack = 123;

这样做会让所有对象都“多出一个属性”,会带来:

  • for...inin 判断被影响
  • 第三方库行为异常
  • 安全风险(原型污染相关问题)

现代项目更推荐:

  • 用工具函数(例如 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)inhasOwnProperty(或 Object.hasOwn)的区别?

  • in:自己有或原型链上有都算。
  • hasOwn:只看自己有没有(不查原型链)。

7)Object.create(null) 有什么特点?

创建一个没有原型的对象,适合做“纯字典”,但也缺少很多 Object.prototype 上的方法。

8)class/extends 和原型链是什么关系?

class 是语法糖;extends 主要做了两条链:Sub.__proto__ -> SuperSub.prototype.__proto__ -> Super.prototype

9)Foo.prototypeFoo.__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 设计好原型关系
  • 或者用组合/委托替代复杂继承