JS闭包技巧:用最简单的方式处理复杂的问题

437 阅读6分钟

本文正在参加「金石计划」

闭包及其作用

什么是闭包?

闭包(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 返回函数 innerFunctioninnerFunction 可以访问 outerFunction 的局部变量 count。每次调用 newFunction(),它都会增加并返回计数器的值。

闭包的作用

它的最大用处有两个

  • 一个是可以读取函数内部的变量
  • 另一个就是让这些变量的值始终保持在内存中

闭包的实现原理

闭包的实现原理是在函数内部创建一个新的作用域,这个作用域可以访问到外部函数的变量。当外部函数返回内部函数时,内部函数会继续保持对外部函数作用域的引用,这就形成了一个闭包。

在JavaScript中,每个函数都有一个作用域链。当函数被调用时,它会创建一个新的执行环境,并将其添加到作用域链的顶部。当函数执行完毕后,它会将自己的执行环境从作用域链中移除。

但是如果这个函数返回了一个内部函数,并且这个内部函数引用了外部函数的变量,那么这个内部函数就会形成一个闭包,它会继续保持对外部函数作用域的引用,即使外部函数已经执行完毕并从作用域链中移除了自己的执行环境。

闭包存在的问题

容易导致内存泄漏

闭包是一种特殊的函数,它可以访问其定义时所在的词法作用域中的变量。如果闭包中的函数引用了外部函数的变量,那么这些变量就不会被垃圾回收机制回收,从而可能导致内存泄漏。

如何避免闭包导致的内存泄漏?

1)、将不再使用的引用设置为null。

2)、尽早释放掉无用的引用,在引用退出作用域后设置为null。

3)、不要在经常调用的地方创建对象/变量,尤其是在循环中。

闭包的实际应用场景

模块化开发

使用闭包可以实现模块化开发,将代码分割成多个模块,每个模块都有自己的作用域,可以避免全局命名空间污染和代码冲突,同时提高代码的可维护性和复用性。

假如现在有一个商品管理模块,需要实现以下功能

  • 添加商品
  • 删除商品
  • 修改商品
  • 获取所有商品
  • 根据条件筛选商品

为了实现这些功能,我们可以将代码分割成多个模块,例如

image-20230419230639293

首先,可以通过使用闭包,将商品管理模块的私有变量和函数封装起来,只暴露公共接口给外部使用。

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原理等等

参考资料:MDN JavaScript Closures


🎨【点赞】【关注】不迷路,更多前端干货等你解锁

往期推荐

👉 Vue的渲染函数render&h

👉 ES6中一些很好用的数组方法

👉 echarts | 柱状图实用配置

👉 JS设置获取盒模型对应宽高的五种方式详解