变量提升是JavaScript中的看起来很trick一个点,但是了解JS代码运行过程之后,一切都会迎刃而解
变量提升是什么
- 用
var
声明的变量会自动被移到函数或者全局代码之上,但是变量赋值不会随之提升 - 显示声明的函数作为一个整体也会提升到全局代码之上
如果你觉得这很难记住,记住也会忘(说明你是个正常人)。想彻底理解为什么会这样,JS代码是如何运行的,那么恭喜你发现宝藏。
JS代码执行过程
对于写好的代码假设有10行,JS引擎并不是以“行”为单位进行解析和运行。而是首先将10行代码从上到下整体解析一遍,构建出一个抽象语法树,之后转化为计算机可识别运行的机器语言,然后再从上到下逐行执行。
为何变量声明提升了,赋值没有提升
结合上图,JS代码运行时,先进行词法解析,然后再执行代码。 变量提升发生在词法解析阶段,而赋值发生在执行代码阶段,执行到那一行才会给变量赋值。 这就是为什么变量声明提升了,赋值没有提升。
JS代码运行例子解析
现有JS代码如下:
var userName= "Jack";
console.log(userName); // "Jack"
console.log(age); // undefined
foo(); // 'Female' undefined
function foo() {
var gender = 'Female';
console.log(gender);
console.log(age);
}
var age = 18;
阶段一:代码解析
全局代码解析阶段,JS引擎内部创建一个对象叫globalObject
,简称GO,主要由三个部分组成:
// 上述代码对应的GO(伪代码)
globalObject = {
// 一些JS的类,方法,函数
String: "类",
Math: "类",
setTimeout: "函数",
// 浏览器环境内置一个window属性,指向自身(node环境中没有
// 它指向GO本身,运行console.log(window)或者console.log(window.window.window),指向的也是GO
window: globalObject,
// 全局作用域下变量声明
userName: undefined, // 代码还没执行,没被赋值
foo: 0xa00,
age: undefined, // 代码还没执行,没被赋值
};
创建的GO对象存储在内存的堆空间中;对于函数foo,V8引擎会在堆内存中开辟一块函数空间,来放置函数体和父级作用域,并将空间的地址0xa00
存在全局变量函数名对应的值中。
阶段二:代码执行
在代码整体解析完成后,开始逐行运行代码,V8引擎会在内存中开辟一块栈空间,叫做执行上下文栈,也叫调用栈,用来存储执行上下文
执行上下文(Execution Context Stack,ECStack)可以理解为当前代码的运行环境,可以分为两类:
- 全局执行上下文(Global Execution Context,GEC):全局环境,代码运行时创建
- 函数执行上下文(Function Execution Context,FEC):函数环境,函数运行时创建
无论是全局执行上下文还是函数执行上下文,生命周期都包括三个阶段:第一个阶段为创建阶段,创建变量对象,之后作用域中涉及什么变量都到变量对象中来找,找不到就去父级变量对象中找。第二阶段为执行阶段,逐行执行代码,执行完之后,从调用栈中弹出,等待被JS引擎垃圾回收。
在 JavaScript 代码运行过程中,最先进入的是全局环境,所以先把全局执行上下文压入调用栈。
全局上下文创建阶段
创建变量对象VO,指向之前创建的全局对象GO;创建作用域链,记录父级作用域;确定this指向。
全局上下文执行阶段
逐行执行代码,代码中涉及到的变量都去变量对VO中寻找,找不到就去父级作用域中找。
var userName = "Jack" // 去VO中寻找username,之后赋值为"Jack" ,由于VO指向的是GO,所以改的是GO
console.log(userName) // 去VO中找username,并打印出来值,为'Jack'
console.log(age) // 去VO中找age,找到了age: undefined,打印出来undefined
foo()// 执行的过程中遇到函数,去VO中找`foo`,找到函数地址0xa00,开始创建函数上下文
函数上下文创建阶段
对函数先解析,再执行。解析时创建一个活跃对象Activation Object(AO),之后就会根据函数体创建一个函数执行上下文,并且加入到调用栈中。函数执行上下文中变量对象VO指向AO。之后确定作用域链:VO+parent scope,其中父级作用域是从0xa00中读取到的,为GO
函数上下文执行阶段
// 执行函数foo()内部代码
var gender = 'Female' //去函数对象VO中(其实是AO)找到gender,将其赋值为字符串'Female'
console.log(gender) //去函数对象VO中(其实是AO)找到gender,并打印
console.log(age) // 去函数对象VO中找到age,没有找到,去父级作用域查找也就是GO,找到了,值目前还是undefined,输出undefined
函数上下文销毁阶段
函数执行完,函数执行上下文出栈,等待被回收,就没有指针指向AO了,AO也会被回收。
- 之后接着执行函数之后的代码,
var age = 18
,去VO中查找变量age,最后在GO中找到,赋值为18
全局执行上下文销毁阶段
此时代码全部执行完毕,全局执行上下文出栈,等待被垃圾回收
变量提升题目
步骤:
- 写出GO,AO
- 在赋值或者输出的语句处判断是对GO还是AO进行更改
- 1
var n = 100;
function foo() {
n = 200;
}
foo ();
console.log(n);
// 答案: 200
解析:函数中将n赋值为200,但是函数本身的变量对象中并没有n,所以要去父级变量对象中找并修改,所以将GO中的n赋值为了200。
- 2
var n = 100;
function foo() {
var n = 200;
}
foo ();
console.log(n);
// 答案: 100
解析:第二题可以和第一题对照理解,函数foo中含有变量n,所以n=200是将AO中的n赋值为200,并没有改变全局变量中n的值,所以依然为100
- 3
function foo() {
console.log(n);
var n = 200;
console.log(n);
}
var n = 100;
foo();
// 答案 undefined, 200
解析:console.log(n)
是在函数调用栈执行,所以n要先从AO中找,赋值之前先输出的是undefined
,赋值之后输出的是200
- 4
var foo=1;
function bar(){
if(!foo){
var foo=10;
}
console.log(foo);
}
}
bar();
// 答案 10
解析:在函数bar中,无论条件语句是否满足条件,var foo = 10
解析时,都会将foo作为函数内变量提升到AO中,且初始值为undefined,所以最后输出为10
- 5
var name = "xs";
function foo1() {
console.log(name);
}
function foo2() {
var name = 'foo2';
foo1();
}
// 答案 "xs"
解析:函数foo1执行时,对其foo1对应的VO中寻找name,没找到,之后去父级作用域中找,父级作用域在函数声明时已经确定,与函数在哪里调用无关,所以父级作用域为全局GO,name为“xs”.
总结
JS中的变量提升其实与代码执行过程有关。变量和函数提升发生在代码解析阶段,代码执行时,遇到变量赋值或者输出操作,会先在当前执行上下文的变量对象中搜索,如果找到该变量,就对其进行操作,否则就延着作用域链去父级作用域中寻找,若都没有找到,就会报错。
码字画图不易,如果对你有些许帮助,请别忘记点个赞哦👍;如果还有没看懂的地方,先收藏一下⭐,早晚会弄明白!
你的支持是我更新的动力(💖)