在JavaScript代码执行过程中,JavaScript引擎把变量的声明部分和函数的声明部分提升到代码开头的"行为"。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的undefined。
执行上下文
一段JavaScript代码在执行之前需要被JavaScript引擎编译,编译完成之后,才会进入执行阶段。 实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被JavaScript引擎放入内存中。
- 编译阶段
将代码分为三部分:执行上下文(变量环境、词法环境)
和可执行代码
。
执行上下文是JavaScript执行一段代码时的运行环境 - 执行阶段
JavaScript引擎开始执行"可执行代码",按照顺序一行一行地执行。
简单例子
javascript
showName()
console.log(name)
var name = 'B'
function showName() {
console.log('showName');
}
编译声明代码:
- 第1行和第2行,这两行代码不是声明操作,所以JavaScript引擎不会做任何处理;
- 第3行,var声明的变量,因此JavaScript引擎将在环境对象中创建一个名为name的属性,并使用undefined对其初始化;
- 第4行,JavaScript引擎发现了一个通过function定义的函数,所以它将函数定义存储到堆(HEAP)中,并在环境对象中创建一个showName的属性,然后将该属性值指向堆中函数的位置
编译完成后,环境变量里长这样:
javascript
VariableEnvironment:
myname -> undefined,
showName -> 堆中存放`function : {console.log(name)}`的位置
编译非声明代码:
此时可以理解成,编译的代码如下,注意到声明部分已经没有了,以下代码会被编译到可执行代码
区域
ini
showName()
console.log(name)
name = 'B'
执行代码:
JavaScript引擎开始执行"可执行代码",按照顺序一行一行地执行。
- 执行到showName函数,JavaScript引擎便开始在变量环境对象中查找该函数,由于变量环境对象中存在该函数的引用,所以JavaScript引擎便开始执行该函数,并输出"showName"。
- 打印"name"信息,JavaScript引擎继续在变量环境对象中查找该对象,由于变量环境存在name变量,并且其值为undefined,输出undefined
- 执行第3行,把"B"赋给name变量,赋值后变量环境中的name属性变为"B"
rust
VariableEnvironment:
name -> "B",
showName -> 堆中位置
重复定义变量/函数
scss
function showName() {
console.log('A');
}
showName();
function showName() {
console.log('B');
}
showName();
编译阶段:
- 第1行,JavaScript引擎将函数定义存储到堆中,并在环境对象中创建一个showName的属性,然后将该属性值指向堆中函数的位置
- 第4行,又发现showName函数定义,但变量环境中已经有showName的属性,所以会将该属性值指向新的堆中函数位置
执行阶段:
- 第一次执行showName,输出"B"
- 第二次执行showName,输出"B"
函数提升方式
函数声明有两种方式:
php
//函数声明式:
function foo () {}
//变量形式声明:
var fn = function () {}
当使用变量形式声明函数时,和普通的变量一样会存在提升的现象;而函数声明式和上面例子所述一样。
scss
fn()
var fn = function () {
console.log(1)
}
// 输出结果:Uncaught TypeError: fn is not a function
foo()
function foo () {
console.log(2)
}
// 输出结果:2
小结
一段代码如果定义了同名的函数,那么最终生效的是最后一个函数
函数内定义的变量,在函数里也会存在变量提升
为什么会有变量提升?
ES6 之前是不支持块级作用域的,没有块级作用域,将作用域内部的变量统一提升无疑是最快速、最简单的设计,不过这也直接导致了函数中的变量无论是在哪里声明的,在编译阶段都会被提取到执行上下文的变量环境中,所以这些变量在整个函数体内部的任何地方都是能被访问的,这也就是 JavaScript 中的变量提升。
使用变量提升有如下两个好处:
1. 提高性能
在JS代码执行之前,会进行语法检查和预编译,并且这一操作只进行一次。如果没有这一步,那么每次执行代码前都必须重新解析一遍变量(函数),但变量(函数)的代码并不会改变。
在解析的过程中,还会为函数生成预编译代码。在预编译时,会统计声明了哪些变量、创建了哪些函数,并对函数的代码进行压缩,去除注释、不必要的空白等。这样做的好处就是每次执行函数时都可以直接为该函数分配栈空间(不需要再解析一遍去获取代码中声明了哪些变量,创建了哪些函数),并且因为代码压缩的原因,代码执行也更快了。
2. 容错性高
ini
a = 1;
var a;
console.log(a); // 1
如果没有变量提升,这两行代码就会报错,但是因为有了变量提升,这段代码就可以正常执行。
总结:
- 解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间;
- 声明提升还可以提高JS代码的容错性,使一些不规范的代码也可以正常执行。
变量提升导致的问题
- 变量被覆盖
从上面例子的undefined即可得到这个结果 - 变量未被销毁
scss
function foo(){
for (var i = 0; i < 5; i++) {
}
console.log(i); // 5
}
foo()
其他大部分语言中,for 循环结束之后,i 就已经被销毁了。但是在 JavaScript 代码中,i 的值并未被销毁,最后打印出来的是 5。这也是由变量提升而导致的,在创建执行上下文阶段,变量 i 就已经被提升了。所以当 for 循环结束之后,变量 i 并没有被销毁。
JS如何解决这个问题
在ES6里,引入了块级作用域概念和const/let关键字。简单结论就是const/let
定义的变量不会变量提升,具体请看这篇
scss
function foo(){
for (let i = 0; i < 5; i++) {
}
console.log(i); // Uncaught ReferenceError: i is not defined
}
foo()