当前位置: 首页 > 后端技术 > Node.js

深入理解JavaScript,从作用域和作用域链开始

时间:2023-04-04 00:18:27 Node.js

1。什么是作用域作用域是运行时代码的某些特定部分中的变量、函数和对象的可访问性。也就是说,作用域决定了变量和函数的可访问范围,即作用域控制了变量和函数的可见性和生命周期。2.JavaScript中的作用域JavaScript中有两种作用域:全局作用域和局部作用域。如果变量在函数外部或花括号{}外部声明,则定义了全局范围。在ES6之前,局部作用域只包含函数作用域。ES6提供的块级作用域也属于本地作用域。2.1全局作用域具有全局作用域的对象可以在代码的任何地方访问。在js中,一般有以下几种情况具有全局作用域:最外层函数和最外层变量:varglobalVariable='global';//最外层的变量functionglobalFunc(){//最外层的函数varchildVariable='global_child';//函数内部变量函数childFunc(){//内部函数console.log(childVariable);}console.log(globleVariable)}console.log(globleVariable);//globalglobalFunc();//globalconsole.log(childVariable)//childVariableisnotdefinedconsole.log(childFunc)//childFuncisnotdefined从上面的代码我们可以看出,globeVariable和globalFunc可以在任何地方访问,相反,做的变量不具有全局作用域特性,只能在其作用域内使用。直接赋值的变量没有定义(由于变量提升成为全局变量)functionfunc1(){special='special_variable';varnormal='normal_variable';}func1();console.log(special);//special_variable控制台。log(normal)//normalisnotdefined虽然我们可以在全局范围内声明函数和变量,使它们成为全局变量,但不建议这样做,因为它可能会与其他变量名冲突。一方面,如果我们然后使用const或者let来声明变量,当命名冲突时就会报错。//变量冲突varglobeVariable="person";letglobeVariable="animal";//错误,东西已经被声明另一方面,如果你使用var声明一个变量,第二个声明的同一个变量将覆盖前一个,这会使你的代码很难调试。varname='koala'varname='xiaoxiao'console.log(name);//xiaoxiao2.2局部作用域与全局作用域相反。本地作用域只能在固定的代码片段中访问。最常见的是函数作用域。2.2.1函数作用域函数中定义的变量在函数作用域内。并且该函数每次调用时都有不同的作用域。这意味着具有相同名称的变量可以在不同的函数中使用。因为这些变量绑定在不同的函数中,并且具有不同的作用域,所以它们不能相互访问。//全局作用域函数test(){varnum=9;//内部可访问console.log("intest:"+num);}//外部测试无法访问console.log("Externaltest:"+num);注意事项:如果在函数中定义变量时不加var关键字,该变量将被提升,该变量将成为全局变量。functiondoSomeThing(){//工作中一定要避免这样写thing='writing';console.log('internal:'+thing);}console.log('external:'+thing)任何一对花括号{Thestatementsetin...}属于一个块。在es6之前,block语句中定义的变量会保留在其现有作用域中:varname='程序员的成长指南';for(vari=0;i<5;i++){console.log(i)}console.log('{}external:'+i);//01234{}external:5我们可以看到变量名称和变量i在兄弟范围内。2.2.2解释ES6块级作用域之前注意变量提升初始(最顶层)语句是一样的。也就是说,看起来变量可以在声明之前使用!这种行为称为“提升”,也就是变量提升,看起来变量声明自动移到函数或全局代码的顶部。看一段代码:vartmp=newDate();函数f(){console.log(tmp);如果(假){vartmp='你好';}}这个问题在面试的时候应该很多朋友都会遇到,但是也有人认为输出的是当前日期。但正确的结果是不确定的。这是由变量的提升引起的。此处声明推广,但不推广定义的内容。提升后对应的代码如下:vartmp=newDate();functionf(){vartmp;控制台日志(tmp);如果(假){tmp='你好';}}F();控制台输出的时候,tmp变量只是声明,没有定义。所以输出undefined。虽然可以输出,但是不推荐这种写法。推荐的方式是在声明变量的时候,把所有用到作用域(全局作用域或者函数作用域)的变量都写在最顶层,这样代码看起来会更清晰,更容易看出哪些变量来自函数作用域,哪些来自函数作用域作用域链。重复声明看例子://varvarname='koloa';console.log(name);//koalaif(true){varname='程序员的成长指南';控制台日志(名称);//程序员成长指南}console.log(name);//程序员成长指南虽然看起来名字声明了两次,但是上面说了js中的var变量只有两种,全局作用域和函数作用域,声明会提升,所以其实名字只会在顶部和开头声明一次,varname='programmer“成长指南”的声明将被忽略,只用于赋值。也就是说上面的代码其实和下面的是一致的。//varvar名称='koloa';控制台日志(名称);//koalaif(true){name='程序员的成长指南';控制台日志(名称);//程序员成长指南}console.日志(名称);//程序员的成长指的是变量和函数的同时提升如果同时声明了函数和变量,会发生什么?看下面但是代码console.log(foo);varfoo='iamkoala';functionfoo(){}输出的是functionfoo(){},也就是如果函数内容是另一种形式怎么办?console.log(foo);varfoo='iamkoala';varfoo=function(){}输出结果是undefined分析解释两种结果:第一种:函数声明。就是上面的第一个,functionfoo(){}的另一种形式:函数表达式。就是上面的第二种,varfoo=function(){}这种形式的第二种形式其实就是var变量的声明和定义,所以上面第二种的输出结果是undefined应该可以理解。第一种形式的函数声明,在提升的时候,会整体提升,包括函数定义的部分!所以第一种形式相当于下面的方式!varfoo=function(){}console.log(foo);varfoo='iamkoala';原因是:函数声明被提升到顶部;该语句只进行一次,因此varfoo='iamkoalalater'语句将被忽略。函数声明的优先级高于变量声明,函数声明会和定义一起被提升(这点和变量不同)。接下来ES6引入块级作用域之后会发生什么!2.2.2块级作用域ES6增加了let和const命令,可以用来创建块级作用域变量。使用let命令声明的变量只在let命令所在的代码块内有效。let声明的语法与var的语法相同。您基本上可以使用let而不是var进行变量声明,但它会将变量的范围限制在当前代码块内。块级作用域具有以下特点:变量不会被提升到代码块的顶部,并且不允许从外部访问块级作用域的内部变量。notdefined`letbar=2;for(leti=0;i<10;i++){console.log(i)}console.log(i);//抛出`ReferenceError`异常:变量`未定义`事实上,这个功能带来了很多好处。当开发人员需要检查代码时,可以避免不小心使用一些范围外的变量,保证变量不会被混淆而是被复用,提高代码的可维护性。就像代码中的示例一样,我只在for循环内部使用的变量不会再污染整个范围。ES6let和const不允许重复声明,这点和var//varfunctiontest(){varname='koloa';不同varname='程序员的成长指南';控制台日志(名称);//程序员的成长指南}//让||constfunctiontest2(){varname='koloa';letname='程序员的成长指南';//UncaughtSyntaxError:Identifier'count'hasalreadybeendeclared}看到这里,是不是觉得块级作用域的出现还是很有必要的。3.作用域链在讲解作用域链之前,先说说JavaScript是如何执行的?3.1JavaScript是如何执行的?JavaScript代码执行分为两个阶段:3.1.1分析阶段javascript编译器在代码生成后进行编译分析。stage)会创建一个AO(ActiveObject主动对象)3.1.2ExecutionPhaseAnalysis分析阶段成功后,会把AO(ActiveObject主动对象)交给执行阶段引擎询问作用域,是否有这个scopeinthescope如果X的范围内有一个变量X,引擎就会使用这个变量。如果范围内没有变量,引擎将继续搜索(到上层范围)。如果最后没有找到该变量,引擎将抛出错误。执行阶段的核心是查找,以及如何查找,LHS查询和RHS查询后面会讲解。3.1.3JavaScript执行示例看一段代码:functiona(age){console.log(age);varage=20console.log(age);functionage(){}console.log(age);}a(18);首先进入上面提到的分析阶段,在函数运行的那一刻,创建一个AO(ActiveObject)AO={}第一步:分析函数参数:形参:AO.age=undefined实参:AO.age=18第二步分析变量声明://第三行代码有varage//但是之前第一步已经存在AO.age=18,并且有一个同名属性。什么都不做就是AO.age=18第三步分析函数声明://第5行代码有functionage//thenpayfunctionage(){}toAO.ageAO.age=functionage(){}函数声明注意:在AO上如果有与函数名同名的属性,会被本函数覆盖。但是在下面的情况下,varage=function(){console.log('25');}声明的函数不会覆盖AO链中的同名属性。进入执行阶段后,分析阶段会传递给AO(ActiveObject(活动对象))到执行阶段,引擎会询问作用域,找到流程。所以AO链中的上述代码最初应该是AO。age=functionage(){}//afterAO.age=20//afterAO.age=20所以最终输出为:functionage(){}20203.2作用域链的概念看完前面完整的javascript函数执行process,说一下作用域链的概念,JavaScript上的每个函数在执行的时候,都会先在自己创建的AO上找对应的属性值,如果找不到就去自己创建的AO上找parentfunction,找不到就往上一层AO,直到找到大佬:window(全局作用域),而由此形成的“AO链”就是JavaScript中的作用域链。3.3搜索过程LHS和RHS查询特殊指令LHS,RHS这两个词在引擎查询变量时出现,在《你不知道的Javascript(上)》中也有很清楚的描述。在这里,我想引用上面freecodecamp的回答来解释一下:LHS=variableassignmentorwritingtomemory。将其视为将文本文件保存到硬盘驱动器。RHS=变量查找或从内存中读取。将其视为从硬盘驱动器打开一个文本文件。学习Javascript,LHSRHS3.3.1LHS和RHS特性都将在所有范围内以严格模式进行查询。当找不到需要的变量时,引擎会抛出ReferenceError异常。在非严格模式下,LHR有点特殊:它会自动创建一个全局变量。查询成功后,如果对变量的值进行了不合理的操作,如:对非函数类型的值进行了函数调用,引擎会抛出TypeError异常3.3.2LHS示例而RHS的例子来自《你不知道的Javascript(上)》functionfoo(a){varb=a;返回a+b;}varc=foo(2);直接看引擎在scope中找到这个进程:LSH(写入内存):c=,a=2(隐式变量分配),b=RHS(读取内存):readfoo(2),=a,a,b(returna+b需要求a和b)3.4作用域链总结最后对作用域链做一个总结,引用《你不知道的Javascript(上)》中的一张图来解释今天分享的那么多,如果对分享的内容感兴趣的话,大家可以关注公众号《程序员成长指南北》,或者加入技术交流群,一起探讨。文章同步至程序员成长指南(ID:coder_growth)作者:koala一个有趣的github博客地址:https://github.com/koala-codi...