现如今,由于我们可以基于 ES6 使用 let / const 声明变量,似乎已经不太需要考虑变量提升机制了。
然而,事实上仍然不能完全用 ES6 的规范代替 ES3,因为有时难免会出现 ES3 和 ES6 混入的情况,而浏览器在出现这种情况的时候,一方面要向后兼容 ES3,一方面要向前兼容 ES6,此时就需要考虑变量提升的问题了。
还是以一道题作为切入热热身
var a = 0;
if(true) {
a = 1;
function a() {}
a = 21;
console.log(a);
}
console.log(a);
变量提升
在当前上下文中(全局/私有/块级), JS 代码自上而下执行之前,浏览器会提前处理一些事情(可以理解为词法解析中的一个环节,而词法解析一定发生在代码执行之前)
- 会把当前上下文中所有带 VAR / FUNCTION 关键字的进行提前的声明或者定义
- var a = 10;
- 声明 declare: var a;
- 定义 defined: a = 10;
- 带 VAR 的只会提前声明
- 带 FUNCTION 的会提前声明和定义
- 项目中更建议使用函数表达式创建函数,因为这样在变量提升阶段只会提前声明,而不会赋值,更加严谨
- 用表达式的方式创建函数时,作为值的函数应具名化,方便内部调用(例如递归操作),此外也能让代码更加规范
- 基于 VAR / FUNCTION 在 全局上下文中 声明的变量(全局变量),会映射到 GO (全局对象 window)上,两者修改同步
/*
* 代码执行之前: 全局上下文中的变量提升
* var a; => 默认值是 undefined
*
*/
console.log(a); //=> undefined
var a = 12; //=> 创建值12,不需要再声明 a (在变量提升阶段已经完成,之后不会再重新处理)
// => 只执行赋值的部分 => a=12
a = 13; //=> 让全局下的变量 a 重新指向 13
console.log(a); //=> 13
//----------------------------------------
/*
* 代码执行之前: 全局上下文中的变量提升
* function func 会提前声明和定义
* 代码开始执行时已经存在 func,因此可以调用
*/
func(); //=> 12
function func() {
var a = 12;
console.log(a);
}
//----------------------------------------
/*
* 代码执行之前: 全局上下文中的变量提升
* var fun; //=> 默认值是 undefined
* 代码开始执行时 fun 的值为 undefined,因此当作函数调用会直接报错
*
* 此处做一个小小的延伸
* var fun = function AAA() { }
* 这种写法的本意是将原本作为值的匿名函数具名化
* 1.虽然该函数取了名字,但这个名字不能在外部访问,也就是不会在当前上下文中创建 AAA 这个名字
* 2.当前函数被赋值给 fun ,可以通过 fun 来调用该函数
* 3.当函数执行时,在形成的私有上下文中,会把这个具名化的函数的名字作为私有上下文中的变量(值就是当前函数)来处理
* 4.此时可以实现递归操作,而避免使用 arguments.callee
*/
fun(); //=> Uncaught TypeError: fun is not a function
var fun = function AAA() { //=> 只有当代码执行到这一行,才为 fun 赋值一个函数
console.log(AAA); //=> 当前这个函数
console.log('ok');
};
//----------------------------------------
//=> Uncaught ReferenceError: a is not a function
console.log(a);
a = 13;
console.log(a);
//----------------------------------------
//=> Uncaught ReferenceError: Cannot access 'a' before initialization 不能在 let 声明之前使用变量
console.log(a);
let a = 12; //=> let / const 不存在变量提升机制
a = 13;
console.log(a);
//----------------------------------------
"use strict"
var a = 12; //=> 全局变量,会映射到 GO(window) 上
console.log(a); //=>12
console.log(window.a); //=> GO(window) => 12
window.a = 13;
console.log(a); //=> 13
console.log(window.a); //=> 13 映射机制是修改将变成同步,严格模式下也不影响
//----------------------------------------
// example 1 => 判断体
/*
* EC(G): 全局上下文中的变量提升
* 出现判断体代码块,无论条件是否成立,都要进行变量提升(但是,条件中的 FUNCTION 在新版本浏览器中只会提前声明,不会提前赋值)
* [老版本]
* => var a;
* => func = 函数;
* [新版本]
* => var a; 全局上下文中声明一个 a 也相当于 window.a
* => func; ...... window.func
* [新老版本划分]: 当前 IE10 及以下都认为是老版本浏览器
*/
console.log(a, func); //=> 新版本浏览器 undefined undefined
if(!('a' in window)) { //=> 'a' in window 检测 a 是否为 window 的一个属性
var a = 1;
function func() { }
}
console.log(a); //=> undefined
//----------------------------------------
// example 2
/*
* EC(G): 全局上下文中的变量提升
* fn => 1
* => 2
* var fn; 已经声明过了
* => 4
* => 5
* 全局上下文中有一个全局变量 fn ,值是输出 5 的函数(此时 window.fn => 5)
*
*/
fn(); //=> 5
function fn() { console.log(1); } //=>不再处理,变量提升阶段已经处理过了
fn(); //=> 5
function fn() { console.log(2); } //=>不再处理,变量提升阶段已经处理过了
fn(); //=> 5
var fn = function() { console.log(3); } //=> 变量提升阶段声明过,此时直接赋值 => fn=window.fn => 3
fn(); //=> 3
function fn() { console.log(4); } //=>不再处理,变量提升阶段已经处理过了
fn(); //=> 3
function fn() { console.log(5); } //=>不再处理,变量提升阶段已经处理过了
fn(); //=> 3
// example 3 => 判断体
/*
* EC(G): 全局上下文中的变量提升
* var foo;
* bar = 函数; [[scope]]: EC(G)
* 与全局对象 GO => window 映射 => window.foo, window.bar
* 全局上下文中的代码执行
*
* EC(bar): bar 函数调用时形成私有上下文
* [[scopeChain]]: <EC(bar), EC(G)>
* 遇到判断体,无论条件是否成立,都会进行变量提升
* var foo; //=> 默认值为 undefined
* 私有上下文中的代码执行
*
*
*
*/
var foo = 1;
function bar() {
if(!foo) {
var foo = 10;
}
console.log(foo); //=> undefined
}
bar();
下面进入本篇文章前面提到的热身题
在开始分析热身题之前,有几个要点需要再次强调一下
- 新版本浏览器要做到向后兼容 ES3 / 5 以及向后兼容 ES6
- 老语法规范里面,判断体和函数体等不存在块级作用域,无论条件是否成立, FUNCTION 都会直接声明和定义
- 新语法规范存在块级作用域,大括号中出现 let / const / function ... 都会被解析为块级作用域
- 新语法规范中的判断体,无论条件是否成立,都仍然会变量提升,但 fucntion 不再提前定义赋值
// 文章前面的热身题
/*
* EC(G): 全局执行上下文
* => VO(G) 全局变量对象
* => 变量提升:
* [老版本变量提升 + 定义] var a; => function a() {} -------- a => 函数 [[scope]]: EC(G)
* => 代码执行:
* a = 0; => a 重新指向 0
* 条件成立 a = 1; => a 重新指向 1
* function() { } => 变量提升阶段已经处理过不再重复执行
* a = 21; => a 重新指向 21
* 全局下的 a 为 21
*
* [新版本变量提升不赋值] var a; => function a; -------- a => 只声明不赋值
* =>代码执行:
* a = 0; => 全局下的 a 指向 0
* 条件成立, { } 中出现 function ,形成块级作用域,也会形成块级私有上下文
* EC(BLOCK) => 进栈执行
* VO(BLOCK) => 块级上下文中的私有变量对象
* [[scopeChain]]: <EC(BLOCK), EC(G)>
* => 变量提升:
* 声明 + 定义一个 a -------- a => 函数 [[scope]] => EC(BLOCK)
* => 代码执行:
* a = 1; => 此时的 a 是私有的, a 重新指向 1
* function a() { } => 不需要再操作,但会将此行代码之前对 a 的操作映射到全局下的 a => 全局下的 a = 1
* //=> 因为要兼容 ES3 和 ES6,function a 在全局下声明过,也在私有下声明过,遇到此行代码私有下不会在处理; 但是浏览器会把当前代码之前,所有对 a 的操作都映射给全局下的 a ,以此兼容 ES3; 以此为分界,后面的代码将不再和全局产生映射关系
* //=> 这一步可以通过在浏览器中打断点来验证,此行代码之前都只是操作私有上下文中的 a ,并不会映射到全局,一旦执行到这一行代码,则会把私有上下文中对 a 的操作映射到全局,这个机制是为了兼容 ES3 的规范; 而后面的操作不再和全局映射,这个机制是为了兼容 ES6 的规范
* a = 21; => 私有 a 重新指向 21
* //=> 私有上下文中的 a 此时是 21,全局下的 a 为 1
*
* ===> 浏览器端执行结果
* [老版本] 21 21
* [新版本] 21 1
*
*/
var a = 0;
if(true) {
a = 1;
function a() {}
a = 21;
console.log(a);
}
console.log(a);