闭包的概念以及相关应用.

定义

1
2
3
4
5
6
7
8
9
10
function greet(whattosay){
return function(name){
console.log(whattosay + ' ' + name);
}
}

// greet('Hi')('Joe');

var sayHi = greet('Hi');
sayHi('Joe');

调用sayHi函数时,greet函数已执行完成,跳出执行栈(execution stack),但greet函数内定义的whattosay变量的值依然能被sayHi函数获取, 这样的情况可能发生是因为 JavaScript中存在 闭包.

代码开始执行时创建了一个全局执行环境, 当执行到var sayHi = greet('Hi')时, 调用greet()函数, 新的执行环境被创建, 传给greet函数的whattosay变量处于greet()执行环境中, 一边执行一边创建了所返回的函数, 函数返回之后, greet()函数跳出执行栈,这个执行环境不再存在, 但存储变量的内存空间以及变量依然存在.

当调用sayHi()时,又创建了一个新的执行环境, 但这个执行环境中没有whattosay变量,于是JavaScript引擎通过作用域链在sayHi函数外部寻找该变量.

closure1
closure2
closure3

闭包定义: The phenomenon of closing in all the variables that it is supposed to have access to is called a closure.

closure-def

闭包是JavaScript语言本身的特性.

应用

闭包与for循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function buildFunctions(){
var arr = [];

for(var i = 0; i < 3; i++){
arr.push(
function(){
console.log(i);
}
)
}
return arr;
}
var fs = buildFunctions(); // {1}

fs[0](); // 3
fs[1](); // 3
fs[2](); // 3

代码执行到行{1}时, buildFunctions被调用, 执行环境被创建, 执行之后赋值给fs, fs是个数组, 其中包含3个元素的均为函数function(){ console.log(i) }. 当调用数组中的函数时, 由于闭包, i值从函数定义时的外部获取, 即在buildFunctions中, 因为循环到i=3时结束, 所以i=3被保存于内存空间, 因此三次调用数组内函数的结果均为输出3.

closure-loop1

closure-loop2

如何让函数输出预期的结果:

1. ES6的解决方式

for循环执行时,因为let关键字创建了块级作用域,i在每次循环中都是内存中的一个新变量.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function buildFunctions2(){
var arr = [];

for(let i = 0; i < 3;i++){
arr.push(
function(){
console.log(i);
}
)
}
return arr;
}
var fs2 = buildFunctions2();

fs2[0](); // 0
fs2[1](); // 1
fs2[2](); // 2

2. ES5的解决方式

运用IIFE使函数在创建的时候执行, 这样每次执行都会创建新的执行环境, 于是每个被push到数组中的函数执行时都处于不同的执行环境, 因此数组中的函数每次执行时都会输出预期的不同值, j值从所创建的闭包中获取, 而不是再外一层的循环中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function buildFunctions2(){
var arr = [];
for(var i = 0; i < 3;i++){
arr.push(
(function(j){
return function(){
console.log(j);
}
}(i))
)
}
return arr;
}
var fs2 = buildFunctions2();

fs2[0](); // 0
fs2[1](); // 1
fs2[2](); // 2

利用闭包创造工厂函数

工厂函数 Function Factories

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function makeGreeting(language) {

return function(firstname,lastname){
if (language === 'en'){
console.log('Hello ' + firstname + ' ' + lastname);
}
if (language === 'es'){
console.log('Hola ' + firstname + ' ' + lastname);
}
}

}

var greetEnglish = makeGreeting('en');
var greetSpanish = makeGreeting('es');

greetEnglish('John','Doe');
greetSpanish('Jane','Doe')

传入外层函数, 在内层函数返回, 尽管在内层函数执行时makeGreeting函数已经跳出执行栈, 但language变量依然存在于内存空间. 调用不同的language, 创造的是不同的执行环境, 因此变量存在于不同的内存空间.

closure-factory

闭包与回调函数

1
2
3
4
5
6
7
8
9
function sayHiLater(){
var greeting = 'Hi';

setTimeout(function(){
console.log(greeting);
},3000)
}

sayHiLater();

在JavaScript中, 函数是一等公民并且可以即时创建函数表达式 所以可以将函数function(){ console.log(greeting); }作为参数传入setTimeout函数, 又因为闭包, greeting的值从sayHiLater执行时保存在内存空间的greeting变量中获取. 因此上述代码成立. 这里的函数function(){ console.log(greeting); }是回调函数.