【JavaScript】学习一下闭包

  • Post author:
  • Post category:java




学习链接


MDN:闭包


深入浅出JavaScript闭包


JavaScript深入之闭包

(👍👍)


现代 JavaScript 教程:变量作用域,闭包

(👍👍👍)


网道教程:闭包


说说你对闭包的理解?闭包使用场景

  • 闭包的定义,为什么说 JavaScript 中的所有函数都是闭包的

  • 关于

    [[Environment]]

    属性和词法环境原理的技术细节



闭包



闭包定义

当函数可以记住并访问所在的词法作用域,即使函数是在当前词法作用域之外执行,这就产生了闭包。

在 JavaScript 中,所有函数都是天生闭包的(只有一个例外

new Function

,见

补充

)。

也就是说:JavaScript 中的函数会自动通过隐藏的

[[Environment]]

属性

记住创建它们的词法作用域

,所以它们

都可以访问外部变量



本质

在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁,

也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

每当创建一个函数,闭包就会在函数创建的同时被创建出来。



两个角度

或许从两个角度来看待闭包,就会少很多分歧和误解:



  1. 理论

    角度:所有的函数都是闭包。

    因为它们都在创建的时候就将上层上下文的数据保存起来了,都可以访问外部的变量。



  2. 实践

    角度:以下函数才算是闭包:

    1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
    2. 在代码中引用了外部函数变量



细节概括


看这里

或者

补充

在 JavaScript 中,每个运行的函数,代码块

{...}

以及整个脚本,都有一个被称为

词法环境(Lexical Environment)

的内部(隐藏)的关联对象。

词法环境对象由两部分组成:



  1. 环境记录

    (Environment Record)

    —— 一个

    存储所有局部变量作为其属性

    (包括一些其他信息,例如

    this

    的值)的对象。


  2. 外部词法环境的

    引用


    ,与外部代码相关联。

一个“变量”只是

环境记录

这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。

  • 变量是特殊内部对象的属性,与当前正在执行的(代码)块/函数/脚本有关。
  • 操作变量实际上是操作该对象的属性。



变量声明

当脚本开始运行,词法环境

预先填充了所有声明的变量

  • 最初,它们处于“未初始化(Uninitialized)”状态。
  • 这是一种特殊的内部状态,这意味着

    引擎知道变量

    ,但是在用

    let


    声明前,不能引用

    它。(

    暂时性死区



函数声明

与变量不同,

函数声明的初始化会被立即完成。



创建了一个词法环境

时,函数声明会立即

变为即用型函数

(不像

let

那样直到声明处才可用)。

所有的函数在“诞生”时都会记住创建它们的词法环境。

所有函数都有

名为

[[Environment]]

的隐藏属性

,该属性保存了

对创建该函数的词法环境的引用

这就是函数

记住它创建于何处

的方式,与函数被

在哪儿调用无关



[[Environment]]

引用在**

函数创建时

被设置




永久保存

**。



函数调用

在一个函数运行时,在

调用刚开始时

,会自动**

创建一个新的词法环境


以存储这个调用的局部变量和参数,并且

其外部词法环境引用获取于 函数的

[[Environment]]

属性**。



访问修改变量

当代码要访问一个变量时 —— 首先会

搜索内部词法环境

,然后

搜索外部环境

,然后

搜索更外部的环境

,以此类推,

直到全局词法环境




变量所在的词法环境



更新变量


,这也就意味着多次调用同一内部函数,使用的也是同一个外部函数的变量。



应用场景

任何闭包的使用场景都离不开这两点:


  • 创建私有变量

  • 延长变量的生命周期

一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的


  1. 可以读取函数内部的变量


  2. 可以使变量的值长期保存在内存中,生命周期比较长

    。因此不能滥用闭包,否则会造成网页的性能问题。


使用闭包模拟私有方法

:数据内部私有,暴露方法或函数供外面使用。


柯里化函数

:把一个多参数函数转化成一个嵌套的一元函数,便于重用。



缺点



性能

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在

处理速度和内存消耗方面

对脚本

性能具有负面影响



内存泄漏

栈内存提供一个执行环境,即作用域,包括全局作用域和私有作用域。


  • 全局作用域

    :只有当页面关闭的时候全局作用域才会销毁

  • 私有的作用域

    :只有函数执行才会产生

一般情况下,函数执行会形成一个新的私有的作用域,当私有作用域中的代码执行完成后,我们当前作用域都会主动的进行释放和销毁。

但当遇到函数执行返回了一个引用数据类型的值,并且在函数的外面被一个其他的东西给接收了,这种情况下一般形成的私有作用域都不会销毁。

function fn(){
    let num = 100;
    return function(){
        return num * 2;
    }
}
const f = fn(); // fn执行形成的这个私有的作用域就不能再销毁了

也就是像上面这段代码,fn函数内部的私有作用域会被一直占用的,发生了内存泄漏。

所谓内存泄漏,指的是

任何对象

在我们

不再拥有或需要

它之后

仍然存在

  • 闭包

    不能滥用

    ,否则会导致

    内存泄露

    ,影响网页的性能。

  • 闭包

    使用完后

    ,要立即

    释放资源

    ,将

    引用变量指向

    null




补充



new Function


现代 JavaScript 教程:“new Function” 语法

通常,闭包是指使用一个特殊的属性

[[Environment]]

来记录函数自身的创建时的环境的函数。它具体指向了函数创建时的词法环境。

但是如果我们

使用

new Function

创建一个函数

,那么

该函数的

[[Environment]]



不指向当前的词法环境


,而是**

指向全局环境

**。

因此,此类函数无法访问外部(outer)变量,只能访问

全局变量

function getFunc() {
  let value = "test";

  let func = new Function('alert(value)');

  return func;
}

getFunc()(); // error: value is not defined

可正常访问全局变量

let value = "test";
let func = new Function('alert(value)');
func(); // value

语法:

let func = new Function ([arg1, arg2, ...argN], functionBody);

由于历史原因,参数也可以按逗号分隔符的形式给出。

以下三种声明的含义相同:

new Function('a', 'b', 'return a + b'); // 基础语法
new Function('a,b', 'return a + b'); // 逗号分隔
new Function('a , b', 'return a + b'); // 逗号和空格分隔



new Function

创建的函数,它的

[[Environment]]

指向全局词法环境,而不是函数所在的外部词法环境。

因此,我们不能在

new Function

中直接使用外部变量。不过这样是好事,这有助于降低我们代码出错的可能。并且,从代码架构上讲,显式地使用参数传值是一种更好的方法,并且

避免了与使用压缩程序而产生冲突的问题



细节展开



变量

矩形表示环境记录(变量存储),箭头表示外部引用。

image-20220712153115490

此时的词法环境只有一个,

全局词法环境

。全局词法环境

没有外部引用

,所以箭头指向了

null

  1. 当脚本开始运行,词法环境预先填充了所有声明的变量。

    • 最初,它们处于“未初始化(Uninitialized)”状态。
    • 这是一种特殊的内部状态,这意味着

      引擎知道变量

      ,但是在用

      let


      声明前,不能引用

      它。(

      暂时性死区

  2. 然后

    let phrase

    定义出现了。它尚未被赋值,值为

    undefined

    。此时就可以使用变量了。

  3. phrase

    被赋予了一个值。

  4. phrase

    的值被修改。

  • 变量是特殊内部对象的属性,与当前正在执行的(代码)块/函数/脚本有关。
  • 操作变量实际上是操作该对象的属性。


词法环境是一个规范对象

“词法环境”是一个规范对象(specification object):它仅仅是存在于

编程语言规范

中的“理论上”存在的,用于描述事物如何运作的对象。我们无法在代码中获取该对象并直接对其进行操作。



函数声明

一个函数其实也是一个值,就像变量一样。


不同之处在于函数声明的初始化会被立即完成。

当创建了一个词法环境(Lexical Environment)时,函数声明会立即变为即用型函数(不像

let

那样直到声明处才可用)。

这就是为什么我们可以在(函数声明)的定义之前调用函数声明。

image-20220712153530128

正常来说,这种行为

仅适用于函数声明

,而

不适用于

我们将函数分配给变量的

函数表达式

,例如

let say = function(name)...



内部和外部的词法环境

在一个函数运行时,在

调用刚开始时

,会自动**

创建一个新的词法环境

**以存储这个调用的局部变量和参数。

image-20220712154630117

在这个函数调用期间,总共有两个词法环境:内部一个(用于函数调用)和外部一个(全局):

  • 内部词法环境与

    say

    的当前执行相对应。它具有一个单独的属性:

    name

    ,函数的参数。
  • 外部词法环境是全局词法环境。它具有

    phrase

    变量和函数本身。

内部词法环境引用了

outer


当代码要访问一个变量时 —— 首先会搜索内部词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境。

  • 对于

    name

    变量,当

    say

    中的

    alert

    试图访问

    name

    时,会立即在内部词法环境中找到它。
  • 当它试图访问

    phrase

    时,然而内部没有

    phrase

    ,所以它顺着对外部词法环境的引用找到了它。



返回函数



尚未运行内部函数

image-20220712155128434

在每次

makeCounter()

调用的开始,都会创建一个新的词法环境对象,以存储该

makeCounter

运行时的变量。

在执行

makeCounter()

的过程中创建了一个仅占一行的嵌套函数:

return count++

。此时

尚未运行

它,

仅创建

了它。

image-20220712155224377

所有的函数在“诞生”时都会记住创建它们的词法环境。

所有函数都有

名为

[[Environment]]

的隐藏属性

,该属性保存了

对创建该函数的词法环境的引用

因此,

counter.[[Environment]]

有对

{count: 0}

词法环境的引用。这就是函数

记住它创建于何处

的方式,与函数被

在哪儿调用无关



[[Environment]]

引用在**

函数创建时

被设置




永久保存

**。



开始运行内部函数



调用

counter()


时,会为该调用

创建一个新的词法环境

,并且

其外部词法环境引用获取于

counter.[[Environment]]


image-20220712155857576



counter()

中的代码查找

count

变量时,它首先搜索自己的词法环境(为空,因为那里没有局部变量),然后是

外部

makeCounter()

的词法环境

,并且

在哪里找到就在哪里修改




变量所在的词法环境



更新变量


执行后:

image-20220712155912369

法环境**,并且

其外部词法环境引用获取于

counter.[[Environment]]


[外链图片转存中…(img-9s2X2S5D-1657782572537)]



counter()

中的代码查找

count

变量时,它首先搜索自己的词法环境(为空,因为那里没有局部变量),然后是

外部

makeCounter()

的词法环境

,并且

在哪里找到就在哪里修改




变量所在的词法环境



更新变量


执行后:

[外链图片转存中…(img-wHJkWP0Q-1657782572539)]



版权声明:本文为Jamcy123原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。