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