返回首页

JS 原型与原型链:把 __proto__ / prototype / class 一次性讲透

原型与原型链是 JS 最被反复"复习"也最容易绕晕的话题。这篇从"为什么有原型"开始,把 __proto__、prototype、constructor 三者画在一张图里,再讲清楚 ES6 class 只是语法糖,以及实战里 7 个常见坑。

31 约 9 分钟 · 12165 字 前端

JS 工程师面试三道送命题:this 指向事件循环原型与原型链。前两个还能背两句,原型链这个话题随便挑一个老 JS 教程都能讲两个小时,绕来绕去 __proto__ / prototype / constructor 看得人头晕。

这一篇我尝试用最少的概念 + 一张图讲透。看完你应该能闭眼回答:

  • __proto__prototype 区别是什么?
  • ES6 class 是不是真的"类"?
  • 为什么 arr.push 没定义在 arr 上,arr.push(1) 却能用?
  • Object.create(null) 创造的对象到底有什么不一样?

开始。

为什么 JS 有"原型"这种东西

Brendan Eich 1995 年设计 JS 的时候,Netscape 给了他 10 天做出一门"看上去像 Java 的脚本语言"。他骨子里喜欢 Lisp / Scheme,但任务要求"长得像 Java",于是搞了一个披着 Java 外衣的函数式语言

继承机制上,他没选 Java 的"基于类(class-based)"路线,而是借鉴了 Self 这门小众语言的基于原型(prototype-based) 设计:

没有类。一切都是对象。一个对象通过"链接"另一个对象作为自己的"原型",从而共享后者的属性和方法。

这就是原型继承的核心思想。对象 → 对象,不需要中间隔一层"类"。

所以 JS 里你看到的所有"类"的影子(function + newObject.createclass),底层全都是同一件事 —— 某个对象的 __proto__ 指针,指向了另一个对象

把这一条牢记住,后面就好理解了。

三个核心概念,先分清

新人最容易迷糊的就是 __proto__prototype 这俩长得太像了。先记住它们的归属:

名字 在谁身上 是什么
prototype 只有函数有(箭头函数除外) 一个对象。当这个函数被 new 调用时,产生的实例的 __proto__ 会指向它
__proto__ 几乎所有对象都有 指向"我"的原型对象,顺着它能找到祖先
constructor 在原型对象上 指回创建这个原型的构造函数

一句话区分:

  • prototype函数才有的属性,是函数为它的实例准备的"模板"
  • __proto__对象的内部链接,指向它的原型

Person.prototype.__proto__ 你看着头疼,但其实就是:Person 函数的原型对象(Person.prototype,本身是个对象)的原型(.__proto__,也就是 Object.prototype)。

一张图把关系讲清楚

function Person(name) {
  this.name = name
}
Person.prototype.sayHi = function() {
  console.log('Hi, I am ' + this.name)
}
const p = new Person('Alice')

执行这段代码后,内存里的对象关系是:

   ┌─────────────────────┐
   │   Person (function) │
   │   ───────────────── │
   │   prototype  ───────┼────┐
   └─────────────────────┘    │

                  ┌──────────────────────┐
                  │  Person.prototype    │
                  │  ────────────────    │
                  │  sayHi: function     │
                  │  constructor ────────┼──── 指回 Person
                  │  __proto__   ────────┼────┐
                  └──────────────────────┘    │

                                  ┌──────────────────────┐
                                  │  Object.prototype    │
                                  │  ────────────────    │
                                  │  toString / hasOwn / │
                                  │  valueOf / ...       │
                                  │  __proto__: null     │
                                  └──────────────────────┘

   ┌─────────────────────┐
   │   p (实例对象)        │
   │   ───────────────── │
   │   name: 'Alice'     │
   │   __proto__  ───────┼────┐
   └─────────────────────┘    │

                  ┌──────────────────────┐
                  │  Person.prototype    │ ← p 的原型,就是 Person 的 prototype
                  └──────────────────────┘

重点关系:

  1. 函数 Person 自带一个 prototype 属性(JS 帮你自动建好的对象)
  2. Person.prototype.constructor === Person(自动建好的,互相指)
  3. 实例 p__proto__ === Person.prototype(new 操作干的事)
  4. Person.prototype.__proto__ === Object.prototype(因为 Person.prototype 本身是普通对象,它的祖宗是 Object.prototype)
  5. Object.prototype.__proto__ === null(原型链的尽头)

把这五条关系记死,原型这块就拿下了。

new 究竟干了什么

new Person('Alice') 这一句,JS 引擎实际做了 4 件事:

function newOperator(Ctor, ...args) {
  const obj = Object.create(Ctor.prototype)
  
  const ret = Ctor.apply(obj, args)
  
  return (ret !== null && (typeof ret === 'object' || typeof ret === 'function'))
    ? ret
    : obj
}

翻译成人话:

  1. 新建一个空对象
  2. 把这个对象的 __proto__ 指向构造函数的 prototype(原型链就在这一步连上)
  3. 把构造函数当成方法调用,this 绑定到新对象(这样 this.name = name 才能挂上属性)
  4. 如果构造函数返回了一个对象,就返回那个对象;否则返回新对象

第 4 步是大多数人忽略的细节 —— 构造函数里如果 return { foo: 1 },那 new 出来就是 { foo: 1 },跟原型一点关系都没有。

原型链是什么

知道了实例 → 原型 → 上层原型 → ... 这条链,原型链(prototype chain) 就只是它的别名。

当你访问 p.sayHi,JS 的查找顺序:

  1. 先看 p 自身有没有 sayHi —— 没有(p 只有 name)
  2. 沿 p.__proto__,也就是 Person.prototype —— 找到了 sayHi,返回
  3. 如果第 2 步也没有,继续沿 Person.prototype.__proto__,也就是 Object.prototype
  4. 还没有,继续沿 Object.prototype.__proto__,但它是 null,链到头了 —— 返回 undefined

整个查找过程是 沿着 __proto__ 链向上,直到 null。这就是 原型链查找

const p = new Person('Alice')

console.log(p.name)          // 'Alice'     ← 在 p 自身找到
console.log(p.sayHi)         // function   ← 在 Person.prototype 找到
console.log(p.toString)      // function   ← 在 Object.prototype 找到
console.log(p.nonExistent)   // undefined  ← 一路到 null 都没找到

这也解释了为什么 arr.push / arr.map 没定义在你创建的 arr 上,却能调用 —— 它们在 Array.prototype 上,数组实例的 __proto__ 指向那里,沿原型链查就找到了。

Object.create —— 直接玩对象

如果说 new 是用"函数 + 模板"造对象,那 Object.create(proto) 就是 直接指定一个对象当原型,跳过函数那一层。

const animal = {
  eat() { console.log('eating') }
}

const dog = Object.create(animal)
dog.bark = function() { console.log('woof') }

dog.bark()    // 'woof'  ← 自身有
dog.eat()     // 'eating' ← 沿原型链在 animal 上找到

Object.getPrototypeOf(dog) === animal  // true

Object.create(animal) 做的事:新建一个空对象,把它的 __proto__ 指向 animal。完事。没有什么构造函数,没有 prototype,纯粹的"对象 → 对象"关系。

这反而是 最贴近 JS 原型继承本质 的写法。new 反而是 Eich 当年为了"看起来像 Java"加的糖衣。

ES6 class 是真的类吗?

答案:不是。它是 function + prototype + new 的语法糖。

class Person {
  constructor(name) {
    this.name = name
  }
  sayHi() {
    console.log('Hi, I am ' + this.name)
  }
}

const p = new Person('Alice')

跑一下:

typeof Person                                    // 'function'  ← 还是函数!
Object.getPrototypeOf(p) === Person.prototype    // true
typeof Person.prototype.sayHi                    // 'function'

完全等同于:

function Person(name) {
  this.name = name
}
Person.prototype.sayHi = function() {
  console.log('Hi, I am ' + this.name)
}

所以 ES6 没有改变 JS 的对象模型,只是加了更顺眼的写法。

不过 classfunction 写法严格得多,这些点容易踩:

区别 function 写法 class 写法
new 调用 可以(this 变全局或 undefined) TypeError
是否提升 function 整体提升 类似 let,暂时性死区
方法是否可枚举 默认可枚举 不可枚举(for...in 不会遍历到)
严格模式 看上下文 永远在严格模式下
是否能当作对象重新赋值 可以 类内部方法体里不能

继承用 extends:

class Animal {
  eat() { console.log('eating') }
}
class Dog extends Animal {
  bark() { console.log('woof') }
}
const d = new Dog()
d.bark()  // 'woof'
d.eat()   // 'eating'  ← 沿原型链找到

底下其实就是把 Dog.prototype.__proto__ 指向 Animal.prototype,把 Dog.__proto__ 指向 Animal(后者是为了 static 方法能被继承)。

__proto__ 已经"过时",但还活着

__proto__ 这个属性最早是浏览器厂商私下加的,严格来说不是 ES5 标准(ES6 才正式收编)。现代代码应该用:

  • :Object.getPrototypeOf(obj)
  • (很少用,有性能问题):Object.setPrototypeOf(obj, newProto)
  • 建对象时指定原型:Object.create(proto)
const arr = [1, 2, 3]

arr.__proto__                       
Object.getPrototypeOf(arr)          
Array.prototype                     

三种写法拿到的是同一个东西,但前两个表达力差太多。生产代码里看到 __proto__ 直接换成 Object.getPrototypeOf 就行,语义清楚。

还有一个坑:__proto__ 属性其实是 Object.prototype 上定义的一个 getter/setter,所以 Object.create(null) 创出来的对象 没有 __proto__(下文有解释)。这也是为什么标准方法 Object.getPrototypeOf 是首选 —— 它在任何对象上都能工作。

7 个实战中常踩的坑

1. Object.create(null) 创造"无原型对象"

const dict = Object.create(null)
dict.foo = 1

Object.getPrototypeOf(dict)  // null
dict.toString                // undefined   ← 连 Object.prototype 都没继承
dict.__proto__               // undefined   ← 也没有这个属性

用途:做 纯字典(map) 用。

为什么?普通 {} 创出来的对象有 Object.prototype 作为原型,所以 obj['toString'] 永远是 function。如果你拿用户输入当 key:

const map = {}
const userInput = '__proto__'  
console.log(map[userInput])    

虽然现代 V8 / 浏览器已经做了 __proto__ 写入的防护,但 toString / hasOwnProperty 这些"看上去就有"的属性,会污染你的字典逻辑。

推荐:做 hash map / key-value 缓存,用 Object.create(null) 或者直接 new Map()

2. 覆盖整个 prototype 会丢 constructor

function Person() {}

Person.prototype = {
  sayHi() { console.log('hi') }
}

const p = new Person()
p.constructor === Person   
p.constructor === Object   

为什么?因为你整体替换Person.prototype 对象,新对象是 { sayHi: ... },它的原型是 Object.prototype,所以 constructor 沿原型链找到的是 Object

正确做法两种:

Person.prototype = {
  constructor: Person,
  sayHi() { console.log('hi') }
}

Person.prototype.sayHi = function() { console.log('hi') }

class 写法自动处理好这件事,所以这个坑只在老 function 写法里有。

3. 箭头函数没有 prototype,不能 new

const Foo = () => {}

Foo.prototype             
new Foo()                 

箭头函数本质上就不是构造器 —— 它没有自己的 this,没有 prototype,也不能 new这是设计,不是 bug。

记忆口诀:箭头函数 = 只用来当回调的"轻量函数"。要做构造器,老老实实写 function 或者 class

4. for...in 会遍历原型链上的属性

const proto = { inherited: 1 }
const obj = Object.create(proto)
obj.own = 2

for (const k in obj) {
  console.log(k)  
}

这个老坑了。for...in 沿着原型链遍历所有 可枚举 属性,不只是自身的。

对策:

for (const k of Object.keys(obj)) { ... }

for (const k in obj) {
  if (Object.hasOwn(obj, k)) { ... }
}

注意 class 定义的方法是不可枚举的,所以 for...in 实例时不会撞到方法名。但你自己 prototype.foo = ... 的话,foo 默认是可枚举的,这就会撞。

5. instanceof 实际是在查原型链

class Animal {}
class Dog extends Animal {}

const d = new Dog()

d instanceof Dog       // true
d instanceof Animal    // true   ← 沿原型链找到
d instanceof Object    // true   ← 继续往上

A instanceof B 的内部逻辑大致是:沿着 A.__proto__ 链向上查,看有没有等于 B.prototype 的节点,找到就 true。

所以你可以手动篡改原型链来骗 instanceof(虽然很少有理由这么干):

function Cat() {}
const c = new Cat()
c instanceof Dog   

Object.setPrototypeOf(c, Dog.prototype)
c instanceof Dog   

6. Object.hasOwnobj.hasOwnProperty 更安全

ES2022 加了 Object.hasOwn(obj, key),强烈推荐用它替代 obj.hasOwnProperty(key):

const obj = Object.create(null)
obj.foo = 1

obj.hasOwnProperty('foo')      
Object.hasOwn(obj, 'foo')      

Object.create(null) 创的对象没有 hasOwnProperty 方法(因为没原型链)。Object.hasOwn 是个静态方法,不依赖原型链,任何对象都能用。

7. Object.setPrototypeOf 会让 V8 放弃性能优化

const obj = { a: 1 }
Object.setPrototypeOf(obj, somePrototype)

V8 等现代引擎会给每个对象建一个隐藏类(Hidden Class) 做属性访问加速。修改一个已存在对象的原型 会让 V8 抛弃这个对象的隐藏类、退回到字典模式,后续属性访问慢 5-10 倍。

对策:

  • 建对象之前就用 Object.create(proto) 指定好原型
  • 不要在运行时 setPrototypeOf
  • 如果非要继承,用 class extends —— 在对象创建时一次性安排好

这条主要影响热路径代码。偶尔一次 setPrototypeOf 没事,但别在循环里干

一些"高级"原型操作

虽然日常用 class extends 就够,了解下面这些有助于读源码 / 库:

function isPrototypeOf(proto, obj) {
  return Object.prototype.isPrototypeOf.call(proto, obj)
}
// 等价于
Animal.prototype.isPrototypeOf(d)  // true,d 沿原型链能找到 Animal.prototype


const o = {}
Object.getOwnPropertyNames(o)         
Object.getOwnPropertyNames(Object.getPrototypeOf(o))
                                      


Object.defineProperty(obj, 'foo', {
  value: 1,
  writable: false,
  enumerable: false,
  configurable: false,
})


const obj = {
  _x: 0,
  get x() { return this._x },
  set x(v) { this._x = v * 2 },
}
obj.x = 5
console.log(obj.x)   // 10

class 内部的 get / set 方法、static 方法的实现,本质上都是用 Object.defineProperty 把方法装到原型上,并设置成不可枚举。所以 class 真的只是糖

现代写法建议

总结几条 2026 年的最佳实践:

  1. 写"类"用 class,不要再写 function Constructor + prototype.xxx = ... —— 简洁、严格、还少踩坑
  2. 要"对象 → 对象"继承用 Object.create(proto),语义清晰,性能也好
  3. 要 hash map 用 new Map()Object.create(null),别用 {}(原型污染)
  4. 读原型用 Object.getPrototypeOf(obj),别用 obj.__proto__
  5. 建对象之后不要改原型(setPrototypeOf 性能坑)
  6. 判断自身属性用 Object.hasOwn(obj, key),不用 obj.hasOwnProperty(key)
  7. 能用 for...of + Object.keys() 就别 for...in

一句话总结

JS 没有类。所谓"类",只是一个对象通过 __proto__ 指针,认了另一个对象当原型。所谓"继承",只是这条 __proto__ 链可以一直往上找。

class / extends / new / instanceof —— 这些看起来很 OOP 的关键字,全是包装在那一根原型链指针上的糖衣。理解了底下的原型链查找,JS 这块就再也不会绕了。


下次见。

-EOF-