无标题

前言

  • 在线音乐戳我呀!
  • 音乐博客源码上线啦!
  • 浑浑噩噩在前端领域磕磕碰碰了接近三年,想看看Vue源码,不知道有没有最近想看源码的猿友,如果JS不够硬,建议跟我一起来重学JS,重学完相信再去看源码,会事半功倍。
  • 尤其清晰的记得毕业期间有一次上课期间手机响起来,接了个面试电话,就问了原型、原型链,真的是怕什么来什么,当时对这块知识较模糊,支支吾吾回答不上来很尴尬。
  • 真花了几天几夜,只为呈现最好的文章。可能一次性看不完,建议点赞收藏,花再多时间也要硬啃下来,一定拿下原型链这块盲区知识,好嘛!!!
  • 接下来我们来看看JS的原型、原型链知识点都可以考些什么。
  • 请脑子清晰,跟着我的节奏,保证一回彻底啃下,Are you ready ?

先来问自己六七八道面试题

  • 说一下原型?

  • 说一下原型链过程?

  • Object.prototype.proto 值为啥?

  • 什么的显式原型、隐式原型都指向它自己?

  • 任何函数都是new Function创建的?

  • Function的显式原型是不是都是new Object出来的?

  • 所有的函数的__proto__都是一样的?

  • 所有函数的显式原型指向对象默认是空object实例对象?

原型与原型链

  • 原型(prototype)

  • 显式原型与隐式原型

  • 原型链

一、原型

1.1 函数的prototype属性

1.1.1. 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)

上段代码方便理解这段话:

function Fun () {}

console.log(Fun.prototype)  // 默认指向一个Object空对象(没有我们的属性)

多图详解,一次性啃懂原型链(上万字)-LMLPHP

我们定义的Fun函数有一个prototype属性,而且打印出来默认指向Object空对象。

❓ 杠精上身:我不信,那为什么Date函数怎么一创建就默认有很多方法?

有图有真相。

多图详解,一次性啃懂原型链(上万字)-LMLPHP

你不是说prototype属性, 它默认指向一个Object空对象,打印出来这么多方法(揭穿你),不过typeof Date.prototype确实是object对象类型。

🙋这个问题我可以回答一下:

其实Date原型prototype上的方法是Date初始化自己往原型上添加的,主要是给实例对象使用的,谁把我创建出来,就给谁用。

多图详解,一次性啃懂原型链(上万字)-LMLPHP

打开Date函数,从源码可以看到Date往prototype添加了很多方法。

懂了,原来是Date函数创建的时候自己往prototype上添加属性方法。

❓ 你说函数的显式原型指向对象默认是空object实例对象?

console.log(Fn.prototype instanceof Object) // true
console.log(Object.prototype instanceof Object) // false
console.log(Function.prototype instanceof Object) // true

Object除外。

1.1.2. 原型对象中有一个属性constructor, 它指向函数对象

我们用代码理解一下:

Date的原型prototype有一个属性constructor,这个属性constructor指向Date对象。

Date.prototype.constructor === Date

我们再用画图理解一下(说好一定要带懂你)

构造函数和原型对象有相互引用的关系图如下:

多图详解,一次性啃懂原型链(上万字)-LMLPHP

你里面有个属性prototype能找到我,我里面有个属性constructor也能找到你。

1.2 给原型对象添加属性(一般都是添加方法)

有何作用:

为了给函数的所有实例对象自动拥有原型中的属性(方法)。

// 给Fun原型添加属性test
Fun.prototype.test = function () {
    console.log('test()')
}

// 你问我有什么用?还不是给你fun实例对象可以调用test方法用的
var fun = new Fun()
fun.test()

二、显式原型与隐式原型

2.1 每个函数function都有一个prototype,即显式原型(属性)

//定义构造函数

function Fn() {
    // 内部语句: this.prototype = {}
}

console.log(Fn.prototype)  // {}

而函数的prototype属性在什么时候会被加上?

函数的prototype属性: 在定义函数时自动添加的, 默认值是一个空Object对象

2.2 每个实例对象都有一个__proto__,可称为隐式原型(属性)

//创建实例对象

var fn = new Fn()  // 内部语句: this.__proto__ = Fn.prototype

console.log(fn.__proto__)   // {}

而对象的__proto__属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值

2.3 对象的隐式原型的值为其对应构造函数的显式原型的值

其实也确实是相等,因为实例对象的__proto__属性在创建对象时自动添加的, 默认值为构造函数的prototype属性值。

二者的地址值都是同一个,自然相等。

function Fn() {}
var fn = new Fn()

console.log(Fn.prototype === fn.__proto__) // true

我们再来上张图更透底搞懂他们的关系:

多图详解,一次性啃懂原型链(上万字)-LMLPHP

⭐请允许我啰嗦两句(建议根据阿泽以下说的,然后画下来,画着画着就懂了):

  • 1.第一行代码定义了Fn函数,那么栈空间产生Fn,因为函数是引用类型,所以赋予0x123(栈空间命名不是这样子叫,只不过在这里给它一个名字)

  • Fn是构造函数,内部语句:this.prototype = {},所以有显式原型prototype属性,指向空object对象0x234。

  • 输出Fn.prototype,打印{constructor: ƒ, prototype:{...}}

  • 2.创建以Fn实例对象的fn,在栈空间新建fn,指向堆空间的0x3456。

  • 实例对象有__proto__属性,实例对象被创建内部语句是:this.__proto__ = Fn.prototype

  • 对应上方2.3 对象的隐式原型的值为其对应构造函数的显式原型的值

  • fn的__proto__和Fn.prototype指向同一个值为0x234。

  • 3.在Fn构造函数的原型上添加test属性,即0x234空object对象上添加test属性。

  • 接着执行fn.test(),所谓点.就是去堆空间里面找对应的属性值,它先去本身0x345找,发现并没有找到,继续去它的隐式__proto__原型链0x234上找,找到了test属性,返回!

❓ 杠精上身:为什么构造函数有prototype属性了,实例对象还要搞一个__proto__的值为构造函数prototype的值呢?

大概作用就是共享内存,减少方法对内存占用,test方法变成公交车,prototype就是现金,__proto__就是扫码,只要给钱都能上(都能访问test)。

2.4 能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)

我们正常都是直接给Fn原型上添加属性方法,而不会直接去操作fn实例对象的__proto__属性去添加方法。

也就是可以直接操作显式原型,而不要直接去操作隐式原型。(虽然现在ES6时代可以去操作隐式原型,但最好不要)。

function Fn() {}
var fn = new Fn()

// 给显式原型添加test方法
Fn.prototype.test = function () {
    console.log('test()')
}

// 通过实例调用原型的方法
fn.test()

三、原型链

⭐建议跟着以下分析,自己把图画出来,程序员的动手能力很重要(如果实在懒,再打开一个窗口放把上面的图,一边看图一边看分析),如果图、分析不结合,有可能无法GET到点哦 ~

如果暂时没时间,建议收藏,等有时间慢慢看。

3.1 聊原型链前,我们先来看一道面试题目。(请耐下性子,好好看下去,必有收货)

请输出以下程序执行的结果。

function Fn() {
    this.test1 = function () {
      console.log('test1()')
    }
}

Fn.prototype.test2 = function () {
    console.log('test2()')
}

var fn = new Fn()

fn.test1()  // test1()
fn.test2()  // test2()
console.log(fn.toString())  // '[object Object]'
fn.test3()  // Uncaught TypeError: fn.test3 is not a function

面试官:别急,请根据上述代码块画出函数的栈堆、以及原型链指向。

是不是很多面试者突然不知道怎么画了呢?

其实也正常,对原型链概念模模糊糊,画不出来,正常,不过这也是本篇存在的意义。

记住三句话:

  • 每个函数都有一个prototype属性, 它默认指向一个Object空对象。

  • 每个实例对象都有一个__proto__,可称为隐式原型。

  • 实例对象的隐式原型属性等于构造函数显式原型属性的值。

来吧,唐伯虎附体。冲!

多图详解,一次性啃懂原型链(上万字)-LMLPHP

  • 左边的程序块大局观大概是:

    • Object对象0x567是引擎创建了它,程序再执行的时候也就有了右边堆空间的产生;

    • 无论是Fn函数的显式原型也好,隐式原型也罢,都是Object的实例对象;

    • 然而Object虽然是大家的爸爸,但也是有原型的,是0x345。(爸爸的爸爸叫爷爷)

多图详解,一次性啃懂原型链(上万字)-LMLPHP

⭐请允许我啰嗦两句(建议根据阿泽说的,然后画下来,画着画着就懂了):

  • 1.第一行代码定义了Fn函数,那么栈空间产生Fn,因为函数是引用类型,所以赋予0x123(栈空间命名不是这样子叫,只不过在这里给它一个名字)

  • 堆空间也产生0x123的function对象,并有prototype属性。

  • prototype默认会指向一个Object空对象,我们给这个空对象命名0x234。

  • 对应上面 1.1.1 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)

  • 0x234有一个__proto__属性。既然都有__prototy__隐式原型,那0x234是Object的实例对象。

  • __proto__指向一个Object对象,我们给这个原型对象命名为0x345。

  • 对应上面 2.2 每个实例对象都有一个__proto__,可称为隐式原型(属性)

  • 要讲这个0x345,我们先来引进Object对象。

  • 先问大家一个问题,Object对象先创建还是我们代码块的Fn函数先创建?

  • Object对象在我们代码还没有执行的时候,JS引擎已经创建了Object。

  • 验证下:看图的0x234实例对象的__proto__隐式原型属性 = 构造函数Object的显式原型,那竟然是等于(赋值运算),那肯定是右边先有的。右先创建才有的左边。

  • 所以Fn之前,Object已经先创建了,我们给Object命名为0x567。

  • 0x567的Object函数对象有一个prototype属性。

  • prototype指向Object原型对象,我们给这个对象命名0x3456。

  • 我们知道prototype默认会指向一个Object空对象,那这里Object的prototype也是指向一个空对象?

  • 总得有个尾吧,这里就是尾了。所以0x3456里面有很多属性。如:toString()、valueof()、...

  • 当然,它还有__proto__属性,只不过值为null。

  • 所以toString方法,我们在函数对象就可以调用它,因为函数也是对象,对象的原型最终为Object,Object的prototype显式原型就有toString方法。

  • 回归上方的0x234的__proto__指向的就是Object的prototype显式原型。

  • 2.OK,到这里我们画完代码块中的Fn函数的创建(是不是觉得我就定义一个函数,就这么复杂了,其实再走下去就通了),来,我们继续GO!

  • 3.接着Fn.prototype.test2给Fn原型添加test2属性,我们在0x234对象里面添加test2属性。

  • 4.var fn = new Fn(),创建了一个Fn的实例对象fn,在栈中创建fn,命名为0x456。

  • 堆空间有0x456这块内存,有着test1属性。

  • 有人可能会问了,test1不是Fn构造函数的吗?为什么test1写在fn实例对象里面?

  • 请注意:在Fn中的test1是this.test1 = function(){}创建的,上篇讲到想写好面向对象的代码,这篇一定要看讲到this指向,谁调用我,this就指向谁,在这里是fn调用的,那this.test1当然是写在fn里面。

  • 当然,0x456是一个实例对象,那就应该有__proto__隐式属性。

  • 该隐式属性指向Who?

  • 对应前面2.3 对象的隐式原型的值为其对应构造函数的显式原型的值

  • 所以,这里的0x456实例对象隐式原型的值指向0x123构造函数显式原型的值,即指向:0x234。

  • 5.OK,左边代码块继续走,fn.test1方法执行,输出test1()

  • 6.fn.test2()方法执行,先找到fn(先在自身属性中查找,找到返回)。

  • 然而并没有找到,fn里面只有test1、__proto__属性。(如果没有, 再沿着__proto__这条链向上查找, 找到返回)

  • 我们沿着__proto__这条线寻找下去,找到0x234,0x234有test2、__proto__属性,OK,找到test2,返回,停止查找。输出test2。

  • 7.继续走,打印fn.toString(),先找到fn,并没有toString,再沿着__proto__这条链向上查找,找到0x234,还是没有找到,再沿着__proto__这条链向上查找,来到0x345的Object原型对象,找到了toString属性,打印[object Object]。

  • 8.最后一步,fn.test3(),先找到fn,并没有toString,再沿着__proto__这条链向上查找,找到0x234,还是没有找到,再沿着__proto__这条链向上查找,来到0x345的Object原型对象,还是没有找到,再沿着__proto__这条链向上查找,但此时的__proto__为null。

  • 说到这里,我们再来聊一个情况。

  • 如果是fn.test3,到Object的__proto__隐式原型为null,那到最尽头了,找不到怎么办呢?会报错?还是返回null?

  • 其实都不是,是undefined。

  • 所以fn.test3输出undefined,但你fn.test3(),undefined()执行,把undefined当做函数执行,那不得报错。

打完收工。

⭐建议跟着以上分析,自己把图画出来,程序员的动手能力很重要(如果实在懒,再打开一个窗口放把上面的图,一边看图一边看分析),如果图、分析不结合,有可能无法GET到点哦 ~

看完暂时还模模糊糊,请反复看以上分析。

如果暂时没时间,建议收藏,等有时间慢慢看。

3.2 再来一条?(易出错)

请输出以下程序执行的结果。

function Fn() {
    this.test1 = function () {
      console.log('test1()')
    }
}

console.log(Fn.prototype) // {constructor: ƒ}
Fn.prototype.test2 = function () {
    console.log('test2()')
}
console.log(Fn.prototype) // {test2: ƒ, constructor: ƒ}

多图详解,一次性啃懂原型链(上万字)-LMLPHP

其实上面输出的答案是错的。

啊?

正确答案如下:看图

多图详解,一次性啃懂原型链(上万字)-LMLPHP

可以看到展开前,并没有test2(上面的答案是对的),但如果展开后,第一个打印却又有test2函数。

来,请听我分析一波。

  • 第一个输出Fn.prototype展开有test2这个属性,但没有test2的实质内容。

  • JS申明的变量都会提前,所以这里prototype在一开始就知道有个叫test2的属性的。

  • 但是实际的内容prototype不知道,只有当Fn.prototype.test2 = function () {}执行完成才知道。可以在添加原型后,打印输出再看一次prototype。

  • 会发现test2的样子和没添加原型不一样了。此时test2正式作为函数加入了prototype中。

  • 是开始的时候变量申明提前造成的。即一开始就有test2存在了。

3.3 访问一个对象属性时,原型链的内心世界

  • 访问一个对象的属性时:

    • 先在自身属性中查找,找到返回
    • 如果没有, 再沿着__proto__这条链向上查找, 找到返回
    • 如果最终没找到, 返回undefined
  • 原型链的别名:隐式原型链

  • 其实从上面的例子得知,每次访问对象属性,其实都会去寻找__proto__隐式原型链上去找,所以原型链被人起了别名叫隐式原型链。

  • 小误区:var fn = new Fn()这段代码的fn,这个是不是也在原型链中找?

  • 不是,它是在作用域里,变量查找是在作用域中找的;原型链查找的是对象的属性。

  • 原型链的作用:查找对象的属性(方法)

3.4 构造函数/原型/实体对象的关系(图解一)

var o1 = new Object()
var o2 = {}

面试官:这段代码执行完,栈堆空间是怎样的?

多图详解,一次性啃懂原型链(上万字)-LMLPHP

  • 首先:o1、o2都是Object构造函数的实例对象,那实例对象必定有__proto__属性。

  • 其次:实例对象的__proto__隐式原型属性指向构造函数显式原型属性的值。

  • 最终:可以看到图中o1、o2的__proto__属性与Object的prototype都指向同一个值Object.prototype

3.5 构造函数/原型/实体对象的关系(图解二)

面试官:相信我,最后一道了。(说出来你可能不信)

多图详解,一次性啃懂原型链(上万字)-LMLPHP

最后一道我们就轻松一点,请画出function Foo(){ }原型链。

直接上图。

多图详解,一次性啃懂原型链(上万字)-LMLPHP

⭐请允许我再多啰嗦两句:

  • 可以看到Foo函数有两条线,一条是function Foo(),还有一条是__proto__。

  • 第一条线为什么会存在?

  • 1.function Foo()它是一个构造函数,这没问题吧,在上面2.1 每个函数function都有一个prototype,即显式原型(属性) 一开始我们就讲到了,所以自然有这条线存在。

  • 构造函数有显式原型属性prototype。

  • 第二条线为什么会存在呢?

  • 2.看到了图中第二条线有__proto__这个属性,谁有这个属性,实例对象吧,啥,你不是说function Foo()是构造函数吗?现在又说是实例对象?

  • 别急,function Foo(){ },其实是一个省略的写法,它还可以写成var Foo = new Function(),这种写法是相等的,那我这样一写,Foo岂不是是Function的实例对象,那也很自然有了第二条线是__proto__。

  • 透过本质去看问题,将迎刃而解。

  • 换句话说:所有的函数对象都有隐式、显式原型属性;隐式原型属性指向大写Function的显式原型属性。(你怎么不早说.....现在这不是说了嘛)

  • 3.Foo的__proto__和function Function的显式原型prototype都指向了Function.prototype。

  • Foo是Function构造函数new出来的吧,Foo是实例对象;function Function是构造函数,构造函数有prototype不过分吧。所以有了__proto__、function Function()都指向Function.prototype这条线,再次验证上面的2.3 对象的隐式原型的值为其对应构造函数的显式原型的值。(注意结合上图)

  • 4.有个奇怪的现象:Function的显式、隐式原型都指向自己?

多图详解,一次性啃懂原型链(上万字)-LMLPHP

  • 有隐式原型,那就是实例对象;有prototype显式原型,那就是构造函数。

  • 说明Function,也是自己new出来的,即:Function = new Function(这个其实就是先有鸡,还是先有蛋的问题)

  • 只有这样才能说明显式、隐式原型都相等,别的函数是没有这个特点的,如:上面的var Foo = new Function(),Foo分了两条线,显式原型一条、隐式原型另一条。

  • 只有一个显式、隐式原型属性都是自己的,就是Function。(⭐画重点,面试要考的)

  • 也就是说:任何函数创建都是new Function出来的,Function自己也不例外。

  • 5.函数的显式原型是new Object。

  • 解释一下:

    • 函数是由Function生的,你自己的函数它的__proto__隐式原型指向它父亲,也就是Function的prototype显式原型。

    • Function唯一不同的就是它的__proto__指向它自己。

    • 也就是说Function.__proto__找不到它的父亲了(这个很关键)

    • 但是Function的prototype在Function创建后就存在了。

    • Function.prototype是一个对象,是对象,所以它的__proto__当然指向的是Object。

  • 6.为什么会有Object的__proto__指向Function.prototype这条线?

多图详解,一次性啃懂原型链(上万字)-LMLPHP

  • 隐式原型怎么有的?(构造函数的显式原型给的)

  • Object怎么来的?(其实Object也是new Function出来的,哈哈哈😁)

  • 又因为Object是Function的实例对象。线是从Object的__proto__指向Function的。

  • 一切Function都是new Function,所以Function才是这原型链的主谋。

  • 可以看到下图function Object()的隐式原型指向Function的显式原型,所有对象函数都是Function创建出来的。

  • 好,大家不是说一切皆对象吗?那你这什么都是Function创建的,不是应该一切皆函数吗?

  • 是啊,函数是最底层的,函数可以叫函数对象,函数只是对象的一种特殊表示,所以一切皆对象。这里有一篇不服,老湿说的万物皆对象,你也信?

  • 7.所有函数的__proto__都是一样的。因为都是new Function产生的。

  • new Function显式原型是不会变的,那实例对象的__proto__隐式原型指向着我的显式原型prototype,那大家都是我创建的,我不会变,自然大家也不会变。

3.6 原型继承

构造函数的实例对象自动拥有构造函数原型对象的属性(方法)

利用的就是原型链。

原型上的属性方法就是为了给实例对象用的,也就是它的孩子,谁创建了我,我就给谁用。

四、面试

现在随便问你几道题,应该能对答如流了吧!

Are You OK?

4.1 Object.prototype.proto 值为啥?

🙋:null。最小的孩子为null。

Object的原型对象才是原型链的尽头。

4.2 什么的显式原型、隐式原型都指向它自己?

🙋:Function。

显式原型Function是构造函数;隐式原型Function也是实例对象。

要是你能给面试官再画上【3.5 构造函数/原型/实体对象的关系(图解二)】这张图,那明天来上班。

4.3 任何函数都是new Function创建的?

🙋:是的,甚至Function它自己也不例外。

Function.__proto__ === Function.prototype

看,所有的都指向了Function的显式原型属性。

多图详解,一次性啃懂原型链(上万字)-LMLPHP

不太明白的童鞋可以再看看【3.5 构造函数/原型/实体对象的关系(图解二)】这张图。

好,大家不是说一切皆对象吗?那你这什么都是Function创建的,不是应该一切皆函数吗?

是啊,无论是内置函数、自定义函数、object都是通过new Function出来的,函数是最底层的,函数可以叫函数对象,函数只是对象的一种特殊表示,所以一切皆对象。这里有一篇不服,老湿说的万物皆对象,你也信?

4.4 Function的显式原型是不是都是new Object出来的?

🙋:是的。

  • Function.prototype.__proto__ === Object.prototype

  • 函数是由Function生的,你自己的函数它的__proto__隐式原型指向它父亲,也就是Function的prototype显式原型。

  • Function唯一不同的就是它的__proto__指向它自己。

  • 也就是说Function.__proto__找不到它的父亲了(这个很关键)

  • 但是Function的prototype在Function创建后就存在了。(A = B,肯定是B先产生才有了A)

  • Function.prototype是一个对象,是对象,所以它的__proto__当然指向的是Object。

4.5 所有的函数的__proto__都是一样的?

🙋:是的。

因为所有的函数都是通过new Function出来的。

new Function的隐式原型和Function的显式原型相等。(隐式是实例,谁赋值给它的,构造函数的显式原型)

Function是不会变的。

所以所有的函数的__proto__都是一样的。

不太明白的童鞋可以再看看【3.5 构造函数/原型/实体对象的关系(图解二)】最后的总结。

4.6 所有函数的显式原型指向对象默认是空object实例对象?

🙋:并不是。

console.log(Fn.prototype instanceof Object) // true
console.log(Object.prototype instanceof Object) // false
console.log(Function.prototype instanceof Object) // true

Object除外。Object内置很多属性。

最后

想起有一次加班的时候,一个开发五年的大佬再向我讲解自己的代码的时候,说到hasOwnPrototype()这个方法的时候,说这个方法是for in遍历对象的时候VS Code自动生成的hasOwnPrototype,如下图:

多图详解,一次性啃懂原型链(上万字)-LMLPHP

大佬说我也不知道这个方法是干嘛的,我看别人都有加,最好还是加上吧。

也是笑了笑,其实就是为了严谨,确保我这个对象的属性都是自定义添加的。

很多开发了很多年的大佬,说到底层原理尴尬的摸了摸头。

不知道,不明了,不重要,重要的是不懂就要学,写了不止八小时,好几天拼凑起来的文章。

以往推荐

尤大大说我的代码全部不加分号

老湿说的万物皆对象,你也信?

Vue-Cli3搭建组件库

Vue实现动态路由(和面试官吹项目亮点)

项目中你不知道的Axios骚操作(手写核心原理、兼容性)

VuePress搭建项目组件文档

koa2+vue+nginx部署

vue-typescript-admin-template后台管理系统

原文链接

https://juejin.cn/post/7010942653915201543/

09-23 20:53