creeperyang / blog

前端博客,关注基础知识和性能优化。
MIT License
2.63k stars 211 forks source link

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

Open creeperyang opened 9 years ago

creeperyang commented 9 years ago

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

不过不是开玩笑,本文的确打算从__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终止。
creeperyang commented 9 years ago

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而产生。

showtestlog commented 9 years ago

十分感谢

Kelichao commented 8 years ago

会玩!

creeperyang commented 8 years ago

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

zhuanna commented 8 years ago

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

For-me commented 8 years ago

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

SiZapPaaiGwat commented 8 years ago

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

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

typeof Function.prototype === 'function' && (Function.prototype instanceof Function === false)
stkevintan commented 7 years ago

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

WesleyQ5233 commented 7 years ago

get

igblee commented 7 years ago

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

creeperyang commented 7 years ago

@igblee 截图看看

whcxxb commented 7 years ago

学习一下

Evllis commented 7 years ago

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

jawil commented 7 years ago

又被骗进来了吧😄 @Evllis

creeperyang commented 7 years ago

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

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

Cacaci commented 7 years ago

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

jawil commented 7 years ago

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的问题。

creeperyang commented 7 years ago

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

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
noobalex commented 7 years ago

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

rccoder commented 7 years ago

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

jawil commented 7 years ago

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

rccoder commented 7 years ago

@jawil 邮件会 push 啊...

OwlAford commented 7 years ago

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

vanishcode commented 7 years ago

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

wangcansunking commented 7 years ago

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的实例啊

yangblink commented 7 years ago

@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()
Chersquwn commented 7 years ago

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

wangcansunking commented 7 years ago

@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;这句话的作用是什么呢?

Chersquwn commented 7 years ago

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

wangcansunking commented 7 years ago

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

GrowingFun commented 7 years ago

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

creeperyang commented 7 years ago

@yuantingjun 是的,规范可以看 https://www.ecma-international.org/ecma-262/6.0/#sec-functionallocate

LongchongWang commented 7 years ago

精华贴

WangNingning1994 commented 7 years ago

学习了

ClarenceC commented 7 years ago

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

honchy commented 7 years ago

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

这里的__proto__ 是不是应该是prototype

mystorp commented 7 years ago

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

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谢谢

creeperyang commented 7 years ago

@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

zhoubhin commented 7 years ago

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

deqing commented 6 years ago

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

我试了一下:

 > function bar() {}
 > bar.prototype
 {constructor: ƒ}
creeperyang commented 6 years ago

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

初始化一个函数对象

2018-01-08 9 16 57

MakeConstructor:

2018-01-08 9 18 53

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

yunfeiyang27 commented 6 years ago

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

yunfeiyang27 commented 6 years ago

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

yunfeiyang27 commented 6 years ago

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

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

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

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

zhouchao941105 commented 6 years ago

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

Jomsou commented 6 years ago

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

whosesmile commented 6 years ago

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

另外关于 instanceof 的解释:

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

有一个例外情况:

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
zxc5800 commented 6 years ago

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

whosesmile commented 6 years ago

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

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

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