JavaScript夯实基础系列(二):闭包
在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夯实基础系列(二):闭包的更多相关文章
- JavaScript夯实基础系列(四):原型
在JavaScript中有六种数据类型:number.string.boolean.null.undefined以及对象,ES6加入了一种新的数据类型symbol.其中对象称为引用类型,其他数据类 ...
- JavaScript夯实基础系列(三):this
在JavaScript中,函数的每次调用都会拥有一个执行上下文,通过this关键字指向该上下文.函数中的代码在函数定义时不会执行,只有在函数被调用时才执行.函数调用的方式有四种:作为函数调用.作为 ...
- JavaScript夯实基础系列(五):类
JavaScript中没有类,是通过使用构造函数和原型模式的组合来实现类似其它面向对象编程语言中"类"的功能.ES6引入的关键字class,形式上向其它面向对象编程语言靠拢,其 ...
- JavaScript夯实基础系列(一):词法作用域
作用域是一组规则,规定了引擎如何通过标识符名称来查询一个变量.作用域模型有两种:词法作用域和动态作用域.词法作用域是在编写时就已经确定的:通过阅读包含变量定义的数行源码就能知道变量的作用域.Jav ...
- 夯实基础系列四:Linux 知识总结
前言 前三节内容传送门: 夯实基础系列一:Java 基础总结 夯实基础系列二:网络知识总结 夯实基础系列三:数据库知识总结 现在很多公司项目部署都使用的是 Linux 服务器,互联网公司更是如此.对于 ...
- 【C++自我精讲】基础系列二 const
[C++自我精讲]基础系列二 const 0 前言 分三部分:const用法.const和#define比较.const作用. 1 const用法 const常量:const可以用来定义常量,不可改变 ...
- 快速掌握JavaScript面试基础知识(二)
译者按: 总结了大量JavaScript基本知识点,很有用! 原文: The Definitive JavaScript Handbook for your next developer interv ...
- JavaScript设计模式基础(二)
JavaScript 设计模式基础(一) 原型模式 在以类为中心的面向对象编程语言中,类和对象的关系就像铸模和铸件的关系,对象总是从类中创建.而原型编程中,类不是必须的,对象未必从类中创建而来,可以拷 ...
- JavaScript笔记基础篇(二)
基础篇主要是总结一些工作中遇到的技术问题是如何解决的,应为本人属于刚入行阶段技术并非大神如果笔记中有哪些错误,或者自己的一些想法希望大家多多交流互相学习. 1.ToFixed()函数 今天在做Birt ...
随机推荐
- 【递推】Bzoj3612[Heoi2014]平衡
Description 下课了,露露.花花和萱萱在课桌上用正三棱柱教具和尺子摆起了一个“跷跷板”. 这个“跷跷板”的结构是这样的:底部是一个侧面平行于地平面的正三棱柱教具, 上面 摆着一个尺 ...
- BZOJ_1803_Spoj1487 Query on a tree III_主席树+dfs序
BZOJ_1803_Spoj1487 Query on a tree III_主席树 Description You are given a node-labeled rooted tree with ...
- laravel 查询数据返回的结果
laravel查询数据返回的结果 在插入数据库的时候,发现查询数据返回的结果是一个对象;即使是空数据 返回的不是true或者false 那么要判断该结果是否查询有结果 该如果呢? 学习源头: http ...
- ply python 图片压缩 图片裁剪 旋转
http://tech.seety.org/python/python_imaging.html
- Drrols规则引擎
1.什么是规则引擎? 规则引擎是一种嵌套在应用程序中的组件,它实现了将业务规则从应用程序代码中分离出来.规则引擎使用特定的语法编写业务规则,规则引擎可以接受数据输入.解释业务规则.并根据业务规则做出相 ...
- TensorFlow从1到2(八)过拟合和欠拟合的优化
<从锅炉工到AI专家(6)>一文中,我们把神经网络模型降维,简单的在二维空间中介绍了过拟合和欠拟合的现象和解决方法.但是因为条件所限,在该文中我们只介绍了理论,并没有实际观察现象和应对. ...
- flutter初体验
flutter初体验 和flutter斗争了两个周末,基本弄清楚了这个玩意的布局和一些常用组件了. 在flutter里面,所有东西都是组件Widget.我们像拼接积木一样拼接Widget,拼接的关键词 ...
- es6学习笔记-class之继承
继承 上一篇学习了class的概念,在es5时,对象的继承有好几种,原型链继承,借用构造函数继承,组合继承,原型式继承,寄生式继承以及寄生组合式继承,都是按照函数的形式去集成的,现在class也有它的 ...
- angular开发环境搭建及新建项目
最近一个星期准备学习一下angular前端框架,因为之前在学习abp框架的时候,都要求前端要掌握angular,所以不得不回来恶补一下了,学习的过程有时间的话会记录在这里,方便以后复习. 闲言少叙,下 ...
- js 异步转同步
在项目中有些逻辑或者请求依赖另一个异步请求,大家常用的方法是回调函数.现在有个高大上的解决方案:await async . async 是“异步”的简写,而 await 可以认为是 async wai ...