闭包是JS的一个很重要的特性,是前端必须掌握的。本文要结合之前介绍过的一些JS机制,解释闭包的特性。
![](https://i-blog.csdnimg.cn/blog_migrate/bf19654ba104eac5d61c6a274fe46a34.png)
解释闭包
闭包起码要有两个函数组成,其中一个函数在进入执行上下文中,如果定义了一个内部函数,这个内部函数访问了在执行上下文中函数的变量对象。此时会形成闭包。
看一个例子:
function A(){ // A函数
var n1 = 10;
var n2 = 20;
function B(){ // B函数
return n1 + n2;
}
return B;
}
var n = A();
console.log(n()); // 30
- 上面代码中,有一个定义在全局上下文中的A函数,A函数内部定义了B函数。
- 调用A时返回了B,但是并没有调用B(B被缓存在变量n中)。
- B执行时( n() ),访问了A中变量对象中的值,此时产生闭包(Closure)。
要注意的是: 大部分参考书籍和技术文章中都认为B函数是闭包。但是在chrome的Scope中会看到Closure(闭包)指向A函数。
![](https://i-blog.csdnimg.cn/blog_migrate/36d219b8ec309ff911ea944d353d3c90.png)
要形成闭包,至少要有A、B两个函数。本文以Chrome为标准,认定A函数为闭包。
之前的系列文章内存机制中介绍过JS的垃圾回收机制。当一个变量这内存中失去引用时,JS的内存回收机制算法会找到这个变量并回收,释放它的内存。
执行上下文中介绍了当函数在执行完毕之后,生命周期结束,该函数上下文会失去引用。占用的内存空间会被垃圾回收器释放。闭包的形成阻止了这个过程。
看这个例子:
var B;
function A() {
var num = 2;
B = function() {
console.log(++num);
}
}
A();
B(); // 3
B(); // 4
- 调用A函数之后,把A活动对象中的匿名函数缓存到全局变量B中
- B中保存着对A函数上下文的引用,同时A的变量对象也被保存了下来
- 因为A的变量对象被保存在B中,调用B函数时,会在A函数上下文中变量num先自加再打印
闭包的实际使用场景
实现私有变量
如果你看过jQuery的源码,应该知道全部代码被包裹在一个立即执行函数(IIFE)中。把想要暴露给函数外部的对象挂载在window上实现封装。
(function(window, undefined) {
var jQuery = function() {}
// ...
window.jQuery = window.$ = jQuery;
})(window);
- 立即执行函数(IIFE)传入实参window对象,形参第一个传入的也是window,第二个参数就真的是undefined
- 内部定义了jQuery函数表达式
- 通过把jQuery挂载到window对象的jQuery变量中实现对外部暴露
- window.$是别名,为简写用。
我们可以照样子写一个类似的实现,根据半径求圆面积:
(function(window){
var R = 3;
function getCircleArea(r){
var r = r || R;
var s = r * r * Math.PI;
return s.toFixed(2);
}
window.getCircleArea = getCircleArea;
})(window);
// console.log( R ); // 报错 R is not defined
console.log( getCircleArea() ); // "28.27"
console.log( getCircleArea(4) ); // "50.27"
- 立即执行函数(IIFE)可以看做是个闭包,它只暴露一个方法getCircleArea
- R可看做是立即执行函数的私有变量,在立即执行函数外不可访问
- 调用暴露出的getCircleArea函数,如果不传半径参数,会默认按私有变量R计算出结果
- getCircleArea如果传参,会按传入的数值计算面积
函数柯里化(Currying)
函数柯里化(Currying)在表现上是将接受多个参数的函数变换成接受一个单一参数
有个计算方法
function add(a, b) {
return a + b;
}
console.log(add(1,2)); // 3
将它柯里化后
function add(a) {
return function(b){
return a + b;
}
}
console.log(add(1)(2)); // 3
代码上看函数柯里化是一个闭包的实现,它比较实际的用途有函数防抖、截流之类,函数柯里化具体会在后续文章介绍。
函数式编程(Functional Programming)
函数式编程中的高阶函数有两种体现:
- 函数作为参数传入
- 函数作为返回值输出
第二种函数作为返回值输出的场景用到了闭包
具体例子:
function isType(type) {
return function (obj) {
return obj.constructor === type;
}
}
const isArray = isType(Array);
const isObject = isType(Object);
const isFunction = isType(Function);
console.log(isArray([])); // true
console.log(isObject({})); // true
console.log(isFunction(function(){})); // true
高阶函数 isType(既闭包) 返回的匿名函数判断了参数对象的构造函数和高价函数传入类型的比较。调用高阶函数将具体对应类型缓存起来。这种体现了闭包的实际作用。
注意内存损耗
这是最后要说的,使用闭包时,闭包函数上下文没有出栈,里面的变量对象保存在内存中,一定会造成性能损耗。要结合实际的使用场景,合理使用类似闭包这样的JS特性。