1.1 理解作用域
理解作用域
变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果没有声明过),然后在运行时引擎会作作用域中查找该变量,如果能找到就会对它赋值。
当变量出现在赋值操作的左侧时进行LHS查询
,右侧进行RHS查询
。
RHS查询
与简单地查找某个变量的值别无二致。即赋值的源头
LHS查询
则是试图找到变量的容器本身,从而可对其赋值。即赋值目标
作用域嵌套
引擎从当前的执行作用域开始查找变量,如找不到,向上查找。当抵达最外层全局作用域时,无论找到还是没找到,都会停止。
不成功的RHS引用会导致抛出
ReferenceError
异常。不成功的LHS引用会导致自动隐式地创建一个全局变量(非严格模式下),该变量使用LHS引用的目标作为标识符,或者抛出ReferenceError
异常(严格模式下)
1.2 词法作用域
词法阶段
词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变。
- 包含着整个全局作用域,其中只有一个标识符
foo
- 包含着
foo
所创建的作用域,其中有三个标识符:a
bar
b
- 包含着
bar
所创建的作用域,其中只有一个标识符:c
作用域气泡由其对应的作用域块代码写在哪里决定,它们是逐级包含的。
无论函数在
哪里
被调用,也无论它如何
被调用,它的词法作用域都只由
函数被声明是所处的位置决定。
1.3 函数作用域和块作用域
函数中的作用域
函数作用域的含义是指,属于这个函数的全部变量都可在整个函数的范围内使用及复印。
隐藏内部实现
可把变量和函数包裹在一个函数的作用域中,然后用这个作用域来"隐藏"它们。即最小授权或最小暴露原则。
“隐藏”作用域中的变量和函数所带来的另一个好处,是可避免同名标识符之间的冲突。
函数作用域
我们已知道,在任意代码片段外加包装函数,可将内部的变量和函数“隐藏”起来。如下:
var a = 2;
function foo() {
var a = 3;
console.log(a); //3
}
foo();
console.log(a); //2
但是并不理想,如果函数不需要函数名,并且能自动运行,这将会更加理想。
var a = 2;
(function foo(){
var a = 3;
console.log(a); //3
})();
console.log(a); //2
包装函数的声明以 (function...
而不是以function...
开始。函数会被当作函数表达式而不是一个函数声明来处理。
区分函数声明和表达式最简单的方法是看
funciton
关键字出现在声明中的位置。如果function
是声明中的第一个词,就是函数声明,否则就是一个函数表达式。
函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定的何处。
第一个片段中foo被绑定在所在作用域中,可直接通过foo()来调用调。
第二个片段中foo被绑定在函数表达式自身的函数中而不是所在作用域中。
换句话说(function foo(){...})
作为函数表达式意味着foo只能在...处被访问,外部作用域则不行。
匿名和具名
setTimeout( function() {
console.log("I waited 1 second!");
}, 1000)
这是匿名函数表达式
始终给函数表达式命名是一个最佳实践。
IIFE
var a = 2;
(function IIFE(global){
var a = 3;
console.log( a ); // 3
console.log(global.a); // 2
})(window);
console.log(a); // 2
块作用域
try/catch
try/catch的catch分句会创建一个块作用域
let
let 关键字可将变量绑定到所在的任意作用域中(通常是{..}内部)。
1.4 提升
变量和函数声明会被提升,但是函数表达式不会被提升。
函数声明会优先被提升,然后才是变量
应尽可能避免在块内部声明函数。
1.5 作用域闭包
当函数可记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo(){
var a = 2;
function bar() {
console.log(a); //2
}
}
foo();
这是闭包吗?
技术上来讲,也许是。根据上面定义,确切地说并不是。
bar() 对 a 的引用的方法是词法作用域查找规则,而这些规则只是
闭包的一部分
。
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz(); // 2 这就是闭包
bar()可被正常执行,这个例子中,它在自己定义的词法作用域以外的地方执行。
在foo() 执行后,通常会期待foo()的整个内部作用域都被销毁。而闭包的“神奇”之处正是可阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?原来是bar()本身在使用。
bar()依然持有对该作用域的引用,而这个引用就叫做闭包。
无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可观察到闭包。
function foo() {
var a = 2;
function baz() {
console.log(a); //2
}
bar(baz);
}
function bar(fn){
fn(); // 这就是闭包
}
传递函数当然也可是间接的
var fn;
function foo(){
var a = 2;
function baz() {
console.log(a);
}
fn = baz; // 将baz分配给全局变量
}
function bar() {
fn(); // 这就是闭包
}
foo();
bar(); //2
无论通过何种手段将内部函数传递
到所在词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。
现在我懂了
在定时器,事件监听器、Ajax请求、跨窗口通信、Web Workers 或者任何其他的异步(或同步)任务中,只要使用了回调函数
,实际上就是在使用闭包!
循环和闭包
for ( var i = 1; i <=5; i++) {
setTimeout(function timer(){
console.log( i );
}, i * 1000);
}
正常情况下,预期是分别输出数字1~5。每秒一次
但是,会每秒一次输出五次6
这是为什么?
首先6从哪来的呢?这个循环的终止条件是 i 不再 <=5。 条件首次成立时i 是6。因此,输出显示的是循环结束时 i 的最终值。
缺陷是我们试图假设循环中的每个迭代在运行时会给自己“捕获”一个i的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
每个函数需要自己的变量,用来在每个迭代中储存 i 的值。
for(var i=1; i<=5; i++) {
(function(j){
setTimeout( function timer() {
console.log( j );
}, j * 1000);
})( i );
}
重返块作用域
let 声明,可用来劫持块作用域,并且在这个块作用域中声明一个变量。
for(let i = 1; i<=5; i++){
setTimeout( function timer(){
console.log( i );
}, i * 1000)
}