来看几行代码:
console.log(x) //未定义变量直接打印,会报引用错误
-------------------------------------
console.log(x) //会打印出"undefined"
var x
-------------------------------------
x=1
console.log(x) //会打印出1
var x
-------------------------------------
console.log(x) //会打印出"undefined"
var x=1
我们在实际编写javascript中会发现:var变量的赋值与使用如果早先于变量声明,代码执行并不会报错。
这是因为:JavaScript代码的执行首先要通过预编译,预编译中变量声明会被扫描并且添加到词法环境(Lexical Environment) 中,这样就能在(代码中)实际声明前使用它们。而所有声明语句像是被提升到代码的头部,此称之为“声明提升”(Hoisting)。
什么是词法环境?
词法环境是一种包含标识符变量映射的数据结构。(标识符是指变量/函数的名称,变量是对实际对象或原始值的引用)。 概念上,词法环境可以理解成这个样子:
LexicalEnvironment = {
标识符:<值>,
标识符:<函数对象>
}
精简地讲,词法环境是程序执行期间存储变量和函数的地方。
var的声明提升
js引擎只能够提升变量的声明,无法提升实际赋值的初始化。但有趣的是,js添加var到词法环境时,会用undefined初始化var变量。也就是说,未实际执行var变量赋值的代码,var变量将属于undefined类型,值也就成了undefined。接着在执行期间,当运行代码到实际赋值时,js才会赋值到变量中。
在初始词法环境中,var声明提升看起来如同:
词法环境 = {
x: undefined
}
运行到实际赋值时,词法环境会更新:
词法环境 = {
x: 'hello world!'
}
let与const的声明提升
当用let声明变量时,声明提升会有些不同:
console.log(m)//报错,Uncaught ReferenceError: m is not defined
let m
console.log(x)//报错,Uncaught ReferenceError: x is not defined
const x=2
这里需要强调:声明提升适用所有声明(var、let、const、function等),js引擎在预编译会用undefined初始化var变量,但并不会用任何值去初始化let变量与const常量。因此访问未初始化的变量/常量会报错。此时两者的词法环境看起来像:
lexicalEnvironment = {
x/m: <uninitialized>
}
如果访问let变量在实际声明之后:
let x
console.log(x)//输出undefined
这里要跟上面做个区分,在编译阶段,被声明提升的let变量同样被添加到词法环境并且也没有被任何值初始化。但是在执行阶段,实际声明是在访问变量之前,实际声明时js发现没有任何赋值就会用undefined绑定x,因此此时不会报错并且输出undefined。
class的声明提升
类的声明提升与let/const的声明提升相似,类被添加到词法环境时也不会被任何值初始化,因此在实际声明类之前访问类会报错。
函数的声明提升
函数可以通过声明定义,也可以是一个表达式。对于不同方式定义的函数,它的声明提升会有些不同:
test1()//输出test!
function test1(){
console.log('test!')
}
test2()//Uncaught TypeError: test2 is not a function
var test2=function(){
console.log('test!')
}
声明定义的函数整个都会被提升(预处理的时候函数会被添加到内存中),而通过表达式定义的函数只有变量会被提升(var变量预编译会被undefined初始化),因此此时词法环境中的test2被视为变量而不是函数。
总结
1)声明提升适用所有变量、常量、类、函数。但声明提升只能提升声明定义,不能提升实际赋值。
2)var变量被添加到词法环境时会被undefined初始化,但let与const不会。
3)声明定义的函数能够被整体提升,表达式定义的函数只有外部变量被提升。