本文正在参加「金石计划」
闭包及其作用
什么是闭包?
闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。 — MDN
简单的说,闭包就是指有权访问另一个函数作用域中的变量的函数。
当一个函数返回了一个函数后,其内部的局部变量还被新函数引用,这个新函数就被称为闭包。闭包可以让你从内部函数访问外部函数作用域。
例如:
function outerFunction() {
let count = 0;
function innerFunction() {
count++;
return count;
}
return innerFunction;
}
const newFunction = outerFunction();
console.log(newFunction()); // 输出1
console.log(newFunction()); // 输出2
在这个例子中,函数outerFunction
返回函数 innerFunction
。innerFunction
可以访问 outerFunction
的局部变量 count
。每次调用 newFunction()
,它都会增加并返回计数器的值。
闭包的作用
它的最大用处有两个
- 一个是可以读取函数内部的变量
- 另一个就是让这些变量的值始终保持在内存中
闭包的实现原理
闭包的实现原理是在函数内部创建一个新的作用域,这个作用域可以访问到外部函数的变量。当外部函数返回内部函数时,内部函数会继续保持对外部函数作用域的引用,这就形成了一个闭包。
在JavaScript中,每个函数都有一个作用域链。当函数被调用时,它会创建一个新的执行环境,并将其添加到作用域链的顶部。当函数执行完毕后,它会将自己的执行环境从作用域链中移除。
但是如果这个函数返回了一个内部函数,并且这个内部函数引用了外部函数的变量,那么这个内部函数就会形成一个闭包,它会继续保持对外部函数作用域的引用,即使外部函数已经执行完毕并从作用域链中移除了自己的执行环境。
闭包存在的问题
容易导致内存泄漏
闭包是一种特殊的函数,它可以访问其定义时所在的词法作用域中的变量。如果闭包中的函数引用了外部函数的变量,那么这些变量就不会被垃圾回收机制回收,从而可能导致内存泄漏。
如何避免闭包导致的内存泄漏?
1)、将不再使用的引用设置为null。
2)、尽早释放掉无用的引用,在引用退出作用域后设置为null。
3)、不要在经常调用的地方创建对象/变量,尤其是在循环中。
闭包的实际应用场景
模块化开发
使用闭包可以实现模块化开发,将代码分割成多个模块,每个模块都有自己的作用域,可以避免全局命名空间污染和代码冲突,同时提高代码的可维护性和复用性。
假如现在有一个商品管理模块,需要实现以下功能
- 添加商品
- 删除商品
- 修改商品
- 获取所有商品
- 根据条件筛选商品
为了实现这些功能,我们可以将代码分割成多个模块,例如
首先,可以通过使用闭包,将商品管理模块的私有变量和函数封装起来,只暴露公共接口给外部使用。
var ProductManager = (function () {
// 私有变量和函数
var products = [];
function getIndexById(id) {
for (var i = 0; i < products.length; i++) {
if (products[i].id === id) {
return i;
}
}
return -1;
}
// 公共接口
return {
//添加
addProduct: function (product) {
products.push(product);
},
//删除
removeProduct: function (id) {
var index = getIndexById(id);
if (index !== -1) {
products.splice(index, 1);
}
},
//修改
updateProduct: function (product) {
var index = getIndexById(product.id);
if (index !== -1) {
products[index] = product;
}
},
//获取
getProducts: function () {
return products;
},
//筛选
filterProducts: function (condition) {
// 根据条件筛选商品
}
};
})();
其次定义一个用户界面模块,用来专门负责渲染商品列表和处理用户交互事件
var UI = (function () {
// 私有变量和函数
var $productList = document.getElementById('product-list');
// 渲染商品列表
function renderProductList(products) {}
// 处理添加商品事件
function handleAddProduct() {}
// 处理删除商品事件
function handleRemoveProduct() {}
// 处理修改商品事件
function handleUpdateProduct() {}
// 处理筛选商品事件
function handleFilterProducts() {}
// 公共接口
return {
init: function () {
// 初始化用户界面
$productList.addEventListener('click', function (event) {
if (event.target.classList.contains('add-button')) {
handleAddProduct();
} else if (event.target.classList.contains('remove-button')) {
handleRemoveProduct();
} else if (event.target.classList.contains('update-button')) {
handleUpdateProduct();
}
});
$filterButton.addEventListener('click', handleFilterProducts);
},
updateProductList: function (products) {
renderProductList(products);
}
};
})();
最后调用上面封装好的模块
//添加一个商品iPhone
ProductManager.addProduct({
id: 1,
name: 'iPhone',
price: 999
});
//添加一个商品iPad
ProductManager.addProduct({
id: 2,
name: 'iPad',
price: 799
});
UI.init();//界面初始化
UI.updateProductList(ProductManager.getProducts());//商品渲染
私有变量
使用闭包可以创建私有变量,这些变量只能在函数内部访问,外部无法访问。这样可以避免全局变量的污染。
比较常见的,就是计数器函数。我们最开始举的例子其实就是创建了一个计数器函数。
function createCounter() {
let counter = 0;
function incrementCounter() {
counter++;
console.log(counter);
}
return incrementCounter;
}
const counter1 = createCounter();
counter1(); // 输出 1
counter1(); // 输出 2
const counter2 = createCounter();
counter2(); // 输出 1
这个示例中,createCounter
函数返回了一个闭包函数incrementCounter
。
当我们调用createCounter
函数时,它会创建一个新的作用域,并在其中声明本地变量counter
然后,它返回了一个函数 incrementCounter
,该函数可以访问并修改变量counter
。
因为 counter
变量在createCounter
函数作用域内,所以它对于外部作用域是不可见的,从而保证了变量的私有性。
异步编程
在异步编程中,闭包经常用于保留函数执行时的环境变量,以便在回调函数中访问它们。
例如,在JavaScript中使用setTimeout或setInterval函数时,可以使用闭包来保存定时器ID和其他相关数据,以便在定时器回调函数中进行操作。
function startTimer() {
var count = 0;
var timerId = setInterval(function() {
count++;
console.log(count);
if (count === 10) {
clearInterval(timerId);
}
}, 1000);
}
startTimer();
在这个示例中,我们定义了一个名为startTimer的函数,该函数创建了两个本地变量:count和timerId。
然后,它调用setInterval函数来设置一个1秒钟的定时器,每次定时器触发时都会增加计数器并将其输出到控制台。当计数器达到10时,我们清除了定时器。
由于我们使用了闭包,所以在定时器回调函数中仍然可以访问count和timerId变量,即使在外部函数执行结束后仍然保持其状态。
问题解决
还有一些问题,我们也可以利用闭包来解决。
比如这个例子中,我们期望的结果是点击按钮时,弹出对应的数字。
但是实际的,输出的结果却是都3。这是因为当事件处理程序被调用时,i 已经变成了3。
<button id="btn-0">Button 1</button>
<button id="btn-1">Button 2</button>
<button id="btn-2">Button 3</button>
<script>
for (var i = 0; i < 3; i++) {
document.getElementById('btn-' + i).addEventListener('click', function() {
alert(i);
});
}
</script>
如果我们使用闭包,就可以避免这个问题。
<button id="btn-0">Button 1</button>
<button id="btn-1">Button 2</button>
<button id="btn-2">Button 3</button>
<script>
for (var i = 0; i < 3; i++) {
(function(j) {
document.getElementById('btn-' + j).addEventListener('click', function() {
alert(j);
});
})(i);
}
</script>
其他
实际上很多场景用到了闭包,比如常见的防抖函数,节流函数,vue中的响应式原理,react 中的fiber,hooks原理等等
🎨【点赞】【关注】不迷路,更多前端干货等你解锁
往期推荐