作用域
作用域
负责收集维护所有变量的一系列查询,并实施一套严格的规则,确保代码对这些变量的访问权限
对于 var a = 0
这一操作,可以分解成如下:
如果变量 a 在作用域中不存在,编译器就在作用域中声明一个(若存在就忽略不会再声明一个,注意只是不再在作用域中声明一个,不要和赋值搞混)
在运行时引擎在作用域查找该变量,并且为它赋值
也就是说,var a = 0 是拆分成编译和运行时两个部分的
编译是在运行时之前先运行的(虽然时间很短)
LHS RHS 查询
LHS
可以理解为找到变量容器的本身,从而可以对其赋值
RHS
可以理解为 Retrieve his source value (取到他的源值),来获取变量的值
查询错误
对于还未声明的变量,进行 LHS 和 RHS 查询抛出的异常不同:
RHS 在查询失败时,会抛出 ReferenceError
RHS 查询成功后,但如果对变量的值进行不合理的操作的时候
如访问 null 的属性,会抛出 TypeError
LHS 在查询失败时,在非严格模式下,会创建一个具有该名称的变量
具体来说,如果在顶层都无法找到变量,就会在全局作用域内创建一个变量
在严格模式下,LHS 查询失败时也会抛出 ReferenceError
ReferenceError 表示作用域判别失败;TypeError 表示作用域判别成功,但是对结果的操作是不合理的。
词法作用域
JavaScript 和大部分语言一样,使用的都是词法作用域。尽管 this 可以模拟类似动态作用域的行为(后文展开)。
词法作用域即:定义在词法阶段的作用域。换言之,他是由你在写代码时将变量放置的位置所决定的。(大部分如此,例外在后面展开)
查找
作用域查找会在第一个匹配时停止。
此外,如果访问 foo.bar.baz
只会对第一个标识符 foo
进行查找,查询成功后,bar
, baz
由对象属性访问规则接管
遮蔽
如果多个层级存在同名变量,内部标识符会遮蔽外部标识符。例如:
可以使用 window
来“越级”访问变量 a。避免了 a 被内部作用域遮蔽。
改变词法作用域
可以通过一些技巧来在运行时改变词法作用域
eval
eval 执行了传入的 str,覆盖了全局变量 a:
在严格模式下,eval 会有自己的作用域:
with
在 with 中可以产生一个 LHS,就可以在全局变量中创建一个变量:
函数作用域
函数声明 vs 函数表达式
一种最简单的区分函数声明和函数表达式的方式,就是看 function
关键字是否出现在声明中的第一个词。如果是,那就是函数声明。
他们之间的区别就是:他们的名称标识符会被绑定在哪里:
具名函数 vs 匿名函数
函数声明不允许使用匿名函数
相比具名函数,匿名函数的缺陷是:
在调用栈上不能显示名称,不利于调试
不能通过函数名递归地调用自己
可读性较差
下面的这种写法也是允许的,因此你也可以使用具名函数:
IIFE 立即执行函数
IIFE 也可以接受具名函数
当然,还可以传入参数:
避免 undefined 被覆盖问题
有时候 undefined 关键词可能会被污染,因此可以使用 IIFE 来解决。IIFE 接受一个 undefined 参数,但在对应的位置不要传入他:
块作用域
JavaScript 虽然会创建函数作用域,但有些情况下并不会创建块级作用域 { }
或者一个更常见的例子,在 for
语句中:
创建块级作用域的方式
在上面的例子中,并不会创建块级作用域。但是在另一些情况下,可以创建:
with
catch 块
catch 块
catch
语句可以创建一个块级作用域,因此可以利用这个技巧来创建一个块级作用域:
let & const
在 ES6 中,使用 let
关键词创建的变量会将该变量劫持在所在的块作用域中:
显式的创建块级作用域
上面的例子中,其实是隐式的创建了一个块级作用域。
你可以用 { }
来显示的创建一个块级作用域。那么 let
就会把变量劫持在内部:
for 循环
使用 let
来代替 var
创建循环变量即可解决作用域泄露问题:
let
关键字不仅将变量 i
绑定在了循环块内部,并且在每次迭代时进行重新绑定
每次都是用上一次循环结束时的值进行重新赋值
变量提升
定义发生在编译阶段
赋值发生在运行时
每个作用域都会做变量提升,但不会提升到外部,比如函数作用域的变量只会提升到自己作用域的前部
函数声明会被提升,但是函数表达式并不会。参考下面例子:
上面的代码变量 foo
被提升到了头部,但是声明并没有。因此在第一行对一个 undefined 进行函数调用时,会抛出 TypeError。
而一个函数声明是可以被提升的:
函数优先
变量和函数都会被提升,但函数的优先级更高
上面的代码其实等价于:
Last updated