[TOC]

110.JavaScript闭包(closure)

1.含义

表达1:

闭包是由函数引用其周边状态(词法环境)绑在一起形成的(封装)组合结构。
闭包首先就是函数本身, 同时加上在这个函数运行时需要用到的count变量.

表达2:

闭包就是function实例以及执行function实例时来自环境的变量.

表达3:

闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

2.特点

2.1JavaScript中所有的function都是一个闭包

2.2不过一般来说,嵌套的function所产生的闭包更为强大,也是大部分时候我们所谓的“闭包”。

function a() {
var i = 0;
function b() {
alert(++i);
}
return b;
}
var c = a();
c();

这段代码有两个特点:

  1. 函数b嵌套在函数a内部;
  2. 函数a返回函数b。

引用关系如图:
这样在执行完var c=a()后,变量c实际上是指向了函数b,b中用到了变量i,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就创建了一个闭包,为什么?因为函数a外的变量c引用了函数a内的函数b,就是说:
当函数a的内部函数b被函数a外的一个变量引用的时候,就创建了一个我们通常所谓的“闭包”。
所谓“闭包”,就是在构造函数体内定义另外的函数作为目标对象的方法函数,而这个对象的方法函数反过来引用外层函数体中的临时变量。这使得只要目标对象在生存期内始终能保持其方法,就能间接保持原构造函数体当时用到的临时变量值。尽管最开始的构造函数调用已经结束,临时变量的名称也都消失了,但在目标对象的方法内却始终能引用到该变量的值,而且该值只能通这种方法来访问。即使再次调用相同的构造函数,但只会生成新对象和方法,新的临时变量只是对应新 的值,和上次那次调用的是各自独立的。

3.闭包创建

创建时间:闭包在每个函数被创建时形成。
创建方式:JavaScript的闭包是隐式的创建的, 而不像其他支持闭包的语言那样需要显式创建.
在C#语言中很少碰到,是因为C#中无法在方法中再次声明方法. 而在一个方法中调用另一个方法通常使用参数传递数据.

4.闭包作用

实际上,由于闭包与它的词法环境绑在一起,因此闭包让我们能够从一个函数内部访问其外部函数的作用域
要使用闭包,只需要简单地将一个函数定义在另一个函数内部,并将它暴露出来。要暴露一个函数,可以将它返回或者传给其他函数。
内部函数将能够访问到外部函数作用域中的变量,即使外部函数已经执行完毕。

5.应用场景

闭包在 JavaScript 中常用来实现对象数据的私有,在事件处理和回调函数中也常常会用到它,此外还有偏函数应用(partial applications)和柯里化(currying),以及其他函数式编程模式。

5.1 使用闭包定义私有变量

闭包的用途之一是实现对象的私有数据。
数据私有是让我们能够面向接口编程而不是面向实现编程的基础。
而面向接口编程是一个重要的概念,有助于我们创建更加健壮的软件,因为实现细节比接口约定相对来说更加容易被改变。
“面向接口编程,别面向实现编程。” 设计模式:可复用面向对象软件的要素
在 JavaScript 中,闭包是用来实现数据私有的原生机制。当使用闭包来实现数据私有时,被封装的变量只能在闭包容器函数作用域中使用。你无法绕过对象被授权的方法在外部访问这些数据。在 JavaScript 中,任何定义在闭包作用域下的公开方法才可以访问这些数据。例如:
通常,JavaScript开发者使用下划线作为私有变量的前缀。但是实际上,这些变量依然可以被访问和修改,并非真正的私有变量。
这时,使用闭包可以定义真正的私有变量:

对象定义私有变量,但对象不是唯一的产生私有数据的方式。

function Product() {
var name;
this.setName = function (value) {
name = value;
};
this.getName = function () {
return name;
};
}
var p = new Product();
p.setName(“Fundebug”);
console.log(p.name);
// 输出undefined
console.log(p.getName());
// 输出Fundebug
代码中,对象p的的name属性为私有属性,使用p.name不能直接访问。

闭包定义私有变量

const getSecret = (secret) => {
return {
get: () => secret,
};
};
test(“Closure for object privacy.”, (assert) => {
const msg = “.get() should have access to the closure.”;
const expected = 1;
const obj = getSecret(1);
const actual = obj.get();
try {
assert.ok(secret, “This throws an error.”);
} catch (e) {
assert.ok(
true,
The secret var is only available<br /> to privileged methods.
);
}
assert.equal(actual, expected, msg);
assert.end();
});
在上面的例子里,get() 方法定义在 getSecret() 作用域下,这让它可以访问任何 getSecret() 中的变量,于是它就是一个被授权的方法。在这个例子里,它可以访问参数 secret。

2.闭包还可以被用来创建有状态的函数,这些函数的执行过程可能由它们自身的内部状态所决定。

const secret = (msg) => () => msg;
// Secret - creates closures with secret messages.
// https://gist.github.com/ericelliott/f6a87bc41de31562d0f9
// https://jsbin.com/hitusu/edit?html,js,output
// secret(msg: String) => getSecret() => msg: String
const secret = (msg) => () => msg;
test(“secret”, (assert) => {
const msg =
“secret() should return a function that returns the passed secret.”;
const theSecret = “Closures are easy.”;
const mySecret = secret(theSecret);
const actual = mySecret();
const expected = theSecret;
assert.equal(actual, expected, msg);
assert.end();
});
在函数式编程中,闭包经常用于偏函数应用和柯里化。为了说明这个,我们先定义一些概念:

3.函数应用:一个过程,指将参数传给一个函数,并获得它的返回值。

4.偏函数应用:一个过程,它传给某个函数其中一部分参数,然后返回一个新的函数,该函数等待接受后续参数。

换句话说,偏函数应用是一个函数,它接受另一个函数为参数,这个作为参数的函数本身接受多个参数,它返回一个函数,这个函数与它的参数函数相比,接受更少的参数。
偏函数应用提前赋予一部分参数,而返回的函数则等待调用时传入剩余的参数。
偏函数应用通过闭包作用域来提前赋予参数。你可以实现一个通用的函数来赋予指定的函数部分参数,它看起来如下:
partialApply(targetFunction: Function, …fixedArgs: Any[]) =>
functionWithFewerParams(…remainingArgs: Any[])
如果你要更进一步理解上面的形式,你可以看这里
partialApply 接受一个多参数的函数,以及一串我们想要提前赋给这个函数的参数,它返回一个新的函数,这个函数将接受剩余的参数。
下面给一个例子来说明,假设你有一个函数,求两个数的和:
const add = (a, b) => a + b;
现在你想要得到一个函数,它能够对任何传给它的参数都加 10,我们可以将它命名为 add10()。add10(5) 的结果应该是 15。我们的 partialApply() 函数可以做到这个:
const add10 = partialApply(add, 10);
add10(5);
在这个例子里,参数 10 通过闭包作用域被提前赋予 add(),从而让我们获得 add10()。
现在让我们看一下如何实现 partialApply():
// Generic Partial Application Function
// https://jsbin.com/biyupu/edit?html,js,output
// https://gist.github.com/ericelliott/f0a8fd662111ea2f569e
// partialApply(targetFunction: Function, …fixedArgs: Any[]) =>
// functionWithFewerParams(…remainingArgs: Any[])
const partialApply = (fn, …fixedArgs) => {
return function (…remainingArgs) {
return fn.apply(this, fixedArgs.concat(remainingArgs));
};
};
test(“add10”, (assert) => {
const msg = “partialApply() should partially apply functions”;
const add = (a, b) => a + b;
const add10 = partialApply(add, 10);
const actual = add10(5);
const expected = 15;
assert.equal(actual, expected, msg);
});
如你所见,它只是简单地返回一个函数,这个函数通过闭包访问了传给 partialApply() 函数的 fixedArgs 参数。

6.示例

例0:

function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2;
}
var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2
f() // 3 //执行fn2
f() // 4 //再次执行fn2
console.log(a); // 会报错:a is not defined

这段代码里的 this 是什么?

1、fn() 里面的 this 就是 window
2、fn() 是 strict mode,this 就是 undefined
3、a.b.c.fn() 里面的 this 就是 a.b.c
4、new F() 里面的 this 就是新生成的实例
5、() => console.log(this) ,这个this指的是外面的 this。

例:1:

先看下面的例子:
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">









count是start函数体内的变量, 通常我们理解count的作用于是在start()函数内, 在调用start()函数结束后应该也会消失.但是此示例的结果是count变量会一直存在,并且每次被加1:
111.JavaScript闭包(closure) - 图1
因为count变量是setInterval中创建的匿名函数(就是包含count++的函数)的闭包的一部分!

例2:

对于闭包(closure),当外部函数返回之后,内部函数依然可以访问外部函数的变量。
function f1() {
var N = 0; // N是f1函数的局部变量
function f2() {
// f2是f1函数的内部函数,是闭包
N += 1;
// 内部函数f2中使用了外部函数f1中的变量N
console.log(N);
}
return f2;
}
var result = f1();
result();
// 输出1
result();
// 输出2
result();
// 输出3
代码中,外部函数f1只执行了一次,变量N设为0,并将内部函数f2赋值给了变量result。由于外部函数f1已经执行完毕,其内部变量N应该在内存中被清除,然而事实并不是这样:我们每次调用result的时候,发现变量N一直在内存中,并且在累加。为什么呢?这就是闭包的神奇之处了!