美文网首页前端攻城狮前端,数据库学习JS 原型与原型链
最详尽的 JS 原型与原型链终极详解,没有「可能是」。(三)

最详尽的 JS 原型与原型链终极详解,没有「可能是」。(三)

作者: Yi罐可乐 | 来源:发表于2016-08-21 21:38 被阅读22063次

七. 函数对象 (复习一下前面的知识点)

所有函数对象proto都指向Function.prototype,它是一个空函数(Empty function)
Number.__proto__ === Function.prototype  // true
Number.constructor == Function //true

Boolean.__proto__ === Function.prototype // true
Boolean.constructor == Function //true

String.__proto__ === Function.prototype  // true
String.constructor == Function //true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Object.__proto__ === Function.prototype  // true
Object.constructor == Function // true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Function.__proto__ === Function.prototype // true
Function.constructor == Function //true

Array.__proto__ === Function.prototype   // true
Array.constructor == Function //true

RegExp.__proto__ === Function.prototype  // true
RegExp.constructor == Function //true

Error.__proto__ === Function.prototype   // true
Error.constructor == Function //true

Date.__proto__ === Function.prototype    // true
Date.constructor == Function //true

JavaScript中有内置(build-in)构造器/对象共计12个(ES5中新加了JSON),这里列举了可访问的8个构造器。剩下如Global不能直接访问,Arguments仅在函数调用时由JS引擎创建,Math,JSON是以对象形式存在的,无需new。它们的proto是Object.prototype。如下

Math.__proto__ === Object.prototype  // true
Math.construrctor == Object // true

JSON.__proto__ === Object.prototype  // true
JSON.construrctor == Object //true

上面说的函数对象当然包括自定义的。如下

// 函数声明
function Person() {}
// 函数表达式
var Perosn = function() {}
console.log(Person.__proto__ === Function.prototype) // true
console.log(Man.__proto__ === Function.prototype)    // true

这说明什么呢?

** 所有的构造器都来自于 Function.prototype,甚至包括根构造器ObjectFunction自身。所有构造器都继承了·Function.prototype·的属性及方法。如length、call、apply、bind**

(你应该明白第一句话,第二句话我们下一节继续说,先挖个坑:))
Function.prototype也是唯一一个typeof XXX.prototypefunctionprototype。其它的构造器的prototype都是一个对象(原因第三节里已经解释过了)。如下(又复习了一遍):

console.log(typeof Function.prototype) // function
console.log(typeof Object.prototype)   // object
console.log(typeof Number.prototype)   // object
console.log(typeof Boolean.prototype)  // object
console.log(typeof String.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
console.log(typeof Object.prototype)   // object

噢,上面还提到它是一个空的函数,console.log(Function.prototype) 下看看(留意,下一节会再说一下这个)

知道了所有构造器(含内置及自定义)的__proto__都是Function.prototype,那Function.prototype__proto__是谁呢?
相信都听说过JavaScript中函数也是一等公民,那从哪能体现呢?如下
console.log(Function.prototype.__proto__ === Object.prototype) // true
这说明所有的构造器也都是一个普通 JS 对象,可以给构造器添加/删除属性等。同时它也继承了Object.prototype上的所有方法:toString、valueOf、hasOwnProperty等。(你也应该明白第一句话,第二句话我们下一节继续说,不用挖坑了,还是刚才那个坑;))

最后Object.prototype的proto是谁?
Object.prototype.__proto__ === null // true
已经到顶了,为null。(读到现在,再回过头看第五章,能明白吗?)

八. Prototype

在 ECMAScript 核心所定义的全部属性中,最耐人寻味的就要数 prototype 属性了。对于 ECMAScript 中的引用类型而言,prototype 是保存着它们所有实例方法的真正所在。换句话所说,诸如 toString()valuseOf() 等方法实际上都保存在 prototype 名下,只不过是通过各自对象的实例访问罢了。

——《JavaScript 高级程序设计》第三版 P116

我们知道 JS 内置了一些方法供我们使用,比如:
对象可以用 constructor/toString()/valueOf() 等方法;
数组可以用 map()/filter()/reducer() 等方法;
数字可用用 parseInt()/parseFloat()等方法;
Why ???

why??

当我们创建一个函数时:

var Person = new Object()
PersonObject 的实例,所以 Person 继承Object 的原型对象Object.prototype上所有的方法:

Object.prototype
Object 的每个实例都具有以上的属性和方法。
所以我可以用 Person.constructor 也可以用 Person.hasOwnProperty

当我们创建一个数组时:

var num = new Array()
numArray 的实例,所以 num 继承Array 的原型对象Array.prototype上所有的方法:

Array.prototype

Are you f***ing kidding me? 这尼玛怎么是一个空数组???

doge
我们可以用一个 ES5 提供的新方法:Object.getOwnPropertyNames
获取所有(包括不可枚举的属性)的属性名不包括 prototy 中的属性,返回一个数组:
var arrayAllKeys = Array.prototype; // [] 空数组
// 只得到 arrayAllKeys 这个对象里所有的属性名(不会去找 arrayAllKeys.prototype 中的属性)
console.log(Object.getOwnPropertyNames(arrayAllKeys)); 
/* 输出:
["length", "constructor", "toString", "toLocaleString", "join", "pop", "push", 
"concat", "reverse", "shift", "unshift", "slice", "splice", "sort", "filter", "forEach", 
"some", "every", "map", "indexOf", "lastIndexOf", "reduce", "reduceRight", 
"entries", "keys", "copyWithin", "find", "findIndex", "fill"]
*/

这样你就明白了随便声明一个数组,它为啥能用那么多方法了。

细心的你肯定发现了Object.getOwnPropertyNames(arrayAllKeys) 输出的数组里并没有 constructor/hasOwnPrototype对象的方法(你肯定没发现)。
但是随便定义的数组也能用这些方法

var num = [1];
console.log(num.hasOwnPrototype()) // false (输出布尔值而不是报错)

Why ???


why??

因为Array.prototype 虽然没这些方法,但是它有原型对象(__proto__):

// 上面我们说了 Object.prototype 就是一个普通对象。
Array.prototype.__proto__ == Object.prototype

所以 Array.prototype 继承了对象的所有方法,当你用num.hasOwnPrototype()时,JS 会先查一下它的构造函数 (Array) 的原型对象 Array.prototype 有没有有hasOwnPrototype()方法,没查到的话继续查一下 Array.prototype 的原型对象 Array.prototype.__proto__有没有这个方法。

当我们创建一个函数时:

var f = new Function("x","return x*x;");
//当然你也可以这么创建 f = function(x){ return x*x }
console.log(f.arguments) // arguments 方法从哪里来的?
console.log(f.call(window)) // call 方法从哪里来的?
console.log(Function.prototype) // function() {} (一个空的函数)
console.log(Object.getOwnPropertyNames(Function.prototype)); 
/* 输出
["length", "name", "arguments", "caller", "constructor", "bind", "toString", "call", "apply"]
*/

我们再复习第八小节这句话:

所有函数对象proto都指向 Function.prototype,它是一个空函数(Empty function)

嗯,我们验证了它就是空函数。不过不要忽略前半句。我们枚举出了它的所有的方法,所以所有的函数对象都能用,比如:

函数对象

如果你还没搞懂啥是函数对象?


去屎 | center

还有,我建议你可以再复习下为什么:

Function.prototype 是唯一一个typeof XXX.prototype为 “function”的prototype

我猜你肯定忘了。

九. 复习一下

第八小节我们总结了:

所有函数对象的 __proto__ 都指向 Function.prototype,它是一个空函数(Empty function)

但是你可别忘了在第三小节我们总结的:

所有对象的 __proto__ 都指向其构造器的 prototype

咦,我找了半天怎么没找到这句话……


doge | center

我们下面再复习下这句话。

先看看 JS 内置构造器:

var obj = {name: 'jack'}
var arr = [1,2,3]
var reg = /hello/g
var date = new Date
var 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

再看看自定义的构造器,这里定义了一个 Person

function Person(name) {
  this.name = name;
}
var p = new Person('jack')
console.log(p.__proto__ === Person.prototype) // true

pPerson 的实例对象,p 的内部原型总是指向其构造器 Person 的原型对象 prototype

每个对象都有一个 constructor 属性,可以获取它的构造器,因此以下打印结果也是恒等的:

function Person(name) {
    this.name = name
}
var p = new Person('jack')
console.log(p.__proto__ === p.constructor.prototype) // true

上面的Person没有给其原型添加属性或方法,这里给其原型添加一个getName方法:

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('jack')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true

可以看到p.__proto__Person.prototypep.constructor.prototype都是恒等的,即都指向同一个对象。

如果换一种方式设置原型,结果就有些不同了:

function Person(name) {
    this.name = name
}
// 重写原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('jack')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // false

这里直接重写了 Person.prototype(注意:上一个示例是修改原型)。输出结果可以看出p.__proto__仍然指向的是Person.prototype,而不是p.constructor.prototype

这也很好理解,给Person.prototype赋值的是一个对象直接量{getName: function(){}},使用对象直接量方式定义的对象其构造器(constructor)指向的是根构造器ObjectObject.prototype是一个空对象{}{}自然与{getName: function(){}}不等。如下:

var p = {}
console.log(Object.prototype) // 为一个空的对象{}
console.log(p.constructor === Object) // 对象直接量方式定义的对象其constructor为Object
console.log(p.constructor.prototype === Object.prototype) // 为true,不解释(๑ˇ3ˇ๑)

十. 原型链(再复习一下:)

下面这个例子你应该能明白了!

function Person(){}
var person1 = new Person();
console.log(person1.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype) //true
console.log(Object.prototype.__proto__) //null

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

var num = new Array()
console.log(num.__proto__ == Array.prototype) // true
console.log( Array.prototype.__proto__ == Object.prototype) // true
console.log(Array.prototype) // [] (空数组)
console.log(Object.prototype.__proto__) //null

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

疑点解惑:

  1. Object.__proto__ === Function.prototype // true
    Object 是函数对象,是通过new Function()创建的,所以Object.__proto__指向Function.prototype。(参照第八小节:「所有函数对象的__proto__都指向Function.prototype」)

  2. Function.__proto__ === Function.prototype // true
    Function 也是对象函数,也是通过new Function()创建,所以Function.__proto__指向Function.prototype

自己是由自己创建的,好像不符合逻辑,但仔细想想,现实世界也有些类似,你是怎么来的,你妈生的,你妈怎么来的,你姥姥生的,……类人猿进化来的,那类人猿从哪来,一直追溯下去……,就是无,(NULL生万物)
正如《道德经》里所说“无,名天地之始”。

  1. Function.prototype.__proto__ === Object.prototype //true

其实这一点我也有点困惑,不过也可以试着解释一下。
Function.prototype是个函数对象,理论上他的__proto__应该指向 Function.prototype,就是他自己,自己指向自己,没有意义。
JS一直强调万物皆对象,函数对象也是对象,给他认个祖宗,指向Object.prototypeObject.prototype.__proto__ === null,保证原型链能够正常结束。

十一 总结

  • 原型和原型链是JS实现继承的一种模型。
  • 原型链的形成是真正是靠__proto__ 而非prototype

要深入理解这句话,我们再举个例子,看看前面你真的理解了吗?

 var animal = function(){};
 var dog = function(){};

 animal.price = 2000;
 dog.prototype = animal;
 var tidy = new dog();
 console.log(dog.price) //undefined
 console.log(tidy.price) // 2000

这里解释一下:

 var dog = function(){};
 dog.prototype.price = 2000;
 var tidy = new dog();
 console.log(tidy.price); // 2000
 console.log(dog.price); //undefined
 var dog = function(){};
 var tidy = new dog();
 tidy.price = 2000;
 console.log(dog.price); //undefined

这个明白吧?想一想我们上面说过这句话:

实例(tidy)和 原型对象(dog.prototype)存在一个连接。不过,要明确的真正重要的一点就是,这个连接存在于实例(tidy)与构造函数的原型对象(dog.prototype)之间,而不是存在于实例(tidy)与构造函数(dog)之间。

聪明的你肯定想通了吧 :)


本文借鉴了:

  1. 《JS原型与原型链终极详解》
    作者:zhangjiahao8961
  2. JavaScript中proto与prototype的关系
    作者:snandy
  3. 《JavaScript 高级程序设计》中文译本 第三版

相关文章

网友评论

  • 绝唱一段芊芊_b635:感谢楼主,三篇看完,赞!懂了
  • 277e8ae15472:请问楼主有关于 闭包 和 面向对象 的文章吗 或者 好的推荐
  • 277e8ae15472:为楼主的无私奉献点赞,十分感谢楼主,本人js小白一个,之前看了很多关于原型和原型链的东西,但对于新手都不是很友好,所以一直处于懵懂状态,今天看了楼主的三篇文章让我茅塞顿开,对于原型和原型链有了比较清晰的理解。
    不过有一点 原型对象是构造函数的一个实例 这句话我也觉得有问题,虽然这句话有助于新手的理解,但也容易带入误区,之前我就进入了一个死循环,所以楼主的这句还是需要改善一下。:pray: 万分感谢
  • 柯柯的呵呵哒:Array.prototype。控制台打出不是[]啊
  • 柯柯的呵呵哒:你开头说,Function.prototype是一个空对象。那么什么说所有的构造函数都继承了Function.prototype的属性跟方法。它是个空函数,并没有属性跟方法啊
  • Twelve_minus:关注了,总算对原型链这个东西有了一些理解
  • M_细花儿:楼主是我前进路上的一盏明灯啊
    Yi罐可乐:@皮卡秀秀 :smile:
  • 613f92fbcb55:感谢作者,任督二脉已打通,不过建议在用代码进行论证时,统一使用更为严谨的“===”:+1:
  • 邓男子:一个月前来看懂了,这次再来复习一遍。写的不错。来一张图就更清楚了。构造函数的prototype指向原型对象,构造函数实例的对象的_proto__指向原型对象。构造函数的constructor指向够照函数。原型对象的__proto__指向他爸爸的原型对象
  • 帅巴扎黑:现在是看懂了 不知道过几天是否会忘记
    过几天 在撸一遍
  • _ethan_:不管别人怎么说,反正我是看懂了。
  • iseeican:大赞
  • d5d7626e55b6:感谢楼主辛勤付出!
  • d5d7626e55b6:console.log(dog.__proto__ === Function.prototype); //true
    tidy.__proto__本来要指向Object.prototype 被重写了so
    console.log(tidy.__proto__ === animal); //true
  • 1491494646b8:关于总结部分是不是因为tidy.__proto__指向的是dog.prototype,因为dog.prototype = animal,所以tidy才拥有属性price。而dog.__proto__指向的是应该是Object.prototype?(不太确定),所以才会提示undefined吗?
  • d8edb7d5c393:厉害的不要不要的 一会儿看懂 一会儿又糊涂了
  • 赵大仁:第八章的开头valueOf()写错了
  • cc4a0ad71b59:最后总结对新手来说太突然了,之所以undefine,是因为构造函数(dog),原型对象(god.prototype),实例(tidy) 其实是三个独立的结构,而将dog的prototype指向animal,对于读取dog.price来说没有用,只有当tidy实例化发生后,底层通过__proto__连接关系,读取tidy的price时,层层读取,先自身属性列表(找不到),然后读取父类dog的属性列表,当然也找不到price,于是向上搜索原型链中的price,最后便在animal中找到了price这个属性。 不知我理解是否正确?
    613f92fbcb55:dog.price //undefined --->
    dog.__proto__ === Function.prototype ---> Function.prototype.price //undefined --->
    Function.prototype.__proto__ === Object.prototype ---> Object.prototype.price // undefined --->
    return undefined
    Yi罐可乐:你手边有《JS高级程序设计》吗,翻一下,我觉得你会立刻豁然开朗
  • 570c1360d771:把原型链说的很详细,谢谢
  • dd0d128eef1f:3篇都看完了,感觉还不错!慢慢懂吧。
  • 252c6bf5a4ea:实例的 prototype 属性是undefined ,实例的constructor等于Object,实例的constructor 类型是 function
  • 0bd5cfa52f77:感谢大大的分享~~~看了两遍终于有眉目了~~~
    开始被绕乱了,凸显自己基础还不够牢固~~又得补回基础类型之类的知识点了。
  • 么么哒慧:我从java转过来的。。我真的看不懂(一把鼻涕一把泪的。。)
  • 见维:绕来绕去,绕进去了愣是没出来,,反正感觉很牛逼,先行谢过楼主,回头我还要研究几遍才得。
  • ff11e024958d:虽然还有点闷闷的,但以后用到的时候,该会比较容易懂~感谢
    Yi罐可乐:@洛浮生 :smile:
    ff11e024958d:字写错了:joy: ,是“懵”
  • 7d82ebfdc4bb:受教了,以前一直半理解 现在半+理解~ 🙏
  • WOTK:第十部分:console.log(Array.prototype)输出不为空啊(windows chrome 61.0.3159.5)
    Yi罐可乐: @Reckless_3662 你搜一下原型链指向图,各种箭头的那个,然后对比文章再理解下。另外我最近有时间的话会更新一下,把那个图加上去。
    WOTK:@Yi罐可乐 还有一个问题:Function.prototype是个function(xxx.prototype中唯一一个function类型),为啥不是Function的实例而是Object的实例(用的 typeof 和 instanceof 判断)
    Yi罐可乐:之前的 Chrome 是打印不出来不可枚举属性的,新版的可以了。你看一下第八章吧,有介绍。
  • 5fd69fb267cb:楼主,还有一个地方有疑问,第9节里面重写原型后我打印了下面三个:
    console.log(p.__proto__);
    console.log(Person.prototype);
    console.log(p.constructor.prototype);
    前两个包含{getName: function(){}},我理解的是p.constructor.prototype为构造函数Person的prototype,这里相当于实例指向了新的原型对象,而构造函数指向旧的原型对象, 我看《JavaScript 高级程序设计》中文译本 第三版P157里重写原型后实例依然指向原始的原型对象,构造函数指向新的原型对象,感觉这两个是不是有点矛盾,还是我理解有误呢?
    Yi罐可乐:以《JavaScript 高级程序设计》为准。
  • 其实我很dou:万物皆为null, 谢谢楼主, 受益匪浅:relaxed:
  • 9338c4ad07e9:楼主,您好,看了您的这篇文章使我对于原型链有了进一步的认识。有疑问的是您说的原型对象也是构造函数的一个实例。为什么我用instanceof检测的时候它输出的是false呢?还有就是我定义了一个构造函数human,里面有一个属性name,然后我console.log(human.prototype)发现里面并没有name属性 。而通过new出来的实例对象是可以清楚地看到里面有name属性的。麻烦您抽空解答一下,谢谢。
    9338c4ad07e9:@Yi罐可乐 感谢您的及时回复!
    Yi罐可乐:「原型对象也是构造函数的一个实例」,我是故意这么说的。文章里有不少地方为了方便新手理解,「自定义」了一些东西,可能经不起推敲,目的只是为了更好的理解。
  • b53c5a68e9c4:看了一个星期,终于是有电眉目了,可不可以这样理解:“功能上讲prototype是给别人用的东西,而且给别人的东西自己不能用,__proto__是借用别人的东西”?
    Yi罐可乐:@Mina_e9a3 哈哈,也可以这么理解
  • d951243590c6:三篇文章我跟着看下来了,首先非常感谢楼主详细的解释,但我还是有点不太理解这个原型链除了可以给函数扩充一些全局的方法之外还有什么用,还有__proto__到底有什么用?
    Yi罐可乐: @痛并快乐着_0270 实际前端开发业务中…几乎用不到,但是你总得了解JS这门语言的本质,对吧😍
  • 0527285fdded:最后的那个例子我愣是没有看懂,到底是个神马原理?
    0527285fdded:@Yi罐可乐 :hushed: hushed: hushed:
    0527285fdded:@Yi罐可乐 前面两篇已经看过了哦~ 我想不通是的 dog.prototype = animal; 是这段,打印了dog.prototype出来显示的是空函数啊,那为嘛tidy.price可以打印出2000呢
    Yi罐可乐:建议先看看前两章,再回过头来看这个
  • 70d3448af4d7:厉害,很详细,但我还是在那个实例对象和构造函数的原型对象的恒等上没懂
    Yi罐可乐:回过头看看前两章,你应该就明白了。
  • 34a2d33bb8f8:Math.construrctor == Object
    JSON.construrctor == Object
    这2句是false,何解??
    Yi罐可乐:@112_cfa3 :joy:
    34a2d33bb8f8:我X,才发现我constructor拼错了多了个r:disappointed_relieved:
    Yi罐可乐:Math.constructor === Object
    true
    JSON.constructor === Object
    true
  • 187d8381dd71:楼主啊,你似乎用错了一个函数:hasOwnProperty(),它是用来寻找对象是否有私有属性的一个方法,而你的文章里将它拼写为hasOwnPrototype(),很容易被读者误解(●'◡'●)
    Yi罐可乐:@FancyTrip 谢谢指正
  • 20fdf6e6202c:prototype的__proto__均指向Object的prototype?
    20fdf6e6202c:@不要盯着我发呆 prototype的__proto__指向prototype的构造函数的prototype,自己指向自己没有意义 所以指向Object的prototype
    Yi罐可乐:@不要盯着我发呆 所有函数对象的__proto__都指向Function.prototype。这是本文强调的,可以再看一下。
    Yi罐可乐:@不要盯着我发呆 我觉得亲自在浏览器控制台输入验证一下,印象会更深刻。
  • 20fdf6e6202c:普通对象的__proto__指向这个对象(this)的构造函数的prototype;
    函数对象的__proto__全部都是指向Function的prototype。
    Yi罐可乐:@不要盯着我发呆 是的
    87a9a30c7b5b:@Yi罐可乐 然后无论是Function的prototype还是构造函数的prototype都指向object.prototype,然后最终object.prototype指向null?是这样吗?
    Yi罐可乐:@不要盯着我发呆 :+1:
  • 小二金刚:纠正一下,链接在实例和实例.proto.prototype之间。先看proto什么,在看proto的protype。是这样吧 @Yi罐可乐
    小二金刚:@Yi罐可乐 好的 我先再看看
    Yi罐可乐:@小二金刚 可以先看一下第二个教程里第一张图,你应该就明白 连接是存在于实例和原型对象之间的。
    我们可以不考虑原型对象的原型对象是什么。
  • 小二金刚:受教了~还是最关键的那句,连接在实例和prototype之间
    Yi罐可乐:@小二金刚 是滴~
  • d86d4f5a98eb:收藏
    Yi罐可乐:@棒棒糖111 谢谢♪(๑ᴖ◡ᴖ๑)♪

本文标题:最详尽的 JS 原型与原型链终极详解,没有「可能是」。(三)

本文链接:https://www.haomeiwen.com/subject/tuswsttx.html