闭包在 Javascript 中出现的频率很大,或许你写代码不经意间也用到了闭包,只是你不知道写的就是闭包,那么闭包的作用是什么?为什么要有闭包?以及闭包的使用场景?本文将围绕下面四个问题进行讲解。
闭包是什么?
高级程序设计 3 对闭包的总结如下
tip
闭包是指有权访问另外一个函数作用域中的变量的函数。可以理解为(能够读取其他函数内部变量的函数)
那么从代码上来看,闭包的定义是这样的,当一个函数 A,返回一个函数 B,如果函数 B 引用了函数 A 的环境变量,就会产生一个闭包
let url = "https://www.1024nav.com";
function A() {
let sum = 1;
return function B(num) {
return sum + num; // B 引用了 sum 变量
};
}
作用域和词法作用域
作用域
作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。有了作用域的限制,就可以保证变量的可访问性。
作用域有全局作用域和局部作用域,块级作用域。
例如下面的例子,函数 B 可以访问到函数 A 的变量,但是在 A 外部不能访问到 A 的 sum 变量,会报错
let url = "https://www.1024nav.com";
function A() {
let sum = 1;
return function B(num) {
return sum + num; // B 引用了 sum 变量
};
}
console.log(url); // https://www.1024nav.com
console.log(sum); // 报错 VM197:9 Uncaught ReferenceError: sum is not defined
可以得出一个结论,内部函数可以访问外部函数的作用域,相反则不可以
词法作用域
Javascript 采用的是词法作用域,即函数的作用域在函数定义式就决定了,Javascript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。例如下面的例子
var scope = "global scope";
function checkscope() {
var scope = "local scope";
return () => {
return scope;
};
}
checkscope()(); // local scope
上面代码中,checkscope
返回的函数在定义时已经确定了作用域,scope 变量指向的是 checkscope
的作用域,所以无论什么时候执行这个函数,返回的结果都是 local scope
。
与词法所用域相反的是动态作用域,即函数的作用域在函数调用时才决定,例如 bash 命令行
正是由于作用域和词法作用域的这些特性,才诞生出了闭包。不信你结合闭包的定义再思考一下?😄
闭包的作用是什么?
从上面闭包的定义可以得出结论,闭包可以维持对函数外部变量的引用,从而使变量可以常驻内存。
闭包的使用场景?
根据闭包的作用,闭包下面几个使用场景
独立私有作用域
可以用闭包来创建私有变量和私有方法,防止全局变量污染
const bar = (() => {
const name = "www.1024nav.com";
return {
getName() {
return name;
},
setName(newName) {
name = newName;
},
};
})();
bar.getName(); // 'www.1024nav.com'
bar.name; // undefined
缓存
通过闭包能常驻内存的特性,可以用闭包来缓存计算结果,避免重复计算
const memoize = () => {
let cache = {};
return function (key) {
cache[key] = cache[key] || calc(key); // cacl(key) 是一个耗时的计算函数,这里不写具体实现
return cache[key];
};
};
插件封装
jquery 插件封装也是利用闭包的特性
var global = typeof window !== "undefined" ? window : this;
var factory; //line 40 第二个参数
(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);
}
// Pass this if window is not defined yet
})(global, factory);
柯里化 / 惰性函数 / compose 函数
闭包和垃圾回收有什么关系?
既然闭包会常驻内存,那么就无法被垃圾回收机制回收,如果使用不当或者过度使用,就会导致内存泄漏。
垃圾回收机制可以查看这篇文章理解 Chrome V8 垃圾回收机制 - Javascript
常见的闭包问题
- for 打印出来的是什么?
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
没错,结果是 5 个 5。那么如何才能打印出 0 1 2 3 4 呢?
使用闭包
定义时候用一个函数包裹起来,这样就可以在函数内部维持对外部变量的访问了。
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(() => {
console.log(i);
}, 1000);
})(i);
}
利用 let 定义变量
let 变量可以在定义的时候确定块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}