在JavaScript中函数是一等公民。所谓一等公民是指函数跟其他对象一样,很普通,可以进行把函数存在数组中、作为参数传递、赋值给变量等操作。当函数作为另一个函数的返回值在外部调用时,跟该函数在函数内部调用时可访问的词法作用域一样,这种现象被称为闭包。
一、什么是闭包
闭包的定义有很多,比如:闭包是指有权访问另外一个函数作用域中变量的函数。或者更本质的定义:函数对象可以通过作用域链关联起来,函数体内部的变量都可以保存在函数作用域内,也就是说函数变量可以隐藏于作用域链之内,看上去是函数将变量“包裹”了起来。个人比较倾向于一种通俗的定义:闭包就是函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域外执行时。
如下代码所示,执行test函数返回print函数,在全局作用域下执行print函数,print函数却能记住自己的作用域,能够引用其在定义时的外层函数test的局部变量。
var a = 1;
function test (){
var a = 2;
function print (){
console.log(a)
}
return print
}
test()() // 2
由于JavaScript中没有块级作用域的概念,因此常常用立即执行函数(IIFE)来模拟块级作用域。
var a = 1;
(function IIFE(){
var a = 2;
console.log( a ); // 2
})();
console.log( a ); // 1
从学术意义上来讲,JavaScript中的每个函数都是闭包:它们都是对象,它们都关联到作用域链。但是从闭包可以在词法作用域外调用也能访问词法作用域的角度来说,IIFE并不是闭包。如下代码所示:在函数test执行时,查找变量a是沿着作用域链逐级查询的,并不能体现闭包的特性。
(function IIFE(){
var a = 1;
function test () {
console.log(a)
}
test() // 1
})();
二、闭包的原理
当某个函数执行时,先复制其外层函数的作用域链(如果函数是在全局环境中则作用域链中只有一个全局对象的引用),赋值给一个特殊的内部属性(即[[Scope]])。然后使用this、arguments和其它命名参数的值来初始化函数的变量对象,最后将该变量对象的引用加入到该函数的作用域链中。
当函数执行完之后,函数的作用域链上的会被删除,相应的变量对象没有了作用域链的引用就会被当做垃圾回收掉。但是闭包的情况却不一样,函数虽然执行完毕,但是函数返回了一个内部函数出去,该内部函数的作用域链上拥有对该函数变量对象的引用,因此函数虽然执行完毕,但该函数的变量对象并没有被销毁,依然可以通过返回的内部函数来访问该函数变量对象上的变量。
需要特别注意的是:闭包只能取得包含函数中任何变量的最后一个值。在for循环中定义函数表达式尤其能体现出这一点。如下代码所示,我们希望test函数返回的函数数组中存放的是可以打印其下标的函数,但是结果却是全部数字10。原因在于我们错误的认为每次循环时都会对i进行一次复制,事实上嵌套的函数不会将作用域内的私有成员复制一份,也不会对所绑定的变量生成静态快照。test函数返回的函数数组中引用的都是同一个变量i,变量i被共享,循环结束时i的值为10,所以执行函数数组中的任意函数结果都是打印出数字10。
function test () {
var arr = []
for(var i=0;i<10;i++){
arr[i] = function () {
console.log(i)
}
}
return arr
}
var print = test()
print[2]() // 10
我们对代码加以改进,来避免数据共享的情况发生。在下面代码中,并不是直接将闭包赋值给数组,而是定义了函数temp,将执行temp函数后的返回值赋给数组。因为函数参数是按值传递的,所以每次调用temp时会复制一份实参i的副本,函数数组中保存的函数都有各自的变量i不同时间段的副本,打破了原本共享数据i的情况,因此能够返回各自不同的值。
function test () {
var arr = []
for(var i=0;i<10;i++){
function temp (j) {
return function () {
console.log(j)
}
}
arr[i] = temp(i)
}
return arr
}
var print = test()
print[2]() // 2
三、闭包的用途
闭包在JavaScript代码中无所不在,主要应用于模块模式以及函数式编程中的柯里化。
1、模块模式
在ES6之前,JavaScript中并没有定义用以支持模块的语言结构,但是可以利用闭包很轻松的实现代码模块化。在函数中定义的变量是函数私有的,在函数之外不能直接访问以及修改函数内部的变量,但是通过函数返回的内部函数能够访问这些变量,返回的内部函数如同暴露在外界的共有接口一样,这种模式被称为模块模式。如下代码所示:
function module () {
var value = 0
function get () {
return value
}
function set (val) {
value = val
}
return {
get: get,
set: set
}
}
var test = module()
var modu = module()
console.log(test.get()) // 0
test.set(1)
console.log(modu.get()) // 0
console.log(test.get()) // 1
test.set(10)
console.log(modu.get()) // 0
console.log(test.get()) // 10
在模块模式中,模块返回值可以是一个对象,也可以仅仅是一个内部函数。模块只是一个函数,所以它可以接收参数。从上面的代码可以看出函数每次执行返回的闭包是独立的,相互不影响。一般模块在使用的时候采用单例模式,可以用IIFE来实现,如下代码所示:
var module =(
function module () {
var value = 0
function get () {
return value
}
function set (val) {
value = val
}
return {
get: get,
set: set
}
}
)()
console.log(module.get()) // 0
module.set(1)
console.log(module.get()) // 1
module.set(10)
console.log(module.get()) // 10
综上所述,模块要求两个关键性质:1、作为模块的函数被调用执行。2、该函数的返回值至少用于一个内部函数的引用。
2、柯里化
柯里化是指把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。柯里化的好处在于提高了适用性,能够实现参数复用的效果。如下代码所示:
function test (a,b,c) {
return a+b+c
}
console.log(test(1,2,3)) // 6
function _test (a) {
return function (b) {
return function (c) {
return a+b+c
}
}
}
console.log(_test(1)(2)(3)) // 6
_test函数利用函数闭包来实现柯里化的效果,每次调用的函数闭包能够访问上次传入的参数并访问。例如lodash等库封装了通用柯里化的函数,传入一个普通函数,返回一个同等功能的柯里化函数,这一部分会在本系列的后续文章详述。
四、总结
闭包就是函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域外执行时。函数执行完毕后,相应的变量对象没有作用域链的引用就会被当做垃圾被回收,但是如果有闭包情况会变得不一样,闭包的作用域链依然对外部函数的变量对象保持引用,因此外部函数的变量对象不会被销毁,闭包依然能够访问外部函数的变量。
在JavaScript中没有模块的语法(ES6之前),闭包可以用来实现模块模式。在函数式编程中,柯里化十分常见,可以利用闭包来实现柯里化。
如需转载,烦请注明出处:https://www.cnblogs.com/lidengfeng/p/9158827.html