变量提升是什么?
在js代码执行过程中,js引擎会把变量和函数的声明部分提到代码前头,提到前头的变量和函数默认给予undefined值。
这个现象是js代码的设计缺陷,会导致和其他语言不太一样的逻辑和结果,因此后来在ES6中,出现了let const和块级作用域,来解决这一问题。但由于向下兼容,这情况一直存在。
es6之前,用var声明变量,其声明的变量不管写在代码哪个位置,该变量都会提升到变量作用域的顶端
var声明变量
全局作用域声明变量
console.log(num) // undefined
var num = 1
等价于
var num
console.log(num)
num = 1
结论:var全局的num被提升到全局作用于顶端
函数作用域声明变量
function getNum() {
console.log(num)
var num = 1
}
getNum() // undefined
等价于
function getNum() {
var num
console.log(num)
num = 1
}
结论:var函数内的num被提升到函数作用于顶端
var声明函数
函数声方式有两种,一种是变量形式,一种是命名函数形式
// 函数的变量声明形式
var fn = function(){}
// 函数的具名声明形式
function fn(){}
这两种都会变量提升,区别是:具名形式内容也会提升,所以具名形式不管哪里调用都不会出错
为什么有变量提升?
js语言也有编译和执行阶段。
编译阶段:js引擎会搜集所有变量和函数的声明,将其提前生效。
执行阶段:除了声明类的代码,其他等到代码执行到具体的代码行,才生效。
这就是变量提升后边的机制,那为什么有这个机制呢?-作用域
作用域
作用域是指变量和函数的可访问位置,作用域决定了变量和函数的生命周期。
在es6 之前,作用域分为全局和函数作用域。
全局作用域:声明的变量在代码任何地方都能访问到,其生命周期跟随页面的生命周期。
函数作用域:在函数内部声明的变量和函数,其只有在函数的内部能够访问,函数执行结束,函数内声明的变量和函数全部销毁。
在es6之后,出现了块级作用域,块级作用域就是一对{}包裹的代码,除了对象的声明,其他:函数体{}、循环体{}、判断体{}都属于块级作用域。甚至一个单独的{}都可以被看作是一个块级作用域。
块级作用域 在块级作用域内部声明的变量在代码块外部访问不到,并且当代码块内的代码执行完毕,代码块内的变量就会被销毁。
在es6之前没有块级作用域,只有全局和函数作用域,因此通过变量提升特性,在对应的作用域将变量进行提前提升,这样在作用域的任何地方都能访问到该变量。
变量提升好处?
容错性好
变量提升可以在一定程度上提高JS的容错性,看下面的代码:
a = 1;
var a;
console.log(a); // 1
如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。
虽然在可以开发过程中,可以完全避免这样写,但是有时代码很复杂,可能因为疏忽而先使用后定义了,而由于变量提升的存在,代码会正常运行。当然,在开发过程中,还是尽量要避免变量先使用后声明的写法。
性能好
在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。这么做就是为了提高性能,如果没有这一步,那么每次执行代码前都必须重新解析一遍该变量(函数),这是没有必要的,因为变量(函数)的代码并不会改变,解析一遍就够了。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。
总结:
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间;
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行。
变量提升可能带来的问题?
变量被覆盖
var num = 1
function getNum () {
console.log(num) // undefined
if(1){
var num = 9
}
}
getNum()
分析:执行getNum时,创建该函数的执行上下文,该执行上下文中,num被提升,但是先使用后声明,因此打印undefined。
注意: var声明的变量在函数/全局 作用域中,不管位置在哪里,(即循环体,方法体,条件体)都会提升
变量未被销毁
function fn(){
for(var i = 0;i<9;i++){
}
console.log(i) // 9
}
fn()
分析: 在fn执行上下文中,var声明的变量i会提升在函数作用域顶端,因此只要在fn内都能访问到,尽管在循环结束后,造成了变量循环外未销毁,还能访问
禁用变量提升
通过let/const 声明变量,不会进行变量提升。声明的变量的生效时机就是执行时机。二者区别是:const常量。
let/const对变量覆盖的解决案例
function getNum(){
var num = 1
if(1){
var num = 2
console.log(num) //2
}
console.log(num) // 2
}
getNum()
分析:
1、函数中,var声明的变量都会提升,不管位置。因此有两个同名变量num被一起提升在getNum函数作用域顶端。
2、num = 1,num=2
3、打印都是2
---官方解释------------------:
在这段代码中,有两个地方都定义了变量 num,函数块的顶部和 if 的内部,由于 var 的作用范围是整个函数,所以在编译阶段,会生成如下执行上下文:
从执行上下文的变量环境中可以看出,最终只生成了一个变量 num,函数体内所有对 num 的赋值操作都会直接改变变量环境中的 num 的值。所以上述代码最后输出的是 2,而对于相同逻辑的代码,其他语言最后一步输出的值应该是 1,因为在 if 里面的声明不应该影响到块外面的变量。
改造:
function getNum(){
let num = 1
if(1){
let num = 2
console.log(num) // 1
}
console.log(num) // 2
}
getNum()
分析:
执行这段代码,其输出结果就和预期是一致的。这是因为 let 关键字是支持块级作用域的,所以,在编译阶段 JavaScript 引擎并不会把 if 中通过 let 声明的变量存放到变量环境中,这也就意味着在 if 中通过 let 声明的关键字,并不会提升到全函数可见。所以在 if 之内打印出来的值是 2,跳出语块之后,打印出来的值就是 1 了。这就符合我们的习惯了 :作用块内声明的变量不影响块外面的变量。
js底层怎么支持块级作用域
js 怎么即能支持函数作用域,又能支持块级作用域?我们通过一个例子来看看,底层的执行上下文做了什么?
function fn(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a) // 1
console.log(b) // 3
console.log(d) // 5
}
console.log(b) // 2
console.log(c) // 4
}
fn()
分析: 执行这段代码时,js引擎会首先编译并创建执行上下文,然后顺序执行代码。
(1)创建执行上下文
可见:
- 通过var声明的变量在编译阶段会存放到变量环境;
- 通过let声明的变量在编译阶段会存放到词法环境;
- 函数内,单独代码块{}内声明的let变量没有放到词法环境中。
(2)执行到代码块的执行上下文
当执行到代码块时,函数内,代码块外,的变量ab已经赋值1,2.而代码块内的let变量会在词法环境进栈,一个代码块一个栈。
可以看到,当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量 b,在该作用域块内部也声明了变量 b,当执行到作用域内部时,它们都是独立的存在。
其实,在词法环境内部,维护了一个栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。这里的变量是指通过 let 或者 const 声明的变量。
(3)执行到代码块末尾的执行上下文
接下来,当执行到作用域块中的
console.log(a)时,就需要在词法环境和变量环境中查找变量 a 的值了,查找方式:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。这样变量查找就完成了
(4)执行完代码块的执行上下文
当代码块执行完毕之后,词法环境中代码块对应的栈,就会从栈顶弹出,销毁对应栈的变量(let)
可见,块级作用域通过执行上下文的词法环境的栈结构实现,而变量提升由变量环境实现,二者结合,js引擎就实现了变量提升和块级作用域
暂时性死区
用let/const声明的变量,在声明前就使用,就会造成暂时性死区。
*1 var name = 'JavaScript';
*2 {
*3 name = 'CSS';
*4 let name;
*5 }
// 输出结果:Uncaught ReferenceError: Cannot access 'name' before initialization
ES6 规定:如果区块中存在 let 和 const,这个区块对这两个关键字声明的变量,从一开始就形成了封闭作用域。假如尝试在声明前去使用这类变量,就会报错。这一段会报错的区域就是暂时性死区。上面代码的第4行上方的区域就是暂时性死区。
本质: 其实这也就是暂时性死区的本质:当程序的控制流程在新的作用域进行实例化时,在此作用域中用 let 或者 const 声明的变量会先在作用域中被创建出来,但此时还未进行词法绑定,所以是不能被访问的,如果访问就会抛出错误。因此,在这运行流程进入作用域创建变量,到变量可以被访问之间的这段时间,就称之为暂时死区。
也就是,程序进入代码块,搜索到声明的变量,再放到词法环境。就会造成暂时性死区。
重要说明
- 同名函数与变量的提升中,函数优先级高于变量。
- 函数(非表达式)在编译阶段解析后,执行时不二次解析并赋值。
- 函数表达式只有执行阶段才会赋值。
- 如果有两个同名函数(非表达式),那么提升阶段也会有覆盖问题,且二者都不会在执行阶段再赋值。