深入理解JavaScript闭包之什么是闭包
前言
在看本篇文章之前,可以先看一下之前的文章 深入理解JavaScript 执行上下文 和 深入理解JavaScript作用域,理解执行上下文和作用域对理解闭包有很大的帮助。
需要回忆的一些知识点:
-
作用域和词法作用域,作用域就是查找变量(去哪儿找,怎么找)的一套规则。词法作用域在你写代码的时候就确定了。
JavaScript
是基于词法作用域的语言,通过变量定义的位置就能知道变量的作用域。 -
作用域链:当某个函数第一次被调用时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性
[[Scope]]
。然后,使用this
、arguments
和其他命名参数的值来初始化函数的活动对象。但在作用域中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,...直至作用作用域链终点的全局执行环境。
一个真实的面试场景
- A: 什么是闭包
- B: 函数 foo 内部声明了一个变量 a, 在函数外部是访问不到的,闭包就是可以使得在函数外部访问函数内部的变量
- A:额,不太准确,那你说一下闭包有什么用途吧
- B: ...,不好意思,一下子想不起来了
- A:今天面试就到这儿了,有后续再通知你。
闭包差不多是面试必问的一个知识点了,记得几年前刚出来找实习的时候问的是这个,现在出去面试还是一直在问这个。很有必要好好学习一下,不仅仅是因为面试,更是因为它在代码中也非常常见。
什么是闭包
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行的。
function foo() {
var a = 1; // a 是一个被 foo 创建的局部变量
function bar() { // bar 是一个内部函数,是一个闭包
console.log(a); // 使用了父函数中声明的变量
}
return bar();
}
foo(); // 1
foo() 函数中声明了一个内部变量 a , 在函数外部是无法访问的,bar() 函数是 foo() 函数内部的函数,此时 foo 内部的所有局部变量,对 bar 都是可见的,反过来就不行,bar 内部的局部变量,对 foo 就是不可见的。这就是javaScript特有的”作用域链“。
function foo() {
var a = 1; // a 是一个被 foo 创建的局部变量
function bar() { // bar 是一个内部函数,是一个闭包
console.log(a); // 使用了父函数中声明的变量
}
return bar;
}
const myFoo = foo();
myFoo();
这段代码和上面的代码运行结果完全一致,其中不同的地方就是在于内部函数 bar 在执行前,从外部函数返回。foo()
执行后,将其返回值(也就是内部的 bar
函数)赋值给变量 myFoo
并调用 myFoo()
, 实际上只是通过不同的标识符引用调用了内部的函数 bar()
。
foo()
函数执行后,正常情况下 foo()
的整个内部作用域被销毁,占用的内存被回收。但是现在的 foo
的内部作用域 bar()
还在使用,所以不会对其进行回收。bar() 依然持有对该作用域的引用,这个引用就叫做闭包。这个函数在定义的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
用一句话描述:闭包是指有权访问另一个函数作用域中变量的函数。创建闭包最常见的方式就是,在一个函数内部创建另一个函数。
常见的一些闭包
function foo(a) {
setTimeout(function timer(){
console.log(a)
}, 1000)
}
foo(2);
foo
执行1000ms
后,它的内部作用域不会消失,timer
函数依然保有 foo
作用域的引用。timer函数就是一个闭包。
定时器,事件监听器,Ajax
请求,跨窗口通信,Web Workers
或者其他异步或同步任务中,只要使用回调函数,实际上就是闭包。
循环和闭包
for(var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
上面的这段代码,预期是每隔一秒,分别输出 0, 1, 2, 3, 4
, 但实际上依次输出的是 5, 5, 5, 5, 5
。首先解释5是从哪里来的,这个循环的终止条件是 i
不再 < 5
,条件首次成立时 i
的值是5
,因此,输出显示的是循环结束时 i 的最终值。
延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的都是 setTimeout(.., 0)
,所有的回调函数依然是在循环结束后才会被执行。因此每次输出一个 5来。
我们的预期是每个迭代在运行时都会给自己 "捕获" 一个 i
的副本。但是实际上,根据作用域的原理,尽管循环中的五个函数都是在各自迭代中分别定义的,但是他们都封闭在一个共享的全局作用域中,因此实际上只有一个 i
。即所有函数共享一个 i
的引用。
for(var i = 0; i < 5; i++) {
(function(j){
setTimeout(() => {
console.log(j);
}, j * 1000);
})(i)
}
代码改成上面这样,就可以按照我们期望的方式进行工作了。这样修改之后,在每次迭代内使用 IIFE(立即执行函数)会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代内部都会含有一个具有正确值的变量可以访问。
for(let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, i * 1000);
}
使用 ES6 块级作用域的 let 替换 var 也可以达到我们的目的。
为什么总是 JavaScript 中闭包的应用都有着关键词 “return”, javaScript 秘密花园 中有一段话解释到:闭包是JavaScript 一个非常重要的特性,这意味着当前作用域总是能够访问外部作用域的变量。因为函数是 JavaScript 中唯一拥有自身作用域的结构,因此闭包的创建依赖于函数。
需要注意的点
容易导致内存泄漏。闭包会携带包含它的函数作用域,因此会比其他函数占用更多的内存。过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。
关于this的情况
在闭包中使用 this
对象。
this对象是运行时基于函数的执行环境绑定的。全局函数中,this指向 window,当函数被作用某个对象的方法调用时,this指向这个对象,不过匿名函数的执行环境具有全局性,因此其this对象通常指向window。之前这篇一文理解this、call、apply、bind文章中也专门讲了this。
var name = 'The window';
var object = {
name: 'my Object',
getName: function() {
return function() {
return this.name;
}
}
}
console.log(object.getName()()); // The window 非严格模式下
- 上面代码创建了一个全局变量 name, 又创建了一个包含 name 属性的对象,这个对象还包含了一个方法
getName()
,它返回一个匿名函数,而匿名函数又返回this.name
。 - 由于
getName
返回一个函数,因此调用object.getName()()
会立即调用它返回的函数。结果就是返回字符串 “The window ”,即全局 name 变量的值。
为什么匿名函数没有取得包含作用域的this对象呢?每个函数在被调用时会自动获取两个特殊的变量:this
, arguments
。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数的这两个变量。
不过把外部作用域中的 this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。
var name = 'The window';
var object = {
name: 'my Object',
getName: function() {
var that = this; // 把this对象赋值给了 that变量
return function() {
return that.name;
}
}
}
console.log(object.getName()()); // my Object
上面代码中把this对象赋值给了 that
变量,that
变量是包含在函数中的,即使函数返回之后,that 也仍然引用 object,所以调用 object.getName()()
返回 “my Object”
arguments 和 this存在相同的问题,如果想访问作用域中的 arguments 对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。
有几种特殊情况下,this的值可能会意外地发生改变。比如下面的代码是修改其前面例子的结果。
var name = 'The window';
var object = {
name: 'my Object',
getName: function() {
return this.name
}
}
console.log(object.getName()); // my Object
console.log((object.getName)()); // my Object
console.log((object.getName = object.getName)()); // The window 非严格模式下
- 第一个就是正常的调用,打印
“my Object”
- 第二个就是在调用这个方法前先给它加上了括号,但是和 object.getName 是一样的,所以打印为
"my Object"
- 第三个是先执行了一个赋值语句,然后再调用赋值后的结果。因为这个赋值表达式是函数本身,所以此时调用,
this
指向的是window
,打印的是"The window"
关于什么是闭包就大概说到这里,下一篇文章会讲一下闭包的应用场景。
总结
- 闭包是指有权访问另一个函数作用域中变量的函数。
- 闭包通常用来创建内部变量,使得 这些变量不能被外部随意修改,同时又可以通过指定的接口来操作。
参考
- 破解前端面试(80% 应聘者不及格系列):从闭包说起[1]
- MDN - 闭包[2]
- 学习Javascript闭包(Closure)[3]
- 闭包详解一[4]
- 搞懂闭包[5]
- 我从来不理解JavaScript闭包,直到有人这样向我解释它[6]
参考资料
[1]破解前端面试(80% 应聘者不及格系列):从闭包说起: https://juejin.im/post/58f1fa6a44d904006cf25d22
[2]MDN - 闭包: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
[3]学习Javascript闭包(Closure): https://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
[4]闭包详解一: https://juejin.im/post/5b081f8d6fb9a07a9b3664b6
[5]搞懂闭包: http://www.alloyteam.com/2019/07/closure/
[6]我从来不理解JavaScript闭包,直到有人这样向我解释它: https://juejin.im/post/5cf468a9f265da1bb77652aa
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- PAT (Basic Level) Practice (中文)1013 数素数 (20 分)
- PAT (Basic Level) Practice (中文)1041 考试座位号 (15 分)
- 《Java 面试问题 一 Spring 、SpringMVC 、Mybatis》
- SAP Spartacus里的product carousel控件的实现cx-product-carousel
- PAT (Basic Level) Practice (中文)1014 福尔摩斯的约会 (20 分)
- 《数据结构与算法_插入排序》
- UGL之标准位图
- Linux(Centos7.X ) 配置Java 环境变量
- CNS图表复现05—免疫细胞亚群再分类
- PAT (Basic Level) Practice (中文)1015 德才论 (25 分)
- 前端下载二进制流文件
- element-ui 表格打印
- PAT (Basic Level) Practice (中文)1016 部分A+B (15 分)
- 【Linux_Shell 脚本编程学习笔记二、打印菜单】
- PAT (Basic Level) Practice (中文)1017 A除以B (20 分)