变量提升
//示例1
showName();
console.log(myname);
var myname = '张三';
function showName() {
console.log('执⾏函数showName');
}
上面这段代码的输出结果为:
执⾏函数showName
undefined
问题1:执行showName();的时候,函数是在这行代码之后被定义的,为什么能正确执行
问题2:执行console.log(myname);的时候,变量是在这行代码之后被定义的,输:出为undefined,那么这个undefined值存储在哪里
问题3:为什么函数能正确执行,而打印变量却得到的是undefined,难道函数和变量有什么不同吗
问题4:提升的变量存储在哪里
要解决这几个问题,先来看下什么是JavaScript中的声明和赋值
变量的声明和赋值
var myname = '张三';
上面那段代码可以看成两行代码组成:
var myname; //变量声明
myname = '张三'; //变量赋值
函数的声明和赋值
//函数声明
function showName() {
console.log('执⾏函数showName');
}
//声明一个变量,再将匿名函数赋值给该变量
var show = function () {
console.log('执⾏函数showName');
}
变量提升
变量提升是指JavaScript在执行过程中,JavaScript引擎将变量和函数的声明提升到代码开头的行为,并且,变量提升后,该变量的默认值为undefined,函数提升后,会将函数在堆中的首地址赋值给函数变量。
此外,变量提升并不是说真正的把var myname = '张三';
这行代码的变量声明var myname
在物理层面上提升到代码的开头处,而是在编译阶段被JavaScript引擎放入内存中。
提升的变量存储在哪里
上面说过变量声明是在JavaScript代码编译阶段放入内存中的,这具体是怎么回事呢?
JavaScript代码执行大致分为两个阶段:编译阶段和执行阶段
对于示例1的代码,变量提升部分和执行部分为:
//变量提升部分
var myname = undefined;
function showName() {
console.log('执⾏函数showName');
}
//执行部分
showName();
console.log(myname);
myname = '张三';
编译阶段
生成执行上下文和可执行代码。执行上下文是JavaScript执行一段代码时的运行环境,比如调用一个函数,就会进入这个函数的执行上下文,然后确定该函数在执行期间用到的变量:this、变量、函数等。
在执行上下文中存在一个变量环境的对象( VariableEnvironment),提升的变量就是保存在该对象中的。
VariableEnvironment:
myname: undefined
showName:存储了函数在堆中的地址
因此,通过编译阶段,示例1的代码生成了如上所示的变量环境对象,该对象包含一个提升的变量myname,值为undefined,一个提升的函数,值为函数在堆中的地址。
执行阶段
按照顺序执行代码的可执行部分
- 执行showName();时,JavaScript引擎开始在变量环境对象中查找该函数,由于VariableEnvironment中存在showName,值为函数定义的地址,因此可以执行该函数
- 执行console.log(myname);,JavaScript引擎继续在变量环境对象中查找变量,找到myname,值为undefined,然后输出
- 执行myname = ‘张三’;将 '张三’赋值给环境变量对象中的myname属性。
小结
在JavaScript代码的编译阶段,先做变量提升,将执行上下文所用到的变量、函数的声明放到环境变量对象中,执行的时候,到自己的执行上下文的环境变量对象中取相应的变量。
实例
实例1:
function showName() {
console.log('张三');
}
showName();
function showName() {
console.log('李四');
}
showName();
结果:
李四
李四
分析:
在编译阶段,变量提升的部分为:
function showName() {
console.log('张三');
}
function showName() {
console.log('李四');
}
当遇到第一个showName函数时,将该函数的声明放入变量环境对象中,当遇到第二个showName函数时,将该函数替换掉已存在的showName。因此,执行showName();的时候,取环境变量对象中查找该函数,只存在第二个showName函数。通过这个实例,反应了JavaScript执行的一个重要概念:先编译,编译生成执行上下文(包括环境变量对象)和可执行代码,再执行。
实例2:
showName();
var showName = function() {
console.log(2);
}
function showName() {
console.log(1);
}
showName();
结果:
1
2
分析:
实例2代码在编译阶段的变量提升部分为:
var showName=undefined;
function showName() {
console.log(1);
}
当存在相同的变量名时,后声明的变量会覆盖先声明的变量,因此环境变量对象中showName的值是函数的地址。
执行部分为:
showName();
var showName = function() { //赋值
console.log(2);
}
showName();
- 执行showName();时,在环境变量对象中找到该函数,打印值为1
- 执行赋值语句时,将showName的值改为另一个函数的地址
- 再次执行showName();时,环境变量对象中的showName存储的值已经改变了,打印出2
通过这个实例,当我们在分析一段JavaScript代码在执行阶段取值时,可以先将整体代码分为声明提升部分和执行部分。
实例3:
这段代码能正确输出Hi",是因为这里是函数声明提升,因此能找到该函数的具体定义方法。
sayHi(); //正常输出"Hi"
function sayHi(){
alert("Hi!");
}
这段代码报错,因为sayHi是一个变量,提升阶段默认值是undefined。
sayHi(); //报错
var sayHi=function(){
alert("Hi!");
}
实例4:
var scope="gloval";
function f() {
console.log(scope); //undefined
var scope="local";
console.log(scope); //"local"
}
f();
console.log(scope); //undefined
这里可能会使人有点疑惑,按理说在var scope="gloval";
执行后,scope变量值应该是"gloval"。但是,上面提到的环境变量对象是存在一个执行上下文中的,f函数有一个自己的执行上下文,因此console.log(scope); //undefined
打印数据的时候,在f函数的上下文的环境变量对象中的scope是var scope="local";
的变量提升,默认值为undefined。
实例5:
function foo() {
for(var i = 0; i < 10; i++) {
}
console.log(i); //10
}
这段代码能很好地体现JavaScript中函数作用域和
其他语言中块级作用域的区别(ES6及其之后也存在
块级作用域),在块级作用域中,console.log(i);
肯定是访问不到i变量的,因为i变量输入for循环块。但是,在JavaScript中,i变量存在于foo函数的上下文中,在这段代码的编译阶段,i变量进行变量提升,执行阶段,for循环执行完后,i的值改为10,因此最后打印出10。
4 从数据存储的角度看闭包原理
第3节只是从原理上讲解了下闭包的作用和概念,这里再从数据存储的角度看闭包原理。
我在《JavaScript数据类型及其存储方式》一文中讲解了JavaScript中,简单类型的变量就存储在栈空间中,引用类型的变量存储在堆空间中。
第3节中说foo函数的闭包存储在内存中,即使foo函数执行结束,通过bar.setName或者bar.getName也能访问到foo函数中的变量,那么这个闭包具体是如何存储的呢?
这里从数据存储的角度分析:
当JavaScript引擎遇到内部函数的时候,会对内部函数做一次词法分析,发现内部函数使用了外部函数中的myname变量和test1变量,于是,在堆空间中创建foo函数的闭包,保存myname变量和test1变量,而test2变量没有被内部函数用到,因此继续保留在栈中。
当foo函数执行到return innerBar;
的时候,调用栈和堆空间如下图: