在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

JavaScript夯实基础系列(二):闭包的更多相关文章

  1. JavaScript夯实基础系列(四):原型

      在JavaScript中有六种数据类型:number.string.boolean.null.undefined以及对象,ES6加入了一种新的数据类型symbol.其中对象称为引用类型,其他数据类 ...

  2. JavaScript夯实基础系列(三):this

      在JavaScript中,函数的每次调用都会拥有一个执行上下文,通过this关键字指向该上下文.函数中的代码在函数定义时不会执行,只有在函数被调用时才执行.函数调用的方式有四种:作为函数调用.作为 ...

  3. JavaScript夯实基础系列(五):类

      JavaScript中没有类,是通过使用构造函数和原型模式的组合来实现类似其它面向对象编程语言中"类"的功能.ES6引入的关键字class,形式上向其它面向对象编程语言靠拢,其 ...

  4. JavaScript夯实基础系列(一):词法作用域

      作用域是一组规则,规定了引擎如何通过标识符名称来查询一个变量.作用域模型有两种:词法作用域和动态作用域.词法作用域是在编写时就已经确定的:通过阅读包含变量定义的数行源码就能知道变量的作用域.Jav ...

  5. 夯实基础系列四:Linux 知识总结

    前言 前三节内容传送门: 夯实基础系列一:Java 基础总结 夯实基础系列二:网络知识总结 夯实基础系列三:数据库知识总结 现在很多公司项目部署都使用的是 Linux 服务器,互联网公司更是如此.对于 ...

  6. 【C++自我精讲】基础系列二 const

    [C++自我精讲]基础系列二 const 0 前言 分三部分:const用法.const和#define比较.const作用. 1 const用法 const常量:const可以用来定义常量,不可改变 ...

  7. 快速掌握JavaScript面试基础知识(二)

    译者按: 总结了大量JavaScript基本知识点,很有用! 原文: The Definitive JavaScript Handbook for your next developer interv ...

  8. JavaScript设计模式基础(二)

    JavaScript 设计模式基础(一) 原型模式 在以类为中心的面向对象编程语言中,类和对象的关系就像铸模和铸件的关系,对象总是从类中创建.而原型编程中,类不是必须的,对象未必从类中创建而来,可以拷 ...

  9. JavaScript笔记基础篇(二)

    基础篇主要是总结一些工作中遇到的技术问题是如何解决的,应为本人属于刚入行阶段技术并非大神如果笔记中有哪些错误,或者自己的一些想法希望大家多多交流互相学习. 1.ToFixed()函数 今天在做Birt ...

随机推荐

  1. MFC学习笔记_关于CSpinButtonCtrl

    CSpinButtonCtrl使用起来比较特殊,使用起来,需要注意一些地方.实际的教程也比较少.为了让后人少走弯路,这里写这篇文章以说明.1.添加EDIT控件2.添加Spin控件如果不是这样的顺序的话 ...

  2. 【最小生成树+贪心】BZOJ1821: [JSOI2010]Group 部落划分 Group

    Description 聪聪研究发现,荒岛野人总是过着群居的生活,但是,并不是整个荒岛上的所有野人都属于同一个部落,野人们总是拉帮结派形成属于自己的部落,不同的部落之间则经常发生争斗.只是,这一切都成 ...

  3. BZOJ_3944_Sum_杜教筛

    BZOJ_3944_Sum_杜教筛 Description Input 一共T+1行 第1行为数据组数T(T<=10) 第2~T+1行每行一个非负整数N,代表一组询问 Output 一共T行,每 ...

  4. Heartbeat实现集群高可用热备

    公司最近需要针对服务器实现热可用热备,这几天也一直在琢磨这个方面的东西,今天做了一些Heartbeat方面的工作,在此记录下来,给需要的人以参考. Heartbeat 项目是 Linux-HA 工程的 ...

  5. Vue学习小结(二)

    接上一批,小结(二). 三.导航内容(含左侧导航及顶部面包屑导航) 其实导航条主要根据element-ui的教程进行编写,官网:http://element-ui.cn/#/zh-CN/compone ...

  6. 基于pytorch的电影推荐系统

    本文介绍一个基于pytorch的电影推荐系统. 代码移植自https://github.com/chengstone/movie_recommender. 原作者用了tf1.0实现了这个基于movie ...

  7. SpringBoot框架与MyBatis集成,连接Mysql数据库

    SpringBoot是一种用来简化新Spring应用初始搭建及开发过程的框架,它使用特定方式来进行配置,使得开发人员不再需要定义样板化的配置.MyBatis是一个支持普通SQL查询.存储和高级映射的持 ...

  8. kubernetes实践之五:深入理解Service及内部DNS搭建

    一.Service存在的意义: 防止Pod失联(服务发现) 定义一组Pod的访问策略(负载均衡) 支持ClusterIP,NodePort以及LoadBalancer三种类型 Service的底层实现 ...

  9. PHP中反射的简单实用(动态代理)

    <?php class mysql{ function connect($db){ echo "连接mysql数据库${db[0]} \r\n"; } } class ora ...

  10. 实例分析Vue.js中 computed和methods不同机制

    在vue.js中,有methods和computed两种方式来动态当作方法来用的 1.首先最明显的不同 就是调用的时候,methods要加上() 2.我们可以使用 methods 来替代 comput ...