js原型及原型链--化抽象为图像的理解方式

271 阅读10分钟

在理解原型和原型链的过程中,遇到最大的困难就是,太绕了。特别是想做到随便揪出来一个变量,就能说出来它的原型和原型链是咋样的,感觉特别容易绕晕。作为图像型记忆的我,打算用图像的方式,把关于原型和原型链的理解【图像化】。

我学习原型以及原型链相关知识,主要源于觉非--《你还没学会javascript原型和原型链吗?》,有幸加到原作者的联系方式,感谢指点迷津,同时感谢我媳妇的指点以及纠正。本文的内容是对于该篇文章关于【原型链】方面的理解。建议先看下觉非--《你还没学会javascript原型和原型链吗?》,再看我的这篇文章。这篇文章既是我的理解,也算是另一角度的延伸。

对于原型已经理解,但对于原型链不那么明确的,可以忽略大部分内容,直接看第五步和第六步。

写在前面

因为要用另一个角度去理解和阐述什么是原型、什么是原型链。所以需要用到图片,下文所有图片均来自于网络,如有侵权,请联系我删除。

1、原型链中的最底层--构造函数!

别的先不管,整个图片来形容下它,我们再去理解什么是构造函数。 现在,请将记住下图,把它想象成浇筑模型用的【铁水】,代表着我们js当中的构造函数:

我是构造函数

1-1、构造函数

其实我个人认为,构造函数的意义,就是整个原型链的缩影。它的初衷我的理解是【可复用的代码段的封装】。 举个栗子: 统计《和平精英》中的三种枪械名称、使用子弹类型以及枪械类型。

1、名称(name):M416,子弹(bullet):5.56mm,类型(type):全自动步枪

2、名称(name):SKS,子弹(bullet):7.62mm,类型(type):射手步枪

3、名称(name):Kar98K,子弹(bullet):7.62mm,类型(type):拉栓狙击枪

......等等枪械

按照常见方式,我们可以创建对象进行统计:

let arms1 = {name: 'M416', bullet: '5.56mm', type: '全自动步枪'}
let arms2 = {name: 'SKS', bullet: '7.62mm', type: '射手步枪'}
let arms3 = {name: 'Kar98K', bullet: '7.62mm', type: '拉栓狙击枪'}

可以看出来,name、bullet、type这三个属性,是所有枪械共有的属性。用组件化的思维去想,可不可以去优化代码呢?(可不可以偷懒呢?)。

这时候,用构造函数就很棒了。我们只需要如下创建构造函数:

function Arms(name, bullet, type) {
    this.name = name
    this.bullet = bullet
    this.type = type
}

关于构造函数创建过程、内存以及返回值相关知识不在本文讨论范围内,网上有很多这方面文章,很通俗易懂,暂不讨论。

像上面这样,一个Arms的构造函数就创建成功了。再需要统计《和平精英》的枪械,就可以直接实例化Arms就可以了,如下:

let newArms1 = new Arms('M416', '5.56mm', '全自动步枪')
let newArms2 = new Arms('SKS', '7.62mm', '射手步枪')
let newArms3 = new Arms('Kar98K', '7.62mm', '拉栓狙击枪')

把抽象变成图像化来理解:

这里的构造函数就是个【浇筑模具】,它规定了模型的样子,你只要浇筑铁水进去,等铁水冷却了就可以生成一个又一个的模型!
换句话说,你浇筑进去的的是内容(你也可以往里面倒液态金子啥的)即使不同,但总体模样是一致的(都有name、bullet、type)。

这就是最简单的构造函数。请记住这个示例,后面我们还会返回来继续提到它。

2、对象类型(Object)

图像化记忆,先找个东西让它形象好记一点,这是一个铁杯子:

我是Object

至于为什么选择铁杯子做记忆……因为我也不知道一般有哪些东西是铁浇筑的模具~

如上文示例的Arms,就是一个对象类型,拿M416举例,其实newArms1的完全体是:

newArms1 = function() {
    name: 'M416',
    bullet: '5.56mm',
    type: '全自动步枪'
}

这就是个普通对象。

js原型和原型链中,除了Object对象,还有个Function,一并上图。

3、函数类型(Function)

假设,Function是一个铁腕,长这样:

我是Function

上文提到的构造函数Arms(),就属于函数类型。

4、prototype和_proto_

这两个变量常常在各类文档、文章中出现。他们是什么呢?这里引用下觉非文章中的内容,很一目了然的东西:

那么,为什么会这样呢?我们把内容在控制台打印一下就知道了。

【--建议可以自己在浏览器的console里面打印一遍--】

首先,普通对象,就取用前文的newArms1就可以了。

我们先把这个对象整个打印出来

console.log(newArms1) // Arms {name: "M416", bullet: "5.56mm", type: "全自动步枪"}

可以看到,打印出来一个Object对象类型的Arms对象。

接下来,我们打印它的_proto_试试看

console.log(newArms1.__proto__) // {constructor: ƒ}

来,我们尝试下打印出newArms1.__proto__的类型看看

console.log(typeof newArms1.__proto__) // object

打印结果是“object”。它的确是个Object对象。依照上图来看,它应该是没有prototype的,我们试试看

console.log(newArms1.prototype) // undefined

ok,没问题~虽然这些步骤在觉非文章中都有做过,但作为个人理解性文章,亲自尝试我觉得很重要。

接下来,我们尝试打印之前的构造函数Arms(),它是一个函数对象。

console.log(Arms) 
//  ƒ Arms(name, bullet, type) {
        this.name = name
        this.bullet = bullet
        this.type = type
    }

继续刚刚试验对象类型的步骤,打印Arms的__proto__。

console.log(Arms.__proto__) // ƒ () { [native code] }

这里出现了个很有意思的东西:

ƒ () { [native code] }

这个是什么?一会儿再说,长成这样,一般也忘不了。记住它先。我们继续打印Arms的.__proto__的类型。

console.log(typeof Arms.__proto__) // function

接着打印Arms的prototype。

console.log(Arms.prototype) // {constructor: ƒ}

好的,这里又出现了第二个不是我们自己设置的东西了,记住它,还会再见的

{constructor: ƒ}

打印Arms。prototype的类型:

console.log(typeof Arms.prototype) // object

关于原型的描述,在觉非的文章里有很明确也很好理解的阐述,到这里,都是我亲自打印出来的东西进行的二次确认。纸上得来终觉浅,绝知此事要躬行。

5、原型链

事实上,最让我迷惑的是原文中的两段内容:

其一:

其二:

我试图去理解它为什么会这样,是怎么联系起来的。

用上文我的枪械例子稍微总结一下:

  • Arms()构造函数,是一个函数类型。
  • Arms()打印prototype得到的是function。
  • Arms()打印prototype的类型是object
  • Arms()打印__proto__得到的是ƒ () { [native code] }(f代表函数)。
  • Arms()打印__proto__的类型是function
  • newArms1、newArms2、newArms3是对象类型。
  • newArms1、newArms2、newArms3打印prototype得到的是undefined。
  • newArms1、newArms2、newArms3打印__proto__得到的是{constructor: ƒ} ,事实上它是指【这个对象的构造函数的构造函数依旧是个function】。
  • newArms1、newArms2、newArms3打印__proto__的类型是object

一个函数,一个对象。

  1. 为什么一个有prototype,一个没有?
  2. 为什么他俩的__proto__却同时是个函数?却长得不一样?
  3. 为什么Arms()这个构造函数的prototype居然是object?
  4. 为什么new实例化出来的newArms1的__proto__的类型也是object?

事实上,根据我的尝试和总结,原因在于prototype和__proto__的意义。

①  prototype其实有点像一种身份辨识符,是指向该对象自己的本身类型的(即原型这个字面意义)
②  _proto_更向是一个路标,表示该对象是从哪儿【继承】来的

以下是我的理解,如果有误,请指正:

Arms()本身的确是个函数类型,所以直接prototype,浏览器会告诉我,这货,是个function。

但是如果我打印Arms()的__proto__,那浏览器就会去找它的原型链了,因为它本身是个函数,所以直接往最底层找,要知道【Function本身就是对象的一种】。所以Arms()的__proto__才会打印出object。

而newArms1这几个对象类型,他们本身就new出来的实例,像上文的图像理解的话,它不是个模具!所以它本身没有prototype。而它拥有父级,所以__proto__会指向object。

typeof打印,其实就是打印的__proto__

那么我们去看上层的Object本身吧!

console.log(Object); // ƒ Object() { [native code] }

console.log(Object.prototype); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

console.log(typeof Object.prototype); // object

console.log(Object.__proto__); // ƒ () { [native code] }

console.log(typeof Object.__proto__); // function

按照刚刚的理解,prototype指的是【本身】,所以Object的本身,打印出来的类型是object。

__proto__指的是它的来源,Object的来源却是个function函数。

记得上面Arms()的__proto__打印出来的是什么吗?

上文说过,Arms()本身是个函数,再往上就找到底层的function去了。而Object对象类型的__proto__同样是打印出的这个东西。也即是说,Object的constructor其实依旧是一个函数。

这里贴上我媳妇的总结:

6、原型链图像化记忆

第一节的标题就是“原型链中的最底层--构造函数”,其实Arms()是一个构造函数,它指向的上层即使最底层的ƒ () { [native code] },Object的__proto__同样是这个ƒ () { [native code] },他们就是最底层的【构造函数】。Arms()是我们声明的构造函数,而ƒ () { [native code] }是js中真正的最底下的构造函数了!它构造了Object和Function!

上文提到过,我们把底层的构造函数当做铁水。

我们用铁水打造出了一个杯子模具 -- Object

又用铁水打造出了一个碗模具 -- Function

所以Object和Function的typeof,也即他们的__proto__都会打印出function。

我们又用碗模具Function打造了一个自定义的构造函数Arms()。

我们来对应上文的总结一一解释:

  • Arms()构造函数,是一个函数类型。
由于Arms()是用碗模具打造的,自身是个函数,所以打出function
  • Arms()打印prototype得到的是function。
prototype指自身,Arms()自身是一个方法,也是个构造函数,它又成了一个新的模具,用于去打造M416、SKS、Kar98K,所以它也拥有了prototype属性,是个Function。
  • Arms()打印prototype的类型是object
打印类型typeof,即是__proto__,Function本身是个对象,所以打印出object。
  • Arms()打印__proto__得到的是ƒ () { [native code] }(f代表函数)。
Arms()指向的最底层的构造函数
  • Arms()打印__proto__的类型是function
最底层的构造函数即是一开始的构造函数,是个function
  • newArms1、newArms2、newArms3是对象类型。
newArms1是用Arms()打造出来的,有【new实例化】这个动作,它不再是模具,而是浇筑出来的枪械,所以它是个对象。
  • newArms1、newArms2、newArms3打印prototype得到的是undefined。
因为实例化了,浇筑出来的,不再是个模具了,所以它没有prototype属性。
  • newArms1、newArms2、newArms3打印__proto__得到的是{constructor: ƒ} ,事实上它是指【这个对象的构造函数的构造函数依旧是个function】。
它的__proto__指向的是Arms(),所以是个function
  • newArms1、newArms2、newArms3打印__proto__的类型是object
newArms1的__proto__指向了Arms()构造函数,Arms()又继承于Function,Function却是一个Object类型对象。所以打出来object。

而最底层的模型只有碗和杯子,所以打印Object和Function本身,就是object和function类型。

打印他们各自的prototype,也是打印出他们自己属性(可以理解为模型自身的样子,就像Arms()下有name、bullet、type一样)

而打印Object和Function的__proto__,则同时指向他们的来源,底层构造函数(都是铁水浇筑的)

而打印typeof Object.__proto__和typeof Function.__proto__都是打印出function,则是因为他们都是打印的铁水(底层构造函数)的类型,即function。

结语

每个内容都打印出来,再对比看,可能更容易看懂原型链的意义。如有理解错误,欢迎指正。

真是令人头秃。。。