# 变量提升(需彻底理解)

在当前作用域下(在当前的执行上下文下),在所有JS代码执行之前,把所有带var和所有带function关键字的提前的声明(创建一个变量)或者定义(给一个变量赋值)

带var的值***提前声明***,

带function的***提前声明+定义***。

贴段代码帮助我们理解这个概念:

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

function getName() {
   console.log(5);
}

getName(); //输出4

为什么这里的getName()输出的是4呢❓下面我们分两个阶段分析😊:

1、代码执行之前: 处理带有var和function关键字的语句,流程如下:

  1. var的提前声明,则定义了一个叫getName的变量,***但是不赋值,因为这是代码执行之前!***
  2. function的提前声明+定义,则定义了一个叫getName的变量,但是发现getName变量已经被定义,那么只是将一个方法赋给了它。此时getName=fuc=>5

2、代码执行时: 给getName复制,此时getName=fuc=>4

# 理解完上面这个概念之后,我们再看下面这道题

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);
}

Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); // 3

# 解析:

明天再写吧.....头疼😫

和老友叙旧2个多小时,为了兄弟们的终身大事,我操碎了心😂!这部分明天周六补上!

周六早上起床真的太难了!!!

# 咱们继续(拖延症得治!!)(ง •_•)ง:

根据上面的变量提升的概念,我们知道在代码运行之前是***变量提升***。

1、所以第一步(变量提升): 寻找function和var关键字的代码

// 第一个function Foo() { ... },function关键字,声明+定义
Foo = AAAFFFOOO  // 这里的AAAFFFOOO假设是Function对象在堆内存中的引用地址

当前内存图: 方法执行图

// 第一个var getName = ... ,var关键字只声明
getName = undefined // 再次再强调一遍,var只声明,不定义,所以此时getName的值是undefined

当前内存图: 方法执行图

// 第二个function getName() { ... },function关键字,声明+定义。但是发现getName变量已经在上一步被声明,所以只需要定义就行,即赋值即可
getName = func=>5 // 这里是伪代码,func=>5表示输出5的那个方法在堆内存中的引用地址,后续将全部按照这种写法

至此,变量提升完毕!我们可以看到的是,目前***全局的getName指向的是func=>5***

当前内存图: 方法执行图

2、接下来第二步(代码执行): 我们一句一句来看

<第1句 />


function Foo() {
   getName = function () {
       console.log(1);
   }
   return this;
}

变量提升阶段已完成,跳过,当前内存图如下: 方法执行图

<第2句 />


Foo.getName = function () {
   console.log(2);
}

为Foo添加属性getName,该属性也是一个引用类型,当前内存图如下: 方法执行图 <第3句 />


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

为Foo的原型对象(prototype)添加属性getName,该属性也是一个引用类型,当前内存图如下: 方法执行图 <第4句 />


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

注意!!!! 这一步是一个赋值操作,因为我们现在处于方法执行阶段,而非变量提升阶段,所以这里一定不能搞混了。当前的内存图为: 方法执行图

<第5句 />


function getName() {
    console.log(5);
}

变量提升阶段已完成,跳过,内存图不变,同上一步。

<第6句 />


Foo.getName();

根据方法内存图,很容易可以看到Foo.getName的引用指向的是输出为2的方法,因此该语句输出如下:

2

<第7句 />


getName();

根据方法内存图,很容易可以看到当前全局下的getName变量的引用指向的是输出为4的方法,因此该语句输出如下:

4

<第8句 />


Foo().getName();

根据JS的运算符执行优先级 (opens new window),先计算Foo(),Foo的方法执行执行栈(ECStack)如下图所示: 方法执行图

此时的内存图为: 方法执行图

根据方法的执行栈可以看出来,Foo()执行返回了this也就是windows对象,接下来计算的就是window.getName(),即全局的getName方法,根据当前的内存图可以看到目前全局的getName的引用指向是输出为1的方法,因此该语句输出如下:

1

<第9句 />


getName();

第八句代码执行分析,我们已知,目前全局的getName的引用指向是输出为1的方法,因为该语句输出如下:

1

<第10句 />


new Foo.getName(); // 2

根据JS的运算符执行优先级 (opens new window),当前优先级最高的是成员访问(… . …)运算符,即先执行Foo.getName,而Foo.getName指向的是输出为2的方法。接下来优先级最高的就是new(带参数列表)(new … ( … ))运算符了,因为上面代码可以理解拆分成下面这两步:

var s = Foo.getName;
var r = new s();

new一个方法,对于方法内部,相当于把这个方法当成一个普通方法执行了,因此该语句输出如下:

2

<第11句 />


new Foo().getName();

运算符优先级如下

1. new Foo() // 看成一个整体xxx
2. xxx.getName()

首先new Foo()创建了一个Foo的实例,然后xxx.getName()相当于访问Foo实例的getName(),先从其自身上面找有无getName()方法,发现没有,则去其原型链找,发现原型链上有getName,并且根据我们上面的内存图中可以看出引用指向的是输出为3的方法,因此该语句输出如下:

3

这里有个问题需要注意,上面的第二句代码Foo.getName = function(){ ... }是给Foo这个对象定义的属性方法,并非***Foo实例***的属性方法,这一点一定注意!! 给Foo实例定义方法我们一般是这么写的:

var Foo = function(){
    this.getName=function(){ ... }
}

<第12句 />


new new Foo().getName();

运算符优先级如下

1. new Foo() // 看成一个整体xxx
2. xxx.getName // 看成一个整体zzz
3. new zzz()

由第11句可以看出来new Foo().getName既是挂在***Foo实例***的原型对象上的方法,引用指向了输出为3的方法,new一个方法,对于方法内部,相当于把这个方法当成一个普通方法执行了,因此该语句输出如下:

3

# 总结

看似简单的一道面试题里面埋了多少的坑,考察了多少知识点,有堆栈内存,有面向对象,有运算符的优先级,有原型,有闭包等等,所以真的是面试造火箭,工作拧螺丝,但是这些知识掌握不透彻,螺丝能拧好吗?

Last Updated: 6/14/2022, 9:04:49 PM