技术分享经典 javaScript原型链面试题

一个小题目

今天我们部门的技术分享上出现了这样一段代码:

function Foo(){
    getName=function(){console.log(1)}
    return this
}//1
Foo.getName=function(){console.log(2)}//2
Foo.prototype.getName=function(){console.log(3)}//3
var getName=function(){console.log(4)}//4
function getName(){console.log(5)}//5

getName()
Foo().getName()
getName()
Foo.getName()
new Foo.getName()
new Foo().getName()
new new Foo().getName()

问我们这段代码的运行结果是怎样的

前置知识

首先我们需要了解以下前置知识:

  1. 变量提升和函数提升
  2. this指针的指向
  3. 原型链是什么
  4. new操作符的工资流程

本文对于这些知识进行浅谈,详细的知识作者还是建议读者去自行查找资料和学习。

变量提升和函数提升

变量提升简单来说就是使用var声明变量的时候,会在编译时将其提升至最顶端,例如本题的代码的前几行:(有错误)

//编译前
function Foo(){
    getName=function(){console.log(1)}
    return this
}
Foo.getName=function(){console.log(2)}
Foo.prototype.getName=function(){console.log(3)}
var getName=function(){console.log(4)}
function getName(){console.log(5)}

//编译后
var getName;//getName=undefined

function Foo(){
    getName=function(){console.log(1)}
    return this
}
Foo.getName=function(){console.log(2)}
Foo.prototype.getName=function(){console.log(3)}
getName=function(){console.log(4)}
function getName(){console.log(5)}

当然细心的读者应该会发现,这个编译后的代码依旧存在一些问题,因为js不仅有变量提升,同时还有函数提升。

在JS中函数在编译后也会提升至最顶端,因此编译后的正确代码为:

function Foo(){
    getName=function(){console.log(1)}
    return this
}
function getName(){console.log(5)}//函数声明
var getName;//变量声明

Foo.getName=function(){console.log(2)}
Foo.prototype.getName=function(){console.log(3)}
getName=function(){console.log(4)}

这里要注意:函数提升的优先级高于变量提升,当函数声明与变量名相同时,只要变量未赋值,此名称依旧是个函数,不会被覆盖;只有当变量赋值后,函数声明才会被同名的变量覆盖。

相信读到这里开篇提到的问题已经能解决一部分了

getName=function(){console.log(4)}

会覆盖同名函数,因此第一行的执行结果为4

this指针的指向

简单来说this是一个指向一个对象的指针,通常情况下你只需要记住调用方法时谁调用,this就指向谁。

例如我在全局中调用Foo()方法,那么此时进入方法里this的指向就是全局(Window对象)

那么我们第二行的运行结果也就很容易就理解了,首先Foo方法对全局里的getName重新赋值,并返回Window对象,所以第二行执行的方法为全局对象里重新赋值后的的getName()

因此第二行和第三行的执行结果为1
读到这里第三行的运行结果和第二行一样也不难理解了

原型链是什么

首先我们知道在js中函数也是一个对象,它也是具有普通对象的一些功能的,而JavaScript 对象体系是基于构造函数和原型链的。继承不通过类,而是通过原型对象实现,原型对象的所有属性和方法,都能被实例对象共享。

既然函数可以是一个对象,那么原题中的第二部分,就是为Foo添加了一个属性getName(),所以第4行的输出结果就是现在为其添加的这个方法的运行结果:2

接下来我们需要了解原题中第三部分的作用,也就是以下代码的作用:

  Foo.prototype.getName = function () { console.log(3) }

首先每个构造函数都有一个prototype属性指向原型对象,用来存放共有属性和方法的地址。

每个实例对象都有一个__proto__属性指向构造函数的原型对象。
例如本题的

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

这样依次类推各个原型组成了“原型链”
在理解了原型链之后你还需要了解下面这句话:
当js试图得到一个对象的属性时,会先去这个对象的本身去寻找,如果这个对象本身没有找到这个属性,那么js就会去它构造函数的’prototype’属性中去寻找,也就是去’proto‘中寻找,如果’prototype’属性本身中依旧没有找到,’prototype’中依旧有一个‘proto’。

读到这里相信第三部分的代码意思也很清晰了,向原型链中添加一个方法:getName()

new操作符的工资流程

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

简单来说new就是用来创建一个对象。

那么new操作符是怎么做到的呢?

第一步:创建一个空的对象(即{})

第二步:将空对象的原型prototype指向构造函数的原型

第三步:改变this指针的指向,并将剩余的参数传入,执行构造函数中的代码

第四步:判断构造函数的返回值,将成功后的对象返回

这时我们分析原题第五行的输出结果:

第一步:创建一个空对象

let obj= {}

第二步:将obj的原型指向Foo对象的getName()的原型

obj.__proto__=Foo.getName.prototype

第三步:改变this指向,传入剩余参数并执行构造函数Foo.getName()。运行到这里输出3。

第四步:判断构造函数的返回值,将成功后的对象返回

最后我们再看第六行的运行过程:
首先new Foo().getName()等价于(new Foo()).getName()

第一步:创建一个空对象

let obj= {}

第二步:将obj的原型指向Foo的原型

obj.__proto__=Foo().prototype```
第三步:改变this指向,传入剩余参数并执行构造函数Foo()

第四步:判断构造函数的返回值,将成功后的对象返回

第五步:执行返回的新对象的getName()方法,可我们的新对象中并没有此方法,于是去原型链上寻找,故输出3

相信读到这里最后一行的答案也不需要作者多说什么了,其运行过程与第六行的基本一致。
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值