当前位置: 首页 > news >正文

专题 函数闭包

你将知道:

  • 什么是闭包
  • 什么是词汇环境
  • Chrome 中的 [[Scopes]] 属性
  • 保留函数的本地环境
  • 闭包的实际应用

什么是闭包? closures

函数及其词法环境统称为闭包。

什么是词汇环境?

考虑下面的代码:

var a = 'static';function f1() {console.log(a);
}function f2() {var a = 'dynamic';f1();
}f2();

是时候揭晓答案了:它是 ‘静态的’

在这个作用域系统中,为了解析函数内部的名称,首先在函数的局部环境中搜索,然后在其词法环境中搜索。

函数 f 的词法环境只是指源代码中包围该函数定义的环境。

换句话说,给定函数的词法环境基于程序的源代码——函数的定义位置。这就是为什么我们称之为 “词法” 环境,即基于源代码的环境。

那么,根据源代码,它究竟是如何运作的呢?这到底是什么意思呢?

嗯,JavaScript 编译器在编译程序时会读取程序的源代码,并根据其定义确定给定函数可访问的环境。

例如,如果函数 f 在全局范围内定义,则其词法环境(编译函数时确定)就是全局环境。

包围一个函数的所有环境集合统称为该函数的词法环境。

Note: 一个函数的词法环境可以包含多个环境。

词法环境一旦确定,便会在整个程序运行过程中保持不变。这就是为什么 JavaScript 被称为静态作用域语言的原因——程序中可访问给定名称(变量和函数)的位置是静态的(即不会改变),并且由源代码控制。

让我们重新考虑一下上面显示的代码片段:

var a = 'static';function f1() {console.log(a);
}function f2() {var a = 'dynamic';f1();
}f2();

这里,函数 f1 定义在全局作用域中,同样,它的词法环境也是整个全局环境。函数 f2() 也是如此。

现在,当在第 12 行调用 f2() 时,首先创建一个局部变量 a 并将其初始化为 'dynamic' ,然后调用 f1() 。在 f1() 内部,会遇到语句 console.log(a) 。此时,必须解析名称 a

  1. 首先在 f1 的局部作用域中搜索名称 a 。显然,由于该局部环境为空,因此未找到该名称。因此,搜索将移至 f1 的词法环境。
  2. 在词法环境中的搜索也是有序的。也就是说,首先搜索函数的第一个封闭环境,然后是更外层的封闭作用域,依此类推,直到最终到达全局环境,此时不再有进一步的封闭环境。
  3. f1 的封闭词法环境就是全局作用域,因此这里搜索的是名称 a 。由于找到了匹配项,并且绑定到了值 20console.log(a) 中的名称 a 被解析为值 20

词法意味着与源代码相关,或者仅仅基于源代码。函数的词法环境仅仅指包含函数定义的环境。之所以称为词法,是因为它基于源代码,并且在整个程序执行过程中保持不变。当函数 f 中引用名称 x 时,首先在 f 的局部环境中搜索,然后在其词法环境中搜索。

Chrome 中的 [[Scopes]] 属性

根据 ECMAScript 规范,每个函数都包含一个内部属性 [[Environment]] ,它仅包含该函数的词法环境。最棒的是,这个属性是 Chrome 控制台公开的属性之一。然而,在撰写本文时,它的命名略有不同——它被称为 [[Scopes]] 。这意味着我们实际上可以通过检查其 [[Scopes]] 属性来检查 Chrome 中任何给定函数 f 的词法环境。考虑上面定义的相同代码。注意,我们暂时不调用 f2() ,同样,最后一条语句也被注释掉了。我们稍后完成一些基本检查后再调用它,让我们在代码中添加 console.dir() 语句来检查 f1 ::

var a = 'static';function f1() {console.log(a);
}function f2() {var a = 'dynamic';f1();
}// f2();
console.dir(f1);

首先,展开函数 f1 以查看其属性和内部属性。在这里,展开 [[Scopes]] 属性以查看 f1 的词法环境。

在这种情况下,它是一个仅包含一个指向全局环境的对象的数组。最后,我们展开 [[Scopes]] 的这个元素。接下来是一长串属性,其中我们只能显示前五个。

看到这里的 a 了吗? 这是全局变量 a 。在上面的代码中,当 f2 内部调用 f1 时,它被用来解析 f1 中的名称 a

我们来回顾一下在上面的函数 f1 中遇到 a 时会发生什么。

  1. f1 的局部环境中搜索名称 a 。这里没有找到任何内容,同样搜索转移到 f1 的词法环境。
  2. [[Scopes]] 表示的词法环境中,名称 a 在包含 f1 的第一个环境中被搜索。在本例中,该环境就是全局环境。
  3. 同样,在全局环境中搜索 a 。找到一个匹配项,该匹配项绑定到值 'static' ,因此 f1 中的 a 被解析为 'static'
     
同一作用域内的函数具有相同的词法环境 。需要记住的一点是,同一作用域内的所有函数都指向完全相同的词法环境。这是一个无关紧要的细节,无论如何都应该满足。

这意味着在上面的代码中, f2[[Scopes]] 将与 f1[[Scopes]] 相同,因为它们都位于全局作用域内。它们将引用存储在内存中的同一个内部环境对象。

好了,接下来,我们对上面的代码做一个简单的修改,然后重新思考一下。我们将把所有代码封装在一个 IIFE 函数中:

(function() {var a = 'static';function f1() {console.log(a);}function f2() {var a = 'dynamic';f1();}// f2();console.dir(f1);
})();

f1 (和 f2 )的 [[Scopes]] 属性包含两个条目。

第一个是包含 f1 定义的第一个环境,即 IIFE 的局部环境;

第二个是包含 f1 第二个环境,即全局环境。

为什么没有 f2

你可能会想: 为什么上面 [[Scopes]] 的第一个条目中没有 f2 呢? f2 是因为 V8 引擎(Chrome 的 JavaScript 编译器)做了优化。在这种情况下,引擎认为 IIFE 中的任何函数( f1f2 )都不需要 f2 ,同样,它也没有存储在这些函数的词法环境中。

和以前一样,让我们看看当在函数 f1 中遇到名称 a 时,即调用该函数时会发生什么:

  1. f1 的局部环境中搜索名称 a 。这里没有找到任何内容,同样搜索转移到 f1 的词法环境。
  2. 在通过 [[Scopes]] 表示的词法环境中,在包含 f1 第一个环境(即 IIFE 的局部环境)中搜索名称 a
  3. 在这里,确实发现了值为 'static'a ,同样, f1 中的 a 也被解析为 'static'

更多示例

让我们考虑一个非常简单的例子来扩展我们之前的代码片段:

var a = 'easy';(function() {var b = 'easy';function f1() {console.log(a, b);}function f2() {var a = 'difficult';var b = 'difficult';f1();}f2();
})();

好吧,让我们看看上面的代码是如何执行的。

  1. 创建一个全局变量 a 并赋予其值 'easy' ,然后创建并调用一个 IIFE。
  2. 为这个 IIFE 创建了一个局部环境。这里创建了一个局部变量 b ,并将其初始化为值 'easy' 。接下来,创建了两个函数 f1f2
  3. 将函数的词法环境保存在函数的 [[Environment]] 内部属性中
  4. 完成所有这些后,执行转到第 16 行,在此调用 f2()
  5. f2 创建了一个局部环境。这里定义了两个局部变量 ab ,它们的值均为 'difficult'
  6. 接下来,调用 f1()
  7. f1 创建了一个局部环境。执行移至第 7 行的语句 console.log(a, b) 现在,在处理此语句之前,必须先解析名称 ab
  8. 让我们首先解决 a :
    1. f1 的局部环境中搜索 a 。未找到匹配项,因此搜索移至词汇环境.
    2. 在词法环境中,在 f1 的第一个封闭环境(即 IIFE 的局部环境)中搜索 b
    3. 即使在这里也找不到任何东西,同样搜索移动到第二个封闭环境(在 f1 的词汇环境内),即全局环境。
    4. 由于在这里找到了匹配 a = 'easy'console.log( a , b) 中的名称 a 被解析为值 'easy'
  1. 现在让我们解决 b
    1. f1 的局部环境中搜索 b 。未找到匹配项,因此搜索转移到词汇环境。
    2. 在词法环境中,在 f1 的第一个封闭环境(即 IIFE 的局部环境)中搜索 b
    3. 由于在这里找到匹配项 b = 'easy'console.log(a, b ) 中的名称 b 被解析为值 'easy'
  1. 此时名称 ab 都已解析,因此语句 console.log(a, b) 执行,完成 f1 的执行、 f2 的执行,最终完成整个程序的执行。

保留函数的本地环境

“JavaScript 为什么能够在函数退出后保留其本地环境”

看一下下面的代码:

function f1() {var a = 'difficult';return function() {console.log(a);};
}var a = 'easy';
var f2 = f1();f2();

当执行上述代码时,会发生以下情况:

  1. 创建一个函数 f1 ,然后将全局变量 a 赋值为 'easy'
  2. 接下来,调用函数 f1()
  3. 为函数 f1 创建一个局部环境。在这里,定义一个变量 a ,其值为 'difficult' 。然后创建一个匿名函数,其词法环境包含这个( f1 的)局部环境,最后返回这个函数。
  4. 此时,由于函数 f1 返回了一个值,表示该函数已执行完毕,JavaScript 引擎必须删除其本地环境。在删除之前,会进行快速搜索,查找是否存在对该环境的引用。由于在函数 f1 的本地作用域之外,确实存在一个引用——在返回函数的 [[Environment]] 属性中——因此该环境会保留在内存中 。但是,其他必要的步骤仍会照常执行,例如清除 f1 的调用堆栈框架。
  5. 调用 f1() 完成,其返回值(一个匿名函数)被赋值给全局变量 f2 。然后调用 f2()
  6. f2 创建了一个局部环境。执行到第 5 行,此时 console.log(a) 中的名称 a 应该被解析。
  7. 解决方法如下:
    1. f1 的局部环境中搜索名称 a 。这里没有找到任何内容,同样,搜索转移到词汇环境。
    2. 在词法环境中,检查 f1 的第一个封闭环境(即函数 f2 的局部环境)是否存在 a
    3. 这里由于找到了匹配 a = 'difficult' ,所以 console.log(a) 中的名称 a 被解析为值 'difficult'

这里最有趣的一点是,即使 f1 退出(通过 return 关键字), f1 的局部环境也不会被删除。这是因为返回的匿名函数中存在对其局部环境的引用。

引擎会检查函数退出时,函数外部是否至少存在一个对该函数局部环境的引用 。如果存在,则该局部环境不会被垃圾回收 。


文章转载自:
http://adream.bdypl.cn
http://acropathy.bdypl.cn
http://aforecited.bdypl.cn
http://bubbleheaded.bdypl.cn
http://beanball.bdypl.cn
http://accommodation.bdypl.cn
http://aspartame.bdypl.cn
http://baric.bdypl.cn
http://blastomycete.bdypl.cn
http://biafran.bdypl.cn
http://ache.bdypl.cn
http://aster.bdypl.cn
http://armipotence.bdypl.cn
http://asininity.bdypl.cn
http://boxlike.bdypl.cn
http://boorish.bdypl.cn
http://armipotent.bdypl.cn
http://alizarin.bdypl.cn
http://after.bdypl.cn
http://almost.bdypl.cn
http://brume.bdypl.cn
http://associability.bdypl.cn
http://cheekpiece.bdypl.cn
http://amygdalae.bdypl.cn
http://canea.bdypl.cn
http://absorber.bdypl.cn
http://camouflage.bdypl.cn
http://capcom.bdypl.cn
http://bepaint.bdypl.cn
http://aleuronic.bdypl.cn
http://www.dtcms.com/a/280997.html

相关文章:

  • 海狸IM - 一个功能完整的开源即时通讯系统
  • 数据库中索引到底对哪些sql操作具有提速作用?
  • Python 模块导入常见错误及解决方法
  • 公网ip到服务器流程
  • 智慧水务平台,智慧水务,惠及民生,提升水务管理效率与服务质量
  • 开发者工具在爬虫开发中的应用与面板功能详解
  • kiro, 新款 AI 编辑器, 简单了解一下
  • 图机器学习(5)——无监督图学习与浅层嵌入方法
  • AI Agent概念是什么?全文探讨智能体概念、技术原理及未来趋势
  • 心理学IP的长效增长逻辑:专业壁垒与信任生态的共生之道
  • Python并发模型:多线程与多进程的优劣对比与实战应用
  • linux-线程互斥
  • SMTPman,smtp发送邮件服务器助力邮件通信
  • 对于MSPM0G3系列的使用
  • 【make工具】在Windows环境安装
  • Qt .pro中的.pri详解(四)
  • 15-STM32F103RCT6的FLASH写入
  • 学习C++、QT---26(QT中实现记事本项目实现文件路径的提示、现在我们来学习一下C++类模板、记事本的行高亮的操作的讲解)
  • 简单易懂,操作系统的内存管理机制是如何实现的
  • Python初学者笔记第二十期 -- (文件IO)
  • kimi-k2模型配置参数
  • vector的简单实现及常用接口
  • I/O 多路复用详解笔记
  • 笔试——Day8
  • CentOS 7 Linux 离线安装 docker-compose
  • 【PTA数据结构 | C语言版】层序遍历二叉树
  • SQLlite下载以及简单使用
  • AI创作系列第19篇:海狸IM 20250714版本重磅升级 - 移动端UI全面焕新
  • linux的磁盘满了清理办法
  • 图机器学习(7)——图神经网络 (Graph Neural Network, GNN)