JS 原型与原型链
Category(分类): JavaScript Status: 已整理(2026)
本文保留原文的四道原型题,以及
prototype、__proto__、new、call()、apply()、bind()和this主线。原文中的抓取代码(如functionPerson、newA以及页面代码块格式标记)已恢复为可运行示例,并用 ECMAScript 的[[Prototype]]、[[Construct]]等术语补充现代说明。
原文:JS 原型与原型链
一、先看四道题
题目 1:重新赋值 prototype 会影响已有实例吗?
function A() {}
A.prototype.n = 1
const b = new A()
A.prototype = {
n: 2,
m: 3
}
const c = new A()
console.log(b.n) // 1
console.log(b.m) // undefined
console.log(c.n) // 2
console.log(c.m) // 3
结果的关键不是“实例复制了 prototype”,而是:new A() 创建实例时,把当时的 A.prototype 对象设置为实例的 [[Prototype]]。后来重新给 A.prototype 赋了一个新对象,只影响之后创建的实例。
如果是修改原来的原型对象,则旧实例和新实例通常都能看到修改:
function Box() {}
const oldPrototype = Box.prototype
const first = new Box()
oldPrototype.value = 1
console.log(first.value) // 1
oldPrototype.value = 2
console.log(first.value) // 2
题目 2:Object.prototype 和 Function.prototype 的位置
function F() {}
Object.prototype.a = function () {
console.log('a')
}
Function.prototype.b = function () {
console.log('b')
}
const f = new F()
f.a() // a
try {
f.b()
} catch (error) {
console.log(error.name) // TypeError:f 的原型链不经过 Function.prototype
}
F.a() // a:F 的原型链最终也能到 Object.prototype
F.b() // b:F 是函数对象,Object.getPrototypeOf(F) === Function.prototype
f 和 F 是两条不同的原型链:
f → F.prototype → Object.prototype → null
F → Function.prototype → Object.prototype → null
题目 3:p.__proto__ 和 Person.__proto__
function Person(name) {
this.name = name
}
const p = new Person('Tom')
console.log(Object.getPrototypeOf(p) === Person.prototype) // true
console.log(Object.getPrototypeOf(Person) === Function.prototype) // true
所以原题的答案是:
p.__proto__(历史访问器表达方式)是Person.prototype;Person.__proto__是Function.prototype。
现代代码应优先写 Object.getPrototypeOf(p),不要依赖 __proto__ 访问器。
题目 4:普通对象和函数对象的原型链
const foo = {}
function F() {}
Object.prototype.a = 'value a'
Function.prototype.b = 'value b'
console.log(foo.a) // value a
console.log(foo.b) // undefined
console.log(F.a) // value a
console.log(F.b) // value b
foo 是普通对象,原型链从 Object.prototype 开始;F 是函数对象,原型链从 Function.prototype 开始,之后还会到达 Object.prototype。
修改
Object.prototype或Function.prototype会污染整个 Realm 中大量对象。这里的代码只用于理解原型链,不应作为业务代码实践。
二、prototype、[[Prototype]] 和 __proto__ 不是一回事
2.1 实例的 [[Prototype]]
ECMAScript 用内部槽 [[Prototype]] 表示对象的原型。访问 object.property 时,如果对象没有对应的自有属性,引擎会沿着 [[Prototype]] 继续查找,直到找到属性或遇到 null。
const parent = {
value: 1,
getValue() {
return this.value
}
}
const child = Object.create(parent)
console.log(Object.getPrototypeOf(child) === parent) // true
console.log(child.getValue()) // 1
child.value = 2 // 创建 child 的自有属性,遮蔽 parent.value
console.log(child.getValue()) // 2:调用时 this 是 child
console.log(Object.hasOwn(child, 'value')) // true
console.log(Object.hasOwn(parent, 'value')) // true
继承的方法仍然是一个属性值为函数的属性,并没有脱离普通属性查找。调用 child.getValue() 时,this 由调用形式决定,是 child,而不是保存该方法的 parent。
2.2 函数的 prototype 属性
普通函数(可作为构造器的函数)通常拥有一个名为 prototype 的自有属性。执行 new Constructor() 时,该属性的值通常会成为新实例的 [[Prototype]]:
function User(name) {
this.name = name
}
User.prototype.greet = function () {
return `Hello, ${this.name}`
}
const user = new User('Ada')
console.log(Object.getPrototypeOf(user) === User.prototype) // true
console.log(user.greet()) // Hello, Ada
console.log(User.prototype.constructor === User) // true
但 User.prototype 与 Object.getPrototypeOf(User) 是两个完全不同的关系:
console.log(Object.getPrototypeOf(User) === Function.prototype) // true
console.log(Object.getPrototypeOf(user) === User.prototype) // true
箭头函数、对象方法、class 的静态方法等不可构造的函数通常没有用于构造实例的自有 prototype 属性:
const arrow = () => {}
const objectMethod = { run() {} }.run
console.log(arrow.prototype) // undefined
console.log(objectMethod.prototype) // undefined
typeof value === 'function' 也不能证明该值可以用 new 构造;class 构造器不能用普通函数调用,箭头函数不能构造。
2.3 __proto__ 的历史背景
obj.__proto__ 通常是 Object.prototype 提供的 legacy accessor,用来读写对象的 [[Prototype]]。它广泛存在于浏览器和 Node.js,但不建议在新代码中作为通用 API:
const object = {}
const prototype = { answer: 42 }
Object.setPrototypeOf(object, prototype)
console.log(Object.getPrototypeOf(object) === prototype) // true
console.log(object.answer) // 42
推荐选择:
- 读取:
Object.getPrototypeOf(object); - 创建并指定原型:
Object.create(prototype); - 修改:尽量在创建时完成;确实需要时使用
Object.setPrototypeOf; - 对象字面量中的
{ __proto__: prototype }是标准的原型设置语法,与 legacy accessor 不是一回事。
动态修改已经大量使用的对象原型可能使引擎去优化,因此不应把它当作普通属性赋值来频繁使用。
三、原型链是如何查找属性的
const object = {
own: 'object',
shared: 'object',
__proto__: {
inherited: 'prototype',
shared: 'prototype'
}
}
console.log(object.own) // object:自有属性
console.log(object.inherited) // prototype:沿原型链查找
console.log(object.shared) // object:自有属性遮蔽原型属性
console.log(object.missing) // undefined
console.log('inherited' in object) // true:包括原型链
console.log(Object.hasOwn(object, 'inherited')) // false:只检查自有属性
读属性和写属性要区分:
const parent = { count: 1 }
const child = Object.create(parent)
child.count = 2 // 不会修改 parent.count,通常会创建 child.count
console.log(child.count) // 2
console.log(parent.count) // 1
如果原型上是 setter,写操作可能调用 setter;如果是不可写数据属性,严格模式下还可能抛出异常。不能把所有原型写入都简化为“总会创建自有属性”。
3.1 原型链与继承
function Animal(name) {
this.name = name
}
Animal.prototype.speak = function () {
return `${this.name} makes a sound`
}
function Dog(name) {
Animal.call(this, name)
}
Object.setPrototypeOf(Dog.prototype, Animal.prototype)
Object.setPrototypeOf(Dog, Animal)
const dog = new Dog('Lucky')
console.log(dog.speak()) // Lucky makes a sound
console.log(dog instanceof Dog) // true
console.log(dog instanceof Animal) // true
这里分别设置了两条关系:
Dog.prototype的原型是Animal.prototype,影响实例方法查找;Dog的原型是Animal,影响静态属性/方法查找。
class Dog extends Animal 会同时建立类似的实例原型链和构造器原型链,并且还包含派生构造器、super、私有字段等 class 语义。
四、new 做了什么
下面是便于理解的简化模型,不是可替代规范内部算法的 polyfill:
function construct(Constructor, args) {
if (typeof Constructor !== 'function') {
throw new TypeError('Constructor must be callable')
}
// typeof function 不能证明可构造;Reflect.construct 会执行真正的构造能力检查。
return Reflect.construct(Constructor, args)
}
function Product(name) {
this.name = name
}
console.log(construct(Product, ['book']).name) // book
try {
construct(() => {}, [])
} catch (error) {
console.log(error.name) // TypeError:箭头函数不可构造
}
普通构造过程可以概括为:
- 检查目标是否可构造;
- 根据
newTarget.prototype获取实例原型;若不是对象,规范会回退到相应 Realm 的默认原型; - 创建实例并以它作为
this执行构造器; - 普通构造器返回对象时使用该对象,否则返回新实例。
真实的 new 还涉及 [[Construct]]、new.target、派生 class 构造器和内建对象的特殊行为。上面的 helper 直接委托给 Reflect.construct,只用于展示如何正确检查可构造性;不能用几行普通 JavaScript 完整复制 new。特别是,派生 class 构造器返回非 undefined 的原始值会抛出 TypeError,不能简单套用普通函数规则。
function Person(name) {
this.name = name
}
const person = new Person('Ada')
console.log(person.name) // Ada
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
4.1 构造器的显式返回值
function ReturnPrimitive() {
this.value = 1
return 2
}
function ReturnObject() {
this.value = 1
return { value: 2 }
}
console.log(new ReturnPrimitive().value) // 1:原始值被忽略
console.log(new ReturnObject().value) // 2:对象替代默认实例
五、call()、apply() 和 bind()
5.1 call() 不会永久改变函数的 this
原文用两个函数说明 call():
function fn1() {
this.num = 111
this.sayHey = function () {
return 'say hey'
}
}
function fn2() {
this.num = 222
this.sayHello = function () {
return 'say hello'
}
}
fn1.call(fn2)
console.log(fn2.num) // 111:fn1 执行时 this 是 fn2
console.log(fn2.sayHey()) // say hey
console.log(fn1.num) // undefined:fn1 本身没有被永久修改
准确地说,call(thisArg, ...args) 会立即调用函数一次,并把这次调用的 this 设置为 thisArg。它不会改变函数对象以后每次调用的默认 this,也不会把两个函数“绑定”在一起。
在严格模式下,原始 thisArg 会保持原始值;在非严格普通函数中,null/undefined 以及部分原始值可能按传统规则转换。class、箭头函数和严格模式代码还存在不可调用或不可重新绑定等边界。
5.2 使用 call() 调用父构造器
function Product(name, price) {
this.name = name
this.price = price
}
function Food(name, price, category) {
Product.call(this, name, price)
this.category = category
}
const food = new Food('cheese', 5, 'food')
console.log(food.name, food.price, food.category) // cheese 5 food
这只复制了 Product 构造器执行时写入的自有属性;Food 是否是 Product 的实例,还取决于是否建立了原型链:
Object.setPrototypeOf(Food.prototype, Product.prototype)
console.log(food instanceof Product) // true
5.3 apply() 与 call() 的区别
二者都会立即调用函数,主要区别是参数的传递形式:
function add(a, b) {
return this.base + a + b
}
const context = { base: 10 }
console.log(add.call(context, 1, 2)) // 13
console.log(add.apply(context, [1, 2])) // 13
console.log(Reflect.apply(add, context, [1, 2])) // 13
apply() 接收的是数组或 array-like 参数,并不是“只能调用两个参数”;第一个参数是 thisArg,第二个参数是参数列表。现代代码也可以使用 Reflect.apply,它更明确地表达“执行函数的内部操作”。
5.4 bind() 返回新函数
const object = {
value: 42,
getValue() {
return this.value
}
}
const detached = object.getValue
console.log(detached.call(object)) // 42
const bound = object.getValue.bind(object)
console.log(bound()) // 42
bind() 不会立即执行,而是返回一个新的 bound function。它还可以预置参数:
function multiply(a, b) {
return a * b
}
const double = multiply.bind(null, 2)
console.log(double(4)) // 8
对于可构造的函数,绑定函数仍可能通过 new 调用;此时 new 会忽略预先绑定的 this,但保留预置参数。实际开发中应优先使用箭头函数、方法委托或明确的事件处理器,而不是滥用 bind。
六、this 到底指向哪里
“this 指向调用它的函数”并不准确。普通函数的 this 主要由调用形式决定:
'use strict'
function showThis() {
return this
}
const object = { showThis }
console.log(object.showThis() === object) // true:成员调用
console.log(showThis.call(object) === object) // true:显式绑定
console.log(showThis()) // undefined:严格模式下的普通调用
非严格普通函数的裸调用可能把 this 转换为全局对象,这是历史兼容行为,不应依赖。ES module 顶层自动处于严格模式;Node.js CommonJS、浏览器经典脚本和模块的顶层 this 也不应混为一谈。
6.1 回调中的 this
把对象方法直接作为回调传递时,调用者通常不再是该对象:
const user = {
name: 'Ada',
sayName() {
return this.name
}
}
const callback = user.sayName
console.log(callback.call(user)) // Ada
浏览器事件监听器中的普通函数通常以事件目标作为 this;箭头函数不会获得事件目标的动态 this,而是捕获定义位置的外层 this:
const button = document.querySelector('button')
button?.addEventListener('click', function () {
console.log(this === button) // true:普通函数
})
button?.addEventListener('click', event => {
console.log(event.currentTarget === button) // 应使用 currentTarget 获取事件目标
})
6.2 用箭头函数或 bind() 保留上下文
const counter = {
value: 0,
start() {
setTimeout(() => {
this.value += 1
console.log(this.value) // 1
}, 0)
}
}
counter.start()
箭头函数没有自己的 this、arguments、super 或 new.target,所以它适合捕获外层上下文,但不适合作为需要动态 this 的对象方法或构造器。
6.3 that = this 是历史写法
ES5 时代常见的写法是把 this 保存到局部变量:
const object = {
value: 42,
getValue() {
const that = this
return function () {
return that.value
}
}
}
console.log(object.getValue()()) // 42
现代代码通常使用箭头函数或 bind(),但理解 that = this 有助于阅读旧项目。
七、用现代 API 检查原型关系
function Parent() {}
function Child() {}
Object.setPrototypeOf(Child.prototype, Parent.prototype)
const child = new Child()
console.log(Object.getPrototypeOf(child) === Child.prototype) // true
console.log(Parent.prototype.isPrototypeOf(child)) // true
console.log(child instanceof Parent) // true
console.log(child instanceof Child) // true
三种 API 的侧重点不同:
Object.getPrototypeOf(value):读取直接原型;isPrototypeOf(value):判断某对象是否出现在另一个对象的原型链上;instanceof:默认沿右侧构造器的prototype查找,也可能受右侧的Symbol.hasInstance影响。
不要使用 constructor 属性作为绝对类型证明:它可能被覆盖、继承、重写或跨 Realm 不相等。自有属性判断使用 Object.hasOwn(value, key)。
八、原型链的工程实践
- 原型方法适合无须复制到每个实例的共享行为;实例字段应放在构造器中,避免状态互相共享。
- 不要无故修改
Object.prototype、Array.prototype等内建原型。 - 如果重新赋值
Constructor.prototype,应显式恢复constructor,并注意已有实例和新实例会连接到不同原型对象。 - 优先在创建对象时确定原型;谨慎使用
Object.setPrototypeOf。 Object.create(null)适合字典等场景,但它没有toString、hasOwnProperty等Object.prototype方法,应使用Object.hasOwn。- 对复杂继承优先考虑
class extends、组合和委托;原型链只是实现机制,不是必须使用的面向对象层次。
九、历史图示与来源
原文中的两张原型/原型链图已下载并保存在本地:

图片来源:原始图示。图中的 No1、No2 和“万能术”是帮助入门的拟人化比喻,不是 ECMAScript 规范模型。

图片来源:原始图示。现代术语应以 [[Prototype]]、prototype 和 Object.getPrototypeOf() 的区别为准。
十、总结
- 对象通过
[[Prototype]]连接形成原型链,属性查找会逐级向上; - 函数的
prototype是构造实例时使用的原型对象,不等于函数自身的原型; Object.getPrototypeOf(Constructor) === Function.prototype与Object.getPrototypeOf(instance) === Constructor.prototype可以同时成立;- 重新赋值
Constructor.prototype不会改变已有实例的原型; __proto__是历史访问器,现代代码优先使用Object.getPrototypeOf、Object.create和Object.setPrototypeOf;new会创建实例、设置原型、执行构造器,并处理构造器返回值;call、apply只改变一次调用的this,bind返回绑定后的新函数;- 箭头函数的
this由定义位置捕获,不能通过call、apply或bind重新绑定; - 原型继承是语言机制,工程设计还应考虑组合、模块边界、可维护性和跨 Realm 场景。