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

前端面试必须-一篇理解现代JavaScript中变量提升的文章-let,constandvar

时间:2023-04-03 14:21:26 Node.js

var-b290405adfda,作者SukhjinderArora,内容已部分删除,标题已更改。许多JavaScript程序员将提升解释为JavaScript将声明(变量和函数)移动到其当前范围(函数或全局)顶部的行为。似乎它们实际上已被移至代码的顶部,但事实并非如此。例如:console.log(a);vara='HelloWorld!';他们会说,上面的代码解除后会变成下面这样:vara;console.log(a);a='HelloWorld!';即使它看起来像那样,因为代码工作正常,JavaScript引擎实际上并没有这样做,你的代码仍然在这里。那么,什么是升压?在编译阶段,在代码执行前的几微秒内,扫描函数和变量声明。所有这些函数和变量声明都添加到内存中称为词法环境的JavaScript数据结构中。通过这种方式,甚至可以在它们实际在源代码中声明之前使用它们。什么是词法环境?词法环境用于存储标识符和变量之间的映射。标识符是变量或函数的名称,变量是对实际对象(包括函数对象和数组对象)或原始值的引用。简而言之,词法环境是存储变量和对象引用的地方。词法环境的结构如下:LexicalEnvironment={Identifier:,Identifier:}想了解词法环境的可以看看我翻译的这篇文章。面试要领|一篇文章了解JavaScript中的执行上下文。现在我们知道了提升的内部结构,让我们看看函数和变量(let、const、var)声明的提升是如何发生的。函数声明提升helloWorld();//打印'HelloWorld!'到控制台functionhelloWorld(){console.log('HelloWorld!');}我们已经知道函数声明是在编译期间添加到内存中的,因此我们可以在实际函数声明之前的代码中访问它。所以上面代码的词法环境看起来像这样:lexicalEnvironment={helloWorld:}所以当JavaScript引擎遇到helloWorld()时,它会在词法环境中查找,找到函数并能够执行它。函数表达式提升JavaScript引擎仅提升函数声明,而不提升函数表达式。请参见以下示例:helloWorld();//TypeError:helloWorld不是函数varhelloWorld=function(){console.log('HelloWorld!');}因为JavaScript只提升声明,而不是赋值,helloWorld将被视为变量而不是函数。因为helloWorld是用var声明的变量,在提升阶段引擎会赋值undefined,所以上面的代码会报错。以下代码很好:varhelloWorld=function(){console.log('HelloWorld!');打印'HelloWorld!'}helloWorld();var变量提升让我们看一些例子来理解var变量提升。控制台日志(一);//输出'undefined'vara=3;我们期望3,但未定义。为什么?请记住,JavaScript只提升声明,而不是赋值。也就是说,在编译时,JavaScript只将函数和变量的声明存放在内存中,并不会一起提升赋值操作,而函数声明的函数会整体提升。但为什么它是未定义的?当JavaScript引擎在编译阶段发现一个var变量声明时,会将该变量加入到词法环境中,并为其赋值undefined作为初始值,然后当代码执行到赋值语句时,将实际赋值环境中相应变量的词法环境值。所以上面代码的初始词法环境看起来是这样的:LexicalEnvironment={a:undefined}这就是为什么我们得到undefined而不是3的原因。在执行阶段,当代码执行到实际赋值的行时,值将被分配给词法环境中的相应变量。所以赋值后的词法环境会是这样的:LexicalEnvironment={a:3}letandconstpromotion首先看下面的例子:console.log(a);leta=3;执行代码,会报UncaughtReferenceError:Cannotaccess'a'beforeinitialization。那么,报错是不是因为let和const声明的变量没有被提升?答案要复杂得多。所有声明(函数、var、let、const和class)都被提升,而var声明被初始化为undefined,但let和const声明保持未初始化。它们只有在JavaScript真正执行声明语句后才会被初始化,JS引擎有一个限制,你不能在初始化它们之前使用它们。这就是我们所说的临时死区。如果JavaScript引擎在声明它们的行中仍然找不到let或const的值,它将为它们赋值undefined或如果是const则返回错误。看下面的例子,由于const声明的变量是不可变的,声明的时候不赋值会直接报错。constast//VM275:1UncaughtSyntaxError:Missinginitializerinconstdeclaration再看看下面的例子:leta;console.log(a);//输出undefineda=5;在编译期间,JavaScript引擎遇到变量a并将其存储在词法环境中,但由于它是一个let变量,引擎不会用任何值对其进行初始化。所以,在编译阶段,词法环境看起来像这样:lexicalEnvironment={a:}现在,如果我们尝试在声明变量之前访问它,JavaScript引擎将尝试从词法环境,因为变量没有初始化,所以会抛出引用错误。在执行期间,当引擎到达声明变量的行时,它将尝试为变量赋值,并且由于变量没有与之关联的值,它将被赋值为未定义。所以,执行完第二行之后,词法环境会变成这样:lexicalEnvironment={a:undefined}所以undefined会打印到控制台,然后词法环境中的a会被更新,a会被赋值为5.我们甚至可以在声明变量之前在代码(例如函数体)中引用let和const变量,只要该代码不在变量声明之前执行即可。例如,这段代码是完全有效的。functionfoo(){console.log(a);}leta=20;foo();//这是完全有效的,但下面的代码会抛出错误。函数foo(){console.log(a);//ReferenceError:a未定义}foo();//这不是有效的a=20;原因是后面执行函数时在作用域链中发现a的值已经被赋值为20,如果函数先执行再赋值,访问的a是未初始化的。class声明提升class是ES6中出现的关键字,它也会像letconst一样被提升,同时也会产生一个临时的死区,在赋值执行之前,在initialcase中也是未初始化的。//未捕获的ReferenceError:初始化前无法访问“Person”letpeter=newPerson('Peter',25);console.log(peter);classPerson{constructor(name,age){this.name=name;这个。年龄=年龄;}}因此,要访问类,您必须先声明它们。例如:classPerson{constructor(name,age){this.name=name;这个。年龄=年龄;}}letpeter=newPerson('Peter',25);console.log(peter);//Person{name:'Peter',age:25}我们从词法环境的角度分析一下。在编译阶段,上述代码的词法环境如下:lexicalEnvironment={Person:}当类声明的代码执行时,Person会被初始化为对应的值。lexicalEnvironment={Person:}类表达式像函数表达式一样被提升,类表达式不被提升。例如,此代码将不起作用。//VM266:1UncaughtReferenceError:Cannotaccess'Personbeforeinitializationletpeter=newPerson('Peter',25);console.log(peter);letPerson=class{constructor(name,age){this.name=name;这个。年龄=年龄;}}正确的方法是这样的:letPerson=class{constructor(name,age){this.name=name;这个。年龄=年龄;}}letpeter=newPerson('Peter',25);console.log(peter);//Person{name:'Peter',age:25}结论现在我们知道JavaScript引擎在提升过程中实际上并没有移动代码。正确理解boost机制,有助于避免以后因boost而产生的错误和困惑。为避免未定义的变量或引发像ReferenceError这样的副作用,请始终尝试在其各自作用域的顶部声明变量,并始终尝试在声明时初始化变量。加餐:块级作用域的改进以上章节均为翻译,接下来补漏一个知识点。我们知道,块级作用域的概念是在ES6中提出的,声明在块级作用域中的变量也会有变量提升,只是局部提升的方式与其他作用域略有不同。看看下面这个例子,是不是和我们平时遇到的不一样://undefinedconsole.log('a1',a){//functionaconsole.log('a2',a)a=100//100console.log('a3',a)functiona(){}//100console.log('a4',a)}//100console.log('a5',a)以下内容来自阮一峰-ES6getting开始http://es6.ruanyifeng.com/#docs/let#%E5%9D%97%E7%BA%A7%E4%BD%9C%E7%94%A8%E5%9F%9F:allowinblock在类范围内声明函数。函数声明类似于var,因为它们提升到全局范围或函数范围的头部。同时,函数声明也会被提升到它所在的块级作用域的头部。从上面我们知道声明在块级作用域的函数会有两个操作:1.提升到全局作用域;2.提升到块级范围内。这两个过程和提升的时机用以下代码描述(来自https://stackoverflow.com/questions/31419897/what-are-the-precise-semantics-of-block-level-functions-in-es6)//声明一个函数`compat`functionenclosing(...){...{...functioncompat(...){...}...}...}提升的过程表示为如下:functionenclosing(...){varcompat?=undefined;//函数作用域...{letcompat?=functioncompat(...){...};//块作用域...compat?=compat?;…}…}提升过程分为三步:在块级作用域的外层,生成一个用var声明的变量,赋值为undefined,类似于在作用域内用var声明的变量块级范围;在块级作用域的词法分析阶段,在顶部用let声明一个同名变量,并赋值为thisfunction。请注意,内层不仅被提升而且被分配。在原函数声明的那一行,将内层用let声明的变量的值赋给块级作用域外层用var声明的同名变量。外部变量此时被赋值。从第一步可以知道a1是undefined,从第二步可以知道a2是函数a,从第三步可以知道a5是100过去最后的精彩:前端面试会成为|一文看懂JavaScript中的闭包前端面试必会|JavaScript前端面试中理解作用域和作用域链的一篇文章会|一文看懂执行上下文IntersectionObserver与JavaScript懒加载浏览器渲染原理初探CSS盒模型、布局与包含块详解CSS选择器优先级关注公众号查看更多。感谢阅读,欢迎关注我的公众号云影天空,带你解读前端技术,掌握最本质的技能。关注公众号拉你进讨论群,有什么问题我都会回复。