面试问原型链,很多人能背出「每个对象都有 __proto__,指向构造函数的 prototype」,但再追问一句「那 Function.__proto__ 指向谁」就卡住了。我一开始也是这样,图看过好几遍,一到具体的某条线上就理不清方向。
后来发现问题出在把 prototype 和 __proto__ 当成一回事了。这两个属性长得像,挂的对象不一样,指的方向也不一样。这篇从内置对象开始把这两条线分开走一遍,再顺着 new 的过程看这些连接是什么时候建立起来的。
在本篇文章中,我们将从浅入深,和大家一起学习以下知识:
- JS 内置对象里,哪些是函数哪些只是普通对象
prototype是谁的属性,什么时候被创建constructor到底有什么用,它为什么算历史遗留__proto__和[[Prototype]]的关系new操作符做了哪四件事,手写一个new- 原型链的终点在哪里
JSON和Math为什么不能new
一、JS内置对象
所谓的内置对象指的是,JavaScript 本身就自己有的对象,可以直接拿来就用,例如 Array、String 等等。按老的教材分法,JavaScript 一共有 12 个内置对象。
函数类型有 10 个:
StringNumberBooleanArrayFunctionDateRegExpErrorObjectEvent
函数类型有 __proto__ 和 prototype 属性。
对象类型有 2 个:
MathJSON
对象类型只有 __proto__ 属性。

这个「12 个」的说法是当年教材里的分法,拿来入门够用,但别当成规范。ECMAScript 现在的内置对象远不止这些,Map、Set、Promise、Symbol、Proxy、Reflect、WeakMap 都是。而且列表里的 Event 严格说不属于 ECMAScript,它是浏览器提供的宿主对象,在 Node 里就没有这个全局变量。
真正要记住的不是数字,而是那条分界线,能不能被 new 调用。能的是构造函数,身上有 prototype;不能的就是个普通对象,只有 __proto__。
二、JS原型链
2.1 概述

三条基本规则先摆在这儿:
- 每个函数都有
prototype属性,该属性指向原型 - 每个对象都有
__proto__属性,指向了创建该对象的构造函数的原型。其实这个属性指向了[[Prototype]],但是[[Prototype]]是内部属性,我们并不能访问到,所以使用__proto__来访问 - 对象可以通过
__proto__来寻找不属于该对象的属性,__proto__将对象连接起来组成了原型链
打开浏览器的控制面板,随便输入一个 JS 内置的构造器函数,比如 Array,控制台输出的是一个名为 Array 的函数体,这好像并没有什么稀奇的。但是当你接着输入 Array.prototype,控制面板输出了一堆我们经常用到的 Array 构造器的方法。把目光转移到最下方,有一个叫 __proto__ 的属性,好奇的点开,列表列出的不是 Object 构造器的方法么,里边有我们非常熟悉的 hasOwnProperty 还有 toString 等方法。
如果 Array 是构造器,那么控制面板输出的 Array.prototype 的所有属性中 constructor 又是什么构造器?点开看看,之后就像身处德罗斯特效应中一样,__proto__ 和 constructor,还有 Array 构造器中常用的方法名不断地出现,一层套一层,一层层展开,没有尽头。

拿 Array 举例,Array.prototype 中有一个 constructor 属性,这个属性的值就是 Array 构造器自己。
Array.prototype.constructor === Array //true |
看起来没有尽头,其实是有的。展开的过程之所以像无限循环,是因为原型对象上的 constructor 指回构造函数,构造函数又能点出 prototype,两者互相引用形成了一个环。DevTools 只是老实地把这个环一层层渲染出来而已,真正的原型链本身是有终点的,后面会讲到。
2.2 prototype
这是一个显式原型属性,只有函数才拥有该属性。基本上所有函数都有这个属性,但是也有例外。
let fun = Function.prototype.bind() |
如果你以上述方法创建一个函数,那么可以发现这个函数是不具有 prototype 属性的。
原文只提了 bind 这一种例外,其实没有 prototype 的函数还有几类,顺手补全:
bind返回的绑定函数- 箭头函数,所以箭头函数不能用
new调用 - 对象字面量里的方法简写
{ foo() {} } Math.max这类内置的非构造函数
它们的共同点是「设计上就不打算被 new」。没有 prototype,new 出来的实例就没有原型可以挂,语言干脆直接禁止这么用。
2.2.1 prototype 如何产生的
当我们声明一个函数时,这个属性就被自动创建了。
function Foo() {} |
并且这个属性的值是一个对象(也就是原型),只有一个属性 constructor,constructor 对应着构造函数,也就是 Foo。
2.2.2 constructor
constructor 是一个公有且不可枚举的属性。一旦我们改变了函数的 prototype,那么新对象就没有这个属性了(当然可以通过原型链取到 constructor)。
那么你肯定也有一个疑问,这个属性到底有什么用呢?它可以说是一个历史遗留问题,在大部分情况下是没用的。按我的理解它有两个作用:
- 让实例对象知道是什么函数构造了它
- 如果想给某些类库中的构造函数增加一些自定义的方法,就可以通过
xx.constructor.method来扩展
这里有个坑要注意。老式的继承写法会直接覆盖 prototype:
function Child() {} |
不补这一行的话,new Child().constructor 会顺着原型链找到 Parent,判断类型的代码就会拿到错的结果。所以老代码里经常能看到这么一行看起来莫名其妙的赋值。
顺带说一句,constructor 之所以是不可枚举的,靠的就是属性描述符里的 enumerable: false。这也是为什么 for...in 遍历实例时不会把它列出来。
2.3 __proto__
这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[Prototype]],但是 [[Prototype]] 是内部属性,我们并不能访问到,所以使用 __proto__ 来访问。
因为在 JS 中是没有类的概念的(class 是语法糖),为了实现类似继承的方式,通过 __proto__ 将对象和原型联系起来组成原型链,得以让对象可以访问到不属于自己的属性。
有一点要说明,__proto__ 本身是浏览器厂商各自实现之后才被写进规范附录的遗留特性,规范推荐的标准写法是 Object.getPrototypeOf(obj) 和 Object.setPrototypeOf(obj, proto)。日常调试用 __proto__ 方便,写进代码库建议用标准 API。另外 Object.setPrototypeOf 性能很差,会让引擎放弃已有的内联缓存优化,能在创建时用 Object.create 定好就别后面改。
2.3.1 实例对象的 __proto__ 如何产生的
当我们使用 new 操作符时,生成的实例对象拥有了 __proto__ 属性。
function Foo() {} |
所以可以说,在 new 的过程中,新对象被添加了 __proto__ 并且链接到构造函数的原型上。
有一个例外要记住,Object.create(null) 创建出来的对象没有原型,它的 __proto__ 是 undefined。用它当纯粹的字典结构很合适,因为不用担心 hasOwnProperty、toString 这些名字跟你的键撞车。
2.3.2 new 的过程
调用 new 的过程中会发生四件事情:
- 新生成了一个对象
- 链接到原型
- 绑定
this - 返回新对象
我们也可以试着来自己实现一个 new:
function create() { |
这版能说明思路,但最后那行判断有个漏洞。typeof null 的结果也是 'object',所以构造函数里显式 return null 的话,这个实现会把 null 返回出去,而真正的 new 在这种情况下返回的是新建的那个对象。另外构造函数返回一个函数时,new 也是以返回值为准的。修正后的判断应该是这样:
function create(Con, ...args) { |
对于实例对象来说,都是通过 new 产生的,无论是 function Foo() 还是 let a = { b : 1 }。
对于创建一个对象来说,更推荐使用字面量的方式创建对象。因为你使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。
// function 就是个语法糖 |

这里 Array 是内置对象且是函数类型,所以 Array 有 __proto__ 属性,指向的是函数类型的原型。当我们输出 Array.__proto__.__proto__ 时,就会返回对象类型的原型,但是再向上就是 null 了,因为 Object.prototype 是这条链的顶端,所有对象最终都继承自它。
原文这里写成了 Array.__proto__.proto__,少了两个下划线,是个笔误,正确写法是 Array.__proto__.__proto__。
用代码验证一下这条链更直观:
Array.__proto__ === Function.prototype // true |
JS 内置构造器其中之一的 Array 原本就是一个函数,而所有函数的 __proto__ 都指向 Function.prototype,所以 Function.prototype 上有的方法,JS 内置构造器都有,比如 call()、apply()、bind() 等。我们自定义的函数也一样继承自 Function.prototype,所以我们自己也可以定义构造器。而 Function.prototype 的原型指针又指向了 Object.prototype。
原文这段把「Array 这个函数就是 Function 的 prototype」写混了,准确的说法是 Array.__proto__ === Function.prototype,Array 本身不是 Function.prototype。
// 数组实例的__proto__指向构造器的原型 |
2.4 总结
Object是所有对象的爸爸,所有对象都可以通过__proto__找到它Function是所有函数的爸爸,所有函数都可以通过__proto__找到它Function.prototype和Object.prototype是两个特殊的对象,他们由引擎来创建- 除了以上两个特殊对象,其他对象都是通过构造器
new出来的 - 函数的
prototype是一个对象,也就是原型 - 对象的
__proto__指向原型,__proto__将对象和原型连接起来组成了原型链
关于原型有 3 个相关的概念:
- 函数对象的
prototype属性,可以称之为显式原型属性(简称显式原型) - 实例对象的
__proto__属性,可以称之为隐式原型属性(简称隐式原型) - 原型对象,也就是
prototype属性和__proto__属性指向的对象

图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
看这张图的时候有两条最绕的线值得单独盯一下。一条是 Function.__proto__ === Function.prototype,Function 自己是自己的实例,这是引擎为了让规则自洽做的特殊处理。另一条是 Object.__proto__ === Function.prototype,因为 Object 本身也是个函数。
顺带提一句,正因为原型是靠引用连接的,深拷贝默认不会保留这条链。structuredClone 拷完 class 实例会变成普通对象,就是这个原因,具体在 JavaScript深浅拷贝原理与手写实现 里讲过。
三、JSON和Math
JS 内置的构造器函数都可以使用 new 关键字实例化一个对象,我们称实例化后的这个对象就是某某构造器的一个实例。

我们试试 JSON 和 Math 能不能实例化对象。

JSON 和 Math 不是构造器函数,他们是普通的对象。只有构造器函数才能使用 new 关键字实例化一个对象,而 JSON 和 Math 已经是对象了,所以我们可以不用实例化,直接使用 JSON 和 Math 中的属性和方法。
所以 JSON 和 Math 不属于那 10 个构造器函数,但他们 12 个共同属于 JavaScript 的内置对象。
为什么设计成这样?因为这两个东西压根没有实例状态。Math.PI、Math.max 全都是静态数据和纯函数,造一百个 Math 实例出来没有任何意义。同类的还有后来加的 Reflect,也是一个纯粹的静态方法容器。
判断某个东西能不能 new,最直接的办法是看 typeof。typeof Math 是 'object',typeof Array 是 'function',只有后者才有可能被 new 调用。当然前面说过,是 function 也不一定能 new,箭头函数就不行。
总结
prototype 和 __proto__ 的分工是这块的入口。prototype 只挂在函数上,是「我 new 出来的实例应该长什么样」的模板;__proto__ 挂在每个对象上,是「我实际继承自谁」的指针。两者通过 new 建立连接,obj.__proto__ === Con.prototype。
原型链的终点是 Object.prototype.__proto__ === null。查找一个属性时引擎顺着 __proto__ 一路往上走,走到 null 还没找到就返回 undefined。链越长查找越慢,这也是不建议随便加深继承层次的原因。
手写 new 的四步里,最容易写错的是最后那步返回值判断。typeof null === 'object' 这个历史遗留 bug 会让简化版实现在构造函数返回 null 时给出错误结果,正确的判断要把 null 排除掉,同时把函数也算进「对象」。
constructor 大部分时候用不上,但改写 prototype 之后一定要手动补回来,否则实例的类型判断会指向错误的构造函数。
最后,__proto__ 是遗留写法,代码里请用 Object.getPrototypeOf;创建对象时想指定原型,用 Object.create 而不是事后 setPrototypeOf,后者会让引擎的优化全部失效。