共计 7073 个字符,预计需要花费 18 分钟才能阅读完成。
前言
网络中有各种各样说闭包的文章,有些浅尝辄止,有些 CV 党,又有些只讲一部分。于我而言,要想理解闭包,须要把握词法环境(或者是 ES 中的变量对象)、执行上下文与调用栈、(词法)作用域等等
好在咱们在之前的文章中曾经梳理了这几块内容:词法作用域、词法环境、执行上下文。接下来让咱们解开闭包的面纱,看看这位克利奥帕特拉七世
一句话解释
闭包就是一个绑定了执行环境的函数,它利用了词法作用域的个性,在函数嵌套时,内层函数援用外层函数作用域下的变量,并且内层函数在全局环境下可拜访,就造成了闭包
闭包的定义
各路大神对闭包的定义
winter:
闭包其实只是一个绑定了执行环境的函数
闭包与一般函数的区别是,它携带了执行环境,就像人在外星中须要自带吸氧的配备一样,这个函数也带有在程序中生存的环境
实际上 JavaScript 中跟闭包对应的概念就是“函数”
候策
函数嵌套函数时,内层函数援用了外层函数作用域下的变量,并且内层函数在全局环境下可拜访,就造成了闭包
黑客与画家
闭包(lexical closure)一个函数,通过它能够援用由蕴含这个函数的代码所定义的变量
MDN
闭包是指那些可能拜访自在变量的函数
那什么是自在变量呢?
自在变量是指在函数中应用的,但既不是函数参数也不是函数局部变量的变量
由此,咱们能够看出闭包共又两局部组成
闭包 = 函数 + 函数可能拜访的自在变量
古代 JavaScript 教程
闭包是指一个函数能够记住其内部变量并能够拜访这些变量
《你不晓得的 JavaScript》
闭包是基于词法作用域书写代码时所产生的天然后果,你甚至不须要为了利用它们而无意识地创立闭包。闭包的创立和应用在你的代码中随处可见。你短少的是依据你本人的志愿来辨认、拥抱和影响闭包的思维环境
李兵《浏览器工作原理与实际》
在 JavaScript 中,依据词法作用域的规定,外部函数总是能够拜访其内部函数中申明的变量,当通过调用一个内部函数返回一个外部函数后,即便该内部函数曾经执行完结了,然而外部函数援用内部函数的变量仍然保留在内存中,咱们就把这些变量的汇合称为闭包
闭包造成的原理
就像开篇讲的,要想了解闭包,就先要理解三个知识点:词法作用域、词法环境、执行上下文。咱们疾速梳理下:
词法作用域:这里咱们说的都是函数作用域,它由函数被申明时所处的地位决定。函数作用域有个特点,函数内的变量函数外不能拜访,函数外的变量函数内能拜访
词法环境:在代码编译阶段记录变量申明、函数申明。函数申明形参的合集
执行上下文:调用函数时所带的所有信息。包含词法环境、变量环境、this
咱们讲三者一联合,就有了:
- 一段代码在执行时分为两个阶段,编译阶段和执行阶段(JavaScript 是解释型语言,会逐行执行程序,但还是会有两个阶段,两者相差几微秒)
-
编译阶段会产生「变量晋升」,确定作用域,生成词法环境,词法环境包含环境记录器和 outer,环境记录器记录自在变量,outer 指向父作用域
- 词法环境的环境记录器收集 var、function 等变量
- 变量环境的环境记录器收集 let、const、class 等变量
- 执行阶段会创立执行上下文,它包含词法环境、变量环境和 this,以及确定作用域链
也就是说执行上下文除了 this,像词法环境、变量环境在编译阶段就曾经确定了,其中词法环境的变量 var、function 会进行变量、函数晋升,并初始化,而变量环境中的变量尽管晋升了,但不会被初始化;而两者的 outer 则雷同,它们都指向父作用域
当代码要拜访一个变量时,首先会搜寻本身的作用域(即外部词法环境)是否有此变量,再沿着 outer,去父作用域(外部环境),而后搜寻更内部的环境,以此类推,直到全局词法环境,这被关系被称为作用域链,是在函数调用时被确认的
咱们用一例子来解释闭包:
function foo() {
var a = 1;
var b = 2;
return function bar() {console.log(a++);
};
}
var baz = foo();
baz();
- 在任何代码执行之前,先创立全局执行上下文,并往调用栈中压栈
- 接着创立词法环境,注销函数申明 foo 和变量申明 baz(此时处于编译阶段)
- 因为全局词法环境没有内部援用,所以箭头指向了 null
- 代码开始执行,执行
foo()
,创立foo()
的函数执行上下文,并往调用栈中压栈 - 在开始执行 foo 函数内代码前,即函数内的编译阶段,创立 foo 的词法环境,注销函数申明 bar 和变量申明 a、b。它的 outer 指向父作用域——全局作用域
-
代码执行至
function bar
时,创立 bar 的词法环境,它没有变量,outer 指向父作用域 foo- 所有函数在“诞生”时都会记住创立它们的词法环境
- 所有函数都有个
[[Environment]]
的暗藏属性,该属性保留了对创立该函数的词法环境的援用 - 咱们说过作用域与它创立于哪里相干,与在哪儿调用无关
- 调用完函数
foo()
,弹出调用栈,foo 中的函数 bar、变量 b 随着 foo 出栈而被开释 -
因为函数 bar 的后果赋值给了全局变量 baz,baz 相当于多个了暗藏属性
[[Environment]]
,它指向父作用域 foo,而 bar 又援用了 foo 作用域下的变量 a,所以变量 a 无奈被开释- 因而,
baz.[[Environment]]
有对{a: 0}
词法环境的援用 [[Environment]]
援用在函数创立时被设置并永恒保留
- 因而,
- 调用函数
baz()
,创立baz()
执行上下文,并将其压入调用栈中 - 并在执行代码前(编译阶段),创立一个新的词法环境,并且它的 outer 指向
baz.[[Environment]]
,即父作用域 foo - 当它查找变量 a 时,先在本人的词法环境中找,找不到,沿着 outer 往它的父作用域找,在 foo 词法环境中找到了变量 a,并在变量所在的词法环境中更新变量
如此,调用完 baz,因为 baz 始终存在全局词法环境中,它的暗藏属性 [[Environment]]
始终援用着 foo 函数中的 a 变量(即便 foo 函数曾经被销毁了)
当再次调用 baz 时,就会再往调用栈中压入 baz()
,并生成一个新的 bar 的词法环境,它的 outer 还是援用 baz.[[Environment]]
,即上图中的 foo 词法环境
不晓得解释分明闭包了没有?
简略来说:闭包是自带变量的函数。首先先嵌套函数,内层函数援用外层函数的变量。因为词法作用域的性质,在函数定义时就确定了作用域、词法环境,所以即便在调用完外层函数,外层函数中的变量也不会被垃圾回收,因为内层函数曾经赋值给了全局变量,因为变量存在,所以外层函数的变量不会被开释
其内在逻辑是:内层函数赋值给全局变量,内层函数又援用外层函数变量,所以外层函数变量就不会被开释
简言之:闭包 = 函数 + 自在变量
闭包的实质是函数在执行时,会压入执行栈中执行,执行完结后被退栈。 然而作用域成员因为内部的援用而不能被开释 ,因而外部函数仍然能够拜访内部函数的成员
function checkAge(min){return function(age){ // 函数作为返回
return age > min; // 援用内部函数变量
}
}
// 若用 ES6 语法会更简洁
const checkAge = min => age => age > min
const checkAge18 = checkAge(18)
checkAge18(lucy.age)
闭包的作用
试想一下,闭包解决了什么问题?
如果没有闭包,要你实现一个利用,写很多页面,同时引入一些库和框架,本人又写了一些工具函数,当它们在一个(全局)作用域下,就会产生变量抵触问题(尽管能够通过命名标准等解决,但理论开发中难免会遇到命名雷同的问题)
假如你定义了一个变量 a,一个函数 b,间接写在全局环境下,这个模块实现了性能 A。当初咱们的程序须要开发性能 B,你也想用变量 a,函数 b 标识符来示意,那就难堪了,因为曾经在性能 A 上应用过,不能再应用,而如果不必变量 a 和函数 b 标识符来示意语义化上又不失当
你兴许想到了 c、d 两个名字,然而当你调试时,发现原来这两个标识符也曾经被其余的工具函数应用过
命名抵触的起因是因为同作用域下已存在雷同的变量名,要解决以上问题,就要从作用域上着手
即——一个模块应该有本人的作用域,来保障模块的失常运行
全局作用域必定不行,咱们只用函数作用域能够实现这一性能。所以闭包其实是利用了函数作用域实现的一种变量爱护机制
它的作用是对模块中变量的爱护。即在函数作用域中写好代码后,将要应用的变量 return 语句裸露到外界
function outer() {
var a = '公有变量,只能在 outer 中应用';
function inner() {console.log('我是 outer 中的公有函数,只能在 outer 中应用');
}
return function closureOuter() {inner();
console.log(a);
}
}
var bar = outer();
bar();
// 我是 outer 中的公有函数,只能在 outer 中应用
// 公有变量,只能在 outer 中应用
咱们以这个 demo 为例,来说一说整个过程。因为作用域个性,外层函数不能拜访内层函数的变量。所以函数 outer
不能应用 outer
内的变量 a
和函数 inner
。然而如果咱们调用函数 outer
时赋值给 bar
,返回的是函数 outer
内的函数 closureOuter
,此时的 bar
为函数 closureOuter
,函数 closureOuter
因为词法环境的起因,能拜访变量 a
和函数 inner
。当调用 bar
时,执行函数 clousureOuter
,执行 inner
和打印变量 a
咱们也能够这样了解,outer
就是一个模块,它裸露 closureOuter
给外界,外界调用 outer
模块,能应用 outer
的变量,然而不能对外部的变量做批改(爱护变量)
所以说闭包的作用是:闭包能创立函数的公有变量,且这个变量不会随着函数执行完就被垃圾回收机制回收。而这能对某些场景下的变量起爱护作用
闭包的优缺点及误区
-
长处
- 爱护公有变量
- 防止全局变量净化
- 让这些变量始终存在内存中(是长处)
-
毛病
- 始终存在内存中(也是毛病)
网上说闭包会造成内存泄露吗?
这话不对,内存泄露是指你用不到的变量,任然占用内存空间。然而闭包里的变量就是咱们须要的变量,怎么能说是内存泄露呢
闭包的利用
闭包的利用场景有两处,一是作为返回值,二是作为参数传递。有没有很相熟,这不是就咱们在 Function 中讲函数是第一公民的理由吗?
函数的个性让其能作为返回值,以及作为参数传递。所以说所有的函数天生就是闭包(只有一个例外,即 new Function 语法,它的 [[Environment]]
并不指向以后的词法环境,而是指向全局环境)
作为返回值
和如下例题很类似,也是咱们平时见过最多的闭包模式,外层函数返回内层函数
function foo() {
var a = 1;
return function bar() {console.log(a)
}
}
var baz = foo();
baz(); // 1
作为参数传递
function foo() {
var a = 1;
function bar() {console.log(a)
}
baz(bar)
}
function baz(fn) {fn()
}
baz(foo) // 1
PS:或者这个例子更能阐明闭包吧,foo 函数作用参数传递进 baz 函数中,尽管在 baz 函数中执行,然而能拜访到 foo 函数中的变量(即 a,自在变量)
像咱们平时开发中,无心中会用到各种闭包
公有实例变量
function Person(name, age, like) {
return {toString() {return `${name} ${age} ${like}`
}
}
}
const johnny = new Person('johnny', 28, 'sayhi')
console.log(johnny.toString())
toString
造成了闭包
函数式编程
function add(a) {return function (b) {return a + b}
}
add(2)(3) // 5
面向事件编程
定时器、事件监听、Ajax 申请、跨窗口通信、Web Workers 或者任何异步,只有应用了回调函数,实际上就是在应用闭包
// 定时器
function wait(message) {setTimeout( function timer(){console.log( message);
}, 1000 );
}
wait("Hello, closure!");
// message 是 wait 函数的变量,然而被 timer 函数援用,就造成了闭包
// 调用 wait 后,wait 函数压入调用栈,message 被赋值,并调用定时器工作,随后弹出,1000ms 之后,回调函数 timer 压入调用栈,因为援用 message,所以就能打印出 message
// 事件监听
let a = 1;
let btn = document.getElementById('btn');
btn.addEventListener('click',function callback(){console.log(a);
});
// 变量 a 被 callback 函数援用,造成闭包
// 事件监听和定时器一样,都属于把函数作为参数传递造成的闭包。addEventListener 函数有两个参数,一为事件名,二为回调函数
// 调用事件监听函数,将 addEventListener 压入调用栈,词法环境中有 click 和 callback 等变量,并因为 callback 为函数,并有作用域函数造成,援用 a 变量。之后弹出调用栈,当用户点击时,回调函数触发,callback 函数压入调用栈,a 沿着作用域链往上找,找到全局作用域中的变量 a,并打印出
// AJAX
let a = 1;
fetch("/api").then(function callback() {console.log(a)
})
// 同事件监听
只有是回调函数,函数中引入了变量,那就造成了闭包
能够说,在 JavaScript 中,所有函数都是天生闭包(除了 new Function 这个特例)
模块化
用闭包模仿公有办法
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {privateCounter += val;}
return {increment: function() {changeBy(1);
},
decrement: function() {changeBy(-1);
},
value: function() {return privateCounter;}
}
})();
console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */
例子起源:MDN
React hooks
在 React 的函数式组件中,咱们会用 hooks 来管制组件状态,但也有因闭包而带来闭包陷阱
如下例子:
function ProfilePage(props) {const showMessage = () => {alert('Followed' + props.user);
};
const handleClick = () => {setTimeout(showMessage, 3000);
};
return (<button onClick={handleClick}>Follow</button>
);
}
点击按钮后,在 3 秒中切换 user,打印进去的式原先的 user,线上例子可看这里
其起因是函数式组件会捕捉渲染时的值。而点击时,那时候的 user 就被捕捉了,并传入 showMessage 函数中,使得 3 秒后,展现出的 user 时 3 秒前的快照 …
总结
本文介绍了闭包。从它是如何造成的到它的优缺点,再到它的利用,笔者想,如此解释,差不多能对它有个交代了
参考资料
- 说说我对 JavaScript 闭包的了解
- 变量作用域,闭包
- how-do-javascript-closures-work
- 发现 JavaScript 中闭包的弱小威力
- 函数那些事:JS 闭包难点分析
- MDN
系列文章
- 深刻了解 JavaScript——开篇
- 深刻了解 JavaScript——JavaScript 是什么
- 深刻了解 JavaScript——JavaScript 由什么组成
- 深刻了解 JavaScript——所有皆对象
- 深刻了解 JavaScript——Object(对象)
- 深刻了解 JavaScript——new 做了什么
- 深刻了解 JavaScript——Object.create
- 深刻了解 JavaScript——拷贝的机密
- 深刻了解 JavaScript——原型
- 深刻了解 JavaScript——继承
- 深刻了解 JavaScript——JavaScript 中的始皇
- 深刻了解 JavaScript——instanceof——找祖籍
- 深刻了解 JavaScript——Function
- 深刻了解 JavaScript——作用域
- 深刻了解 JavaScript——this 关键字
- 深刻了解 JavaScript——call、apply、bind 三大将
- 深刻了解 JavaScript——立刻执行函数(IIFE)
- 深刻了解 JavaScript——词法环境
- 深刻了解 JavaScript——执行上下文与调用栈
- 深刻了解 JavaScript——作用域 VS 执行上下文