(上一)5-闭包

闭包定义

闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用 域之外执行

其具体表现为:

  • 函数在定义时的词法作用域以外的地方被调用
  • 函数持有对该作用域的引用,这个引用即为闭包

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

1
2
3
4
5
6
7
8
9
10
11
// 一个非常典型的例子

function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 这就是闭包的效果。

闭包的神奇之处在于阻止了垃圾回收机制。正常一个函数执行后(foo()执行后),foo() 的整个内部作用域都被销毁,但因为 bar() 本身在使用内部作用域,因此内部作用域依然存在,因此没有被回收

应用场景

回调函数

本质上无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一 级的值类型并到处传递,你就会看到闭包在这些函数中的应用。

在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包

IIFE

首先纠正一个常用技术立即执行函数(IIFE)

1
2
3
4
var a = 2;
(function IIFE() {
console.log( a );
})();

IIFE 从严格意义来讲它并不是闭包,因为函数并不是在它本身的词法作用域以外执行的。它在定义时所在的作用域中执行

尽管 IIFE 本身并不是观察闭包的恰当例子,但它的确创建了闭包,并且也是最常用来创建 可以被封闭起来的闭包的工具。因此 IIFE 的确同闭包息息相关,即使本身并不会真的使用 闭包

循环和闭包

一个很常见的例子

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

正常我们想每隔一秒分别打印12345,但实际上每隔一秒打印66666。这与 js 运行机制有关(event loop),setTimeout 为异步任务会在主线任务执行完毕后执行其回调,此时 i 已经变为 6。

解决办法一个是闭包

  • IIFE 会通过声明并立即执行一个函数来创建作用域
    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);
    }

另一种办法是实名 let

  • let 声明,可以用来劫 持块作用域
1
2
3
4
5
for (let i=1; i<=5; i++) { 
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}

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

模块

怎么才算是一个模块。模块模式需要具备两个必要条件:

  1. 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
  2. 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态

注意:一个具有函数属性的对象本身并不是真正的模块。一个从函数调用所返回的,只有数据属性而没有闭包函数的对象并不是真正的模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 这个模式在 JavaScript 中被称为模块。最常见的实现模块模式的方法通常被称为模块暴露, 这里展示的是其变体
function CoolModule() {
var something = "cool";

function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
}
var foo = CoolModule();
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3

// 下面并不算一个真正的模块
var notModule = {
doSomething() {...}
}

function notModule() {
return {
a: '123'
}
}

上面的属于一个独立的模块创建器,每次调用都会创建一个新的模块实例,当只需要一个实例时,可以对这个模式进行简单的改进来实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var foo = (function CoolModule() {
var something = "cool";
var another = [1, 2, 3];
function doSomething() {
console.log( something );
}
function doAnother() {
console.log( another.join( " ! " ) );
}
return {
doSomething: doSomething,
doAnother: doAnother
};
})();

模块也是普通的函数,因此可以接受参数:

1
2
3
4
5
6
7
8
9
10
function CoolModule(id) {
function identify() {
console.log( id );
}
return {
identify: identify
};
}
var foo = CoolModule( "foo" );
foo.identify(); // "foo"

模块模式另一个简单但强大的变化用法是,命名将要作为公共 API 返回的对象。这样可以在内部使用公共 API 对模块实例进行修 改,包括添加或删除方法和属性,以及修改它们的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var foo = (function CoolModule(id) { 
function change() {
// 修改公共 API
publicAPI.identify = identify2;
}
function identify1() {
console.log( id );
}
function identify2() {
console.log( id.toUpperCase() );
}
var publicAPI = {
change: change,
identify: identify1
};
return publicAPI;
})( "foo module" );

foo.identify(); // foo module
foo.change();
foo.identify(); // FOO MODULE

现代的模块机制

大多数模块依赖加载器/管理器本质上都是将这种模块定义封装进一个友好的 API。下面是一些核心概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var MyModules = (function Manager() {
var modules = {};
function define(name, deps, impl) {
for (var i=0; i<deps.length; i++) {
deps[i] = modules[deps[i]];
}
modules[name] = impl.apply( impl, deps );
}
function get(name) {
return modules[name];
}
return {
define: define,
get: get
};
})();

// 如何使用它来注册模块呢?
MyModules.define( "bar", [], function() {
function hello(who) {
return "Let me introduce: " + who;
}
return {
hello: hello
};
} );
MyModules.define( "foo", ["bar"], function(bar) {
var hungry = "hippo";
function awesome() {
console.log( bar.hello( hungry ).toUpperCase());
}
return {
awesome: awesome
};
} );
var bar = MyModules.get( "bar" );
var foo = MyModules.get( "foo" );
console.log( bar.hello( "hippo" ) ); // Let me introduce: hippo
foo.awesome(); // LET ME INTRODUCE: HIPPO

这段代码的核心是 modules[name] = impl.apply(impl, deps)。为模块的定义引入了包装函数(可以传入任何依赖),并且将返回值,也就是模块的 API,储存在一个根据名字来管理的模块列表中

未来的模块机制

ES6 中为模块增加了一级语法支持。

相较于之前的函数模块,ES6 模块更稳定:

  • 基于函数的模块并不是一个能被稳定识别的模式(编译器无法识别),它们 的 API 语义只有在运行时才会被考虑进来。因此可以在运行时修改一个模块 的 API
  • ES6 模块 API 更加稳定(API 不会在运行时改变),由于编辑器知道这一点,因此可以在(的确也这样做了)编译期检查对导入模块的 API 成员的引用是否真实存在。如果 API 引用并不存在,编译器会在运行时抛出一个或多个“早期”错误,而不会像往常一样在运行期采用动态的解决方案。

小结

1.什么才算是一个闭包?

一个理想的闭包应该满足下面两个条件:

  • 函数持有对该作用域的引用,这个引用即为闭包
  • 函数在定义时的词法作用域以外的地方被调用

2.闭包有哪些应用?

闭包是基于词法作用域书写代码时所产生的自然结果,我们的代码中随处可见闭包。你只需要理解并善用它

  • 在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使用了回调函数(函数存在对该作用域的引用),实际上就是在使用闭包
  • 循坏与闭包。在循环中有异步操作时很难捕捉到变量 i 变化,利用闭包为每一个异步操作创建独立的作用域达到目的

    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);
    }
  • 模块。在前端很多模块加载器中,都使用了闭包。模块要求有一个独立的作用域并返回内部方法,符合闭包的成型条件。

3.前端模块化的发展?

0%