creeperyang/blog

从__proto__和prototype来深入理解JS对象和原型链

Opened this issue · 76 comments

就标题而言,这是七八篇里起得最满意的,高大上,即使外行人也会不明觉厉! 😂

不过不是开玩笑,本文的确打算从__proto__prototype这两个容易混淆来理解JS的终极命题之一:对象与原型链

__proto__prototype

__proto__

引用《JavaScript权威指南》的一段描述:

Every JavaScript object has a second JavaScript object (or null ,
but this is rare) associated with it. This second object is known as a prototype, and the first object inherits properties from the prototype.

翻译出来就是每个JS对象一定对应一个原型对象,并从原型对象继承属性和方法。好啦,既然有这么一个原型对象,那么对象怎么和它对应的?

对象__proto__属性的值就是它所对应的原型对象:

var one = {x: 1};
var two = new Object();
one.__proto__ === Object.prototype // true
two.__proto__ === Object.prototype // true
one.toString === one.__proto__.toString // true

上面的代码应该已经足够解释清楚__proto__了:grin:。好吧,显然还不够,或者说带来了新的问题:Object.prototype是什么?凭什么说onetwo的原型就是Object.prototype

prototype

首先来说说prototype属性,不像每个对象都有__proto__属性来标识自己所继承的原型,只有函数才有prototype属性。

为什么只有函数才有prototype属性?ES规范就这么定的。

开玩笑了,其实函数在JS中真的很特殊,是所谓的_一等公民_。JS不像其它面向对象的语言,它没有类(class,ES6引进了这个关键字,但更多是语法糖)的概念。JS通过函数来模拟类。

当你创建函数时,JS会为这个函数自动添加prototype属性,值是空对象 值是一个有 constructor 属性的对象,不是空对象。而一旦你把这个函数当作构造函数(constructor)调用(即通过new关键字调用),那么JS就会帮你创建该构造函数的实例,实例继承构造函数prototype的所有属性和方法(实例通过设置自己的__proto__指向承构造函数的prototype来实现这种继承)。

小结

虽然对不熟悉的人来说还有点绕,但JS正是通过__proto__prototype的合作实现了原型链,以及对象的继承。

构造函数,通过prototype来存储要共享的属性和方法,也可以设置prototype指向现存的对象来继承该对象。

对象的__proto__指向自己构造函数的prototypeobj.__proto__.__proto__...的原型链由此产生,包括我们的操作符instanceof正是通过探测obj.__proto__.__proto__... === Constructor.prototype来验证obj是否是Constructor的实例。

回到开头的代码,two = new Object()Object是构造函数,所以two.__proto__就是Object.prototype。至于one,ES规范定义对象字面量的原型就是Object.prototype

更深一步的探讨

我们知道JS是单继承的,Object.prototype是原型链的顶端,所有对象从它继承了包括toString等等方法和属性。

Object本身是构造函数,继承了Function.prototype;Function也是对象,继承了Object.prototype。这里就有一个_鸡和蛋_的问题:

Object instanceof Function // true
Function instanceof Object // true

什么情况下会出现鸡和蛋的问题呢?就是声明一个包含所有集合的集合啊!好了,你们知道这是罗素悖论,但并不妨碍PL中这样设计。

那么具体到JS,ES规范是怎么说的?

Function本身就是函数Function.__proto__是标准的内置对象Function.prototype

Function.prototype.__proto__是标准的内置对象Object.prototype

以上均翻译自http://www.ecma-international.org/ecma-262/5.1/#sec-15,_鸡和蛋_的问题就是这么出现和设计的:**`Function`继承`Function`本身,`Function.prototype`继承`Object.prototype`。**

一张图和总结

原型链

Update: 图片来自 mollypages.org

相信经过上面的详细阐述,这张图应该一目了然了。

  1. Function.prototypeFunction.__proto__都指向Function.prototype,这就是鸡和蛋的问题怎么出现的。
  2. Object.prototype.__proto__ === null,说明原型链到Object.prototype终止。

ObjectFunction的鸡和蛋的问题

ES5关于ObjectFunction的规定:

Object

Function

从上面的规定再结合其它,理出以下几点:

  1. 原型链的尽头(root)是Object.prototype所有对象均从Object.prototype继承属性。

    prototype
  2. Function.prototypeFunction.__proto__同一对象

    function prototype

    这意味着: Object/Array/String等等构造函数本质上和Function一样,均继承于Function.prototype

  3. Function.prototype直接继承root(Object.prototype)。

    function object

    通过这点我们可以弄清 继承的原型链:Object.prototype(root)<---Function.prototype<---Function|Object|Array... 如下图所示:

    chain

以上3点比较容易理解,或者说规范里就这样定义的。由以上3点导出我们最后的问题:ObjectFunction的鸡和蛋的问题。

回答这个问题,必须首先更深入一层去理解Function.prototype这个对象,因为它是导致Function instanceof ObjectObject instanceof Function都为true的原因。

回归规范,摘录2点:

  1. Function.prototype是个不同于一般函数(对象)的函数(对象)。

    The Function prototype object is itself a Function object (its [[Class]] is "Function") that, when invoked, accepts any arguments and returns undefined.

    The value of the [[Prototype]] internal property of the Function prototype object is the standard built-in Object prototype object (15.2.4). The initial value of the [[Extensible]] internal property of the Function prototype object is true.

    The Function prototype object does not have a valueOf property of its own; however, it inherits the valueOf property from the Object prototype Object.

    1. Function.prototype像普通函数一样可以调用,但总是返回undefined
    2. 普通函数实际上是Function的实例,即普通函数继承于Function.prototypefunc.__proto__ === Function.prototype
    3. Function.prototype继承于Object.prototype,并且没有prototype这个属性。func.prototype是普通对象,Function.prototype.prototypenull
    4. 所以,Function.prototype其实是个另类的函数,可以独立于/先于Function产生。
  2. Object本身是个(构造)函数,是Function的实例,即Object.__proto__就是Function.prototype

    The value of the [[Prototype]] internal property of the Object constructor is the standard built-in Function prototype object.

    The value of the [[Prototype]] internal property of the Object prototype object is null, the value of the [[Class]] internal property is "Object", and the initial value of the [[Extensible]] internal property is true.

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

十分感谢

会玩!

@Kelichao 哈哈,欢迎提出见解。

相当绕啊,不过终于让我明白透了这关系。

再次观摩男神。。再学习一遍

这东西感觉要隔一段时间就看,不然很容易被绕迷糊。
感觉把Object.prototype和Function.prototype这两看成浏览器的私货,比如下面这操蛋的一对:

typeof Object.prototype === 'object' && (Object.prototype instanceof Object === false)

typeof Function.prototype === 'function' && (Function.prototype instanceof Function === false)

@simongfxu typeof 这个操作符跟原型链没什么必然的关系. typeof null 还等于 'object'呢.

为什么我试了一下第一个例子都是返回false,用的是chrome。

@igblee 截图看看

学习一下

我喜欢你画的图,好有感觉。么么哒

jawil commented

又被骗进来了吧😄 @Evllis

@Evllis 忘记从哪偷的图了... 很久很久以前就有这张图了。

Update: 找不到图片具体哪来的,但目前看起来,一个可信的来源是 mollypages.org

图是从王福朋那里来的 他写的一系列关于原型链的文章,大家伙可以去看看

jawil commented

http://www.cnblogs.com/wangfupeng1988/p/4001284.html

@bonzstars 是这个吗?前不久刚看完,还得多看几次。

顺便请教博主一个问题:@creeperyang

在JavaScript中,Function构造函数本身也算是Function类型的实例吗?
Function构造函数的prototype属性和__proto__属性都指向同一个原型,是否可以说Function对象是由Function构造函数创建的一个实例?

还有就是JavaScript 里 Function 也算一种基本类型吗?Function继承了Object的所有属性,那为什么还要单独搞个Function,直接在Object延伸,然后其他类型继承Object,非要转个弯。

感觉看了这篇文章,以前的疑问又出来了,就是这个Function.proto === Function.prototype的问题。

其实我们可以先看这样一个问题:

Object.prototype 是对象吗?

  1. 当然是。An object is a collection of properties and has a single prototype object. The prototype may be the null value. 这是object的定义,Object.prototype显然是符合这个定义的。
  2. 但是,Object.prototype并不是Object的实例。 这也很好理解Object.prototype.__proto__null

2017-04-19 8 11 34

这已经某种程度上解开了鸡和蛋的问题:Object.prototype是对象,但它不是通过Object函数创建的。

下面我们来看Function/Function.prototype

2017-04-19 8 20 59

似乎可以看出一点东西:

在chrome的console中,Array.prototype以数组的形式输出,Map.prototype以Map的形式输出,Function.prototype输出function () { [native code] }... 可以反过来讲继承了某个prototype,console就认为是对应的类型(以prototype来判断)。

2017-04-19 8 32 46

就上图而言,我们有疑惑的其实就是为什么 Function.prototypeFunction.__proto__是同一个对象

  1. Function本身也是function。
  2. Function.prototype是所有function的原型(包括Function自己)。
  3. 但反过来,Function.prototypeFunction并没有反向的什么关系(除了正向的Function继承了Function.prototype)。

所以疑惑就可以解除了:Function.prototypeFunction.__proto__相同不代表Function这个函数是由自身创建的。先有了Function.prototype这个对象(其实也是函数,下面说明),然后才有了其它函数而已。

那么问题来了,Function.prototype/Function.__proto__是 function 吗(对比开头的问题)?

  1. 当然是。比如我们可以正常执行Function.prototype()。当然,还是看定义更好:

    member of the Object type that may be invoked as a subroutine. In addition to its properties, a function contains executable code and state that determine how it behaves when invoked. A function’s code may or may not be written in ECMAScript.

    ECMAScript function objects encapsulate parameterized ECMAScript code closed over a lexical environment and support the dynamic evaluation of that code. An ECMAScript function object is an ordinary object and has the same internal slots and the same internal methods as other ordinary objects.

  2. 然而 Function.prototype 不是 Function 的实例。


下面附加一幅图帮助理解:

2017-04-19 9 17 51

用issue写博客。。。。。这奇葩的方式也是没谁了,用github pages发布个静态博客有那么难?

@eddiebai 这不是难不难的问题吧,issue 相比 github pages 更加方便吧...

jawil commented

你们怎么都来得这么快?都睡在GayHub里面吗?@rccoder

@jawil 邮件会 push 啊...

po主开放issue也不过是更方便讨论,尤其是这种技术性的东西,难免会有疏漏和误差,开issue没什么不妥,别那种质疑的口气,我不觉得po主连github pages都不知道。

github pages 是来供开发者展示自己项目的,用pages搭blog其实不妥,po主用issue没毛病

po主上面说 我们的操作符instanceof正是通过探测obj.proto.proto... === Constructor.prototype来验证obj是否是Constructor的实例。
如下

function a(){}
function b(){}
function c(){}
b.prototype = a;
c.prototype = a;
var cInstance = new c();
cInstance instanceof b/// true???? 

可是 cInstance 并不是 b的实例啊

@wangcansunking
假设你的 c.prototype = a; 写错了 应该是c.prototype = b;
cInstance instanceof b 是拿 cInstance.proto.proto...(无限递归__proto__属性) 跟 b.prototype比较
你这里

cInstance.__proto__ == c.prototype == b
cInstance.__proto__.__proto__  == b.__proto__ == Object.prototype

中间没有 b.prototype

应该这么写 才能得到你要的结果

c.prototype = new b()

有点奇怪Array.prototype的类型是数组,但是又跟object一样的写法和属性访问,而且Array.prototype.length为0,不知道这是怎么实现的

@yangblink
我想要的是实现b继承a,c也继承a。
和同事讨论这个问题,他给我的答案是

function a(){}
function b(){}
function c(){}
b.prototype = new a();
c.prototype = new a();
var cInstance = new c();
cInstance instanceof b/// false

然后还有的推荐

function a(){}
function b(){}
function c(){}
b.prototype = new a();
b.prototype.constructor = b;
c.prototype = new a();
c.prototype.constructor = c;
var cInstance = new c();
cInstance instanceof b/// false

b.prototype.constructor = b;这句话的作用是什么呢?

@wangcansunking
你试一下不重设构造函数c的原型的构造方法就知道,就是去掉c.prototype.constructor = c;,然后看看cInstance instanceof a
构造函数声明的时候就会有prototype属性,然后prototype上会有constructor方法,这个方法就是构造函数本身,但是你再b.prototype = new a()的时候修改了它,使得它变成了构造函数a,如果不重设,那么这时候通过构造函数b创建出来的实例就会出现继承紊乱,你会发现这个实例它同时是b和a的实例

@bingchenqin
因为这个实现的是继承的机制,我在本地进行测试无论是否加
c.prototype.constructor = c
cInstance instanceof acInstance instanceof c都是 true
而本质上如果实现继承的**的话,我想要的也就是这两个都是 true
如果是为了保持prototype的一致性我可以理解,毕竟一个构造函数的prototype.constructor要指向本身。

当你创建函数时,JS会为这个函数自动添加prototype属性,值是空对象吗?

精华贴

学习了

学习了学习了,大神总结到位,一目了然.

首先来说说prototype属性,不像每个对象都有__proto__属性来标识自己所继承的原型,只有函数才有prototype属性。

这里的__proto__ 是不是应该是prototype

这个真的是厉害了!!
不过我是来提问的,不知道大神会不会看到。。

function Parent(){}

Parent.prototype.hello = function(){
    console.log("hello");
};

function Child(){
    Parent.call(this);
}

Child.prototype = Object.create(Parent.prototype);

Child.prototype.constructor = Child;

Child.prototype.haluo = function(){
    console.log("haluo");
};

var c = new Child;

上面的代码仅仅是一个简单的继承,这没有问题。但是我在 chrome devtools 里面发现问题了(可能要劳烦大神写个html测试一下了)。

简单的说明:在 devtools DEBUG 模式看 Scope 变量,这个变量后面会显示它的来源信息,也正是这个来源信息让我产生了疑惑。

用 devtools 观察实例对象 cc.__proto__ 属性没有问题,c.__proto__.__proto__ 属性也没有问题,有问题的是 c.__proto__ 后面显示的是 Parent, c.__proto__.__proto__ 后面显示的是 Object。这我就无法理解了。c 作为一个实例对象,c.__proto__ 后面的数据不应当是 Child 吗?(我用ES6重写了这段代码,也是这个现象)

作为对比,我查看了 Array , WebSocket 等对象的实例,它们的就没有这个问题,比如:

var x = [];

x.__proto__ 的后面显示的是 Array,而且 x.__proto__ 下面显示了 Array.prototype 的各种方法。

大神帮忙看看是不是我哪里理解的还不到位?O(∩_∩)O谢谢

@mystorp

c.__proto__ 为什么显示 Parent ?

因为 c = new Child ,所以,c.__proto__ 就是 Child.prototype (即 Object.create(Parent.prototype))。

问题即可以转化为 var x = Object.create(Parent.prototype), 为什么 x 显示为 Parent

很简单啊,Object.create 就是指定 x.__proto__Parentx 就是 Parent 的实例,x instanceof Parent // true

c.__proto__.__proto__ 为什么显示成 Object?

同上,问题即 Parent.prototype.__proto__ 显示为 Object。因为默认情况下 function.prototype 就是普通对象,继承 Object.prototype

@jawil
http://louiszhai.github.io/2015/12/17/prototype/
在JS里,对象构造器Object既是对象,又是构造器,也是函数。一图流:http://louiszhai.github.io/docImages/prototype.png

当你创建函数时,JS会为这个函数自动添加prototype属性,值是空对象

我试了一下:

 > function bar() {}
 > bar.prototype
 {constructor: ƒ}

@deqing 你是对的,感谢指正。可能15年写的时候规范和浏览器中的行为还不够明确,但现在ES2015规范已经明确:

初始化一个函数对象

2018-01-08 9 16 57

MakeConstructor:

2018-01-08 9 18 53

prototype 是一个有 constructor 属性的对象,不是空对象。

“Object和Function的鸡和蛋的问题”中的第2点:
Function.prototype和Function._proto__为同一对象。出现以下情况:
console.log(Function.prototype === Function.proto
); // false

《JavaScript权威指南》其实是一本很烂的书,不知怎么入了楼主的法眼。这个终极问题被你描述后,变得更加难懂了

技术博客搬到GitHub是一种很low的做法,直白点,就是吸星的

  1. 《JavaScript权威指南》是我初学JS的入门书籍,对我帮助很大,我本人不觉得它烂。另外《你不知道的JavaScript》非常好,推荐现在学JS的人看。

  2. 博客搬到 issue 的出发点是更利于相互讨论,共同进步。我写的东西不一定对,大家讨论指正是非常好的;同时也可以利用github的生态。至于 吸星,能吸星自然是再好不过了,有人表扬是件愉快的事。

@yunfeiyang27 大家更宽容一点,专注于技术是最好的。另外你有问题/建议/指责可以邮件我,不要过多干扰其他人的时间线。issue里面最好都是技术相关的。thx。

今天被问到__proto__和prototype在什么情况下相等,想半天没想出来,看了博主的解释,醍醐灌顶,感觉之前对原型链的理解还是浅了一些,学习了

谢谢大神的分享,另外我觉得用issue写博客,朴实,方便直接,写博客目的不外乎就是总结知识的同时分享,能讲清楚就行了,也不需要太华丽。

写的很精彩,获益匪浅,感谢。

另外关于 instanceof 的解释:

对象的__proto__指向自己构造函数的prototype。obj.proto.proto...的原型链由此产生,包括我们的操作符instanceof正是通过探测obj.proto.proto... === Constructor.prototype来验证obj是否是Constructor的实例。

有一个例外情况:

  • 简单类型并不完全准守这个规则,估计ECMA规范里面应该有另外的描述,我用number类型举例,string和boolean类似。
var num1 = 100;
var num2 = Number(100);
var num3 = new Number(100);

num1 === num2; // true
num1 === num3; // false
num2 === num3; // false

num1.__proto__ === Number.prototype; // true
num2.__proto__ === Number.prototype; // true
num3.__proto__ === Number.prototype; // true

num1 instanceof Number; // false
num2 instanceof Number; // false
num3 instanceof Number; // true

@whosesmile 此处修改答案, 你说的对, 是我考虑不周, 等我想到再来回复

@zxc5800 基本类型当然要遵循原型链规则,不然基本类型上就无法做原型链上的方法调用了啊:

var num = 100.001;
var str = 'hello';

console.log(num.toFixed(2)); // ok
console.log(str.substring(3)); // ok

@whosesmile @zxc5800 基本类型(primitive types)不是对象,没有原型链。

但是,基本类型有 Boxing / Unboxing 啊 😄

Aha, Thanks...
我刚才还在想,如果简单类型也是对象,内存中该如何表达,都放到堆内存,计算起来该多么慢...

Function也是对象,继承了Object.prototype。
Function构造函数的.__proto__不是应该指向Function.prototype么?那Function是不是应该继承自Function.prototype?怎么会继承Object.prototype?
请帮忙解答一下。

Function是构造函数也是一个函数对象,是否可以这样理解。
Function是函数对象的角度看时,那函数对象的构造函数是谁还有原型?
Function.prototype。是继承自Object.prototype,,那构造函数就是Object(),但是现在还没有Object()这函数对象了。那怎么会凭空出现Function.prototype呢?而Object()又是继承自Function.prototype。所以构造函数是Function()。那Function这构造函数又是怎么变出来的呢?如果没有Function()这构造函数,Objete函数对象就不会产生。但是没有Object()这函数对象,Function.prototype对象又不会产生,这样是不是有点矛盾。
希望给解惑一下。总感觉有个地方不对,但又说不出,有点乱。

@Aiyx923 没错,万事万物皆对象。 function Function () {} 这个构造函数也是对象。 只要是对象,就会有__proto__ 属性,那么 function Function__proto__ 指向谁呢? 首先,必然就是Function.prototype啦!因为 Function.prototype 是所有函数的原型啦!


那Function是不是应该继承自Function.prototype?怎么会继承Object.prototype?

然后你第二个问题,我理解为 Function.prototype 是继承自谁,继承自谁的问题,就是说找到这个对象的__proto__对象的问题。看看它的__proto__是哪个,也就是 Function.prototype.__proto__是哪个。那么,我的答案是:任意一个对象,他最终的__proto__都将是Object.prototype,不信你可以自己在console.log里面看看每个对象的__proto__到最深处,是不是Object.prototype


其实理解原型链,记住一点,

只要是对象,就会有原型,就会有__proto__

任意函数(因为函数本身也是对象),就是说 function xxx() {} 的原型(通过__proto__往上找)就是 Function.prototype (还没完),而普通对象(比如{},或者xxx.prototype)的原型(通过__proto__往上找)就是 Object.prototype,但是 Function.prototype 也有__proto__ (原因参照上方加粗加大黑体字), 并且 __proto__ 还是指向 Object.prototype,因为 Object.prototype 才是所有人的爸爸

mark

非常感谢,讲的很清楚,搞懂了

好厉害~

楼主,想问下,自己写的构造函数和实现继承,图是怎样的

这张图很棒哎 一下就看懂了

解释非常详尽,感谢,我的理解是 Function instanceof Object 的表达式中,左值代表的是constructor function, 右值代表的是prototype, Javascript为function(Function)和prototype(Object)定义了不同的继承路径。

Function.prototype继承Object.prototype而产生,
image
这张图说明的难道不是Function.prototype是Object的一个实例,继承是类与类之间的关系,楼主是不是搞错了

@waterVenice7当然没有搞错。一个最简单的例子:

const parent = { x: 1 };
const child = Object.create(parent);

child 继承了 parent的说法,显然是合理。

如果你有一些其它面向对象语言的基础,那么问出这个问题是比较自然的,因为对Java/C++之类的语言来说,类(class)和实例(instance)就是完全不同的两个概念。它通过类的继承来实现继承。

但是,对JavaScript而言,它没有类(class)和实例(instance)的区别,它只有对象(object)。它通过原型链来实现继承(指定某个对象的原型对象 __proto__)。

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

看到这里,关于鸡蛋问题我的唯一疑问就是,Object.prototype 显然也是个对象,而对象最终都必须由 Function 生成,但混沌之初,此时还并没有 Function。于是又往下看:

这已经某种程度上解开了鸡和蛋的问题:Object.prototype是对象,但它不是通过Object函数创建的。

验证了一下,果然是这样:

Object.prototype instanceof Object // false
Object.prototype instanceof Function // false

所以,大概回答就是,Object.prototype 是个神之对象,由它诞生了 Function.prototype,以之为原型又诞生了 FunctionObject,接着创造了对象世界的万物吧。

自荐几篇博客,记录了对原型这个技术问题的思考哟:

只能说,在JS的世界里,先有Object.prototype后有天,然后有了Function.prototype,接着就有了Function,Object等等

写的非常好,理清了我的思路。

所以鸡蛋的问题是通过规定解决的么:
Function.proto === Function.prototype,(Function是Function的实例);
Function.prototype.proto === Object.prototype,(Function.prototype是Object的实例);

所以鸡蛋的问题是通过规定解决的么:
Function.proto === Function.prototype,(Function是Function的实例);
Function.prototype.proto === Object.prototype,(Function.prototype是Object的实例);

真实的世界不知道,但是计算机的世界回到最开始的地方就是通过规定解决的吧

“Object和Function的鸡和蛋的问题”中的第2点:
Function.prototype和Function.__proto__为同一对象。出现以下情况: console.log(Function.prototype === Function.proto_); // false

你用Function.prototype 去和一个不存在的Function.proto去做比较,得出false,然后说博主有问题?并且说出 权威指南这本书很烂,还说博主用github写博文是为了吸星。
你是不是对编程有什么误解?博主说 Function.prototype === Function.**_ _ proto _ _ **
前后两个 _ _ 请你看清楚了。如果你得不到true, 麻烦你去查别的资料。

原来这个原型链这么深奥。。

勘误:”Function也是对象,继承了Object.prototype“,应改为"Function.prototype也是对象,继承了Object.prototype"

讲的太好啦!