JavaScript预解析(变量提升)问题 (大杂烩)
问题背景
遇到了个小问题,问大家都说是作用域的问题.我觉得不对啊.于是做个笔记记录下来.
本文涉及到 JavaScript执行机制 / 作用域 / 作用域链 / Js预解析 / this指向 / 改变this指向 / var重复声明.菜比较多.
首先先牵扯到的是JavaScript是单线程语言.代码的执行顺序是自上而下的执行代码.
我的理解就像是高速公路收费站一样,一次只能过一辆车.但是如果真的完全是这样的话,就会出问题.好比说:如果调用函数在函数声明前执行,那么就会报错.
- 所以.聪明的JavaScript的执行机制其实是:
- 第一步 : 创建
- 创建作用域链 (包含当前变量对象和所有父级变量对象)
- 创建变量对象 (包含参数/变量/函数声明) [也可以叫做预解析吧.博主猜的😕]
- 创建this (this是在运行时绑定的.并不是在编写时绑定.它的上下文取决于函数调用时的各种条件.this的绑定和函数声明的位置没有任何关系.只取决于函数的调用方式 --<你不知道的js上>. !!创建this并不是确切的说法)
- 第二步 : 执行
- 变量赋值 / 函数引用等
- 第一步 : 创建
以上执行机制内容来自JavaScript闭包 - Web前端工程师面试题讲解
下图来自掘金:
两个内容的差异是名词叫法的不同.
为了简短文字.下文描述JavaScript预解析阶段,只描述创建步骤只描述创建变量对象这一步.
代码
代码一
// console.log(a); 这个时候b是等于undefined的.并没有进行赋值.
var a = 66;
function test1(a) {
console.log(a); // function a(){};
function a() { };
var a = 5;
}
test1(a);
这段代码的console.log(a)的结果是 function a(){};
- 首先是全局作用域(在浏览器下全局作用域是window)的预解析
- 解析出 var a (这个时候b是等于undefined的.并没有赋值) 和 function test1
- 之后是执行代码.执行到test(a)代码的时候.给参数a赋值66(参数就是一个局部变量).
- 当执行test(a)的时候.复制全局变量a=66传递给 function test1(a)中的啊;
- 进入fuction test1()的作用域.执行function的预解析.
- 解析出 function a(){} 和 var a.
- 这个时候如果函数名称和变量名称的变量名是一样的话.那么该变量名会指向函数.和代码执行顺序没有关系
代码二
var b = 66;
function test2(b) {
console.log(b); // 输出 66
var b = 5;
}
test2(b);
这段代码的console.log(b)的结果是 66.
- 首先是全局作用域的预解析 var b. function test()
- 代码执行阶段:
- 把全局变量的b复制给test(b)中的b(局部变量).传递给函数test().
- 执行function test2()的预解析.
- var b.因为已经存在一个相同的局部变量(函数传递进来的b),那么就不会再执行这次的预解析 var b (参考博文 :为什么var可以重复声明)
- 执行阶段:
console.log(b)的结果是 66 .这个66是通过函数进来的66
而 :
var b = 66;
function test2(b) {
var b = 5;
console.log(b); // 输出 5
}
test2(b);
输出的结果就会是5.因为 var b = 5 把 局部变量给覆盖掉了.进行了一次重新赋值
重复声明时:首先编译器对代码进行分析拆解,从左至右遇见var a,则编译器会询问作用域是否已经存在叫a的变量了。如果不存在,则招呼作用域声明一个新的变量a;若已经存在,则忽略 var 继续向下编译,这时 a = 2被编译成可执行的代码供引擎使用。
参考博文 : 为什么var可以重复声明
代码三
function test3(b) {
console.log(b); // 输出 undefined
var b = 5;
}
test3();
这段代码的console.log(a)的结果是 undefined
- 首先是全局作用域(在浏览器下全局作用域是window)初始化. function test3
- 之后执行代码阶段. 执行test3()
- 而在函数中test3(b)需要一个局部变量b.函数并没有传递进来参数.此刻函数中的参数b为undefined.
- 之后执行test3()局部作用域的预解析阶段.
- 开始预解析 var a .但是此刻局部作用域中已经存在变量名等于 b的局部变量.所以忽略此次.
- 函数的执行阶段. console.log(b); 此刻的b是等于undefined的.输出undefined
代码四
var a = 100;
function fns() {
console.log(a); //undefined
var a = 200;
console.log(a); //200
}
fns();
console.log(a); //100
var a;
console.log(a); //100
var a = 300;
console.log(a); //300
输出结果见上面的注释.
- 首先是全局作用域的预解析阶段:
- var a ; function fns ;
- 之后是执行全局作用域的代码.
- 执行 fns()
- 局部作用域 fns()的预解析.
- 解析出 var a.
- fns()执行阶段
- console.log(a). 此刻的a只是预解析.并没有进行赋值.所以第一个console.log(a)输出undefined
- 等到函数体内第二次执行 console.log(a)的时候.此刻上一行代码已经赋值给a一个200.此刻输出是200
- 局部作用域 fns()的预解析.
- fns()代码执行结束.函数体内的局部变量被销毁.再次执行console.log(a);此刻输出的是100.当再次声明var a;之后,再进行输出仍然是100.因为此刻全局作用域已经存在名称为a的全局变量了.并且没有进行赋值.JavaScript会直接跳过去(在Chrome中Debug下.此刻代码直接跳过去,并未执行.).接着执行 var a = 300; 此刻赋值给全局作用域中的a一个300.当再次 console.log(a),a等于300
代码五
var a = 12;
var a; // 因为值没有覆盖
console.log(a); // 12
赋值时:引擎遇见a=2时同样会询问在当前的作用域下是否有变量a。若存在,则将a赋值为2(由于第一步编译器忽略了重复声明的var,且作用域中已经有a,所以重复声明会发生值的覆盖而不会报错);若不存在,则顺着作用域链向上查找,若最终找到了变量a则将其赋值2,若没有找到,则招呼作用域声明一个变量a并赋值为2(这就是为什么第二段代码可以正确执行且a变量为全局变量的原因,当然,在严格模式下JS会直接抛出异常:a is not defined)。
重复声明时:首先编译器对代码进行分析拆解,从左至右遇见var a,则编译器会询问作用域是否已经存在叫a的变量了。如果不存在,则招呼作用域声明一个新的变量a;若已经存在,则忽略 var 继续向下编译,这时 a = 2被编译成可执行的代码供引擎使用。
来自 https://blog.csdn.net/DurianPudding/article/details/87953939
代码六
var num1 = 55;
var num2 = 66;
function fn_2(num, num2) {
console.log(num);// 66
num = 100; // 预解析阶段,并不会执行此行代码.但是在代码执行到这一行的时候.会寻找此局部作用域中是否存在num.num在此作用域中存在(函数的参数).所以将100赋值给局部作用域中的num变量.
num1 = 100; // 预解析阶段,并不会执行此行代码.但是在代码执行到这一行的时候.会寻找此局部作用域中是否存在num1.num1在此作用域中不存在.顺着作用域链在全局作用域中找到了num1.所以将100赋值给全局作用域中的num变量.
num2 = 100;// 预解析阶段,并不会执行此行代码.但是在代码执行到这一行的时候.会寻找此局部作用域中是否存在num2.num2在此作用域中存在.所以将100赋值给局部作用域中的num2变量.
numall = 888; // 预解析阶段,并不会执行此行代码.但是在代码执行到这一行的时候.会寻找此局部作用域中是否存在numall.numall在此作用域中不存在.顺着作用域链在全局作用域中寻找是否存在numall.全局作用域中也不存在numall.所以会在全局作用域中创建一个属性.也就是window.numall.
console.log(num);// 100
console.log(num1); //100
console.log(num2); // 100
}
fn_2(num1, num2);
console.log(num1); // 100
console.log(num2); // 66
//console.log(num); // 报错 这行代码会报错.因为全局作用域中不存在num
console.log(numall); // 100
console.log(window)
首先执行全局作用域的预解析. var num1 ; var num2 ; function fn_2; 此刻函数表达式是可以立即执行的,但是变量是等于undefined.
读取到fn_2(num1, num2); 函数fn_2()需要两个局部函数 num和num2. 在调用fn_2的时候(fn_2(num1,num2)). 传递进去(赋值)的参数实际上是全局变量的num1与num2.
- 执行函数fn_2(num,num2).
- 预解析fn_2(num,num2)的局部作用域里的内容.
- 并没有var 和 函数表达式. 预解析完成.
- (我猜是是存在var变量的,会先预解析函数表达式中的两个参数(局部变量).之后最先执行赋值把num1 与 num2 传递给 num, num2)
- 执行代码.
- console.log(num). 此刻输出是值是66
- 之后执行下面四行赋值语句.(如果变量前面有加var的话,并且顺着作用域链找到此变量的话,那么会在全局对象下创建一个属性.(也有人说是全局变量))具体参见 https://www.cnblogs.com/liuna/p/6140901.html
- 预解析fn_2(num,num2)的局部作用域里的内容.
而 num = 1;
事实上是对属性赋值操作。首先,它会尝试在当前作用域链(如在方法中声明,则当前作用域链代表全局作用域和方法局部作用域etc。。。)中解析 num; 如果在任何当前作用域链中找到num,则会执行对num属性赋值; 如果没有找到num,它才会在全局对象(即当前作用域链的最顶层对象,如window对象)中创造num属性并赋值。
[]()
代码七
fn3();
console.log(c); // 9
console.log(b); // 9
console.log(a); // 报错
function fn3() {
var a = b = c = 9;
/*
以上代码相当于
c = 9;
b = c;
var a = b;
*/
console.log(a);//9
console.log(b);//9
console.log(c);//9
}
- 首先执行全局作用域的预解析: function fn3()
- 执行代码.遇到fn3(). 执行函数fn3
- 执行函数fn3()的预解析. var a
- 执行代码:
- c = 9;b=c;var a = b;
- c = 9 : 首先执行c = 9.会查看当前所在的局部作用域中是否存在c:c不存在.沿着作用域链向上查找.查找全局作用域(window)中是否存在c:c也是不存在.所以会在全局作用域中创建一个c的属性.window.c = 9
- b = c : 首先会查找c.c是否存在当前作用域:c不存在.c是否存在全局作用域:c存在.之后取得其值.然后查找b.b是否存在当前局部作用域:不存在.b是否存在全局作用域:不存在.之后会在全局作用域下创建一个window.b的属性.window.b的值等于window.c的值
- var a = b: 首先会查找b的值.b是否存在当前作用域:b不存在.b是否存在全局作用域:b存在.之后取得其值.接着会查找a是否存在于当前作用域:a存在.之后把window.b的值赋给var a.
js赋值语句执行顺序 // js赋值语句执行:自右向左(仅指简单的赋值.如上).
代码八
var n = 0;
function a() {
var n = 10;
function b() {
n++;
console.log(n);
return n;
}
b();
return b;
}
var c = a(); // 11
var now = c(); //12
console.log(n); // n == 0 上图是一个闭包
console.log(now) // 12
这是一个闭包. var c 在引用 function b(){...}.
概念性的东西写起来太长.推荐看书去理解.
代码九
var x = 10;
function fn() {
console.log(x); // 答案是10
console.log(this); //window
}
function show(f) {
var obj = { x: 20 };
f();
}
show(fn);
全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
作用域与作用域链
var x1 = 10;
function fn1() {
console.log(this.x1);
}
function show1(f) {
let obj = { x1: 66 };
var x1 = 20;
f();//10
f.call(obj); // 使用call改变了this指向 =>66
}
show1(fn1);
代码十
var fn9 = function () {
console.log(fn9);
}
var objs = {
fnn: function () {
console.log(fn9);
console.log(this.fn9);
console.log(this); // this指向obj.
}
}
objs.fnn();
代码十一
console.log(test_haha); // 函数声明的优先级是高于var的
function test_haha() { };
var test_haha = 66;
经过测试,在预解析阶段,函数声明的优先级是高于var变量的. console.log(test_haha);输出的是函数体,尽管var在最下面.
代码十二
var objs = {
fn9: function () {
console.log(fn9); // 输出 var fn9 = function () {console.log(fn9);}
console.log(this.fn9); // 输出 objs.fn9
console.log(this); // this指向obj.
},
}
objs.fn9();
谁调用this,this指向谁.
js里只有全局作用域与函数作用域.在ES6中出现了块级作用域.还没有学到.
深入理解JavaScript作用域和作用域链
代码十三
var a;
if (true) {
a = 5;
function a() {
console.log('我没有被楼下的骚a给霸占了');
};
a = 0
console.log(a);
}
console.log(a)
这段代码在chrome与ie下的输出结果是不一致的.
不要在判断语句里定义函数.