当前位置: 首页 > Web前端 > JavaScript

js作用域、作用域链及其一些优化_0

时间:2023-03-26 23:16:15 JavaScript

前言作用域和作用域链是所有JavaScript开发者每天都会接触和应用的。无论是面试中作用域链的面试考察,还是日常代码开发中变量和作用域链的构建,几乎无处不在。它就像一顶好的厨师帽,我们每次进厨房都要戴上、戴上。如果没有把它整齐地戴在头上,你就不是一个好的JavaScript工程师。其实作为一名前端工程师,我也有过疑惑:基本上所有的计算机语言都有作用域的概念,但是为什么JavaScript开发者总是执着于作用域的概念呢?直到在写代码的过程中多次遇到涉及作用域的问题,才逐渐理解这个问题,并认真研究。而这篇文章,我想和你谈谈JavaScript作用域和作用域链,以及我们针对它们的一些代码优化技巧。内容几乎所有编程语言最基本的功能之一就是在变量中存储一个值,并且以后能够访问和修改这个值。这种能力的引入是程序状态存在的基础。但是,能力的引入需要我们解决几个问题,比如:变量存储在哪里?它以什么形式存储?当需要读取和修改变量时,如何获取变量?显然,要解决这些问题,我们需要一套精心设计的规则来存储变量并使以后更容易找到它们。同时,一套完整规则的设计会衍生出附加规则的概念。范围是从这套规则衍生出来的概念。范围我们可以将范围理解为上述规则集下的限定范围。作用域的职责是在这个有限的作用域内按照这套设计好的规则存储声明的变量,并为修改变量提供支持。在变量访问权限安全方面,scope还承担了权限保护的作用,保护当前作用域内的变量不被外部作用域访问。以此类推,我们可以将作用域想象成一个气泡。在此气泡中声明的变量成员包含在其中。每个bubble都配备了一位有原则的管家,负责管理所有成员并保护他们免受其声明的职位和要求的影响。当气泡中的代码语句要访问和修改变量成员时,管家会将相应的访问和修改操作与变量成员的要求关联起来。随着ECMAScript标准的不断发展和完善,JavaScript目前有四种作用域:全局作用域(GlobalScope):JavaScript语言环境的顶级作用域,在语言环境初始化时创建。ModuleScope:由ECMAScript模块标准(ESModule)引入,在解析ECMAScript模块时创建。函数作用域:当函数声明function(){}或()=>{}时创建。BlockScope:由ECMAScript2015的变量声明标识符let和const引入,使用这两个进行变量声明时,根据最近的一对大括号{}创建。/*Globalscopestart,在初始化JavaScript语言环境时创建*//*Modulescopestart,在作为ESModule解析执行时创建*/letname='Wu';{/*Block-level作用域开始,const变量声明在最近的花括号内创建{}*/constprefix=Hardy;名字=前缀+名字;/*block-levelscopeend*/}exportfunctionsayMyName(myName){/*functionscopestart,函数声明时自动创建,默认初始化包含函数的形参变量*/if(!myName){/*块级作用域开始*/constnoNameAnswer='对不起!';console.log(noNameAnswer);返回;/*块级作用域结束*/}constwordPrifix='嗨!我的名字是';constanswer=wordPrifix+myName+'.';控制台日志(答案);/*functionscopeend*/*moduleScopeend*//*globalscopeend*/作用域的嵌套作用域在使用上有嵌套的特点。作用域可以在自身内部创建新的作用域,形成内部作用域和外部作用域的嵌套关系。全局作用域作为JavaScript的初始作用域,是所有其他作用域的最外层作用域。另外,每个ESModule都有自己的顶级作用域(top-levelscope)。模块中的顶级作用域变量和函数都包含在本模块的顶级作用域中,模块作用域的外部作用域为全局作用域。函数作用域和块作用域相对灵活,可以相互嵌套。参考视频讲解:进入学习范围的一些实现细节在JavaScript中,每个函数、代码块{...}和脚本脚本执行之前,都会有一个对应的内部称为词法环境(LexicalEnvironment)的关联对象是创建。LexicalEnvironment由两部分组成:EnvironmentRecord:一个对象,将所有局部变量存储为其属性(包括一些执行上下文信息,例如this的值)。外部词法环境引用(Outer):对外部词法环境的引用,与外部词法环境相关联。在代码执行过程中,每一个局部变量和局部函数的声明都会作为属性字段添加到环境记录中,后续读取变量和函数都会通过相应的标识符在环境记录中查找。根据以上概念,我们可以通过如下对象结构来理解词法环境:lexicalEnvironment={environmentRecord:{:,:,},outer:,}让我们通过下面的代码示例来理解词法环境:/*当前模块运行时,创建模块的词法环境,moduleLexicalEnvironment={environmentRecord:{name:,sayName:,},outer:,}*/letname='Hardy';/*变量声明和赋值,修改环境记录的字段属性值,moduleLexicalEnvironment={environmentRecord:{name:'Hardy',sayName:,},outer:,}*/functionsayName(myName){/*函数执行时,创建函数的词法环境,functionLexicalEnvironment={environmentRecord={myName:'Hardy',},outer:,}*//*通过读取环境记录对应的标识符字段属性值获取myName的变量值*/console.log(myName);}说名字();//Hardy,我们来分析一下上面的代码示例。根据提前声明的特点,变量名和函数sayName会在模块的词法环境创建时被添加到环境记录中。但是由于let的临时死区特性,变量名在自己声明和初始化赋值之前处于未引用和未初始化状态。函数的声明是不同的。除了提前声明外,函数的引用也会被初始化。这就是为什么我们可以在函数执行其声明语句之前调用函数。另外,当一个函数的词法环境被创建时,相应函数的参数会在环境记录中被初始化,并被赋值为调用函数时传递的值或函数参数的默认值。在外引用方面,模块LexicalEnvironment的外引用指向了JavaScript最外层的全局词法环境globalLexicalEnvironment,而函数LexicalEnvironment的外引用指向了外部模块LexicalEnvironment。我们可以看出词法环境是JavaScript内部对作用域概念的技术实现。它是JavaScript引擎在创建执行上下文时创建的用于存储变量和函数声明的环境。在代码执行期间,通过它访问存储在其中的变量和函数。代码执行后,执行上下文会被销毁并从栈中回收,词法环境也会根据情况被销毁(如果词法环境被其他外部词法环境引用,则不会被销毁和回收,例如闭包)。作用域链作用域可以嵌套,嵌套作用域可以访问外层作用域声明的变量和函数。通过上面对词法环境的介绍,我们大概知道作用域的嵌套关系是通过在词法环境的外部词法环境中引用outer来实现的。该词法环境的外部引用的关联构建了词法环境的单向链。这就是我们常说的作用域链。本质上,作用域链是由JavaScript引擎为其执行的代码维护的词法环境链。代码执行时对外部作用域中变量的引用,通过这个链来查找、读取、修改变量。代码执行中对变量的访问大致如下:当代码要访问变量时,首先搜索当前内部的词法环境。如果搜索成功,则返回一个变量值或变量引用,结束搜索。如果搜索不到,则通过外引用继续搜索外部词法环境,以此类推,直到搜索到全局词法环境。如果在任何地方都找不到该变量,则在严格模式下会报错。根据上面的概念,我们来看下面这个例子:letphrase='Hello';functionsayHello(name){/*函数的作用域链,functionLexicalEnvironment{name:'Hardy'}==outer==>moduleLexicalEnvironment{phrase:'Hello'}==outer==>globalLexicalEnvironment变量名被找到并从当前函数LexicalEnvironment中获取,变量phrase沿着作用域链搜索,从moduleLexicalEnvironment中找到并获取*/console.log(`${phrase},${name}!`);}sayHello('Hardy');//你好,哈代!上面的例子中,函数sayHello在内部引用了name和phrase这两个变量,函数调用执行时会创建函数LexicalEnvironment>moduleLexicalEnvironment>globalLexicalEnvironment的作用域链。其中,变量名是作为函数参数属于当前函数作用域的局部变量,该变量可以直接从当前函数的词法环境函数LexicalEnvironment中找到并返回相关信息。变量短语属于外部作用域声明的变量,存放在外部模块词法环境moduleLexicalEnvironment中。函数sayHello引用了变量phrase,它会先从自身函数词法环境的functionLexicalEnvironment中查找。如果找不到,它会参考外部词法环境找到模块词法环境moduleLexicalEnvironment,并继续从中查找变量,如果找到相关信息则返回该变量。值得注意的是console.log()是全局内置对象console上的方法,调用该方法需要引用console。对该变量的引用将沿着作用域链搜索到全局LexicalEnvironment,从中查找并返回相关变量信息。变量标识符解析和引用的过程就是沿着作用域链遍历,查找变量是否在作用域链节点中,并返回变量的相关信息的过程。相关优化结合上述标识符解析过程、作用域和作用域链的关系,我们可以了解到,变量标识符解析的性能与变量标识符在作用域链中的位置密切相关。变量标识符所在的作用域节点越靠近整个作用域链的前端,沿作用域链的迭代搜索次数越少,变量标识符的解析速度越快,性能越好。这种标识符解析性能的规律使我们可以得出以下使用变量的优化点:对于频繁引用的外部作用域中的变量,可以根据情况将其声明为当前作用域中的局部变量并赋值。减少范围增强with语句的使用。多次引用外部作用域变量标识符会导致在执行过程中沿着作用域链频繁执行标识符解析。这个搜索在第一次解析引用的时候是必须的,但是后面的解析引用都是重复的。的。通过在当前作用域内声明和赋值的方式将外部作用域变量赋值给局部变量,可以优化后续搜索需要经过的作用域链节点数量,获得一定的性能提升。with语句可以在当前作用域链的前面临时增加一个词法环境,从而可以原地构造和使用新的作用域链。但这种方式的问题也很明显:作用域链变长,除了前端词法环境中存储的变量外,其他变量的标识符解析性能会变差。因此,我们应该减少with语句的使用。总结随着JavaScript语言的发展,语言中作用域的类型也越来越丰富。它们不再局限于函数作用域作为最小变量声明作用域,而是可以基于更小的跨级作用域进行管理。我们的变量指的是范围。变量管理变得更加灵活和安全。作用域链是作用域链嵌套结构的产物,所有变量标识符的解析和引用都会沿着作用域链查找。词法环境是JavaScript范围的内部技术实现。对词法环境的深入了解,也让我们更好地理解解析变量标识符时代码的内部执行过程。同样基于这个过程,我们大致总结出作用域和变量使用两个性能优化点。作用域的使用是每个JavaScript开发者的必修课。只有深刻理解,才能在使用时不再迷茫。它就像空气一样,存在于JavaScript的很多地方,值得去了解一下。