时隔多年再谈对闭包的理解

引言

首先我们得清楚一个东西,闭包不是一个原理,是我们在特殊使用JavaScript作用域时产生的一个概念。这句话说的又不清不楚了,也就是说,我们在学习闭包时不需要学习新的语法知识,也不需要学习新的语言模式,闭包可以看作是一些对作用域应用的特殊形式。换言而知,只要你用了闭包的书写形式那么这就是闭包。

闭包的书写形式

我们知道函数是JavaScript的一等公民,当函数被当作参数传入到另一个函数里面时,我们习惯把这种函数叫做高阶函数,当一个函数被他的爸爸函数return出去的时候,还保留着对爸爸函数属性的访问那么我们就叫做闭包。

    function f() {
        var mes = '看这就是闭包';
        function speack() {
            console.log(mes)
        }
        return speack;
    }
    var baz = f();
    baz(); // 快看呀 这就是闭包的效果

闭包的书写形式进阶

在很长的时间断,我以为只要是上面的书写形式就是闭包,但是这远远是不够的

// 没有函数被返回 只是不在当前闭包函数的作用域内既可以产生闭包的效果
function foo() {
    var a = 2;
    function baz() {
        console.log(2);
    }
    bar(baz);
}
function bar(fn) {
    fn(); // 看呀,这就是闭包的效果
}
foo();

// 既没有函数被返回 也没有在外边被调用
function wait(mes) {
    setTimeout(function timer() {
        console.log(mes)
    }, 1000)
}
wait('我也产生了闭包的效果呀')

所以,到底怎么才能彻底概括闭包?

  • 《你不知道的JS》中概括到:当函数可以记住并访问所在的词法作用域(这时函数就有了闭包的功能),即使函数是在当前词法作用域之外执行(闭包的效果),这时就产生了闭包
  • 阮一峰的博客:闭包就是能够读取其他函数内部变量的函数。由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"。所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
  • MDN web docs:闭包是函数和声明该函数的词法环境的组合。

好了,那么我们的闭包就学完了!!!

闭包当然是学完了,但是闭包之所以难以理解,并不是在于闭包的概念有多复杂,完全是在于我们往往还不理解【词法作用域】,或者足够理解【词法作用域】就来试着理解闭包。这样的话,闭包对于我们来说,当然是晦涩难懂,充满神秘色彩。然而闭包的本质就是对词法作用域的巧妙应用。

闭包诞生的基石:词法作用域 

词法作用域是什么鬼?变量作用域,函数作用域,我大概知道是什么东西,对es6的学习也知道了块作用域大致是什么鬼。

首先我们来看作用域的定义:是一套规则,是一套管理如何存储变量的规则,这套规则用来管理引擎如何在当前作用域以及嵌套的子作用域中根据标识符名称进行变量查找。

作用域:分为词法作用域和动态作用域,好了我要偷懒了,我们知道JavaScript是词法作用域就可以了,有关作用域的详细信息推荐阅读《你不知道的JavaScript 上》。

在这里我们知道作用域的一些相关性质有助于理解闭包就好了,毕竟我是来说闭包的,我才不管我忽略了最重要的东西。

作用域的某些性质

  • 作用域是可以嵌套的
  • 内部作用域可以访问外部作用域的参数和变量,直至全局作用域
  • 外部作用域不能访问内部作用的参数和变量

我们就阮一峰闭包博客的思考题来感受下作用域对理解闭包的重要作用

var name = "The Window";

var object = {
    name : "My Object",

    getNameFunc : function(){
        // 这里闭包访问的属性是window作用的变量name
        return function(){
            return this.name; // this 换成 object 就是所期待的值
        };
    }
};

alert(object.getNameFunc()());

估计相当一部分人,看到这个题目很蒙,认为执行的结果是"The Window",但是这道题完全是在用this这个东西误导大家的判断,getNameFunc的第一个()是属于方法调用,所以this绑定到了object对象,自然this.name为"My Object",但是闭包函数无法访问这个this,它只能访问到全局的this。我们再看下一题。

var name = "The Window";

var object = {
    name : "My Object",

    getNameFunc : function(){
        var that = this;
        // 这下 这个闭包访问的变量就是object这个作用域里面的属性了
        return function(){
            return that.name;
        };
    }
};

alert(object.getNameFunc()()); // 啊呀 舒服了 是期待的值

阮一峰说:如果你能理解上面两段代码的运行结果,应该就算理解闭包的运行机制了。经过分析下来,其实是要完全理解作用域的机制,才能完美的理解闭包的运行机制。

总结

我更想把闭包理解成一中规律,因为作用域的一些特性,我们编写代码时正好让这些特性产生了一些好玩的规律,然后我们把这种规律叫做闭包。理解闭包的关键在于理解作用域。推荐《你不知道的JavaScript》

 

参考文章:

MDN web docs 闭包

阮一峰 学习Javascript闭包(Closure)

《你不知道的JavaScript》

 

### 闭包的概念 在 JavaScript 中,**闭包(Closure)** 是指一个函数能够访问并记住其词法作用域,即使该函数在其作用域外执行。简单来说,闭包是指内部函数可以访问外部函数的变量和参数,即使外部函数已经返回。这种机制使得函数可以在调用后仍然保持对外部作用域中变量的引用[^1]。 闭包的核心特性在于它可以“捕获”定义时所处的作用域,并在后续执行时继续使用这个作用域中的变量。JavaScript 的执行环境本身就是一个作用域链,每个函数都可以访问其父级作用域中的变量,这构成了闭包的基础[^3]。 例如: ```javascript function outerFunction() { let count = 0; function innerFunction() { count++; console.log(count); } return innerFunction; } const counter = outerFunction(); counter(); // 输出 1 counter(); // 输出 2 ``` 在这个例子中,`innerFunction` 是一个闭包,它保留了对 `count` 变量的引用,即使 `outerFunction` 已经执行完毕。每次调用 `counter()`,都会修改并输出 `count` 的值[^4]。 ### 闭包的作用 闭包在 JavaScript 中具有多种重要作用: 1. **封装私有变量** - 闭包可以用于创建私有变量和方法,避免全局污染。通过将变量限制在外部函数的作用域中,只有内部函数可以访问这些变量,从而实现数据隐藏。 - 示例: ```javascript function createCounter() { let count = 0; return function () { return ++count; }; } const counter = createCounter(); console.log(counter()); // 输出 1 console.log(counter()); // 输出 2 ``` 在此示例中,`count` 是一个私有变量,只能通过返回的函数进行修改和访问。 2. **函数工厂与高阶函数** - 闭包可用于生成具有不同配置的函数实例。例如,可以创建一个通用的计数器工厂,根据不同的初始值生成独立的计数器。 - 示例: ```javascript function createCounter(start) { return function () { return start++; }; } const counter1 = createCounter(5); const counter2 = createCounter(10); console.log(counter1()); // 输出 5 console.log(counter2()); // 输出 10 ``` 这两个计数器分别维护自己的状态,互不干扰[^4]。 3. **事件处理与回调函数** - 闭包广泛应用于事件监听和异步编程中,确保回调函数可以访问定义时的上下文。 - 示例:节流函数控制滚动加载 ```javascript function throttle(fn, delay) { let valid = true; return function () { if (valid) { setTimeout(() => { fn.apply(this, arguments); valid = true; }, delay); valid = false; } }; } window.addEventListener("scroll", throttle(function () { console.log("页面正在滚动"); }, 1000)); ``` 上述代码中,`throttle` 函数利用闭包保存了 `valid` 状态,确保在指定的时间间隔内只执行一次回调[^5]。 4. **模块模式与单例设计** - 闭包常用于实现模块化开发模式,提供私有属性和方法,并暴露有限的公共接口。 - 示例: ```javascript const CounterModule = (function () { let count = 0; function increment() { count++; } function getCount() { return count; } return { increment, getCount }; })(); CounterModule.increment(); console.log(CounterModule.getCount()); // 输出 1 ``` 此示例中,`count` 是私有的,无法直接访问或修改,只能通过暴露的方法进行操作[^1]。 ### 闭包的使用场景 1. **数据缓存与记忆化函数** - 利用闭包可以缓存函数的计算结果,避免重复计算,提高性能。 - 示例: ```javascript function memoize(fn) { const cache = {}; return function (arg) { if (cache[arg]) { return cache[arg]; } const result = fn(arg); cache[arg] = result; return result; }; } function factorial(n) { if (n === 0) return 1; return n * factorial(n - 1); } const memoizedFactorial = memoize(factorial); console.log(memoizedFactorial(5)); // 输出 120 console.log(memoizedFactorial(5)); // 直接从缓存读取 ``` 2. **定时器与异步操作** - 闭包在 `setTimeout` 和 `setInterval` 中非常常见,确保在异步执行时仍能访问正确的上下文。 - 示例: ```javascript for (var i = 1; i <= 5; i++) { setTimeout(function () { console.log(i); // 所有输出都是6 }, i * 1000); } ``` 由于 `var` 没有块级作用域,所有 `setTimeout` 共享同一个 `i`。可以通过闭包解决这个问题: ```javascript for (let i = 1; i <= 5; i++) { setTimeout(function () { console.log(i); // 正确输出1到5 }, i * 1000); } ``` 或者使用 IIFE(立即调用函数表达式)显式创建闭包: ```javascript for (var i = 1; i <= 5; i++) { (function (i) { setTimeout(function () { console.log(i); }, i * 1000); })(i); } ``` 3. **函数柯里化与偏应用** - 闭包支持将多参数函数转换为一系列接受单个参数的函数,提升函数的复用性。 - 示例: ```javascript function add(a) { return function (b) { return a + b; }; } const add5 = add(5); console.log(add5(3)); // 输出 8 console.log(add5(10)); // 输出 15 ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值