从prototype与__proto__窥探JS继承之源 | 掘金技术征文

3,856 阅读8分钟

之前分享了一篇文章JS原型链与继承别再被问倒了,发现继承的问题研究到后面,一定会触及到Object.prototype和Function.prototype这些概念,为了解答疑惑,本篇就抽丝剥茧,从prototype与__proto__来推出函数与对象的深层关系。
原文:详解prototype与__proto__

概念

  1. prototype 是函数(function) 的一个属性, 它指向函数的原型.
  2. __proto__ 是对象的内部属性, 它指向构造器的原型, 对象依赖它进行原型链查询,instanceof 也是依赖它来判断是否继承关系.

由上, prototype 只有函数才有, 其他(非函数)对象不具有该属性. 而 __proto__ 是对象的内部属性, 任何对象都拥有该属性.

栗子

下面我们来吃个栗子帮助消化下:

function Person(name){
  this.name = name;
}
var p1 = new Person('louis');

console.log(Person.prototype);//Person原型 {constructor: Person(name),__proto__: Object}
console.log(p1.prototype);//undefined

console.log(Person.__proto__);//空函数, function(){}
console.log(p1.__proto__ == Person.prototype);//true

吃栗子时我们发现, Person.prototype(原型) 默认拥有两个属性:

  • constructor 属性, 指向构造器, 即Person本身
  • __proto__ 属性, 指向一个空的Object 对象

而p1作为非函数对象, 自然就没有 prototype 属性; 此处佐证了概念1

下面来看看__proto__属性:

Person.__proto__ 属性 指向的是一个空函数( function(){} ), 待会儿我们再来研究这个空函数.

p1.__proto__ 属性 指向的是 构造器(Person) 的原型, 即 Person.prototype. 此处佐证了概念2

这里我们发现: 原型链查询时, 正是通过这个属性(__proto__) 链接到构造器的原型, 从而实现查询的层层深入.

概念1 不太理解的同学, 说明你们不会吃栗子, 咱们忽略他们. 对 概念2 不太理解的同学, 我们来多吃几个栗子, 边吃边想:

var obj = {name: 'jack'},
    arr = [1,2,3],
    reg = /hello/g,
    date = new Date,
    err = new Error('exception');

console.log(obj.__proto__  === Object.prototype); // true
console.log(arr.__proto__  === Array.prototype);  // true
console.log(reg.__proto__  === RegExp.prototype); // true
console.log(date.__proto__ === Date.prototype);   // true
console.log(err.__proto__  === Error.prototype);  // true

可见, 以上通过 对象字面量 和 new + JS引擎内置构造器() 创建的对象, 无一例外, 它们的__proto__ 属性全部指向构造器的原型(prototype). 充分佐证了 概念2 .

__proto__

刚才留下了一个问题: Person.__proto__ 指向的是一个空函数, 下面我们来看看这个空函数究竟是什么.

console.log(Person.__proto__ === Function.prototype);//true

Person 是构造器也是函数(function), Person的__proto__ 属性自然就指向 函数(function)的原型, 即 Function.prototype.

这说明了什么呢?

我们由 "特殊" 联想到 "通用" , 由Person构造器联想一般的构造器.

这说明 所有的构造器都继承于Function.prototype (此处我们只是由特殊总结出了普适规律, 并没有给出证明, 请耐心看到后面) , 甚至包括根构造器Object及Function自身。所有构造器都继承了Function.prototype的属性及方法。如length、call、apply、bind(ES5)等. 如下:

console.log(Number.__proto__   === Function.prototype); // true
console.log(Boolean.__proto__  === Function.prototype); // true
console.log(String.__proto__   === Function.prototype); // true
console.log(Object.__proto__   === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true
console.log(Array.__proto__    === Function.prototype); // true
console.log(RegExp.__proto__   === Function.prototype); // true
console.log(Error.__proto__    === Function.prototype); // true
console.log(Date.__proto__     === Function.prototype); // true

JavaScript中有内置(build-in)构造器/对象共计13个(ES5中新加了JSON),这里列举了可访问的9个构造器。剩下如Global不能直接访问,Arguments仅在函数调用时由JS引擎创建,Math,JSON是以对象形式存在的,无需new。由于任何对象都拥有 __proto__ 属性指向构造器的原型. 即它们的 __proto__ 指向Object对象的原型(Object.prototype)。如下所示:

console.log(Math.__proto__ === Object.prototype);  // true
console.log(JSON.__proto__ === Object.prototype);  // true

如上所述, 既然所有的构造器都来自于Function.prototype, 那么Function.prototype 到底是什么呢?

Function.prototype

我们借用 typeof 运算符来看看它的类型.

console.log(typeof Function.prototype) // "function"

实际上, Function.prototype也是唯一一个typeof XXX.prototype为 “function”的prototype。其它的构造器的prototype都是一个对象。如下:

console.log(typeof Number.prototype)   // object
console.log(typeof Boolean.prototype)  // object
console.log(typeof String.prototype)   // object
console.log(typeof Object.prototype)   // object
console.log(typeof Array.prototype)    // object
console.log(typeof RegExp.prototype)   // object
console.log(typeof Error.prototype)    // object
console.log(typeof Date.prototype)     // object

JS中函数是一等公民

既然Function.prototype 的类型是函数, 那么它会拥有 __proto__ 属性吗, Function.prototype.__proto__ 会指向哪里呢? 会指向对象的原型吗? 请看下方:

console.log(Function.prototype.__proto__ === Object.prototype) // true

透过上方代码, 且我们了解到: Function.prototype 的类型是函数, 也就意味着一个函数拥有 __proto__ 属性, 并且该属性指向了对象(Object)构造器的原型. 这意味着啥?

根据我们在 概念2 中了解到的: __proto__ 是对象的内部属性, 它指向构造器的原型.

这意味着 Function.prototype 函数 拥有了一个对象的内部属性, 并且该属性还恰好指向对象构造器的原型. 它是一个对象吗? 是的, 它一定是对象. 它必须是.

实际上, JavaScript的世界观里, 函数也是对象, 函数是一等公民.

这说明所有的构造器既是函数也是一个普通JS对象,可以给构造器添加/删除属性等。同时它也继承了Object.prototype上的所有方法:toString、valueOf、hasOwnProperty等。

Object.prototype

函数的 __proto__ 属性指向 Function.prototype, 如: Person.__proto__ —> Function.prototype

Function.prototype 函数的 __proto__ 属性指向 Object.prototype, 如: Function.prototype.__proto__ —> Object.prototype.

那么Object.prototype.__proto__ 指向什么呢?

console.log(Object.prototype.__proto__ === null);//true

由于已经到顶, JS世界的源头一片荒芜, 竟然什么都没有! 令人嗟叹不已.

都说一图胜千言, 我也不能免俗. 下面附一张 stackoverflow 上的图:

这张图也指出:

  • Object.__proto__ == Function.prototype,
  • Function.__proto__ == Function.prototype.
//虽然上面做过测试, 我们还是再次测试下
console.log(Object.__proto__   == Function.prototype);//true
console.log(Function.__proto__ == Function.prototype);//true

由于对象构造器 (Object) 也是构造器, 又构造器都是函数, 又函数是一等公民, 函数也是对象.

故, 对象构造器 (Object) 拥有3种身份:

  • 构造器
  • 函数
  • 对象

推而广之, 所有构造器都拥有上述3种身份.

由于构造器是 对象 (身份3), 理所当然拥有 __proto__ 属性, 且该属性一定指向其构造器的原型, 也就是指向 函数 (身份2) 构造器(Function)的原型, 即 Function.prototype. 于是我们证明了上面那句 所有的构造器都继承于Function.prototype (身份1).

注: 上面代码中用到的 __proto__ 目前在IE6/7/8/9中并不支持。IE9中可以使用Object.getPrototypeOf(ES5)获取对象的内部原型。


附上网友的疑问(问题提得特别好,问出了函数与对象最尖锐的归宿问题):

问题背景:先有 Object.prototype(原型链顶端),Function.prototype 继承 Object.prototype 而产生,最后,Function 和 Object 和其它构造函数继承 Function.prototype 而产生。

以下是具体问题:

  1. 先有 Object.prototype,再有 Object,那么先有 Object.prototype 里面的这个 Object 代表的是什么呢?

  2. Function.__proto__=== Function.prototype;
    Function 构造函数的 prototype 属性和__proto__属性都指向同一个原型,是否可以说 Function 对象是由 Function 构造函数创建的一个实例?

  3. Object instanceof Function // true
    Function instanceof Object // true
    Object 本身是构造函数,继承了 Function.prototype; Function 也是对象,继承了Object.prototype。感觉成了鸡和蛋的问题了。

  4. 比如说:
    function Person(){}
    Person.prototype 是一个对象,为什么 Function.prototype 却是一个函数呢,当然函数也是一个对象,为什么要这么设计呢?

这里是疑惑:

感觉有些地方很难理解,总感觉有种悖论一样,还有人扯到罗素悖论,各有说辞,不知道楼主怎么看。。。

通读全文如果你能回答这些问题,说明看懂了本文。否则请允许我建议你再读一篇,别打我。


---华丽丽的分割线---

以下是回答:
答1: 先有的一定是Object,它是BOM对象,打印出来为:function Object() { [native code] },然后才是Object.prototype。这不矛盾,请看分析。
首先:Object.create(null)你应该知道,它可以创建一个没有原型的对象。如下:
Object.prototype.__proto__ == Object.create(null).__proto__
结果为true,也就是说,Object.prototype是使用这种方式生成的,然后才绑在Object的prototype属性上。为什么这里没有用===,因为__proto__指向的是对象,对象是没法比较相等的。
要知道,对象可以先生成,然后再赋予新的属性。

答2: Function.__proto__=== Function.prototype 这没毛病。
我们都知道,Function是一个函数,只要是函数,它的__proto__就指向 Funtion.prototype. 这是继承的设定。那么我们怎么理解这种构造器本身就继承本身的prototype属性的现象呢?
Function生成时,是没有__proto__属性的,它是一个BOM对象,打印出来为:function Function() { [native code] },Function.prototype同样是BOM对象,打印出来为:function () { [native code] },那么可以这么理解:
Function的__proto__和prototype属性都是后面才指向同一个BOM对象的。

答3: 这不是蛋和鸡的问题,计算机中没有蛋和鸡。
Object instanceof Function // true 。Object是构造器函数,你必须认,有new Object()为证。
答2已经给了结论:只要是函数,它的__proto__就指向 Funtion.prototype。Object也是函数。因此它的__proto__属性会指向Funtion.prototype。故而Object instanceof Function 为 true。

答4: Function.prototype是一个函数,也是对象。这是自然的。
那么为什么这么设计呢?你注意到没有 typeof Function.prototype === "function" 结果为true。注意:只有Function的prototype是函数,其他都是普通对象。它是一个桥梁,我们一直说函数也是对象,只有这个满足了, 函数才能是对象,关于『js中函数是一等公民』的定理,Function.prototype同时是函数, 又是原型对象便是佐证。

说了说去,都回到了Function.prototype、Function和Object的问题上,本质上它们都是BOM对象,即它们都在浏览器global对象(这个js是访问不到的)生成之前生成的,后面的指向关系,可以认为是js世界继承规律的一种设定,有了这个设定,js游戏才能玩下去。


本文就讨论这么多内容,大家有什么问题或好的想法欢迎在下方参与留言和评论.

本文作者: louis

本文链接: louiszhai.github.io/2015/12/17/…

本次活动链接:juejin.cn/post/684490…

参考文章