前言
很多同学都有过,面试的时候被问到闭包,一时间不知道从哪里说起的情况。 其实闭包只是 js 的一种现象(或者说特性),没有想象中的那么可怕。
什么是闭包
函数及函数对其词法环境的引用共同组成闭包。 闭包让我们从内部函数访问外部函数的作用域。
那么什么是词法环境呢?
词法环境
词法环境理解为函数在定义时确定的作用域环境。 举个例子:
function a() {
var i = 0;
return function inner() {
i++;
return i;
};
}
// 或者这样
function a() {
var i = 0;
function inner() {
return i;
}
}
内层函数的词法环境就是变量 i 所在的环境。
闭包的 “闭” 理解为:
内层函数所能访问的作用域是在声明的时候确定的,而不是调用的时候,其词法作用域是对外闭合的。
闭包的 “包” 理解为:
内层函数保留对其词法环境的引用,就像是身上背着一个小背包,这个背包里装着对其词法环境中引用的变量,内层函数访问 i 时,在局部作用域中找不到时,就会到背包里看看。
闭包的作用
-
闭包使函数的私有变量不受外部干扰
-
是变量存于内存中不被销毁
举个例子:
function a() {
var i = 0;
return function () {
i++;
return i;
};
}
var y = a();
y(); // 1
y(); // 2
y(); // 3
函数 a 中的变量 i 不能在函数 a 之外被访问。
- 闭包中的变量 i 保存在哪里?
保存在父作用域中,每次访问函数 y 时, 在函数 y 中找不到变量 i, 会顺着作用域链一直向上找,直到全局作用域中也没找到为止。
闭包的应用场景
闭包常被见于实现单例模式、柯里化、防抖、节流、模块化
2-0 单例模式
实例仅创建一次、避免重复创建带来的内存消耗
function singleIns(name) {
this.name = name;
}
singleIns.getInstance = (function () {
var instance = null;
return function (name) {
if (!this.instance) {
this.instance = new singleIns(name);
}
return this.instance;
};
})();
var a = singleIns.getInstance("a");
var b = singleIns.getInstance("b");
a === b; // true
2-1 柯里化
入参可拆解后传入
function curry(func) {
return function curried(...args) {
if (args.length >= func.length) {
func.apply(this, args);
} else {
return function (...args2) {
curried.apply(this, args.concat(args2));
};
}
};
}
// example
function a(x, y, z) {
console.log(x + y + z);
}
var b = curry(a);
b(1, 2, 3); // 6
b(1, 2)(3); // 6
b(1)(2, 3); // 6
b(1)(2)(3); // 6
2-2 防抖
一段时间内一直触发仅执行一次
// 仅执行最后一次
function debounce(func, time) {
let timer = null;
return function (...args) {
timer && clearTimeout(timer);
setTimeout(function () {
func.apply(this, args);
}, time);
};
}
// 仅执行第一次
function debounce(func, time){
let timer = null;
func.id = 0;
return function(...args){
if(func.id !== 0){
clearTimeout(timer;)
}
timer = setTimeout(function () {
func.id = 0;
func.apply(this, args);
}, time);
func.id = func.id + 1;
}
}
2-3 节流
一段时间内一直触发,每隔固定时间内触发一次
// 时间戳方式
function throttle(func, time) {
let start = new Date();
return function (...args) {
if (new Date() - start >= time) {
func.apply(this, args);
start = new Date();
}
};
}
2-4 模块化
模块化封装中将内部逻辑封装在模块内,将方法暴露到模块外。
function module() {
var pool = [];
function add(item) {
pool.push(item);
return pool;
}
function remove(num) {
pool.forEach((item, index) => {
if (item === num) {
pool.splice(index, 1);
}
});
return pool;
}
return {
add: add,
remove: remove,
};
}
var foo = module();
foo.add();
foo.remove();
仔细观察上面的例子是不是很容易理解闭包啦?
闭包的坏处
滥用闭包可能会造成内存泄漏(无用变量存于内存中无法回收,一直占用内存)。解决此问题的方法是,清除变量(设为 null)。
function a() {
var i = 0;
return function () {
i++;
return i;
};
}
var y = a();
y();
y = null; // 清除变量后,引用消失,闭包就不存在了
常见面试题
-
- 考察输出结果
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
输出结果为:连续输出 5 个 5 请修改以上代码,使其输出 0、1、2、3、4
方法一: 使用 setTimeout 的第三个参数
for (let i = 0; i < 5; i++) {
setTimeout(
function () {
console.log(i);
},
0,
i
);
}
方法二:使用 let
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
let 创建了块级作用域。
方法三:使用自执行函数
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(function () {
console.log(i);
}, 0);
})(i);
}
自执行函数将 i 传递进入一个新的执行上下文。
方法四: 借助函数参数
function output(i) {
setTimeout(function () {
console.log(i);
}, 0);
}
for (var i = 0; i < 5; i++) {
output(i);
}
函数传参是按值传递。
方法五:借用 async
const timeout = function (time) {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve();
}, time);
});
};
async function print() {
for (var i = 0; i < 5; i++) {
await timeout(1000);
console.log(i);
}
}
await print();
- 考察输出结果
var a = 100;
function create() {
var a = 200;
return function () {
console.log(a);
};
}
var fn = new create();
fn();
这题很简单,就是闭包概念的体现,输出 200,这里不再解释啦。
- 实现一个与 sum(x,y)功能相同的 sum(x)(y)函数
function sum(a) {
return function (b) {
return a + b;
};
}
sum(1)(2); // 3
考察的是柯里化的概念
tips
是不是所有的闭包都需要手动的清除?
有用的闭包不需要清除,无用的闭包才需要清除。
如果是 dom 事件中的闭包,vue、react 中组件拥有生命周期,卸载时会自动解除 dom 上绑定的事件,内存会自动回收。
开发时不要使用闭包?
闭包无处不在,每当我们模拟私有变量的时候,闭包就已经产生了。不要让 length 太长的对象和数组一直存于内存当中,合理的使用闭包即可。
如何排查内存泄漏?
闭包并不是导致内存泄漏的唯一原因,借助 chrome 开发者工具(devTools) 的 perfomance 工具 和 memory 工具可以详细观察内存情况。这篇不是讲内存泄漏的,不再赘述使用方法,推荐学习devtools 官方文档。
总结
闭包作为前端八股文之一,难倒了很多正在找工作的同学。小姐姐现在也在一个学习的过程中,可能有不准确、不正确的地方,欢迎大家点赞、讨论,一起进步呀。希望对一些同学有帮助。
此文章为 【前端基础】 系列文章,关注小姐姐,一起学一学!
本系列文章: