javascript中的作用域
前言
本篇是基于对 《你不知道的JavaScript(上卷)》中的第一、二、三、四章的总结理解。
编译原理
在代码执行之前进行的操作叫“编译”,一般有三个步骤:
- 分词/词法分析:这个操作是将有意义的代码生成词法单元;
- 解析/语法分析:将词法单元生成为AST(抽象语法树);
- 代码生成:将AST转换为可执行代码。
但对于JavaScript来说,编译过程更加复杂一些,会对运行性能进行优化,以及对冗余的元素进行优化,因为 js 是动态语言,编译操作在代码运行的几微秒前,编译后马上执行。
执行原理(宏观)
JS 执行代码大概涉及到三个东西,分别是,编译器(进行编译)、引擎(执行)、作用域(维护变量作用域)。
-
例1:
假设有 var a = 1 这段代码,编译器、引擎、作用域之间的协同工作是:
首先编译器进行编译,编译器会检查当前作用域下是否存在相同的变量a,如果存在就忽略(注意,let、const声明会报错),不存在就会在当前作用域中添加一个新的变量,命名为a。最后生成引擎运行时需要的代码。
最后引擎执行时会首先检查变量a是否存在在当前作用域中(不存在会向外层作用域查找),如果直到全局作用域都没有变量a,则会抛出一个异常,如果存在,就处理 a = 2 这个赋值操作。
-
例2:
假设有var a = 1; var b = a; :
这里编译过程跟例1相同,需要注意 var b = a,这里引擎会先在作用域中查找a的值是什么(没找到就报错),查找b是否存在在作用域中(没找到报错),最后进行赋值。
在编译器或引擎查找某个变量是否存在作用域中叫“LHS”查找;而查找某个变量的值是什么叫 “RHS” 查找。
在当前作用域进行查找时,如果没有找到某个变量,则会向外层作用域进行查找。
作用域
js中,作用域一般分为:全局作用域和函数作用域。没有块作用域的概念。如下:
if(1) { var a = 1 } console.log(a) // 1
a 仍然输出为1。在其他语言中可能会报错,因为 a 在 if 的块作用域中。而在js中仍然是全局作用域。
var a = 2 function fun(){ var a = 1 function fun2() { console.log(a) // 1 } fun2() } fun()
在函数(fun)作用域内声明了 a ,查找过程中没有在fun2当前作用域内找到,所以到上层作用域(fun)内查找,有 a,所以打印出 1。此时没有往全局作用域内查找。
-
JS使用块作用域
在ES6出现之前,想要使用块作用域,只能使用with 或者 try/catch。
try{ throw 2 } catch(a){ console.log(a) // 2 } console.log(a) // 报错
说明 catch 中是有块作用域的。
也可以使用let、const将变量绑定在当前块作用域中,但需要支持ES6。
-
欺骗作用域
var a = 1 function fun(e) { console.log(a) // 2 } fun(eval("var a = 2"))
上面代码打印出的是 2 ,并不是1,是因为使用了eval,就好像在当前作用域中声明了一个跟全局作用域中 a 相同的变量,按照查找规则,就在当前作用域中找到了 a,所以打印出 2,上面代码相当于:
var a = 1 function fun() { var a = 2 console.log(a) } fun()
当然,这在js非严格模式下有用。
-
IIFE:立即执行函数表达式
普通的定义一个函数,它的函数名本身就“污染”了作用域。正如一些类库,都会使用 IIFE 来避免“污染”。
(function IIFE(){ })()
函数名字不是必须的,可以是匿名函数。
函数定义外层有一对括号,加了括号就变成了表达式。
括号将函数的作用域绑定在了表达式自身,从而不会“污染”外层作用域。
IIFE 还可以传参,在函数执行中传入参数。
(function IIFE(a){ console.log(a) // 1 })(1)
-
作用域中的声明提升
代码在执行前会对声明在 当前作用域内 进行提升:
// 提升前代码 var a = 1 console.log(a) // 提升后代码 var a a = 1 console.log(a) /////////////////////////////////////
// 提升前代码 a () function a () { console.log(1) } // 提升后代码 function a () { console.log(1) } a ()
因为声明经过提升,所以函数a定义在函数执行后才能正确执行。
// 提升前 fun() function fun () { console.log(a) // undefined var a = 1 } // 提升后 function fun () { var a console.log(a) // undefined a = 1 }
fun()
上面代码,虽然变量a经过提升,但是在打印前并没有经过赋值,所以打印出 undefined。
// 提升前 fun() // 报错 var fun = function () {} // 提升后 var fun fun() fun = function () {}
上面代码执行报错,因为var fun = function 是表达式,所以只会提升var fun声明。
另,变量与函数同时提升,那么会先提升函数:
// 提升前 var a = 1 function fun(){} // 提升后 function fun(){} var a a = 1