JavaScript中的作用域与变量提升
1.静态作用域
变量的作用域是程序源代码中的一个区域,在这个区域中变量是有定义的。不同的程序设计语言会采用静态作用域或者动态作用域,JavaScript采用了静态作用域。在使用静态作用域的语言中,由一个声明引入的标识符在这个声明所在的作用域中可见,而且在其作用域嵌套的每个作用域中也可见。当内部被嵌套的作用域声明了与外部作用域同名的标识符,则外部作用域中的标识符会被覆盖。为了找到某个给定标识符所引用的对象,编译器首先会在当前最内层的作用域中查找。如果此时找到了一个对应的声明,也就可以找到该标识符所引用的对象,否则就会在上一层作用域中查找。如果没有找到对应的声明,编译器就会向外逐层查找,直到最顶层的作用域,也就是全局对象声明所在的作用域。如果在所有层次的作用域中都没有找到对应的声明,编译器就会报错。
1 let x = 1; 2 3 function f1() { 4 console.log(x); 5 } 6 7 function f2() { 8 let x = 2; 9 f1(); 10 } 11 12 f2(); //1
在定义函数f1时,f1已经知道了自己应该输出在顶层作用域中声明的变量x。在使用静态作用域的程序设计语言中,当调用一个函数时,如果函数中使用的变量在函数体中没有定义,那么编译器就会去定义该函数的位置查找该变量,因此执行函数f2将输出1。
2.作用域提升与函数作用域
在ES6之前,JS中声明变量的唯一方式是使用var关键字,我们无法通过var声明常量。使用var声明的变量具有函数作用域而不是块级作用域,这种变量的作用域为包含它的函数体。使用var声明变量时,该声明会被“提升”到函数的顶部,但变量的初始化仍然在声明语句的位置完成。
1 var x = 1; 2 function f3() { 3 console.log(x); 4 var x = 2; 5 } 6 7 f3(); //undefined
执行函数f3将输出undefined,因此其函数体内的变量x具有函数作用域,可以在函数内部的任何地方使用而不会报错,其声明被提升到了f3的最顶部,但其初始化仍然在声明语句处进行,因此f3输出undefined。这种特性会成为某些bug的来源,因此我们应该使用let声明变量。
var x = 1; function f3() { console.log(x); let x = 2; } f3(); //ReferenceError: Cannot access 'x' before initialization
在使用let声明f3内部的变量x后,在其声明语句之前访问它将会出现错误。从报错信息可以看出,在函数内部使用let语句声明的变量在某种程度上也进行了作用域提升,但JS不允许我们在其初始化前访问它,这种特性就是暂时性死区。
除了变量声明,函数声明也可以进行作用域提升。
1 function f4() { 2 f5(); 3 4 function f5() { 5 console.log(1); 6 } 7 } 8 9 f4(); //1
执行函数f4将输出1,这是因为函数f5的声明被提升到f4的顶部,因此调用f5的语句可以出现在声明f5的语句之前。但此时需要注意的是,当以函数表达式的形式声明f5时,程序会报错,因为在初始化之前f5等于undefined,它不是一个函数。
1 function f4() { 2 f5(); 3 4 var f5 = function() { 5 console.log(1); 6 } 7 } 8 9 f4(); //TypeError: f5 is not a function
变量与函数的声明都会进行作用域提升,但函数声明的“提升优先级”更高,即函数会先于变量声明。
1 var f6 = 1; 2 function f6() {} 3 4 console.log(typeof f6); //number
上述代码将输出number,这是因为f6函数先于f6变量被声明,变量的声明覆盖了函数的声明。
3.块级作用域
ES6引入了块级作用域。通过let声明的变量和通过const声明的常量具有块作用域,这些变量和常量只在let和const语句所在的代码块中有定义。粗略地说,如果某个变量或者常量声明在一对花括号中,这对花括号就限定了它的作用域。
1 function f7() { 2 let x = 1; 3 4 { 5 let x = 2; 6 var y = 2; 7 } 8 9 console.log(x, y);
10 } 11 12 f7(); //1 2
执行函数f7将输出1和2。显然,x的值为1而不是2,因为f7内部的代码块中的变量x仅具有块级作用域,它只在该代码块中可见,而在该代码块中声明的变量y具有函数作用域,其在整个函数体中都可见。具有块级作用域的变量不允许在其所在代码块中存在同名变量。
1 let x = 1; 2 var x = 2; 3 console.log(x); //SyntaxError: Identifier 'x' has already been declared
使用var在同一个代码块中声明同名变量不会出现上述错误。
1 var x = 1; 2 var x = 2; 3 console.log(x); //2
4.作用域链
当我们访问一个变量时,会先在当前作用域中查找,如果没找到就继续到外层作用域中查找。如果直到全局作用域都没有找到该变量,程序就会报错。
1 let x = 1; 2 3 function f8() { 4 function f9() { 5 console.log(x); 6 } 7 8 f9(); 9 } 10 11 f8(); //1
变量x的作用域链上有三个对象:f9作用域 -> f8作用域 -> 全局作用域。这条作用域链也是查找变量x的顺序。有两种语句会延长作用域链,一种是try-catch语句,另一种是with语句。
1 try { 2 throw ("Hello"); 3 } catch (e) { 4 console.log(e); //Hello 5 }
当catch语句被执行时,其后面的代码块对应的作用域将被添加到作用域链中。with语句也能延长作用域链,它可以将某个对象添加到作用域的最前端,即最先在该对象中查找所需变量。在with执行结束后,作用域链将恢复正常。
1 function f10(obj, x) { 2 with (obj) { 3 console.log(x); 4 } 5 6 console.log(x); 7 } 8 9 f10({x:1}, 2); //1 2
执行函数f10将输出1和2,因为with语句将obj对象添加到了作用域链的最前面,程序将首先在obj中查找x,因此第一条输出语句输出1。而在with语句执行结束后,obj从作用域链中移除,因此第二条输出语句输出2。
5.循环中的块级作用域
在使用var声明的变量作为循环变量时,即使在循环结束之后也能访问它。
1 for (var i = 0; i < 10; i++) { 2 3 } 4 5 console.log(i); //10
由于使用let声明的变量具有块级作用域,使用let声明循环变量可以避免上述情况的发生。
1 for (let i = 0; i < 10; i++) { 2 3 } 4 5 console.log(i); //ReferenceError: i is not defined
长久以来,var声明让开发者在循环中创建函数变得异常困难,因为变量在循环结束之后仍能访问。
1 var funcs = []; 2 3 for (var i = 0; i < 10; i++) { 4 funcs.push( 5 function () { 6 console.log(i); 7 } 8 ) 9 } 10 11 funcs.forEach( 12 function (func) { 13 func(); //输出10个10 14 } 15 )
上述代码将输出10个10,这是因为循环的每次迭代都共享着同一个循环变量i,而循环内部创建的函数都保留了对同一个变量的引用。由于循环结束时i的值为10,因此在执行funcs中的函数时都将输出10。解决这个问题的方法有两种,一种是采用立即调用函数表达式:
1 var funcs = []; 2 3 for (var i = 0; i < 10; i++) { 4 funcs.push( 5 (function (value) { 6 return function () { 7 console.log(value); 8 } 9 }(i)) 10 ); 11 } 12 13 14 funcs.forEach( 15 function (func) { 16 func(); //输出0-9 17 } 18 )
在循环内部,立即调用函数表达式为每次迭代时的变量i都创建了一个副本并存储到了value中,这个value的值就是每次迭代创建的函数所使用的值,因此最后会输出0-9。另一种方法则是使用let声明循环变量。使用let声明循环变量将在每次迭代时创建一个新变量,并之前的迭代中同名变量的值将其初始化。
1 var funcs = []; 2 3 for (let i = 0; i < 10; i++) { 4 funcs.push( 5 function () { 6 console.log(i); 7 } 8 ) 9 } 10 11 funcs.forEach( 12 (funcs) => { 13 funcs(); //输出0-9 14 } 15 )

浙公网安备 33010602011771号