一、起源
先来看这样一个例子:
console.log('1、value: ' + a, ' type: ' + typeof a);
var a = 0;
if (true) {
console.log('2、value: ' + a, ' type: ' + typeof a);
a = 1;
console.log('3、value: ' + a, ' type: ' + typeof a);
a = 2;
console.log('4、value: ' + a, ' type: ' + typeof a);
function a() {
}
a = 3;
console.log('5、value: ' + a, ' type: ' + typeof a);
a = 4;
console.log('6、value: ' + a, ' type: ' + typeof a);
}
console.log('7、value: ' + a, ' type: ' + typeof a);
输出结果为:
1、value: undefined type: undefined
2、value: function a() {
} type: function
3、value: 1 type: number
4、value: 2 type: number
5、value: 3 type: number
6、value: 4 type: number
7、value: 2 type: number
为了弄清楚这个打印结果为什么会是这样,有了如下的探索!
二、作用域
什么是作用域?作用域
是指程序源代码中定义变量的区域。 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限
。 JavaScript
采用词法作用域(lexical scoping)
,也就是静态作用域
。
JS 总共有 9 种作用域:
-
- Global 作用域:
全局作用域
,不在任何函数内声明的变量(显式定义)或在函数内省略var声明的变量隐式定义)都称为全局变量
,它在同一个页面文件中的所有脚本内都可以使用,在浏览器环境下就是 window,在 node 环境下是 global
。
- Global 作用域:
-
- Local 作用域:
本地作用域
,或者叫函数作用域。
- Local 作用域:
-
- Block 作用域:
块级作用域
,ES6提供的let关键字声明的变量
称为块级变量
,仅在花括号中间有效,如if、for或while语句等。
- Block 作用域:
-
- Script 作用域:
let、const
声明的全局变量会保存在Script 作用域
,这些变量可以直接访问,但却不能通过 window.xx 访问。
- Script 作用域:
-
- 模块作用域:其实严格来说这也是
函数作用域
,因为 node 执行它的时候会包一层函数,算是比较特殊的函数作用域,有module、exports、require等
变量。
- 模块作用域:其实严格来说这也是
-
- Catch Block:
catch 语句的作用域
可以访问错误对象。
- Catch Block:
-
- With Block 作用域:
with
语句的作用域就是传入的对象的值。
- With Block 作用域:
-
- Closure 作用域:函数返回函数的时候,会把用到的外部变量保存在
Closure 作用域
里,这样再执行的时候该有的变量都有,这就是闭包。eval 的闭包比较特殊,会把所有变量都保存到 Closure 作用域。
- Closure 作用域:函数返回函数的时候,会把用到的外部变量保存在
-
- Eval 作用域:
eval 代码声明的变量
会保存在Eval 作用域
。
- Eval 作用域:
根据上面介绍的作用域分类,我们例子中会用到全局作用域
和块级作用域
,其他作用域感兴趣的小伙伴可以自行查阅资料探索。
三、变量提升
什么是变量提升?变量提升
是当栈内存作用域形成时,JS代码执行前,浏览器会将带有var, function
关键字的变量提前进行声明 declare
(值默认就是 undefined
),定义 defined
(就是赋值操作),这种预先处理的机制
就叫做变量提升机制
也叫预定义
。
在变量提升阶段:带 var 的只声明还没有被定义,带 function 的已经声明和定义。所以在代码执行前有带 var 的就提前声明。
四、函数声明提升
我们来看一下不同书籍对函数声明提升的解释:
-
1、《你不知道的JavaScript》(上册)
-
4.2 函数声明会提升,函数表达式却不会被提升。
-
4.3 函数声明和变量声明都会被提升。但是函数会首先被提升,然后才是变量。
-
4.3 函数声明会被提升到普通变量之前。
-
4.3 一个普通块内部的函数声明通常会被提升到所在作用域的顶部,这个过程不会像代码暗示那样可以被条件判断所控制。
-
-
2、《ES6标准入门》(第3版)
-
2.2.3 ES5规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
-
2.2.3 ES6规定,在块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。
-
-
3、《JavaScript高级程序设计》(第4版)
-
3.3.1 使用var关键字声明的变量会提升到函数作用域顶部。
-
10.7 JavaScript引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。
-
10.16 任何定义在函数或块中的变量,都可以认为是私有的。
-
-
4、《JavaScript权威指南》(第6版)
-
3.10 在函数体内, 局部变量的优先级高于同名的全局变量。
-
8.1 一条函数声明语句实际上声明了一个变量, 并把一个函数对象赋值给它。
-
五、分析样例
1、第一部分
首先根据上文二中的作用域介绍,if包裹的代码形成了一个块级作用域,因此进行独立分析,所以先分析
全局作用域下的代码,也就是前两行:
console.log('1、value: ' + a, ' type: ' + typeof a);
var a = 0;
这段代码在很多书中都有介绍,属于典型的
var
关键字声明变量提升,首先代码会把a
变量提升到console.log
上面,之后打印console.log
,然后对变量a
进行赋值操作,a
的值在还没有赋值之前被打印,所以值和类型都是undefined
,1处
会打印出1、value: undefined type: undefined
。
1、value: undefined type: undefined
2、第二部分
接下来我们重点分析第二部分,也就是由if
包裹的块级作用域的部分。
console.log('1、value: ' + a, ' type: ' + typeof a);
var a = 0;
if (true) {
console.log('2、value: ' + a, ' type: ' + typeof a);
a = 1;
console.log('3、value: ' + a, ' type: ' + typeof a);
a = 2;
console.log('4、value: ' + a, ' type: ' + typeof a);
function a() {
}
a = 3;
console.log('5、value: ' + a, ' type: ' + typeof a);
a = 4;
console.log('6、value: ' + a, ' type: ' + typeof a);
}
console.log('7、value: ' + a, ' type: ' + typeof a);
在分析这部分代码之前,我们先来看一下下面代码的执行结果:
console.log(a); // [Function: a]
var a
console.log(a); // [Function: a]
function a() {
}
a = 1;
console.log(a); // 1
结合上文四中函数声明提升的内容,我们得出几个结论:
- 1、
JavaScript
引擎将函数名视同变量名,所以采用function
命令声明函数时,整个代码块会提升到它所在的作用域的最开始执行。 - 2、在
JavaScript
中,函数是第一公民。 - 3、函数提升优先级比变量提升要高,且不会被变量声明覆盖,但是会被变量赋值覆盖。
有了这个结论,我们再回头来分析例子中的代码:首先在局部作用域中,函数声明被提升到最顶部,此时Block.a = ƒ a();Global.a = 0,
2处
会打印出2、value: function a() {} type: function
。
当执行完a = 1
的赋值操作,块级作用域中的a改变,此时Block.a = 1;Global.a = 0,3处
会打印出3、value: 1 type: number
。
同上一步,执行完a = 2
的赋值操作,块级作用域中的a改变,此时Block.a = 2;Global.a = 0,4处
会打印出4、value: 2 type: number
。
执行完function a() {},它会把此时块级作用域中的a的值提升到全局,此时Block.a = 2;Global.a = 2。
为了更好的理解这一步,可以看一下下面几段代码的执行结果。
第一段:
console.log(a) // undefined
if(true) {
function a() {
} // 把函数提升出去了
}
console.log(a) // [Function: a]
第二段:
console.log(a) // undefined
if(true) {
function a() {
} // 把函数提升出去了
a = 2
}
console.log(a) // [Function: a]
第三段:
console.log(a) // undefined
if(true) {
a = 1
function a() {
} // 把1提升出去了
a = 2
}
console.log(a) // 1
接着执行
a = 3
的赋值操作,依旧只会修改块级作用域中的a,此时Block.a = 3;Global.a = 2,5处
会打印出5、value: 3 type: number
。
这一步赋值操作跟上面类似,执行a = 4
的赋值操作,只会修改块级作用域中的a,此时Block.a = 4;Global.a = 2,6处
会打印出6、value: 4 type: number
。
最后,打印全局作用域中的a,根据上面的执行结果,此时Block.a = 4;Global.a = 2,7处
会打印出7、value: 2 type: number
。
参考文献:
1、JS 的 9 种作用域,你能说出几种?【 Author:zxg_神说要有光 】
2、彻底解决JS变量提升的面试题【 Author:林一一 】
3、JS变量提升和函数提升【 Author:纸鹤视界 】
4、javaScript变量提升以及函数提升【Author:wflynn 】
5、《你不知道的JavaScript》
6、《ES6入门指南》(第3版)
7、《JavaScript高级程序设计》(第4版)
8、《JavaScript权威指南》(第6版)