介绍
本文是 JavaScript 高级深入浅出系列的第五篇,详细介绍了 JS 中的闭包相关知识点
正文
1. JS 中函数是一等公民
与 Java 等面向对象的语言不同,在 JS 中函数是一等公民,函数的使用是非常灵活的,并且函数可以作为参数甚至返回值来使用。高阶函数:把一个函数作为参数或者作为返回值,那么此函数就是一个高阶函数
function foo(fn) {
console.log('before execute fn')
fn()
}
function bar() {
console.log('bar')
}
// 函数作为参数使用
foo(bar)
一个例子:
function calc(num1, num2, calcFn) {
console.log(calcFn(num1, num2))
}
function add(num1, num2) {
return num1 + num2
}
function sub(num1, num2) {
return num1 - num2
}
calc(1, 2, add) // 3
calc(3, 1, sub) // 2
函数作为返回值的例子:
function foo() {
function bar() {
console.log('bar is running')
}
return bar
}
let baz = foo() // 或者直接: foo()()
baz() // bar is running
function adderFactory(count) {
return (num) => {
return count + num
}
}
let add10 = adderFactory(10)
console.log(add10(5)) // 15
console.log(add10(20)) // 30
大家是不是觉得很怪异,adderFactory中的 count 的值在add10阶段就已经执行完毕,照理说已经弹出 ECStack 了,后续使用add10函数时,内部的 AO 是找不到count这个值的,但是为什么使用起来还是能接收到呢?正是因为闭包
2. 一些常用的高阶函数
数组中的高阶函数
// 筛选
let nums = [1, 5, 10, 20, 12, 25]
newNums = nums.filter(item => item % 2 === 0)
console.log(newNums) // [ 10, 20, 12 ]
// 映射
newNums = nums.map(item => item * 10)
console.log(newNums) // [ 10, 50, 100, 200, 120, 250 ]
// 遍历
nums.forEach(item => console.log(item))
// 查找
console.log(nums.find(item => item === 20)) // 20
// 查找索引
console.log(nums.findIndex(item => item === 20)) // 3
// 累加
console.log(nums.reduce((prev, curr) => prev + curr, 0)) // 73,如果运算结果是 NaN 返回第二个参数,这里是 0
3. JS 中闭包的定义
在计算器科学中闭包的定义:
闭包(Closure),又称词法闭包(Lexical Closure)或函数闭包(Function Closure)
是在支持头等函数的编程语言中,实现词法绑定的一种技术
闭包在实现上是一个结构体,他存储了一个函数和一个关联的环境(相当于一个符号查找表)
闭包跟函数的最大区别在于,当捕捉闭包时,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它照样运行
闭包的概念最早出自于 60 年代,有 Scheme 程序所实现。这也就说通了为什么 JS 中有闭包:
- JS 的设计大量借鉴了 Scheme
MDN 对于 JS 闭包的解释:
- 一个函数和对其周围的状态(Lexical Environment,词法环境)的引用捆绑在一起(也可以说函数被引用包围),这样的组合就是闭包(Closure)
- 也就是说,闭包让你可以在一个内层函数中访问其外层函数的作用域
- 在 JS 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
3.1 闭包在内存中的表现
function foo() {
var name = 'foo'
function bar() {
console.log('bar & name is', name)
}
return bar
}
var fn = foo()
fn()
- JS 引擎内部 ECStack 创建全局执行上下文 GEC,GEC 创建 GO,GO 中包含
foo: 内存引用地址0xaa,fn: undefined
- 执行代码, 遇到函数执行,开始全量解析函数,创建 FEC,创建 AO,AO中包括 name 和 bar
- 执行 foo 函数,为 foo 的 AO 中的
name赋值,返回bar(其实就是返回了引用地址),至此,FOO的 FEC 弹出 ECStack,fn 获得 bar 函数的引用地址 0xb00
- 执行 fn 函数,其实就是执行了 bar 函数,打印 name。创建 fn 的 FEC 和 AO,执行代码。检测到访问name,本 AO 中找不到 name,就根据 FEC 的父级作用域链去找上级作用域,这里找到了 foo 的 AO。为什么是 foo ? 大家一定要记得一个原则:在解析函数时(词法解析阶段)就已经获取到了此函数的作用域。
在执行bar函数的时候,本应销毁的父级作用域foo的变量仍能获取到,说明:
foo函数体是一个闭包,闭包由两部分组成:函数+可以访问的自由变量。闭包函数 bar 中的自由变量就是 foo 函数的name。因此后面 bar 就可以访问到 name 的值- 简单来理解就是,JS 中的一个函数,如果有访问上层作用域的变量,那么它就是一个闭包,从狭义的角度来说,即便这个函数可以访问上层作用域的变量但是没有访问过,这个函数就不算是一个闭包
4. 闭包的内存泄漏
闭包虽然很好用,但是存在一个致命的问题:所有的自由变量都不会自动销毁,在第四篇中我们说到,程序需要使用完要主动释放内存,这样才能充分利用内存。而闭包不会自动销毁内存,就会导致内存泄漏。那么为什么自由变量不会自动销毁?
function foo() {
var name = "foo"
function bar() {
console.log(name)
}
return bar
}
var fn = foo()
fn()
- 开始解析代码,执行上下文栈创建全局执行上下文,GO 中填充
foo: 0x100(内存地址),fn: undefined
- 开始执行代码,遇到
foo()开始全量解析foo函数。创建 foo 的函数执行上下文压入执行上下文栈中,创建 foo 的 AO。AO 中填充name: undefined,bar:0x200(内存地址)
- 开始执行函数内的代码,将
name赋值为foo,将 bar 返回(其实就是返回内存地址) foo函数执行完毕,弹出执行上下文栈,此函数执行上下文销毁。注意:此时 foo 所属的 AO 并不会销毁,因为根据标记清除原则,根对象中的 fn 属性关联了 bar 储存空间,bar 储存空间关联了 foo 的 AO
- fn 接收到 foo 函数返回的内存地址,将 GO 中的 fn 的值更新为 bar 的储存空间的内存地址
- 执行 fn 函数,找到 bar 储存空间,其实就是执行 bar 函数,创建属于 bar 的函数上下文,并创建数据 bar 的 AO
- 打印
name,从 bar 的 AO 中找不到 name,就根据 bar 的函数执行上下文的 scope chain 找到parent scope,根据 bar 的parent scope找到foo的 AO,最终从 foo 的 AO 中找到 name ,打印"foo" - fn 函数也就是 bar 函数执行完毕,弹出 ECStack,当前 bar 所属函数执行上下文销毁,bar 的 AO 因为没有引用关系也就销毁掉了
- fn 函数执行完毕,如果只会执行一次 fn,那么就不应该保留 bar 和 foo 的 AO。但是因为 fn 对于 bar 储存空间一直有引用关系,bar 储存空间和 foo 的 AO 就一直不被销毁,最终造成内存泄漏
4.1 解决内存泄漏
将闭包函数在使用后赋值 null,上文例子中
fn = null // 解决闭包内存泄漏
fn 为 null 后,将不再关联 bar 的储存空间,那么就会触发 GC 机制,将 bar 储存空间和 foo 的 AO回收掉
5. 闭包空闲变量的销毁
function foo() {
var name = "foo"
var age = 18
function bar() {
console.log(name)
}
return bar
}
var fn = foo()
fn()
上面示例代码中,foo 函数中声明的变量age在整体的代码中并没有被引用,虽然 ECMA 规范了这里的 age 不能销毁掉,但是 JS 引擎为了优化性能,一般情况下会将没有用到的变量删除掉,所以这里的age就会从内存中删掉
总结
本文中,你学到了 2 个知识点
什么是高级函数
在 JS 中函数是一等公民,函数可以做为函数的参数和返回值。将函数作为参数或返回值的函数叫做高级函数。同时我们也列举了一些数组中的高级函数
关于闭包
我们了解了闭包的官方定义,从第一步开始一步一步拆解闭包。同时我们也了解了为什么闭包会存在内存泄漏以及如何解决。最后我们认识到了 JS 引擎对于闭包中空闲变量的处理