作用域

作用域

负责收集维护所有变量的一系列查询,并实施一套严格的规则,确保代码对这些变量的访问权限

对于 var a = 0 这一操作,可以分解成如下:

  • 如果变量 a 在作用域中不存在,编译器就在作用域中声明一个(若存在就忽略不会再声明一个,注意只是不再在作用域中声明一个,不要和赋值搞混

  • 在运行时引擎在作用域查找该变量,并且为它赋值

也就是说,var a = 0 是拆分成编译和运行时两个部分的

编译是在运行时之前先运行的(虽然时间很短)

LHS RHS 查询

LHS

可以理解为找到变量容器的本身,从而可以对其赋值

RHS

可以理解为 Retrieve his source value (取到他的源值),来获取变量的值

function foo(n) {	// 这里其实有个隐式赋值 n = 2,对 n 做了一次 LHS
    console.log(n)	// 对 n 做了一次 RHS,并且对 console 也做了一次 RHS
}

foo(2)	// 对 foo 做了一次 RHS,来找到函数

查询错误

对于还未声明的变量,进行 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