在变量中存储值是编程中的一个基本概念。变量的“范围”决定了它在整个程序中何时可用和不可用。了解JavaScript中的变量作用域是在该语言中打下坚实基础的关键之一。本文将解释JavaScript的作用域系统是如何工作的。您将了解声明变量的不同方式、局部作用域和全局作用域之间的区别,以及称为“提升”的东西-一种JavaScript怪癖,它可以将看似无辜的变量声明变成一个微妙的错误。变量范围在JavaScript中,变量的范围由变量声明的位置控制,它定义了程序中可以访问特定变量的部分。目前,在JavaScript中有三种声明变量的方法:使用oldvar关键字,以及使用newlet和const关键字。在ES6之前,使用var关键字是声明变量的唯一方法,但现在我们可以使用let和const,它们的规则更严格,代码更不容易出错。我们将在下面探讨所有三个关键字之间的区别。范围规则因人而异。JavaScript有两个作用域:全局和局部。局部作用域有两种变体:旧函数作用域和ES6引入的新块作用域。值得注意的是,函数作用域实际上是一种特殊类型的块作用域。全局作用域在脚本中,最外层的作用域是全局作用域。在此范围内声明的任何变量都成为全局变量,可以从程序中的任何位置访问://GlobalScopeconstname="Monique";functionsayHi(){console.log(`Hi${name}`);}sayHi();//你好Monique如这个简单示例所示,变量名称是一个全局变量。它是全局定义的,可以在整个程序中访问。然而,尽管这看起来很方便,但在JavaScript中不鼓励使用全局变量。例如,这是因为它们可能会被其他脚本或程序中的其他地方覆盖。局部作用域在块内声明的任何变量都属于该特定块并成为局部变量。JavaScript中的var函数定义了使用、let和const声明的变量的范围。在该函数内声明的任何变量只能从该函数和任何嵌套函数内访问。代码块(if、for等)仅定义使用letandconst关键字声明的变量的范围。var关键字仅限于函数作用域,这意味着新作用域只能在函数内部创建。letandconst关键字具有块作用域,它为声明它们的任何块创建一个新的本地作用域。您还可以在JavaScript中重新定义独立的代码块,它们类似地界定一个范围:{//standaloneblockscope}函数和块范围可以嵌套。在这种情况下,使用多个嵌套范围,可以在其自己的范围内或从内部范围访问变量。但在其范围之外,该变量不可访问。帮助可视化范围的简单示例为清楚起见,让我们使用一个简单的比喻。我们世界上的每个国家都有边界。这些边界内的一切都属于国家的权限。每个国家都有很多城市,每个城市都有自己的城市范围。国家和城市就像JavaScript函数或块。他们有自己的本地范围。大陆也是如此。尽管它们很大,但它们也可以定义为语言环境。另一方面,世界海洋不能定义为具有局部范围,因为它们实际上包含所有局部对象-大陆、国家和城市-因此,它们的范围被定义为全球范围。让我们在下一个例子中想象一下:varlocales={europe:function(){//欧洲大陆的局部作用域varmyFriend="Monique";varfrance=function(){//法国国家的本地范围varparis=function(){//巴黎城市的本地范围console.log(myFriend);//输出:Monique};巴黎();};法国();}};locales.europe();在这里,myFriend变量可以从paris函数中获得,因为它是在france函数的外部范围内定义的。如果我们交换myFriend变量和控制台语句,我们将得到ReferenceError:myFriendisnotdefined因为我们无法从外部作用域到达内部作用域。现在我们了解了什么是本地和全局范围以及它们是如何创建的,是时候开始学习JavaScript解释器如何使用它们来查找特定变量了。回到给定的类比,假设我想找一个叫莫妮克的朋友。我知道她住在巴黎,所以我开始在那里寻找。当我在巴黎找不到她时,我会更上一层楼,将搜索范围扩大到整个法国。但话又说回来,她不在那里。接下来,我进一步扩大了我的搜索范围。最后,我在意大利找到了她,在我们的例子中是欧洲的本地范围。在前面的例子中,我的朋友Monique有代表myFriend的变量。在最后一行中我们调用了europe()函数,它调用了france(),最后当调用paris()函数时,搜索开始。JavaScript解释器从当前执行的范围开始工作,直到找到有问题的变量。如果在任何范围内都找不到该变量,则会引发异常。这种类型的查找称为词法(静态)范围。程序的静态结构决定了变量的作用域。变量的范围由其在源代码中的位置定义,嵌套函数可以访问在其外部范围内声明的变量。无论从何处调用函数,甚至如何调用函数,其词法范围都仅取决于声明函数的位置。现在让我们看看新的块范围是如何工作的:functiontestScope(n){if(true){constgreeting='Hello';让名字=n;console.log(问候语+“”+姓名);//输出:Hello[name]}console.log(greeting+""+name);//输出:ReferenceError:greetingisnotdefined}testScope('David');在这个例子中,我们可以看到使用and声明的andgreeting变量名在块外是不可访问的。constletif.现在让我们替换andconst看看会发生什么:functiontestScope(n){if(true){vargreeting='Hello';变量名=n;console.log(问候语+“”+姓名);//输出:Hello[name]}console.log(greeting+""+name);//输出:Hello[name]}testScope('David');如您所见,当我们使用var关键字时,该变量在整个函数范围内都是可访问的。在JavaScript中,可以在多层嵌套作用域中指定同名变量。在这种情况下,局部变量优先于全局变量。如果声明了同名的局部变量和全局变量,那么在函数或块中使用时,局部变量优先。这种类型的行为称为阴影。简而言之,内部变量会影响外部变量。这是JavaScript解释器在尝试查找特定变量时使用的确切机制。它从当时正在执行的最内层作用域开始,一直持续到找到第一个匹配为止,而不管外层是否存在其他同名变量。让我们看一个例子:vartest="I'mglobal";functiontestScope(){vartest="I'mlocal";控制台日志(测试);}测试范围();//输出:我是localconsole.log(test);//output:I'mglobal即使名称相同,函数执行后局部变量也不会覆盖全局变量testScope()。但情况并非总是如此。让我们考虑一下:vartest="I'mglobal";functiontestScope(){test="我是本地人";控制台日志(测试);}控制台日志(测试);//输出:我是globaltestScope();//输出:我是localconsole.log(test);//output:I'mlocal(全局变量重新赋值)这一次,局部变量test覆盖了同名的全局变量。当我们在testScope()函数中运行代码时,全局变量被重新分配。如果一个局部变量在没有先用关键字声明的情况下被赋予了值var,它就变成了一个全局变量。为避免此类不良行为,您应该始终在使用局部变量之前声明它们。在函数中使用关键字var声明的任何变量都是局部变量。声明变量被认为是最佳实践。注意:在严格模式下,没有先声明就给变量赋值是错误的。提升JavaScript解释器在幕后执行许多事情,其中??之一就是“提升”。如果您不知道这种“隐藏”行为,可能会造成很多混乱。考虑JavaScript变量行为的最佳方式是始终将它们可视化为由两部分组成:声明和初始化/赋值:varstate;//变量声明state="ready";//变量赋值varstate="ready";//声明加赋值在上面的代码中,我们首先声明了变量state,然后我们给它赋值“ready”。在最后一行代码中,我们看到这两个步骤可以合并。但你需要记住的是,即使它们看起来像一个语句,JavaScript引擎实际上将单个语句视为两个单独的语句,就像示例的前两行一样。我们已经知道在范围内声明的任何变量都属于该范围。但我们还不知道的是,无论变量在特定范围内的何处声明,所有变量声明都会移至其范围(全局或局部)的顶部。这称为提升,因为变量声明被提升到范围的顶部。请注意,boost仅移动声明。任何分配都留在原地。让我们看一个例子:console.log(state);//输出:undefinedvarstate="ready";如您所见,当我们记录state的值时,输出是未定义的,因为我们在实际赋值之前引用了它。您可能希望抛出一个ReferenceError,因为状态尚未声明。但你不知道的是,undefined变量在幕后被声明并初始化为默认值。以下是JavaScript引擎解释代码的方式:varstate;//移动到topconsole.log(state);状态=“准备好”;//留在原地重要的是要注意变量没有物理移动。Hoisting只是一个描述JS引擎在幕后做了什么的模型。现在,让我们看看提升如何与let变量一起工作:{//Temporaldeadone(TDZ)从作用域的开头开始console.log(state);//output:"ReferenceError:Cannotaccess'state'beforeinitializationletstate="ready";//TDZ结束。TDZ在实际变量声明处结束}在这个例子中,控制台输出不是未定义的,而是引用错误thrown.Why?letvsvariable,varvariable它们在完全初始化之前是不能读/写的。它们只是在实际声明它们的代码中被完全初始化。所以let变量声明被提升而不是用未定义的值初始化,这是变量var的情况。从块的开头到实际变量声明的部分称为临时死区。这是一种确保更好的编码实践的机制,强制您在使用变量之前声明它。如果我们移动TDZ的控制台语句,我们得到了预期的输出:ready。{//Temporaldeadone(TDZ)在范围的开头开始letstate="ready";//TDZ结束。TDZ在实际变量声明处结束console.log(state);//output:ready}withkey用单词const声明的变量与变量let具有相同的行为。函数提升也会影响函数声明。但在我们看一些例子之前,让我们了解函数声明和函数表达式之间的区别:functionshowState(){}//functiondeclarationvarshowState=function(){};//函数表达式区分函数声明和函数表达式做函数表达式最简单的方法是检查函数这个词在语句中的位置。如果函数是语句中的第一件事,那么它就是一个函数声明。否则,它是一个函数表达式。函数声明已完全提升。这意味着函数的整个主体都移到了顶部。这允许您在声明之前调用该函数:showState();//输出:ReadyfunctionshowState(){console.log("Ready");}varshowState=function(){console.log("Idle");};上述代码起作用的原因是JavaScript引擎将showState()函数的声明及其所有内容移到了作用域的开头。代码解释如下:functionshowState(){//移到顶部(函数声明)console.log("Ready");}varshowState;//移动到顶部(变量声明)showState();showState=function(){//留在原地(变量赋值)console.log("Idle");};您可能已经注意到,只有函数声明被提升了,而函数表达式却没有。将函数分配给变量时,规则与变量提升相同(只是声明被移动,赋值留在原地)。在上面的代码中,我们看到函数声明优先于变量声明。在下一个示例中,我们将看到当我们有一个函数声明和一个变量赋值时,最后一个优先:varshowState=function(){console.log("Idle");};functionshowState(){console.日志(“准备就绪”);}showState();//output:Idle这一次,我们在代码的最后一行调用了showState()函数,这改变了情况。现在我们得到输出“Idle”。这是它在被JavaScript引擎解释时的样子:functionshowState(){//移到顶部(函数声明)console.log("Ready");}varshowState;//移到顶部(变量声明)showState=function(){//留在原处(变量赋值)console.log("Idle");};showState();注意:箭头函数的工作方式与函数表达式相同。课程类声明也以与使用let语句声明的变量类似的方式提升://在声明之前使用Person类varuser=newPerson('David',33);//输出:ReferenceError:Cannotaccess'Person'beforeinitialization//类声明classPerson{constructor(name,age){this.name=name;这个。年龄=年龄;在这个例子中,我们可以看到在声明之前使用类Person会产生类似let变量引用错误的内容。要解决这个问题,我们必须在声明后使用类Person://类声明classPerson{constructor(name,age){this.name=name;这个。年龄=年龄;}}//在声明后使用Person类varuser=newPerson('David',33);控制台日志(用户);也可以使用类表达式,使用var或let变量const声明语句来创建类://UsingthePersonclassconsole.log(typeofPerson);//输出:undefinedvaruser=newPerson('David',33);//输出:TypeError:Personisnotaconstructor//ClassdeclarationusingvariablestatementvarPerson=class{constructor(name,age){this.name=name;这个。年龄=年龄;}};在这个例子中,我们可以看到Person类被提升为一个函数表达式,但是它不能被使用,因为它的值是未定义的。同样,为了解决这个问题,我们必须在声明之后使用Person类://UsingthePersonclassconsole.log(typeofPerson);//输出:undefined//使用变量声明的类声明varPerson=class{constructor(name,age){this.name=name;这个。年龄=年龄;}};//在声明后使用Person类varuser=newPerson('David',33);console.log(user);需要记住的事情var变量是函数作用域的let而const变量是块作用域的(这也包括函数)。在执行代码的任何部分之前,所有声明(类、函数和变量)都被提升到包含范围的顶部。首先提升函数,然后提升变量。函数声明优先于变量声明,但不优先于变量赋值。
