当前位置: 首页 > 科技观察

找出JavaScript中的闭包和变量作用域

时间:2023-03-17 13:45:25 科技观察

JavaScript是一种非常面向函数的语言。它给了我们很大的自由。在JavaScript中,我们可以随时创建函数,将一个函数作为参数传递给另一个函数,然后从完全不同的代码位置调用它。我们已经知道函数可以访问它们之外的变量。但是,如果函数创建后外部变量发生变化怎么办?该函数将获得新值还是旧值?如果您将一个函数作为参数传递并从代码中的另一个地方调用它,该函数将访问位置的新外部变量?让我们通过本文来了解在这些场景和更复杂的场景中到底发生了什么。我们将在这里探索let/const。在JavaScript中,有三种声明变量的方式:let、const(现代方式)、var(旧方式)。在本文的示例中,我们将使用let来声明变量。用const声明的变量也表现相同(译注:在作用域等方面与let相同),所以本文也处理用const声明的变量。oldvar与上述两者有明显区别,我们将在Old“var”中详细介绍。1.代码块如果在代码块{...}中声明了一个变量,那么该变量只在代码块中可见。例如:{//使用在代码块外不可见的局部变量做一些工作letmessage="Hello";//仅在该代码块内可见alert(message);//Hello}alert(message);//Error:messageisnotdefined我们可以使用它来隔离执行自己的任务并使用仅属于自己的变量的一段代码:{//showmessageletmessage="Hello";alert(message);}{//showanothermessageletmessage="Goodbye";alert(message);}如果这里没有代码块,会报错。请注意,如果我们使用let重复声明已有的变量,如果对应的变量没有单独的代码块,就会报错://displaymessageletmessage="Hello";alert(message);//displayanothermessageletmessage="再见";//错误:variablealreadydeclaredalert(message);forif,forandwhile等,在{...}中声明变量也只在内部可见:if(true){letphrase="Hello!";alert(phrase);//Hello!}alert(phrase);//错误,没有这样的变量!在这里,当执行if时,则以下警报将看不到该短语,因此会出现错误。太好了,因为这允许我们创建特定于if分支的块局部变量。for和while循环也是如此:for(leti=0;i<3;i++){//变量i只在这个for循环内部可见alert(i);//0,then1,then2}alert(i);//错误,nosuchvariable视觉上,让i在{...}之外。但是for结构在这里很特殊:在其中声明的变量被认为是块的一部分。二、嵌套函数当一个函数是在另一个函数中创建的,那么这个函数就被称为“嵌套”。这在JavaScript中很容易实现。我们可以使用嵌套来组织代码,比如这样:functionsayHiBye(firstName,lastName){//辅助嵌套函数使用如下函数alert("Bye,"+getFullName());}这里创建的嵌套函数getFullName()是为了更方便。它可以访问外部变量,因此可以返回全名。嵌套函数在JavaScript中很常见。更有趣的是,可以返回嵌套函数:作为新对象的属性或作为结果。然后它可以在其他地方使用。无论在哪里调用,它仍然可以访问相同的外部变量。下面的makeCounter创建了一个“计数器”函数,每次调用它时都会返回下一个数字://0alert(counter());//1alert(counter());//2虽然很简单,但是稍加修改就有很强的实用性,比如作为随机数生成器,为自动化测试。这是如何运作的?如果我们创建多个计数器,它们会独立吗?这里的变量是怎么回事?了解这些东西对于掌握JavaScript的整体知识非常有帮助,对于更复杂的场景也是有帮助的。非常有益。因此,让我们继续深入挖掘。3.LexicalEnvironment为了使内容更加清晰,这里将对LexicalEnvironment逐级进行讲解。步骤1.变量在JavaScript中,每个运行的函数、代码块{...}和整个脚本都有一个内部(隐藏)关联对象,称为词法环境。一个LexicalEnvironment对象由两部分组成:EnvironmentRecord-一个将所有局部变量存储为其属性的对象(包括一些其他信息,例如this的值)。对与外部代码关联的外部词法环境的引用。“变量”只是特殊内部对象环境记录的一个属性。“获取或修改一个变量”是指“获取或修改词法环境的一个属性”。例如,这个没有函数的简单代码只有一个词法环境:这就是所谓的与整个脚本相关的全局词法环境。上图中,矩形代表环境记录(变量存储),箭头代表外部引用。全局词法环境没有外部引用,所以箭头指向空。随着代码开始并继续运行,词法环境发生变化。下面是更长的代码:右边的矩形显示了全局词法环境在执行过程中是如何变化的:当脚本开始运行时,词法环境预先填充了所有声明的变量。最初,它们处于“未初始化”状态。这是一种特殊的内部状态,这意味着引擎知道该变量,但在使用let声明之前不能引用它。几乎就像变量不存在一样。然后let短语定义进来了,它还没有被赋值,所以它的值是undefined。从这一刻起,我们就可以使用变量了。短语被分配了一个值。修改了phrase的值。现在看起来很简单,不是吗?变量是与当前正在执行的(代码)块/函数/脚本相关的特殊内部对象的属性。操作变量实际上就是操作对象的属性。词法环境是规范对象:“词法环境”是规范对象:它只是存在于编程语言规范中的“理论”对象,用于描述事物的工作方式。我们无法在代码中获取该对象并直接对其进行操作。但JavaScript引擎也可以对其进行优化,例如清除未使用的变量以节省内存和执行其他内部技巧等,但显式行为应与上述相同。Step2.函数声明函数实际上是一个值,就像一个变量。不同之处在于函数声明的初始化是立即完成的。当LexicalEnvironment被创建时,函数声明立即成为一个随时可用的函数(不像let,它在声明之前不可用)。这就是为什么我们可以在定义(函数声明)之前调用函数声明。例如,这是添加函数时全局词法环境的初始状态:通??常这种行为仅适??用于函数声明,而不适用于我们将函数分配给变量的函数表达式,例如letsay=function(name)....步骤3.内部和外部词法环境当函数运行时,在调用开始时,会自动创建一个新的词法环境来存储调用的局部变量和参数。例如,对于say("John")它看起来像这样(当前执行位置在箭头标记的行上):在这个函数调用期间我们有两个词法环境:内部环境(用于函数调用)和外部环境one(global):当前执行say对应的内部词法环境。它只有一个属性:名称,函数的参数。我们正在调用say("John"),所以name的值为“John”。外部词法环境是全局词法环境。它有一个短语变量和函数本身。内部词汇环境是指外部。当代码想要访问一个变量时——首先搜索内部词法环境,然后搜索外部环境,然后搜索外部环境,依此类推直到全局词法环境。如果到处都找不到该变量,在严格模式下会报错(在非严格模式下,为了向后兼容,给一个未定义的变量赋值会创建一个全局变量)。在本例中,搜索过程如下:对于name变量,当say中的alert尝试访问name时,立即在内部词法环境中找到。当它试图访问短语时,里面没有短语,所以它按照对外部词法环境的引用来找到它。步骤4.返回函数让我们回到makeCounter示例。functionmakeCounter(){letcount=0;returnfunction(){returncount++;};}letcounter=makeCounter();在每次makeCounter()调用开始时,都会创建一个新的词法环境对象来存储makeCounterruntime的变量。因此,我们有两个嵌套的词法环境,就像上面的例子:不同的是在makeCounter()的执行过程中,创建了一个只有一行的嵌套函数:returncount++。我们还没有运行它,只是创建了它。所有函数都是“天生的”,记住创建它们的词法环境。从技术上讲,这里没有魔法:所有函数都有一个名为[[Environment]]的隐藏属性,它包含对创建函数的LexicalEnvironment的引用。因此,counter.[[Environment]]引用了{count:0}个词法环境。这就是函数如何记住它是在哪里创建的,而不管函数是在哪里被调用的。[[Environment]]引用在函数创建和持久化时设置。稍后,当调用counter()时,会为调用创建一个新的词法环境,并在counter处获取其外部词法环境引用。[[Environment]]:现在,当counter()中的代码查找count变量时,它首先搜索自己的词法环境(它是空的,因为那里没有局部变量),然后是外部makeCounter()的词法环境,并修改它找到的地方。在变量所在的词法环境中更新变量。这是执行后的状态:如果我们多次调用counter(),count变量将在同一位置递增到2、3等。闭包开发人员通常应该知道通用编程术语“闭包”。闭包意味着内层函数始终可以访问它所在的外层函数中声明的变量和参数,即使在外层函数返回(生命结束)之后。在某些编程语言中,这是不可能的,或者应该以特殊的方式编写函数来实现它。但如上所述,在JavaScript中,所有函数本质上都是闭包(有一个例外,在“newFunction”语法中有描述)。也就是说:JavaScript中的函数通过隐藏的[[Environment]]属性自动记住它们的创建位置,因此它们都可以访问外部变量。在面试中,前端开发人员通常会被问到“什么是闭包?”正确答案应该是闭包的定义,并解释为什么JavaScript中的所有函数都是闭包,并且可能还有[[Environment]]属性和词法环境如何工作的技术细节。四、垃圾回收通常,函数调用完成后,词法环境和其中的所有变量都会从内存中删除。因为现在不再有对它们的引用。与JavaScript中的任何其他对象一样,词法环境仅在可访问时保存在内存中。但是,如果有一个嵌套函数在函数结束后仍然可以到达,它就会有一个[[Environment]]属性来引用词法环境。在下面的示例中,即使在函数执行完成后,词法环境仍然可以访问。所以这个嵌套函数仍然有效。例如:functionf(){letvalue=123;returnfunction(){alert(value);}}letg=f();//g.[[Environment]]存储了对应f()的词法环境的引用call注意,如果f()被多次调用,返回的函数被保存,所有对应的LexicalEnvironment对象也被保存在内存中。下面代码中有3个这样的函数:functionf(){letvalue=Math.random();returnfunction(){alert(value);};}//数组中有3个函数,每个函数都有对应的f词法环境关联()letarr=[f(),f(),f()];当词法环境对象变得不可访问时,它就会死亡(就像任何其他对象一样)。换句话说,它仅在至少一个嵌套函数引用它时才存在。在下面的代码中,嵌套函数被删除后,其封闭的词法环境(及其值)也将从内存中删除:functionf(){letvalue=123;returnfunction(){alert(value);}}letg=f();//当g函数存在时,该值会保留在内存中g=null;//...现在内存被清理了五、实际开发中的优化我们可以看到,理论上,当a函数是可达的,它之外的所有变量也将存在。但实际上,JavaScript引擎会尝试对其进行优化。他们分析变量的使用情况,如果代码中显而易见,则删除未使用的外部变量。V8(Chrome、Edge、Opera)中的一个重要副作用是此类变量在调试中不可用。打开Chrome的开发者工具并尝试运行下面的代码。当代码执行暂停时,在控制台输入alert(value)。functionf(){letvalue=Math.random();functiong(){debugger;//在控制台中:输入alert(value);Nosuchvariable!}returng;}letg=f();g();如你所见--没有这样的变量!理论上,它应该是可访问的,但是引擎优化了它。这可能会导致有趣的(如果不是那么耗时的话)调试问题。其中之一——我们可以看到的是一个同名的外部变量,而不是预期的变量:letvalue="Surprise!";functionf(){letvalue="theclosestvalue";functiong(){debugger;//在控制台中:输入alert(value);Surprise!}return;}letg=f();g();你真的应该知道V8引擎的这个特性。如果你打算使用Chrome/Edge/Opera进行代码调试,迟早会遇到这样的问题。这不是调试器错误,而是V8的一个特殊功能。也许以后会修改。您始终可以通过运行本文中的示例来进行检查。