JavaScript 闭包工作原理解析

如果是新手 JavaScript 开发人员,你可能已经听说过闭包这个关键字,但是大多数人可能还没有。或者,你是正在寻找新机会的开发人员,并且以前从未用过闭包,但是你知道这对于 JavaScript 开发人...

如果是新手 JavaScript 开发人员,你可能已经听说过闭包这个关键字,但是大多数人可能还没有。或者,你是正在寻找新机会的开发人员,并且以前从未用过闭包,但是你知道这对于 JavaScript 开发人员职位的面试至关重要。


通常要了解闭包,仅阅读一篇文章是不够的。但在本文中,你将学习所有用到的概念来进行解释它,以及这些概念如何一清二楚地协同工作。


闭包存储一个函数中的当前定义以及作用域信息,将其传递给外层作用域。通常内层作用域无法从词法作用域环境中的外层作用域访问。


这个定义一开始可能看起来很吓人,但现在只关注关键字。首先让我从作用域关键字开始,它还将告诉你有关词法作用域的信息。


作用域

在编程中,作用域是一个基础概念,负责命名绑定。当在一个代码块中声明一个函数或者变量时,这些命名绑定从代码的其它部分可以访问或者不能访问。我们通常称之为作用域。


编程语言是在这个作用域概念的基础上创建的,以支持开发人员的安全性和持久性。


有两种常见的作用域方法,即词法(静态)作用域和动态作用域。


大多数已知的编程语言都在使用词法作用域,也称为静态作用域。这些编程语言是 C、C++、Java、JavaScript、Python 等。


在 Perl 和 Lisp 中,你可能更愿意用动态作用域。而且 Logo 作为一种编程语言也建立在动态作用域的基础上。


因此,下面我们首先从词法(静态)作用域开始。


词法(静态)作用域

在词法作用域环境中,命名绑定是在编译期确定的,也称早绑定。它为命名绑定带来了严格的可访问性规则。我们大致可以将这些规则描述为:


内层作用域可以访问其外层作用域

外层作用域不能访问其内层作用域

所以我们可以说,在词法作用域环境中,我们有从内层作用域到外层作用域的单向访问。对于开发者来说,这种作用域策略易于读懂,易于跟踪代码。


动态作用域

在动态作用域环境中,命名绑定是在运行期确定的,也称晚绑定。与词法作用域不同,动态作用域对于命名绑定没有可以访问性限制。


在这种作用域策略中,对于编程语言来说很容易实现,但是对于开发者来说,很难跟踪变量的值。因为对于代码不同部分中的每个命名绑定,对于相似的函数或者变量名都将存在竞争条件。


当然,这些定义不足以简单展示它们的含义。因此,下面我们来看一下对比中的作用域界定策略。


词法(静态)作用域与动态作用域

如下只是一个基础的 Javascript 代码段:

let a = 1;
const printParentScopeA = () => {
    console.log(a);
}
const printChildScopeA = () => {
    let a = 10;
    console.log(a);
}
printChildScopeA();
printParentScopeA();


在这段代码中,我们总共有三种作用域定义:


全局作用域

函数 printParentScopeA 作用域

函数 printChildScopeA 作用域

调用 printChildScopeA() 函数时,它将打印在其作用域(即函数 printChildScopeA 的作用域)中定义的变量 a。因此它将立即打印 10。

调用 printParentScopeA() 函数时,它首先查找变量 a 的定义,而在其作用域(即函数printParentScopeA 的作用域)中找不到。然后在上层作用域中搜索 a 的定义,并在全局作用域中找到了。所以它打印出 1。

遵循的这种作用域逻辑是词法(静态)作用域。


假设它是一个动态作用域环境而不是词法作用域,那么输出又是什么呢?


在这种情况下,所有函数和变量定义都将存储在一个通用作用域内,所有相同名称的声明将指向相同的变量或函数,而最后一个具有同名的定义或声明会起最终作用。


因此在动态作用域环境中,我们的代码段会工作为:


调用 printChildScopeA() 函数时,首先,它将变量a的值更改为10(其定义存储在通用作用域内)。然后,它将打印定义在通用作用域中的变量a。因此它打印10。

调用 printParentScopeA() 函数时,它会在通用作用域内查找变量a的定义,然后找到其值10。因此它将再次打印10。

如您所见,这是不寻常的行为,对于大型代码库而言,很难跟踪。这就是为什么大多数编程语言都建立在词法(静态)作用域之上的原因。


现在,我们理解了词法作用域到底是什么,以及它如何定义作用域,如何在这些作用域定义中搜索命名绑定。


最后,我们再来谈谈 JavaScript 闭包。


闭包

正是由于闭包,我们才可以绑定一个词法作用域环境,并将其传递给外层作用域。如之前所读,通常在词法作用域中是不允许进入内层作用域环境的,但是闭包是允许我们这样做的一项至关重要的 JavaScript 功能。


那么,我们该怎么做呢?


我们可以通过将一个函数定义在另一个函数中,从而封装一个词法环境。在此定义过程中,我们可以把当前词法作用域内容存储在内层函数定义中。


那么下面我们来看一下一个简单的闭包例子。


闭包的例子

const powerOf = (x) => {
    const power = (y) => {
        return y**x;
    };
    return power;
}
const square = powerOf(2);
const cube = powerOf(3);
console.log(square(2));  // 打印 4
console.log(cube(2)); // 打印 8


在上面的代码示例中,我们可以看到,当 powerOf() 函数被带参数调用时,它是在返回 power() 函数而不是执行它,并且在该函数返回的过程中,参数 y 和变量 x 的当前值被保存了。


因此,当 powerOf(2) 被调用时,它是在返回:


((y) => {
    return y**2;
});


而当 powerOf(3) 被调用时,它是在返回:

((y) => {
    return y**3;
});


因此,正是由于 JavaScript 闭包,我们才得以将 power() 函数的当前定义和词法作用域存储在一起,并且我们从一个函数定义就得到了 square() 和 cube() 两个不同的函数定义。


JavaScript 闭包是一个很强大的功能,它甚至被用于实现一种称为模块模式的设计模式。正是由于这种实现,JavaScript 才在没有 public、private 关键字的情况下有了封装。


对于模块模式的实现,JavaScript 还用了另一个强大的功能,称为立即调用的函数表达式(IIFE)。


那么什么是 IIFE 呢?


立即调用的函数表达式(IIFE)

IIFE 是一种很基础的 JavaScript 功能,它让我们能在函数定义的过程中就能执行该函数。


如下是一个简单的 IIFE 例子:

(() => {
    console.log('I am an IIFE');
})(); // 打印: I am an IIFE


在上例中,它立即打印 'I am an IIFE',而不需要等待单独的函数调用。


为实现模块模式,我们会在一个 IIFE 中定义我们函数,这个 IIFE 中可能就带有闭包。


模块模式实现

这里有基础的模块模式实现示例:

var myModule = ((() => {
    var myPrivateVariable = 'private';
    var myPublicVariable = 'public';
    var myPrivateMethod = () => {
        console.log(myPrivateVariable);
    }
    
    return {
        myPublicVariable,
        myPublicMethod: () => {
            myPrivateMethod();
        }
    };
})());
console.log(myModule.myPublicVariable);
// 打印: public
myModule.myPublicMethod();
// 打印: private
console.log(myModule.myPrivateVariable);
// 打印: undefined
myModule.myPrivateMethod(); 
// 排除 TypeError: myModule.myPrivateMethod is not a function


正如我们从上例中所看到的,我们已经在一个 IIFE 中定义了公有和私有变量以及函数。


通过确定应返回哪些变量和函数,我们将其公开。如果不返回在模块定义中函数变量,那么它就变成了私有的。


如果试图访问私有变量,它会是 undefined。如果视图访问私有函数,它会抛出一个类型错误。


模块模式是 JavaScript 中一个很常见的模式,并且由于 IIFE 和闭包,它很容易实现。


总结

我希望现在已经清晰地理解了 JavaScript 闭包是什么以及其工作原理。同时,通过模块模式示例,我希望你看到了 JavaScript 中闭包和 IIFE 的常见用法。

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
王凯
王凯

92 篇文章

作家榜 »

  1. admin 651 文章
  2. 粪斗 185 文章
  3. 王凯 92 文章
  4. 廖雪 78 文章
  5. 牟雪峰 12 文章
  6. 李沁雪 9 文章
  7. 全易 2 文章
  8. bngvitmrbj 0 文章