原型链?不!意大利黑手党!第一部分

168 阅读15分钟

Any application that can be written in JavaScript,will eventually be written in JavaScript

任何可以用Javascript来实现的应用,最终都会被Javascript实现。

这句话被前端工作者们吹了十来年,这句话还有下句,但是人们往往总是断章取义,就好像那句”酒肉穿肠过,佛祖心中留“的下句是”世人若似我,如同堕魔道。“

同样,这句流传甚广的前端名言的下句是

The strength of JavaScript is that you can do anything.The weekness is that you will.

javascript的优点是可以用来完成任何你想完成的事情,但是你还真有勇气啊你敢用javascript写?

卧槽你怎么敢的啊?

在上一篇文章最后我留下了一个问题,希望大家可以思考一下,是先有的Object还是先有的Object.prototype。

所以本篇将会分为两个部分,第一部分来解答上一篇文章留下的问题,第二部分来讲黑帮的故事,第一部分如果你对原型链的理解比较浅,仅仅停留在可以面试的层面,我建议你越过第一个部分直接去看第二部分黑帮风云,否则你对原型链的理解将会直接陷入混乱,如同堕魔道。

听着,我不是在搞笑。

在原型链这个部分,Javascript这门语言的随意将会到达第一个顶峰,即使是这样,他的随意程度,依然干不过css。

至于css有多随意呢,这个是后话了,我可以举一个简单的例子,在正常结构化语言中,当你声明了一个变量a赋值为a,之后又声明了一个变量b赋值为b,他们彼此是不冲突的。

在css中,当你声明了一个样式a赋值为a,之后声明了一个样式b赋值为b,你会发现,样式a自杀了。当然,这种情况只会存在于css的初学者中。

并且在css中,完全没有所谓的结构可言。在学css的初期,写css,就像哄女朋友。

这是另一段故事了,关于第一部分,基础不好的话听过来人一句劝,别看了,中国人不骗中国人。

而至于我在起笔的时候别人问我看完了第二部分能不能回头看第一部分......

我觉得还是先别吧,你先消化消化,我怕你看完第二部分本身就接受不了,然后看第一部分就更难受了。

第一部分:上帝已死

CHAPTER 1 : Object和Object.prototype谁更先出现?

这其实只是问题的第一层,我觉得这个问题完全可以拿来当面试题。

如果你有对这个问题下功夫动手去敲代码思考,你会发现,Object这个函数的[[prototype]]其实是Function.prototype。

我坚持用[[prototype]]而不用__proto__,是为了防止你进一步崩溃,实际上他俩是一个东西。

所以我们会发现第一个好玩的事情,我们本来想知道Object和Object.prototype这两个东西谁更早出现,但是很明显Function.prototype比Object出现的要更早,那么,这个问题就演变成了,Function.prototype和Object.prototype谁出现的更早。

沽名钓誉者会说,你追究这个问题干什么,吃饱了撑的?

原因很简单,好奇。

在学习上,千言万语万般努力,抵不过一句,感兴趣。所有踏足山巅的人,都离不开一个原因,他们对这个事物好奇,想要探究其原理,想要知道为什么。

换一种说法,你甘心活在迷雾之下?

世界这么大,我想去看看。后来这句话被广泛的应用在了辞职报告上。

我们很快就能知道这个问题的答案了。如果我们运行这段代码

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

我们会发现,返回的结果是,true。

哦我的上帝,问题迎刃而解,这很明显Object.prototype早于Function.prototype早于Object,你这个坏蛋竟然骗我说会让我整个人陷入混乱,逼得我学了好久的原型链才敢来看这个部分。

朋友,地狱的大门从现在开始为你打开。

拿起你的纸和笔,从现在开始这个问题只能通过画图来理解。

如果我们打印一下Function.prototype,你会发现打印结果为

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

????????????在我们一贯的理解中,原型只有对象,为什么Function的原型会是一个函数?????如果Function.prototype是一个函数,那么和Function.prototype.__proto__三等的Object.prototype又是个什么东西,三等可是连类型一起比较的啊。难道Object.prototype是特么的该死的函数,那所有从Object.prototype继承下来的对象都是函数?整个javascript所有对象全是函数?

这个问题,引发了第一次javascript危机,人们的世界观崩塌了。

开个玩笑,这点逼事人们随便就解决了。

当人们打印Function.prototype.__proto__这个东西的时候,会发现,他是一个对象,也就是说,问题出在了Function.prototype和他的原型Function.prototype.[[prototype]]上。我们通过原型链的学习,常常会产生一种误解,比如Array的原型就是Array.prototype,我们就会觉得Function的原型就是Function.prototype,实际上,这两个东西半毛钱关系没有。

CHAPTER 2 :Function 和Function.prototype的爱恨情仇

“我是你的爹,但我也是所有人的爹。”

这是Function.prototype对Function说的一段话。

首先,大家要坚信一个概念,Function的原型,和Function.prototype,是两个东西。

在原型链中,有两条线,一条是原型线,一条是函数线,他们分别可以通过[[prototype]]去往上查找自己的原型,是的,不仅原型对象们有自己的原型链,函数对象其实也有。

如果我们没事闲的,去查找Object,String,Number,fn这些方法的原型,我们无疑会得到这么一条信息。

Number.__proto__ === Function.prototype //true

而我们去查找Function的原型,会发现,也是Function.prototype

Function.__proto__ === Function.prototype //true

是的,所有的函数,往上追查原型,全都是Function.prototype,包括Function自己。

\

而设计理念很简单,原型有一套自己的逻辑,原型对象有自己的构造函数,构造函数生成实例,实例指向原型对象。原型对象又指向他的原型对象,

那函数是不是对象,是啊,Javascript说所有的对象都要有自己的原型。那么函数也一定要有自己的原型。

那该怎么设计,设计原型对象那一条原型链已经很累了,如果按照那一套来设计函数,是不是构造函数也要有自己的原型对象,构造函数的原型对象又有构造函数的构造函数,构造函数的原型对象通过构造函数的构造函数来生成构造函数实例。

那特么构造函数的构造函数是不是对象?是!那么构造函数的构造函数的原型链应该怎么处理。

你会发现,这是一个无尽的循环。

这一切必须终止。

JavaScript创始人拍了拍自己的脑袋,有了!让所有构造函数的原型都是Function.prototype不就好了么,你还往上找什么原型链啊?就这样了,Function.prototype是所有函数的原型。别玩什么原型链了,太恶心了。

这就是,随意。还原型链,原个头。

所以,名义上你以为Function.prototype是Function的原型,其实不是,他是所有函数的爹。

CHAPTER 3 :孩子,我是阴阳人。

我们在前言中提过,当你打印Function.prototype的类型的时候,会得到一个function的返回结果,这说明这个东西,他是函数。

道理很简单,对象的原型是不是对象,那函数的原型也应该是函数,哪怕他是所有人的函数。

所以Function.prototype是一个函数类型就可以理解了。但是,即使你是所有函数的原型对象,你归根结底不还是个原型对象么,既然是原型对象,那你就给我滚到原型对象那条线上去。

所以我们可以得出结论,Function.prototype,既是函数,也是原型对象,如果非要说,其实他是函数多一点,毕竟系统给出的类型是函数。Function.prototype成了整个Javascript世界中唯一一个横跨原型线和函数线两条线的男人,他用自身阴阳人的身份,终结了函数原型查找的无限循环。

牺牲小我,成全大我。

而让我们回到原型线,原型线就简单多了,无中生有,一生二,二生三,三生万物。古人在千百年前就掌握了原型线的逻辑。

一切的始祖为null,null创造了Object.prototype。之后Object.prototype创造了Function.prototype。之后和函数相关的让Function.prototype去干,而原型相关的由Object.prototype自己干。

下面的图可以帮助你理顺思路。最右边是原型线。

\

此图转载自听风是风,因为我真的懒的画。

\

此图转载自Hursh Jain

由图我们可以看出,函数们找自己的原型,都是在函数线上找他们唯一的神,Function.prototype。而Function.prototype找自己的原型,是滚回原型线上,去找Object.prototype。

这就是,原型链的起始。

第二部分:黑帮风云

CHAPTER 1 : 鸡哥·鸭哥·农场主

这是我最喜欢写的一部分,我们的老朋友鸭哥和鸡哥在这一章也会和大家见面。

先不说黑手党,让我们先用一个小故事来讲述一下什么是原型链吧。

鸭哥跟几个关系一直都很好,主要的原因是农场主是个神经病,总是提出一些无理的要求,迅速促进同事间感情的秘诀是什么?是有一个不按套路出牌的老板。

所以从鸡哥刚出生的时候,鸭哥就开始照着鸡哥。

有一天,农场主找来了鸡哥,说,你给我来声鸭子叫。

鸡哥懵逼了,你萌死了?鸡和鸭你分不清,你**有问题?我

鸡哥没有办法,去找鸭哥,鸭哥过来给农场主叫了一个,农场主满意的走了。

这就是原型链。

CHAPTER 2 : 你甚至不愿意叫我一声,Godfather......

\

美国

星期四的下午,教父看着眼前的鸭子,陷入了沉思。

“哦,教父,您的名声在外,我们都尊敬您。我的农场主,您也知道,他在这里的声誉不好,他之前让我学鸡叫,天哪,我只是一只可怜的鸭子,好在我是个多态,他让我学我便学了,可是现在,他竟然让我弹钢琴,我怎么会弹钢琴呢,只不过是感恩节来临,他想要了我的命作火鸡罢了。”

“不好意思,恕我打断,你只是一只鸭子。”

“哦对,我这该死的记性,他只是把我当成一只烤鸭。该死,一个美国人为什么会这么喜欢吃北京烤鸭?教父,我走投无路,只好来寻求您的帮助,我愿意献上我对您的忠诚,只求您能帮我渡过难关”

“这真是一个棘手的问题,好在我认识云迪李,我可以让他帮你弹钢琴,但是你也知道,他最近遇上了一些麻烦。好在这件事你不用再担心了。”

“感谢您,教父。”

从前有一个钢琴家,他叫云迪李,他遇到了一些问题,教父帮他解决了,作为代价,教父给了他一个任务,让他帮一只鸭子弹钢琴。

let yundili = {
   name: '李钢琴',
   Play_Piano(){
     console.log('哒哒哒哒哒哒哒哒哒哒哒哒')
   }
}

let duck = {
   name: '鸭哥',
   Duck_Call(){
     console.log('嘎')
   },
   Rooster(){
     console.log('咯')
   },
}

duck.__proto__ = yundili
duck.Play_Piano()
//哒哒哒哒哒哒哒哒哒哒哒哒

云迪李很完美的完成了这个任务,鸭哥也避免了变成烤鸭的命运。

注意,这里只是让鸭哥的原型对象变成了云迪李。我并不想过早引入构造函数这个概念,这会让大家混乱。

在我看来,所谓的原型对象,无非就是两个字,大哥。你遇到了问题,你解决不掉,找你大哥。就像最开始的故事鸡哥去找了鸭哥,鸭哥去找了李云迪。都是一个道理。这其实就是原型链。当遇到了问题,一个对象解决不了,往上不断找大哥,最后找到我们的教父身上,也就是Object.prototype。这位哥是真的哥,他是所有人的大哥。

duck.__proto__ = yundili

而这段代码,表示换大哥,一开始鸭哥的大哥是教父,这里是让鸭哥的大哥变成李云迪,所有人遇到问题默认找大哥解决。不能找其他的人,如果想要更好的去解决问题,只有两个办法,一个是换大哥,第二个是找你大哥的大哥。

CHAPTER 3 :宝藏秘籍,构造函数。

让我们引入一个全新的概念,构造函数。

什么是构造函数,标题已经告诉你了,就是宝藏秘籍,像是教父,教父确实很厉害,但是他总不能一个人把所有的事都干了吧,上一个这么亲力亲为的朱元璋,当然他也确实完美的完成了工作,但是不是每一个人都是朱元璋,我们总要分工合作的。

如果教父想要找一个杀手,那么应该怎么做。

很简单,找一个路人,给你一把枪,教会你开枪的技能。你就是杀手了。

是的,就这么简单,不要想的太复杂,比如学习成本忠诚度什么的。编程世界是对现实世界的一种模仿。我们无法将所有的事情都加入进去。

而宝藏秘籍,就是构造函数,其中的宝藏是构造函数的属性,秘籍是构造函数的方法。

光说不练假把式,我们来举一个例子。

let killer = function (){
   this.name = '杀手';
  this.shoot = function (){
console.log('任务已完成')
   }
}

letkiller1= new killer()
killer1.shoot()
//任务已完成

是的,只要通过宝藏秘籍,就可以无限的制造杀手,比如上文的killer1,在javascript中,它叫做实例。

而给killer1宝藏秘籍的那个人,在javascript中是没有自己名字的,这很正常,幕后黑手的身份总是不为人知。在javascript中,我们只能管这个人叫killer1.proto,意思是杀手1的老大。

很有趣的是,一开始某某的老大正常应该是killer1.[[prototype]]。但是这个属性并不会被识别。所以人们才换成了killer1.proto,__proto__和[[prototype]]是一个东西,你不用在意,只是换了个名字。

无论是杀手1,还是杀手1的老大,他们都是对象,只要是对象,就有老大,而杀手1的老大的老大是谁呢?我们可以这么表示killer1.protp.proto,他们不断往上找,不断地__proto__,最后的老大,就是教父。

而同样,我们也可以通过构造函数也就是宝藏秘籍去找到拥有宝藏秘籍的老大。毕竟每一样财宝都对应着主人,比如已知构造函数killer,他的老大就是killer.prototype。

而如果老大想要知道他的珠宝财富,比如我们知道老大killer.prototype.则可以通过killer.prototype.constructor这个方法去获取。

是的,老大没有自己的名字,他只能叫做自己财宝的prototype。引入《你不知道的javascript》中我最喜欢的一段话。

——————————————————————

这个对象通常被称为Foo的原型,因为我们通过名为Foo.prototype的属性引用来访问它。然而不幸的是,这个术语对我们造成了极大的误导,稍后我们就会看到。如果是我的话就会叫它“之前被称为Foo的原型的那个对象”。好吧我是开玩笑的,你觉得“被贴上‘Foo点prototype’标签的对象”这个名字怎么样?

——————————————————————

这里我在犹豫要不要讲一下类,要不要谈一下如何实现继承,但是犹豫过后我决定放到后面说,因为类和继承只是JS设计模式中的一种模式。他并不是JS的核心,这里讲多了只会让人思绪混乱。如果这里说了实现继承,寄生继承组合继承什么的,那是什么,不成了面向面试在学习了么。设计模式有很多,大可以到后面再去讨论这些事情。

原型链真的很简单。这就是javascript的核心机制了。找大哥,记住一句话,出了事,找领导,会让你在工作中受益无穷。

值得注意的是,对于原型链来说,这种不断查找,你可以理解为委托,就像任务一样,你解决不掉,交给我。这种委托是在对象和对象直接传递的,而构造函数其实也有他们的委托,毕竟函数也是一种对象,只不过他们的委托要更简单一点。所有的函数都只能委托一个人,就是Function.prototype。他们不用再往上找老大了,所有人的老大都是他,没有中间商赚差价。

而我们回顾上一个例子。我们会发现,我们在定义killer这个宝藏秘籍,用到了this.name。这里你可以理解为所有通过这个宝藏秘籍找来的杀手,名字都必须叫’杀手’。

那么这里的this有什么用,为什么要用this。这就涉及下一个问题了。

this是用来干什么的?

下一章:聊斋志异,apply和call,鸭哥夺舍。