详细聊聊闭包在js中充当着什么角色
什么是闭包
开篇明义,概念先行。闭包是什么,为什么说在js中处处充满了闭包。
闭包就是函数有权访问另一个函数作用域中的变量,此函数和被引用的变量一起构成了闭包
文字描述文绉绉的难以理解,看一下代码就能够一目了然了
function test() { var a = 1 var b = function() { console.log(a) } return b }
在上面的代码例子中,变量 a 处于函数 test 的作用域中,但是函数 b 中可以对变量 a 进行访问。
套用闭包的概念,也就是函数 b 有权访问函数 test 作用域中的变量 a,此时函数 b 与变量 a 就形成了一个闭包。
看了上面的例子,大家是否恍然大悟,这不就是我们在代码中经常写的吗。所以说js中处处充满了闭包。
如何观察闭包
如果一开始我们对于闭包的认识还不是很深刻,我们怎么知道在代码中写出了一个闭包呢?一招教你找出闭包
function test() { let a = 1 return function test1() { debugger console.log(a) } } test()()
如上的一段代码,在执行到 debugger 关键字的时候,我们可以打开浏览器的开发者调制工具,此时我们可以从调用栈中看到 Closure 的字样,这就是代表我们写出了一个闭包啦
闭包的错误认识
说完了闭包的概念,再来说说可能大家会对闭包产生的一些错误认识。
1.闭包的产生需要使用 return 暴露出去
首先从闭包的概念上来看就没有说到闭包需要暴露到函数外才叫闭包,而是只要引用了不属于当前函数作用域中的变量就已经产生闭包了。
为什么会有这样的错误认识,是因为我们使用闭包引用了外部作用域中的变量,一般是为了将这个变量或者是这个函数暴露出去,让我们在外部也可以访问到这个变量或者函数,也就是说将闭包暴露到函数外部只是我们的业务需求,而不是闭包的必要条件。
2.闭包会导致内存泄漏
首先我们要知道为什么闭包会导致内存泄漏,是因为我们将闭包暴露到函数外部的时候,闭包内部仍然引用着其外部作用域中的变量,导致外部作用域中的变量无法被垃圾回收机制回收,如果循环引用闭包的话就容易造成内存泄漏的现象。但这是由于我们在使用闭包过程中所引起的,而不是闭包本身的性质所决定的,因此说闭包一定会导致内存泄漏是不严谨的。
(另外在 IE9 之后也对浏览器的垃圾回收机制做了优化,现在已经不容易导致内存泄露了)
闭包导致的问题
作为 js 中八大陷阱之一的循环陷阱,就是由于闭包引起的
for (var i = 0; i < 4; i++) { setTimeout(() => { console.log(i) }, 1000) } // 4, 4, 4, 4
执行以上代码,会发现 1s 之后打印了 4个 4,为什么不是打印 0, 1, 2, 3,就是因为 setTimeout 中的回调函数是一个闭包,引用了外部作用域中的 i 变量,但是 i 只有一个,并不会在每个回调中生成新的 i,因此在 1s 后打印的时候访问的是同一个作用域中的 i 变量,因此打印的结果就是 4个 4
如何解决以上问题,有两个方法:
- 一种是使用 es6 的 let 语法生成块级作用域,这样每个块级作用域中的 i 变量就不是指向同一个 i 变量,不会对彼此产生影响
for (let i = 0; i < 4; i++) { setTimeout(() => { console.log(i) }, 1000) } // 0, 1, 2, 3
- 一种是使用立即执行函数,每个立即执行函数中的变量 i 都是当前外部变量 i 的一个快照
for (let i = 0; i < 4; i++) { (function(i) { setTimeout(() => { console.log(i) }, 1000) })(i) } // 0, 1, 2, 3
闭包的使用场景
说了这么多闭包的性质,甚至闭包还会引发循环陷阱这么重大的问题,那么闭包到底有什么用?面试官问到的时候总不能说 js 处处都是闭包,所以 js 到处都是闭包的使用场景吧。那么我们就来说说闭包的几个经典使用场景
1. 单例模式
var CreateSingleton = (function() { var instance = null var CreateSingleton = function() { if (instance) return instance return instance = this } return CreateSingleton })()
单例模式是设计模式的一种,目的是为了保证全局中只有一个实例对象,上述代码利用 instance 创建一个闭包。单例模式在组件库保证全局中只有一个弹窗组件尤其好用。
2. 函数柯里化
柯里化是将一个多参数的函数转化成几个单参数的函数嵌套的形式,例如: function test(a, b, c) => function test(a)(b)(c)
function currying(fn, args) { var _this = this var len = fn.length var args = args || [] return function() { var _args = Array.prototype.slice.call(arguments) Array.prototype.push.apply(args, _args) if(_args.length < len) { return currying.call(this, fn, _args) } return fn.apply(this, _args) } }
3. 与立即执行函数配合使用完成类库的封装
闭包往往配合着立即执行函数来一起使用,能够发挥出强大的效果。因此,往往很多人容易被误导,认为闭包与立即执行函数之间有什么关系,甚至认为立即执行函数就是闭包。但这种认识其实是错误的,立即执行函数与闭包之间没有任何关系。
在 jQuery 盛行的年代,各类规范百花争艳,其中 umd 规范能够兼容多种环境,主要在于其 umd 头部结构的实现
(function( global, factory ) { "use strict"; if ( typeof module === "object" && typeof module.exports === "object" ) { module.exports = global.document ? factory( global, true ) : function( w ) { if ( !w.document ) { throw new Error( "jQuery requires a window with a document" ); } return factory( w ); }; } else { factory( global ); } })( typeof window !== "undefined" ? window : this, function( window, noGlobal ) {})
以上这种代码的写法使用过 jQuery 开发的人应该非常熟悉吧。
4. 保存私有变量
在实际开发过程中,我们有时候需要对于计算结果进行缓存,或者是保存私有变量而不被外部访问到,就可以使用闭包来实现。
另外,在目前流行的两大前端框架 Vue 和 React 中其实也大量用到了闭包进行相关功能的实现,具体大家可以自己翻翻源码啦~