JavaScript作用域与闭包知识整理

262 阅读8分钟

作用域

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

  • 定义:静态作用域(词法作用域)是一种作用域规则,决定了如何通过变量名在嵌套的作用域中查找变量。它是在编写代码时确定的,基于变量和块的物理位置决定变量的作用域。简而言之,词法作用域就是代码中变量和块的作用域是由代码在写的位置决定的。
  • 特点:在静态作用域(词法作用域)语言中,你可以仅仅通过阅读函数定义的位置就知道变量的作用域,无需考虑函数如何被调用。

举例说明:

var x = 10;
function outer() {
    var y = 20;
    function inner() {
        var z = 30;
        console.log(x, y, z); // 可以访问x, y, z
    }
    inner();
}
outer();

在这个例子中:

  • x 在全局作用域中定义。
  • y 在 outer 函数的作用域中定义。
  • z 在 inner 函数的作用域中定义。

根据词法作用域的规则,inner 函数可以访问 outer 函数内部以及全局作用域中的变量。这是因为 inner 函数在物理上(在代码中)被定义在 outer 函数内部,因此它可以“看到” outer 函数和全局作用域中的变量。

动态作用域

  • 动态作用域:与静态作用域不同,动态作用域是在函数调用时确定函数作用域的。这意味着函数的作用域不是在定义时确定的,而是在运行时,根据函数的调用栈来确定。
  • JavaScript的选择:JavaScript选择了静态作用域(词法作用域),因为它使得代码更容易理解和预测。开发者可以通过查看代码结构,而不是运行时的调用方式,来确定变量的作用域。

总结来说,虽然在JavaScript的文档和讨论中不常见“静态作用域”这个术语,但JavaScript的作用域机制实际上是静态的,也就是词法作用域。这种设计选择帮助开发者更容易地推理代码的行为,尤其是在涉及变量作用域和闭包时。

JS中作用域(静态作用域)的分类

在JavaScript中,作用域主要分为以下几种:

  1. 全局作用域:在代码的最外层定义的变量拥有全局作用域。全局作用域中的变量在代码的任何地方都可以被访问和修改。
  2. 局部作用域(函数作用域):在函数内部定义的变量拥有局部作用域。这些变量只能在其定义的函数内部被访问和修改。每个函数调用时都会创建一个新的作用域。
  3. 块级作用域:ES6引入了let和const关键字,允许开发者声明块级作用域的变量。块级作用域是指变量在声明它的一对花括号 {} 内有效。这包括了控制结构如if语句和for循环。
  4. 模块作用域:在模块系统中(如使用ES6模块或CommonJS模块),每个模块文件都被视为一个独立的作用域。在一个模块内部定义的变量,函数,类等默认情况下在模块外部是不可访问的,除非它们被明确地导出(export)和在其他模块中导入(import)。

作用域链(Scope Chain)

当代码在一个环境中执行时,JavaScript需要查找变量和函数的定义。作用域链是一个包含当前环境及其所有父环境的作用域的列表,它决定了如何查找变量。

每个JavaScript函数在创建时都会创建一个作用域链。这个链条的目的是保证对执行环境有效的变量和函数的有序访问。

作用域链的工作原理:

  1. 当代码需要访问一个变量时,它首先在当前作用域中查找。
  2. 如果在当前作用域中找不到该变量,搜索会继续向上移动到外部作用域中查找。
  3. 这个过程会一直持续到达全局作用域。
  4. 如果在全局作用域中仍然找不到该变量,则会抛出引用错误(ReferenceError)。

举例说明

var x = 'global';
function outer() {
    var x = 'outer';
    
    function inner() {
        var x = 'inner';
        console.log(x); // 输出 'inner'
    }
    
    inner();
    console.log(x); // 输出 'outer'
}
outer();
console.log(x); // 输出 'global'

在这个例子中:

  • inner 函数中的console.log(x)首先在inner函数的局部作用域中查找变量x,找到了值为'inner'。
  • outer 函数中的console.log(x)在outer函数的局部作用域中查找变量x,找到了值为'outer'。
  • 最后的console.log(x)在全局作用域中查找变量x,找到了值为'global'。

这个例子展示了作用域链如何确保在函数内部可以访问到外部作用域中的变量,同时也展示了不同作用域中可以有相同名称的变量,它们是互不干扰的。

总结

  • 作用域是定义变量和函数可访问性的规则集合。
  • 作用域链是当查找变量时,由当前环境及其所有父环境的作用域组成的链条,它确保了正确的变量访问顺序。
  • JavaScript中的作用域和作用域链是理解闭包、变量生命周期以及变量解析规则的基础。

词法环境(Lexical Environment)

词法环境是执行上下文的一部分,它具体实现了词法作用域的概念。每个词法环境都包含了一个环境记录(Environment Record),这是一个存储变量和函数声明的标识符与其对应值的映射,以及一个外部词法环境的引用(Outer Lexical Environment Reference),这个引用指向外部的词法环境。

var x = 10;
function outer() {
    var y = 20;
    function inner() {
        var z = 30;
        console.log(x, y, z); // 可以访问x, y, z
    }
    inner();
}
outer();

基于上述例子的词法环境:

  • 全局词法环境包含变量 x 和函数 outer 的定义。
  • outer 函数的词法环境包含变量 y 和函数 inner 的定义,它的外部词法环境引用指向全局词法环境。
  • inner 函数的词法环境包含变量 z,它的外部词法环境引用指向 outer 函数的词法环境。

当 inner 函数尝试访问变量 x、y 和 z 时,JavaScript引擎会首先在当前的词法环境中查找(在这个例子中是 inner 函数的词法环境)。如果找不到,它会沿着外部词法环境的引用链向上查找,直到找到变量或到达全局词法环境。

总结

  • 词法作用域是一个静态的作用域,由代码的书写位置决定。
  • 词法环境是实现词法作用域的具体机制,每个执行上下文都有一个词法环境,它通过环境记录和外部词法环境的引用来存储和查找变量。

这种机制确保了在JavaScript中,函数的执行能够访问到它们在定义时就能访问到的变量,而不仅仅是在调用时能访问到的变量。这是JavaScript闭包的基础。

闭包(Closure)是一个函数和声明该函数的词法环境的组合。闭包让你可以从内部函数访问外部函数作用域中的变量。在JavaScript中,闭包是一种非常强大的特性,它允许实现数据封装和模块化等。

闭包

定义

闭包(Closure)是一个函数以及该函数声明时所在的词法环境的组合。闭包让这个函数即使在其声明的词法作用域之外执行时,也能访问到其声明时的作用域中的变量。

创建闭包的条件

  1. 嵌套函数:在一个函数内部定义另一个函数。
  2. 内部函数引用外部函数的变量:内部函数可以访问并操作外部函数作用域中的变量。

会不会造成内存泄漏:

只有在内部函数被return出来,保存在外部执行的时候才会内存泄漏,不return的话不会。

闭包的特点

  • 访问外部函数作用域中的变量:即使外部函数已经返回,内部函数仍然可以访问外部函数作用域中的变量。
  • 封装私有变量:可以使用闭包来创建私有变量,使得这些变量不会暴露在全局作用域中,只能通过特定的函数访问和修改。

示例

function createCounter() {
  let count = 0; // `count` 是一个只能通过 `increment` 和 `getCount` 访问的私有变量
  return {
    increment: function() {
      count += 1;
    },
    getCount: function() {
      return count;
    }
  };
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 输出: 1
console.log(count); // ReferenceError: count is not defined

在这个示例中:

  • createCounter 函数创建了一个 count 变量和两个函数(increment 和 getCount),这两个函数都在同一个词法环境中与 count 变量共存。
  • 返回的对象形成了一个闭包,允许从外部访问 increment 和 getCount 函数,但不允许直接访问变量 count。
  • 这样,count 变量就被封装起来了,只能通过 counter 对象提供的方法访问和修改。

闭包的用途

  • 创建私有变量:利用闭包可以封装私有变量,避免全局命名汇聚。
  • 模块化代码:闭包允许创建模块,其中模块的公共API暴露给外部使用,而内部状态和实现细节保持私有。
  • 函数工厂:可以使用闭包动态创建函数,这些函数可以访问同一作用域中的变量,实现特定的功能。
  • 在异步操作中保持变量状态:在异步操作(如setTimeout或网络请求)中,闭包可以用来保持对特定变量的引用,确保即使在外部函数执行完毕后,这些变量仍然可用。