引言

对于变量提高这个问题,我想从事前端的同学都或多或少以为我懂这个。从前,我也是这样以为的,我懂变量提高,并且能够从变量在 Chrome 中的内存分配讲起,以及中心发生了什么。

可是,在一次面试中,我遇到了几个一同面前端的同学(当然技术水平参差不齐,并不是很高),在和他们聊这次书面考试中的变量提高的问题时,发现咱们都支支吾吾的,很多讲的都是值的掩盖

其时的面试题是这样的:

functionfn(a){
consoF 3 ! ^ a t 2 `le.log(a)
vara=2
functiona(){}
console.log(a)
}
fn(1)

这个题目,最终会输出function a(){}2。那么,为什么是这个答案,这个进程发生了什么很重E 6 ] g e ` X要。所以,今天咱们就来完全刨析一下变l s 2量提高的进程。

一、变量在内存中的分配

在剖析[ | h . c g ! &整个进程前,咱们~ 0 {先来回顾一下 JavaScript 中变量在内存中的分@ W配。

咱们都知道的是,对于原始类型会存储在栈空间中,对于引证类型会将引证存储在栈空间中,将数据存储在堆空间中

其实这个进程还牵扯到函数上下文的创立,而每一个函数上下文中又会创立一个变量环境、词法y ^ s @ 6 /环境。有爱好的同学能够去看李斌老J f { @ + T 6 &师的浏J x * N g o ! )览器作业原理与实践

所以,咱们来看一个简略的栗子,剖析一下它在内存中的分配:
栗子:

vara=1
varb=2
varstudentv _ T F N H o 6={name:'wjc',age:22o @ - + 5}

它内存中的分配:

灵魂拷问,你真的懂 JavaScript 中的变量提升吗?

二、运转前的简略编译

众所周知,JavaScript 是一门动态类型的言语,即它是在运转时确定变量的类型,不同于静态类型言语的先编译再运转的进程。可是,事实是 V8B ? s 引擎在解析运转 JavaSch 1 J v & i }ript 之前是会进行一次简略的编译,也便是咱们一般所r b i S E – ~ & R说的初始化进程。

这个初始化进程,会做O u J 3这几件事:

  • 区别履行代码和} j + o i ? h变量声明代码
  • 变量n p ( = v D |声明代码划分为赋值代码和初始化代码
  • 初始化代码z m F ` R d有两种情况Z ] z y _ 8 Z,一是对变量(原生类型、目标类型)初始化为 und q G D B Adefined;二是对函数的初始化,即直接指向函数在堆空间中的内存

那么,咱们就来看一个简略的K L t + 8栗子:

console.log(a)
sayHi = $ ; o _ G = Si()
vara=2
functionsayHi(){
console.log('Hi')
}

那么按照咱们上面所说,这段代码的m ! . 3 S赋值只要 var a = 2,函数声明只要进行编译阶段的代码会是这样的:

//- j { { g U W ( n编译代码
vara=undefined
varsayHi=function(){
console.log('Hi')
}

此刻,它在内存中的分布:

灵魂拷问,你真的懂 JavaScript 中的变量提升吗?

然后,在履行阶段的代码会是这样:

//履行代码
console.log(a)
sayHi()
a=2

所以,也便是当咱们真实履行的时候会走履行代码,所以很显然会输出:

undefined
Hi

而当走完一切履行代码后,此[ y / Y & %刻内存是这样的:

灵魂拷问,你真的懂 JavaScript 中的变量提升吗?

我想经过这个栗子,咱们应该大致搞懂变量提高的进程。可是,依然存在一个较为特别的情况,便是当函数形参存在时的变量提高,也便是咱们文章开头提及的面试题。M u h e _ u v R

三、函数形参的编译履行

首要,咱们需要对函数调用做e ` ( h S +一个简略的了解,在咱们平常调用函数的时候,真实会经历两个步骤

  • 假如此刻存在形参,则进行函数形参的编译和履行进程
  • 然后进入函数体,进行g f i m O @ `函数体内部的编译和履行

能够看到这儿咱们说到了当函数存在形参时,会先进行函数形参的编译和履行进程。

这儿咱们就来剖析文章开头这个栗子:

funo Y h % + cctionfn(a){
console.log(a)
vara=2
functiona(){& l # o ~}
consolo g S A $ ie.log(a)
}
fn(1)

首要,此刻是存在函数形参的,那么函数形参的编译和履行会是这样:

vara=undefined
a/ r Z 3 S=1

然后,才o Y . W Y会进行函数体的编译和履行:

//编译
at . [ 3=functiona(){}//重点!!!
//履行
console.log(a)
a=2
coy [ X ~ T R p nsole.log(a)

能够看到的是,假如函数体内的变量名和形参的变量名重复时,则不会进行一般变量的编译赋值 uW a Zndefined 的进程。可是,_ n i如存在该变量是函数时,那么则会进行函数变量$ l y A ! ] F 7的编译赋值,即直接指向函Q A ] z n c e ( 5数在堆空间中的地址。

所以,咱们这d R G Q n q _ p个栗子在编译后,能够看作是这样的:

functionfn(){
vara=undefined
a=1
a=functiona(){}
console.log(a)
a=2
console.log(2)
}

很显然,它会输出会输出function a(){}2

写在最后

不知咱们在深度了解过变量提高进程后,是否有和我相同的感受便是学习编程的实质是追溯本源。现今,虽然咱们能够用 ES6letconst 来声明变量来避免 var 的种种缺陷。可是,假如因为这样而不去思考 var 为什么会存在这些缺陷。我想这是非常遗憾的。

写作不易,假如你觉得有收获的话,能够英俊三连击!!!