JavaScript

JavaScript 闭包解释(推荐)

闭包到底是什么


人人都在谈论闭包(closure),认为它是必不可少的语言特性,但有多少人真正明白闭包是什么以及如何使用它们呢?少之又少。闭包是人人都想理解的语言特性,也是所有传统语言都想添加的语言特性。闭包很难学,业界很多受过良好教育的人都这样说;但对本书的读者来说,这根本就不是问题。想知道为什么?不是因为本书通俗易懂,也不是因为我们要创建一个杀手级应用程序来向你介绍闭包,而是因为你早就学习过了,只是那时我们没有称之为闭包而已。

闲话少说,给闭包一个非常正式的定义吧。

闭包:名词,指的是函数和引用环境。

定义层面


函数通常包含局部变量(它们是在函数体中定义的,包括所有的形参),还可能包含不是在本地定义的变量,这些变量被称为自由变量。自由一词源于这样一点:在函数体内,自由变量没有绑定到任何值(换而言之,它们不是在本地声明的)。有了给每个自由变量都提供了值的环境后,便将函数敲定了;而函数和环境一起被称为闭包。

从定义来讲,当一个函数被定义后,其内部可能会引用一些变量。如果这些变量不是通过函数的参数传入,也不是在函数内部用letconstvar等关键字声明的局部变量,那它们就是自由变量。例如

function outer() {
    let a = 10; // 局部变量
    function inner() {
        console.log(a); // a 既不是 inner 函数的参数,也不是 inner 函数内部声明的局部变量,所以 a 是自由变量
    }
    return inner;
}
let fn = outer();
fn(); 

在上述代码中,inner函数内引用的a变量,就是自由变量。outer函数执行完后,按常理其局部变量a的生命周期应该结束,但由于inner函数形成了闭包,使得ainner函数引用,从而继续留在内存中,供inner函数后续使用。

闭包的定义: 在 JavaScript 中,闭包指的是有权访问另一个函数作用域里变量的函数。形成闭包需要满足以下几个关键条件:

  • 存在函数嵌套,也就是一个函数定义在另一个函数内部。
  • 内部函数引用了外部函数作用域中的变量。
  • 外部函数将内部函数返回。

使用闭包实现神奇的计数器


有过实现计数器函数的念头吗?通常以类似于下面的方式来实现:

var count = 0;
// 每次调用counter时,都将全局变量
// count加1,再返回结果。
function counter() {
  count = count + 1;
  return count;
}

可以像下面这样使用这个counter函数:

console.log(counter());
console.log(counter());
console.log(counter());

这种做法存在的唯一问题是,使用全局变量count,协作开发代码时,大家常常会使用相同的变量名,进而导致冲突。这里要告诉你的是,可以使用受保护的局部变量实现计数器。这样,计数器将不会与任何代码发生冲突,且只能通过调用相应的函数(也叫闭包)来增加计数器的值。

要使用闭包实现这种计数器,可重用前面的大部分代码。别眨眼,奇迹就要发生了:

function makeCounter() {
// 我们在函数makeCounter中声明变量count。这样它就是局部变量,而不是全局变量。
var count = 0;
  // 接下来,我们创建函数 counter,它将变量count加1。
  function counter() {
    count = count + 1;
    return count;
  }
  // 然后,返回函数counter。这是一个闭包,在其环境中存储了变量count。
 return counter;
}
var doCount = makeCounter();
console.log(doCount());
console.log(doCount());
console.log(doCount());

闭包的优点是什么


JavaScript 闭包具有许多优点,在实际开发中发挥着重要作用,下面为你详细介绍:

读取函数内部变量

在 JavaScript 里,函数内部的变量具有局部作用域,外部无法直接访问。但闭包能够突破这一限制,让我们在函数外部读取函数内部的变量。

function outer() {
    let innerVariable = '我是内部变量';
    return function() {
        return innerVariable;
    };
}
let getInnerVariable = outer();
console.log(getInnerVariable()); 

在上述代码中,outer函数返回了一个内部函数,这个内部函数形成了闭包,使得我们可以在outer函数外部通过调用getInnerVariable函数来获取outer函数内部的innerVariable变量。

让变量的值始终保持在内存中

闭包能够让函数中的变量始终保持在内存中,不会在函数执行完毕后被销毁。这一特性使得闭包可以用于实现一些需要保存状态的功能。

function createCounter() {
    let count = 0;
    return function() {
        return ++count;
    };
}
let counter = createCounter();
console.log(counter()); 
console.log(counter()); 

在这个例子中,createCounter函数返回了一个闭包。每次调用counter函数时,都会对count变量进行自增操作。由于闭包的存在,count变量的值会一直保存在内存中,不会在createCounter函数执行完毕后被销毁。

实现封装和信息隐藏

闭包可以用于实现封装和信息隐藏,将一些数据和操作封装在闭包内部,只暴露必要的接口给外部使用,从而提高代码的安全性和可维护性。

function createPerson(name) {
    let privateName = name;
    return {
        getName: function() {
            return privateName;
        },
        setName: function(newName) {
            privateName = newName;
        }
    };
}
let person = createPerson('John');
console.log(person.getName()); 
person.setName('Jane');
console.log(person.getName()); 

在上述代码中,createPerson函数返回了一个对象,这个对象包含了两个方法:getNamesetName。这两个方法形成了闭包,它们可以访问和修改createPerson函数内部的privateName变量。外部代码只能通过这两个方法来操作privateName变量,从而实现了数据的封装和信息隐藏。

闭包的机制原理


闭包不被回收主要与 JavaScript 的内存管理机制、作用域链和引用关系有关。下面为你详细解释:

JavaScript 内存管理机制

在 JavaScript 中,内存管理是自动进行的,由垃圾回收机制(Garbage Collection,GC)负责。垃圾回收机制的核心思想是通过标记和清除等算法,找出那些不再被引用的对象,并释放它们所占用的内存空间。简单来说,如果一个对象没有任何引用指向它,那么它就会被视为 “垃圾”,并在合适的时机被回收。

闭包的作用域链和引用关系

当一个函数内部定义了另一个函数时,内部函数会形成一个闭包。闭包会捕获并保留其外部函数的作用域,即使外部函数已经执行完毕,其作用域内的变量也不会被销毁,原因如下:

作用域链的保留

函数在创建时会保存其所在的作用域链。对于闭包而言,内部函数的作用域链包含了外部函数的作用域。这意味着内部函数可以访问外部函数的变量和参数。例如:

function outer() {
    let outerVariable = 'I am from outer function';
    function inner() {
        console.log(outerVariable);
    }
    return inner;
}

let closure = outer();

在上述代码中,inner 函数形成了一个闭包。当 outer 函数执行完毕后,inner 函数的作用域链仍然保留着对 outer 函数作用域的引用。即使 outer 函数的执行上下文从调用栈中弹出,其作用域内的变量 outerVariable 也不会被销毁,因为 inner 函数的作用域链指向了它。

引用关系的存在

只要闭包存在,它就会保持对外部函数作用域内变量的引用。只要有引用存在,垃圾回收机制就不会将这些变量视为可回收的对象。例如,在上面的代码中,变量 closure 引用了 inner 函数,而 inner 函数又引用了 outerVariable。因此,outerVariable 不会被回收,直到 closure 不再引用 inner 函数,并且没有其他引用指向 outerVariable 为止。

示例说明闭包不被回收的影响

下面的示例展示了闭包如何影响变量的生命周期:

function createCounter() {
    let count = 0;
    return function() {
        return ++count;
    };
}

let counter = createCounter();
console.log(counter()); 
console.log(counter()); 

在这个例子中,createCounter 函数返回了一个闭包。每次调用 counter 函数时,都会对 count 变量进行自增操作。由于闭包的存在,count 变量会一直保存在内存中,不会在 createCounter 函数执行完毕后被销毁。只要 counter 变量仍然引用着这个闭包,count 变量就会一直存在。

综上所述,闭包不被回收是因为它保留了对外部函数作用域的引用,使得外部函数作用域内的变量不会被垃圾回收机制视为可回收的对象。这种特性在实现一些功能时非常有用,但如果不注意,也可能会导致内存泄漏。

这个是闭包吗?


function outer() {
    let outerVariable = 'I am from outer function';
    function inner() {
        console.log(“hello”);
    }
    return inner;
}
这样的话 inner算是闭包吗

严格来说,在你给出的代码里inner不算闭包。接下来为你详细解释:

闭包的定义

在 JavaScript 中,闭包指的是有权访问另一个函数作用域里变量的函数。形成闭包需要满足以下几个关键条件:

  • 存在函数嵌套,也就是一个函数定义在另一个函数内部。
  • 内部函数引用了外部函数作用域中的变量。
  • 外部函数将内部函数返回。

对代码的分析

在这段代码中:

  • 存在函数嵌套,inner 函数定义在 outer 函数内部。
  • 外部函数 outer 将内部函数 inner 返回。
  • 然而,inner 函数内部并没有引用 outer 函数作用域里的变量 outerVariable,它仅仅执行了 console.log("hello"); 这一操作。

由于不满足闭包形成的 “内部函数引用外部函数作用域中的变量” 这一关键条件,所以 inner 函数不算闭包。

内存释放的例子


function outer() {
    let outerVariable = 'I am from outer function';
    function inner() {
        console.log("hello");
    }
    return inner;
}这种outerVariable会被内存释放掉吧?

在你给出的代码中,outerVariable 变量会在 outer 函数执行完毕后被内存释放掉。下面从 JavaScript 的垃圾回收机制、闭包特性等方面为你详细解释:

垃圾回收机制与变量引用

JavaScript 具备自动的垃圾回收机制,该机制通常采用标记清除算法来判断哪些变量可以被回收。简单来说,如果一个变量没有被任何活动的引用所指向,那么它就会被标记为可回收的对象,在合适的时机,垃圾回收器会释放该对象所占用的内存空间。

当前代码中 outerVariable 的情况

function outer() {
    let outerVariable = 'I am from outer function';
    function inner() {
        console.log("hello");
    }
    return inner;
}

在上述代码里:

  • outerVariable 是 outer 函数的局部变量,它只在 outer 函数的作用域内有效。
  • inner 函数并没有引用 outerVariable 变量,这意味着 inner 函数的作用域链中不包含对 outerVariable 的引用。
  • 当 outer 函数执行结束后,其执行上下文会从调用栈中弹出,由于没有其他地方引用 outerVariable,垃圾回收机制会将其标记为可回收对象,并在后续的垃圾回收过程中释放它所占用的内存。

对比闭包情况

如果 inner 函数引用了 outerVariable,那么情况就会不同,示例如下:

function outer() {
    let outerVariable = 'I am from outer function';
    function inner() {
        console.log(outerVariable);
    }
    return inner;
}
let closure = outer();

在这个闭包的例子中:

  • inner 函数形成了闭包,因为它引用了 outer 函数作用域中的 outerVariable
  • 当 outer 函数执行完毕后,虽然其执行上下文从调用栈中移除,但 inner 函数的作用域链仍然保留着对 outerVariable 的引用。
  • 只要 closure 变量存在并且引用着 inner 函数,outerVariable 就不会被垃圾回收机制回收,会一直存在于内存中。

综上所述,在最初提供的代码里,由于 inner 函数未引用 outerVariableouterVariable 会在 outer 函数执行完毕后被内存释放。

全部变量 和 局部变量的闭包


  • 局部变量闭包
var a = 5;
function outer() {
    function inner() {
        console.log(a);
    }
    return inner;
}
let fn = outer();
fn();

在这个例子中,a 是全局变量,它的作用域是整个全局环境,生命周期从定义开始直到页面关闭或者全局对象被销毁。无论 outer 函数是否执行完毕,a 都会一直存在于全局作用域中,不会因为 outer 函数的执行状态而改变其生命周期。inner 函数形成的闭包只是引用了全局作用域中的 a,对 a 的生命周期没有影响。

  • 局部变量闭包
function outer() {
    let a = 10;
    function inner() {
        console.log(a);
    }
    return inner;
}
let fn = outer();
fn();

这里的 a 是 outer 函数的局部变量,按照正常的作用域规则,当 outer 函数执行完毕后,其局部作用域会被销毁,a 也会被垃圾回收机制回收。但由于 inner 函数形成了闭包,inner 函数引用了 a,这使得 a 的生命周期得以延长,只要 inner 函数存在,a 就会一直存在于内存中。

变量的可修改性和独立性

  • 全局变量情况:由于 a 是全局变量,任何地方都可以对其进行修改。这意味着除了 inner 函数之外,其他代码也可能会改变 a 的值,从而影响 inner 函数的输出结果。例如:
var a = 5;
function outer() {
    function inner() {
        console.log(a);
    }
    return inner;
}
let fn = outer();
a = 20; // 其他代码修改了全局变量 a 的值
fn(); // 输出 20
  • 局部变量情况a 是 outer 函数的局部变量,只有通过 outer 函数或者 inner 函数(因为闭包的存在)才能访问和修改 a 的值,具有一定的独立性。例如:
function outer() {
    let a = 10;
    function inner() {
        console.log(a);
    }
    function modifyA() {
        a = 20;
    }
    return {
        inner: inner,
        modifyA: modifyA
    };
}
let result = outer();
result.inner(); // 输出 10
result.modifyA();
result.inner(); // 输出 20

内存占用和性能影响

  • 全局变量情况:全局变量本身就一直存在于全局作用域中,inner 函数形成的闭包只是引用了全局变量,不会额外增加太多的内存开销。但如果全局变量过多或者数据量过大,会导致全局作用域的内存占用增加,影响性能。
  • 局部变量情况:由于闭包会延长局部变量的生命周期,使得局部变量不会被及时回收,可能会导致内存占用增加。如果大量创建这种闭包,可能会对内存性能产生一定的影响。但在合理使用的情况下,这种影响是可控的。

综上所述,这两种闭包在变量作用域、生命周期、可修改性、独立性以及内存占用和性能影响等方面都存在明显的区别。在实际开发中,需要根据具体的需求来选择合适的方式。