技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

JS 原型与原型链

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/40-JS 原型与原型链
本文目录11 个章节
  1. 一、先看四道题
  2. 二、prototype、[[Prototype]] 和 __proto__ 不是一回事
  3. 三、原型链是如何查找属性的
  4. 四、new 做了什么
  5. 五、call()、apply() 和 bind()
  6. 六、this 到底指向哪里
  7. 七、用现代 API 检查原型关系
  8. 八、原型链的工程实践
  9. 九、历史图示与来源
  10. 十、总结
  11. 参考资料

JS 原型与原型链

Category(分类): JavaScript Status: 已整理(2026)

本文保留原文的四道原型题,以及 prototype__proto__newcall()apply()bind()this 主线。原文中的抓取代码(如 functionPersonnewA 以及页面代码块格式标记)已恢复为可运行示例,并用 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.prototypeFunction.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

fF 是两条不同的原型链:

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.prototypeFunction.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.prototypeObject.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:箭头函数不可构造
}

普通构造过程可以概括为:

  1. 检查目标是否可构造;
  2. 根据 newTarget.prototype 获取实例原型;若不是对象,规范会回退到相应 Realm 的默认原型;
  3. 创建实例并以它作为 this 执行构造器;
  4. 普通构造器返回对象时使用该对象,否则返回新实例。

真实的 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()

箭头函数没有自己的 thisargumentssupernew.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)

八、原型链的工程实践

  1. 原型方法适合无须复制到每个实例的共享行为;实例字段应放在构造器中,避免状态互相共享。
  2. 不要无故修改 Object.prototypeArray.prototype 等内建原型。
  3. 如果重新赋值 Constructor.prototype,应显式恢复 constructor,并注意已有实例和新实例会连接到不同原型对象。
  4. 优先在创建对象时确定原型;谨慎使用 Object.setPrototypeOf
  5. Object.create(null) 适合字典等场景,但它没有 toStringhasOwnPropertyObject.prototype 方法,应使用 Object.hasOwn
  6. 对复杂继承优先考虑 class extends、组合和委托;原型链只是实现机制,不是必须使用的面向对象层次。

九、历史图示与来源

原文中的两张原型/原型链图已下载并保存在本地:

原型链历史图示

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

构造器与原型关系历史图示

图片来源:原始图示。现代术语应以 [[Prototype]]prototypeObject.getPrototypeOf() 的区别为准。

十、总结

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

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS