1. 变量提升与函数提升
① 变量声明提升:通过var 定义(声明)的变量,在定义语句之前就可以访问到,值为undefined;
② 函数声明提升:通过function声明的函数,在之前就可以直接调用,值:函数定义(对象)
1.1 代码体验
<script> /* 经典面试题 */ var a = 3; function fn() { console.log(a); var a = 4 }; fn(); // 问:输出结果是什么? // 答:输出 undefined /* 分析:上述代码实际运行顺序如下: */ var a; a = 3; function fn() { var a; console.log(a); // 此时根据就近原则输出a的值,而a只声明未赋值,所以为undefined a = 4; }; fn(); /* 体验变量声明提升和函数声明提升的作用 */ console.log(b); // undefined,未定义前可访问到,变量提升 fn2() // 可调用, 存在函数提升 fn3() // 不可调用,只是存在变量提升,没有函数提升 var b=3; function fn2(){ console.log('fn2()'); }; var fn3=function(){ console.log('fn3()'); }; </script>
注意:只有通过function声明的函数才具有提升功能,通过赋值操作设置的函数表达式并不具有,只能在声明函数后,才可进行调用,如上述代码中的函数fn3,不可在前调用。
那么问题来了:变量提升和函数提升是如何产生的呢?这就涉及到执行上下文的相关知识了...
2. 执行上下文
JS代码根据位置可以分为:全局代码 和 函数(局部)代码。从而执行上下文也就分为全局执行上下文和函数执行上下文。
2.1 全局执行上下文
① 在执行全局代码前,将window确定为全局执行上下文
② 对全局数据进行预处理(预解析):
1)var定义的全局变量==> undefined,添加为window的属性
2)function声明的全局函数==>赋值(fun),添加为window的方法
3)this==>赋值(window)
③ 开始执行全局代码
2.2 函数执行上下文
① 在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中)
② 对局部数据进行预处理(预解析):
1)形参变量==>赋值(实参)==>添加为执行上下文的属性
2)arguments==>赋值(实参列表),添加为执行上下文的属性
3)var定义的局部变量==> undefined,添加为执行上下文的属性
2)function声明的函数==>赋值(fun),添加为执行上下文的方法
3)this==>赋值(调用函数的对象)
③ 开始执行函数体代码
2.3 代码体验
<script> // 1. 全局执行上下文--window console.log(a1); // undefined console.log(window.a1); // // undefined console.log(a1 === window.a1); // true,说明全局执行上下文window对var定义的变量a1进行了预处理,将其添加为它的属性 a2(); // a2()调用成功 console.log(a2() === window.a2()); //true,说明把function声明的函数a2添加为window的方法 console.log(this); // window对象,对this赋值 var a1 = 3; function a2() { console.log('a2()调用成功'); }; console.log(a1); // 3, 开始执行全局代码之后,此时的a1被赋值为3,当然其依旧为window的属性 // 2. 函数执行上下文 function fn(a1) { // 预处理阶段是在调用函数后,执行函数体前进行 console.log(a1); // 2,把实参的值赋给形参 console.log(a2); // undefined,var定义的局部变量a2添加为执行上下文的属性 a3(); // a3()调用成功 console.log(this); // window,因为此函数是直接调用的,所以调用者为window console.log(arguments); // 伪数组[2,3] var a2 = 4; function a3() { console.log('a3()调用成功'); }; }; fn(2, 3); </script>
3. 执行上下文栈
① 在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象;
② 在全局执行上下文(window)确定后,将其添加到栈中(压栈),window永远放在栈的最底层
③ 在函数执行上下文创建后,将其添加到栈中(压栈)
④ 在当前函数执行完后,将栈顶的对象移除(出栈)
③ 当所有的代码执行完后,栈中只剩下window
3.1 代码体验
<script> // 1. 进入全局执行上下文 var a = 10; var bar = function(x) { var b = 5; foo(x + b); // 3. 进入foo函数执行上下文 }; var foo = function(y) { var c = 5; console.log(a + c + y); }; bar(10); // 2. 进入bar函数执行上下文 </script>
执行上下文栈中的执行顺序图:
注意:
① 每调用一次函数,就会对应产生这个函数的执行上下文对象;
② 函数执行完函数体代码后,就会被销毁,即从内存中消失,也就不再存在于上下文执行栈中,即出栈。