JavaScript学习笔记4--提升与作用域闭包

  |  

前言

这部分记录《你不知道的JavaScript》的第四章-提升,还有第五章,也是JS中最重要的部分之一,作用域闭包


14



提升

为什么在JS中声明会被提前,这与编译器的处理有关。所以在JS中var a = 2;这个语句会被拆分成两部分。声明:var aa = 2。第一个声明在编译阶段进行,第二个赋值声明会被留在原地等待执行。所以下面这两个示例就很好理解

1
2
3
4
5
6
7
8
\\示例1
a = 2;
var a;
console.log(a); //2

\\示例2
console.log(a); //undefine
var a = 2;

需要注意的是只有声明本身会被提升,而复制或其它运算逻辑会留在原地

每个作用域都会进行提升操作,但并不是把所有声明提到整个程序的最上方,而是将声明提到声明所处作用域的最前。

函数声明也会被提升,但是函数表达式不会被提升

1
2
3
4
5
6
7
8
9
10
11
foo();  //是可执行的
function foo(){
console.log(a); //undefined
var a = 2;
}

foo();
//不是ReferencError而是TypeError,因为声明是存在的,但是foo未被复制为undefined,对于undefined值进行函数调用而导致了非法操作
var foo = function bar(){
// ....
};

函数声明和变量声明都会被提升,但是函数会首先被提升,然后才是变量。

作用域闭包

先来看一个清晰展示了闭包的示例:

1
2
3
4
5
6
7
8
9
10
function foo(){
var a = 2;
function bar(){
console.log(a);
}
return bar'
}

var baz = foo();
baz(); //2 ————这就是闭包的效果

函数bar()的词法作用域能够访问foo()的内部作用域。然后我们将bar()函数本身当作一个值类型进行传递。在这个例子中,我们将bar所引用的函数对象本身当作返回值。

在foo()执行后,其返回值(也就是内部的bar()函数)赋值给变量baz并调用baz(),实际上只是通过不同的标识符引用调用了内部的函数bar()。

在foo()执行后,通常会期待foo()的整个内部作用域都被销毁,因为引擎有垃圾回收器来释放不再使用的内存空间,但是由于闭包的存在,回收被阻止了,内部作用域依然存在。因为bar()本身仍在使用这个内部作用域。

由于bar()定义在foo()内部,所以它拥有覆盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。

bar()依然持有对该作用域的引用,而这个引用就叫做闭包。

函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域

再来看个例子:

1
2
3
4
5
6
7
8
9
10
function foo() {
var a = 2;
function baz(){
console.log(a); //2
}
bar(baz);
}
function bar(fn) {
fn(); //这就是闭包,当baz以实参的形式在这里运行时,它在定义的词法作用域外被调用了
}

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用域的引用,无论在何处执行这个函数都会使用闭包。

再来看一个比较难理解的,关于jQuery的例子

1
2
3
4
5
6
7
function setupBot(name, selector) {
$(selector).click( function activator() {
console.log("Activationg: " + name);
});
}
setupBot( "Closure Bot 1", "#bot_1");
setupBot( "Closure Bot 2", "#bot_2");

这里难理解的是activator是作为对应选择器选择的dom节点的click事件响应函数执行的,所以它还是在其词法作用域以外执行。因为它的词法作用在setupBot()内。

本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,你就会看到闭包在这些函数中的应用。在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或任何其它的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包.

通常认为IIFE模式是典型的闭包例子,但是之间还是有所区别的:

IIFE并不是在它本身的词法作用域以外执行的。它在定义时所在的作用域中执行。但是闭包是发生在定义时的,所以IIFE的确创建了闭包,但是并没有真正使用闭包

循环中的闭包

先来看一个例子:

1
2
3
4
5
for( var i = 1; i <= 5; i++){
setTimeout( function timer() {
console.log(i);
}, i*1000);
}

这个代码的执行结果是每隔1秒输出一个6,为什么是每隔一秒呢。因为第一次定义延时函数的时候是一秒延时,第二次是两秒,第三次是三秒…….。所以每次输出之间间隔1秒。但又为什么每次输出都是6呢?因为所有的回调函数都是在循环结束后才会被执行,而根据作用域的工作原理,虽然循环中的五个函数是在各个迭代中分别定义的,并且延时函数也是在各个迭代中定义的,所以延时函数后的i是改变的。但是五个函数都备份比在要给共享的全局作用域中。而回调函数的执行在循环之后,这是在全局共享的全局作用域中i已经变成了6.

如果想要解决这个问题,我们需要为循环过程中每个迭代都创建一个闭包作用域

1
2
3
4
5
6
7
for(var i = 1; i <= 5; i++){
(function(j){
setTimeout(function timer(){
console.log(j);
}, j*1000)
})(i);
}

这样我们就将原先的作用域分成了五个闭包作用域,每个timer函数都有对应一个闭包作用域,在其对应的闭包作用域中,i就是迭代到的值。

但是我们可以使用一种更简单的方式解决这个问题,就是使用ES6中新定义的let关键字。

1
2
3
4
5
for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
}, i*1000);
}

let声明指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后每个迭代都会使用上一个迭代结束时的值初始化这个变量。

文章目录
  1. 1. 提升
  2. 2. 作用域闭包
  3. 3. 循环中的闭包