02 | var x = y = 100:声明语句与语法改变了JavaScript语言核心性质
02 | var x = y = 100:声明语句与语法改变了JavaScript语言核心性质
讲述:周爱民
时长21:13大小19.43M
声明
从读取值到赋值
赋值
向一个不存在的变量赋值
发生了什么?
知识回顾
思考题
赞 25
提建议
精选留言(63)
- fatme2019-11-14声明和语句的区别在于发生的时间点不同,声明发生在编译期,语句发生在运行期。声明发生在编译期,由编译器为所声明的变量在相应的变量表,增加一个名字。语句是要在运行时执行的程序代码。因此,如果声明不带初始化,那么可以完全由编译器完成,不会产生运行时执行的代码。
作者回复: +1 ^^.
93 - 海绵薇薇2019-11-19hello,老师好啊,研读完文章和评论后还存在如下疑问: 1. var y = "outer"; function f() { console.log(y); // undefined console.log(x); // throw a Exception let x = 100; var y = 100; ... } 老师解释函数内部读取不到外部变量的原因是“函数创建的时候标识符x和y就被创建了”。因为这是一个函数声明,也就是在编译的时候就创建了。 高程上的意思是函数执行的时候会生成一个活动对象当做变量对象,这时候标识符才会生成,包括arguments,形参实参,声明的变量,挂在活动对象上。 两个解释好像都能说明上面的现象。 有点糊涂了。 2. function a() { function b() {} } 在代码执行前连函数b都被创建了吗? 3. 老师对一定了解闭包的本质,后面有机会说到吗?展开
作者回复: “因为这是一个函数声明,也就是在编译的时候就创建了” === 这个是你混乱的根源,因为从“函数声明”到“闭包”之间还有好几个环节的。 “函数创建的时候标识符x和y就被创建了”,这个函数内的x和y都是可以被parser到的,因此在函数静态解析完之后,函数就可以确定内部有x和y了,并且相应的静态作用域也(在语法处理相关的内核逻辑部分)被创建了。但是这些东西是用户代码完全不可见的,你不会知道,也用不到。 然后是一个函数作为表达式得到它的一个实例的过程,也就是 > (function () {}).xxxxx 中“.xxxx”之前的部分作为表达式被独立处理的时候,这种情况下函数会有一个自己的环境,该环境也是“基于前面得到的静态作用域”来创建的。这个环境仍然不为用户所知,只是它能传递,例如你可以把这样的东西“返回(return)”或“赋值(=)”给别的东西,你会发现这个东西的“环境/执行上下文”并没有变,所以它们在“返回(return)”或“赋值(=)”的过程之前就被创建了,并且能被这些操作所“传递”。 最后才是闭包,闭包只发生于“f()”的这个“调用操作()”之后——注意是“之后”,所以高程等等都是说它在“执行之后”标识符才会生成,而我这一章都是在讲“静态语义”,所以我会说他在函数(作为一个静态的对象)创建的时候就已经有这些标识符了。 这两者都不矛盾。根本上来说,函数实例、函数闭包,都不过是“静态词法解析结果(函数定义/(un)Anonymous Function Definition)”的一个映像,这个“函数定义”就是第4小节讲的那个东东。 Ok,既然“x,y都是代码执行前就被(静态分析)创建了的”,那么哪种情况下"x"才不是“词法的”呢? function f() { let y; return x+y } 注意在这个例子中,"x"就不是词法的,它在任何情况下都不被创建,不在f()的标识符列表中,也不在语法/词法分析中。 Ok. 这是第1个问题。 关于第2个问题,答案其实如上所述:如果“创建”是指静态语义中的一个“函数”,那么确实是创建了的。——但它还没有“绑定”到一个闭包的执行上下文中。 关于第3个问题,是这样的,这一整个课程(系列)其实不只20讲,大概会是40~45讲的样子。但编辑同学不允许我公布后续内容的计划(呜……),所以……我只能告诉你,在下一个课程里面,才会讲到闭包。
共 3 条评论22 - Ming2019-11-13〈以下是小生愚见〉 概念纷繁,建议老师将讲解重心放到这门语言的现有特性,贯之历史脉络,是否(怎样)解决了某种设计缺陷。这样,知识纵深感更强,并可指导实际工作以避免踩坑。适当穿插示例代码和图文更佳。
作者回复: 多谢。我在后面的课程中尽量注意这个 ^^.
共 2 条评论19 - Ppei2020-05-02老师你好,词法声明会有提升吗? 一些书里面会说不存在变量提升,但是文中说,是拒绝访问。 我是不是该从编译期跟运行期去理解?
作者回复: @Ppei 你提这个问题是很到位的,严格地说,这是我这一讲中跟ECMAScript描述中不一致的地方。所以我需要非常细致地回复你的问题。 严格来讲,所谓变量提升(Hoisting)指的是如下的现象: ``` function foo() { var x = 100; if (true) { let y = 200; var z = 300; ... } } ``` 在如上示例中,变量z的声明被提升到了x相同的位置声明,相对应的、用作比较的y就没有提升,它声明在if()语句之后的块语句中。 这是严格的ECMAScript规范概念下的“变量提升”,因此它的确描述的是一种语法现象——在语法阶段,通过自然的、表面的识别就可以理解的现象。 在ECMAScript中被明确指出的提升现象还包括函数声明。亦即是说,下面的示例: ``` function foo() { var x = 100; if (true) { let y = 200; function z() { } ... } } ``` 在这个示例中,函数z()和之前的变量z声明都同样被提升到了x的位置。 这两种现象——或这两种“严格意义上的变量提升”是JavaScript 1.x时代的早期设计带来的结果。因为没有块级作用域,因此所有的声明都必须“上浮”到函数或全局一级的作用域来处理。这也是我在《JavaScript语言精髓与编程实践》一书中,需要分析“作用域的等级”的原因。这种等级决定了作用域之间的交互关系,而关系之一,就是所谓“提升”。 除了这两种“严格意义上的变量提升”——函数声明是“隐式的变量声明”——之外,ECMAScript并没有规范其它的提升现象。 然而通常,在我的讲述中会把如下的现象也称为提升。有些时候,为了特别地指出它们,我会强调它们是一种“提升效果”。例如: ``` console.log(x); var x = 100; ... ``` 在如上的例子中,x在它的声明语句之前是能够被访问的。因此,这也是提升。与之相比较的: ``` console.log(y); var x = 100; let y = 200; ``` 这个示例中的`y`就不能被访问,并且提示是: > ReferenceError: y is not defined 所以,在语法概念上,这个`y`是“还没有声明的”。但是,在事实上呢?在事实上,`y`和`x`都是被声明过了的,只不过`y`被声明之后未被初始化,而`x`被初始化成了undefined。 所以,“从实现上来说”,这里的所谓 > 1、var有变量提升(效果),而let没有变量提升(效果) 其实与之前讨论的 > 2、var与function在词法作用域中的变量提升 并不一样。上述规则2是在规范层面真实的存在的,是语法级别的、在构建作用域的阶段就被“提升”的。而规则1却不是,规则1中的var/let声明在相同的位置,只不过let声明成了未初始化的,并且ECMAScript约定:访问一个未初始化的变量(例如y)时,将错误信息显示成“... is not defined”而已。 所以我才会说,这种var提升效果,在本质上是`x`没有被拒绝访问,而相对的`y`被拒绝访问。 回到一些其它的有关Hoisting现象的说明上来,你可以参考一下MDN: https://developer.mozilla.org/zh-CN/docs/Glossary/Hoisting 不过MDN对上述两大类的提升/提升效果也是混在一起讲的,并没有从实现机制的角度来阐释。另外,import和var一样也有“提升效果”,则是“第三种提升”,机制上也是有所不同的,这个我在本专栏中略有讲到,但并不详细。
共 2 条评论18 - Mr_Liu2019-11-13思考题: 小白的我,没有太明确的答案,暂时还不能明确自己理解究竟是否正确,希望听老师后续的课程能够明白 读完今天的这篇理解了昨天的提问,为什么var x = '123' delete x 是false, 即使是 var obj ={ a: '123', b: { name: '123' } } var z = obj.b delete z 返回也是false 所以问了那么delete x 存在有什么意义。 今天老师的科解答了 x = '123' delete x 返回true 是因为你可以删除掉这个动态添加的“变量”,因为本质上就是在删除全局对象的属性。同时也理解了上一讲的“只有在delete x等值于delete obj.x时 delete 才会有执行意义。例如with (obj) ...语句中的 delete x,以及全局属性 global.x。”这句 但上一节关于“delete x”归根到底,是在删除一个表达式的、引用类型的结果(Result),而不是在删除 x 表达式,或者这个删除表达式的值(Value)。后一句理解了,但前一句是否可以理解为实际上是删除引用呢,希望老师解答一下 立一个flag ,每个争取评论下面都有我的,不为别的,就为增加自己的思考展开
作者回复: 是的。 delete从不删除值。delete只能删除引用,例如obj.x,或者with(obj) delete x,这些都是以引用的方式得到的,所以delete才能删除它们。 `delete 0`这种行为并不真的能够发生,它什么也没做,只是返回了true而已。
13 - 孜孜2020-07-11今天写IIFE,突然有点问题想问下老师, 1. 为什么(function f(){ return this}) 可以,(var test=1) 不可以。 2. 两种IIFE的写法,(function f(){ return this})() 和 (function f(){ return this}()) 有何区别。 3. 函数调用()和表达式取值()如何在ECMAScript找到说明?
作者回复: 1. 第一个是“函数表达式”,(expression, ...)的语法能通过;第二个是语句(6种声明语句之一),所以这个语法通不过。 2. 第一种`(function f...)()`是将函数f作为一个“单值表达式”,这里的第一对括号是作为“分组运算符”来用的,它强制将`function f...`作为一个表达式来做语法解析,从而避免了它被“(优先地)解释为函数声明语句”。而第二种,也就是`function f {}()`中的f,也是因为相同的原因(第一对括号作为分组运算符),从而导致解析过程进入表达式解析,所以这个函数与第一种没区别。但是接下来,它进行了函数运算调用——也就是f()。——而这里也是两种用户的不同,第一种用法是第一对括号返回函数,第二种用法中第一对括号返回的是函数调用的结果。 3. 这分别是: https://tc39.es/ecma262/#sec-call https://tc39.es/ecma262/#sec-grouping-operator 不过第二种不称为“表达式取值”,而是“分组表达式”,它的运算效果是“取Result”——也就是说不仅仅是取值,还可以包括取“(ECMAScript规范的)引用”。
共 2 条评论10 - Elmer2019-12-23文中提到:ECMAScript 规定在这个全局对象之外再维护一个变量名列表(varNames) 那么window是怎么取到这些变量的值的,如window.a 不是平级么。在global scope中, window var let const 的关系是什么。 求讲解
作者回复: 这里说到的 varNames在绝大多数情况下是没用的,与你这里想讨论的东西关系也不大。 window在浏览器环境中等同于global。你在浏览器控制台上查看一下就明白了: ``` # 这个globalThis是新EMAScript规范中声明的,它等义于传统的global > window === globalThis true # 用下面的代码可以获得“传统的global”,并比较之 > window === Function('return this')() true ``` 所以所谓window,就是global,相同的东西。那么window中取变量也就是查global这个对象的属性表,之前也都说过了,不再讲了。至于var/let/const之间的关系,在全局域(global scope)中,var声明在global的属性表中,并在varNames中有一个登记;let/const共享使用同一个称为词法环境(lexicallyEnv)的东西,所以它们不能同名。词法环境在后面会讲到。 因此从原理上来说,全局作用域中的global对象属性,与词法环境中的名字其实是可以重名的。——所以,var与let/const的重名限制,主要是来自于语法。 例如: ``` # 添加属性 > global.n = 100 # 看起来有了全局变量的样子(实际上没有在varNames中登记) > n 100 # 现在可以声明let > let n = 200 # 这是一个Let变量 > n 200 # 如果你使用var声明,则与let/const会有重名冲突了 > var x = 300 > let x = 400 SyntaxError: Identifier 'x' has already been declared ```
10 - Isaac2020-06-28「一个赋值表达式操作本身也是有“结果(Result)”,它是右操作数的值。注意,这里是“值”而非“引用”」 老师,你好,这句话从“值类型”的角度可以理解,但是对于引用类型怎么理解? 比如:var x = y = { name: 'jack ma' }。 我的理解: 由于 { name: 'jack ma' } 本身是引用类型,所以 y = { name: 'jack ma' } 的赋值操作的结果也是“一个引用”,所以这里的“值”其实和类型无关,仅仅是一个运算结果。 在回到这句话:「它是右操作数的值」 ,用更通俗易懂话来讲,这里的“值”仅仅是一个运算结果,和类型无关。 请问老师我这样理解正确吗?如果错误的话,该怎么解释 var x = y = { name: 'jack ma' }?展开
作者回复: 你说的“引用类型”,不是我说的“引用(规范类型)”。 以下面的代码为例: ``` x = obj.foo ``` 右操作是`obj.foo`,它的引用是obj.foo整体,这包括“obj这个对象”的信息——这称为“引用(规范类型)”;而它的值是GetValue(obj.foo),GetValue()是引擎的内部操作,是从“引用(规范类型)”中取值,其结果会是foo这个函数。 对于上述赋值表达式来说,`x = obj.foo` 其结果是`x`变成了函数foo,那么它就是右侧操作数的“值”,而不是右操作数“obj.foo”的全部信息。 有没有上述示例的反例呢?也就是一个运算符的结果仍然是“obj.foo的全部信息(引用)”,而不仅是它的“值(GetValue的结果)”呢? 有的。下面的示例: ``` (obj.foo) ``` 这一对括号称为“分组运算符(也有称着强制运算符的)”,这个括号的运算结果就是“操作数的引用”。所以,在下一步的运算中: ``` (obj.foo)() ``` 这个方法调用中的foo函数可以得到this为obj。 最后汇总一下: ``` obj = { foo() { console.log(this === obj) } } // case 1,赋值运算只得到了右侧运算数的“值” x = obj.foo; x(); // false // case 2,分组运算得到了操作数的“引用(全部的信息)” (obj.foo)(); // true ```
9 - Zheng2020-01-14老师,我用node执行这段代码,结果是undefined,但是换成浏览器打印就可以打印出来a的配置信息,这是因为node环境和浏览器的差异还是什么,我试过好多次了,应该不是偶然: var a = 100; x = 200; console.log(Object.getOwnPropertyDescriptor(global,"a")); //浏览器执行的时候global改为globalThis展开
作者回复: 是这样的,node缺省情况下是把.js文件当成模块来加载的,它会为每一个模块(亦即是.js文件)包一层所谓的“模块封装器”。这个封装器是一个函数。所以,.js文件中的代码事实上是运行在函数中的。这样一来,`var a`就变成了声明函数内的局部变量,而不是在全局global上的。 在node的控制台里面输入的代码是作为全局环境下的代码来执行的,因此如果你的代码是在node控制台上逐行执行,就没有问题。另外,如果你使用-e参数来执行,那么也能得到这样的效果(这种情况下node不会添加模块封装器): ``` > node -e "$(cat test.js)" -p { value: 100, writable: true, enumerable: true, configurable: false } ``` > NOTE: 模块封装器,参见:http://nodejs.cn/api/modules.html#modules_the_module_wrapper
8 - 陆昱嘉2019-11-17老师,一个赋值表达式的左边和右边其实“都是”表达式,那么var x=(var y=100);这样就报错,原因是什么?varNames里面的冲突?
作者回复: “var y = 100”不是表达式,而是语句。 所以“var x = y = 100”里面: - “var x ...”是语句语法(Variable Statement) - “= ...”是初始化器(Initializer,严格来说,也不是表达式,而是语法的一部分) - “y = 100”是表达式(expression)。 所以你列的问题,报错的原因是“语句在语法上不支持这种写法”,即在“ = ...”中的“...”上面,必须是一个用来做“初始器”的表达式,而不能“再是一个语句”。 参见ECMAScript: https://tc39.es/ecma262/#sec-variable-statement https://tc39.es/ecma262/#prod-VariableDeclaration
8 - Geek_baa4ad2020-05-04var x = y = 100; Object.getOwnPropertyDescriptor(global, 'x'); Object.getOwnPropertyDescriptor(global, 'y'); {value: 100, writable: true, enumerable: true, configurable: false}configurable: falseenumerable: truevalue: 100writable: true__proto__: Object Object.getOwnPropertyDescriptor(global, 'x'); {value: 100, writable: true, enumerable: true, configurable: false} Object.getOwnPropertyDescriptor(global, 'y'); {value: 100, writable: true, enumerable: true, configurable: false} 得到结果一样吖,x y 是一个相同的东西吧 最新的v8 实现不一样啦?展开
作者回复: 不要在浏览器中测试,也不要在node repl中测试。这些要么受环境的host/global设计的影响,要么受模块加载的影响。 得到纯v8引擎测试环境的方法,要么是直接编译一个v8,要么是使用下面这样方法: ``` // 将下面的代码写文件test.js var x = y = 100; console.log(Object.getOwnPropertyDescriptor(global, 'x')); console.log(Object.getOwnPropertyDescriptor(global, 'y')); ``` 然后: ``` # 在命令行上使用nodejs(在mac/linux环境) > node -e "$(cat test.js)" { value: 100, writable: true, enumerable: true, configurable: false } { value: 100, writable: true, enumerable: true, configurable: true } ``` 好运。:)
7 - 佳民2019-11-13思考题:var声明会声明提升,在语法解析(静态分析)阶段进行,不是在运行阶段执行,这样理解对吗?
作者回复: 是的。不过所有的6种声明都是如此,非独var声明。
共 2 条评论7 - G2020-10-31老师您好,在回过头来重新读这个课程的时候,我产生了一些新的疑惑。 在静态语法解析阶段,会在词法环境中添加所声明的标识符,那么像下面这样的代码: var arr = new Array; for (var i=0; i<5; i++) arr.push(function f() { // ... }); 这段代码是在第八讲中粘贴过来的,第八讲中有说,静态函数f()有且仅有一个。那么这个函数f是什么时候被定义的,又被定义在了什么样的词法环境下呢? 我上面的表述可能不明确,我大概就是想问这么一个问题: let obj = { test:function(cb){ cb(); (()=>{console.log(this);})() } } obj.test(() => {console.log(this);}) () => {console.log(this) 这个箭头函数,是在什么时候被定义的,定义在了哪里。 从 cb执行 this打印来看,应该是定义在了全局环境下。 但是由于它是一个匿名函数,所以我在全局无法打印出它来验证。但是我把 () => {console.log(this)换成function f() {console.log(this)},全局下也没有办法访问到 f 。 我描述的可能不太清楚,我大概是想知道,被当做实参传入的函数,是在什么时候被声明的,声明在了哪里。展开
作者回复: 好问题! 其实你发现了一个有关于执行环境的、很深层面的问题。简单地表述来说是这样:如果具名函数把自己的名字登记在“函数所在的上下文(环境)”中,那么匿名函数如何登记自己呢?——对于这个问题,更深一点的问下去,就会是“如果匿名函数不能登记,那么它怎么执行呢”? 其实,整个问题的核心关键在于:这根本不是匿名/具名函数的问题,而是函数声明/函数表达式的问题。注意以下的讨论中要特别强调的是:你所有几个示例中声明的,都是“具名的函数表达式”,而不是“具名函数(声明语句)”。 好的。真实的情况是这样:1、函数表达式“不会”向当前的环境中登记自己的名字;2、你无法声明出一个匿名函数的语句(这有一个例外,就是export default ...)。 先说第1条。“(所有的)函数表达式”其实都是不会向当前环境中声明的,只是在表达式执行到的时候,这个函数才会被创建。而当它被创建时,它立即创建一个与这个函数相关的上下文,而由于它是“函数表达式”,所以不会“变动(当前的)”环境——即不改变词法的环境,也不改变变量的环境。 然后你应该会注意到这个问题,就是“具名的函数表达式”也是有名字的,对吧。是的,但是这个名字“并不注册在当前的上下文环境中”。在函数表达式的实现中,这是一个非常少有人注意到的技巧,称为“多重的环境(或这里可以称为闭包)”,也就是说这样的函数其实有两个闭包,一个外层的,登记了函数名字;一个内层的,登记了参数表等等。这样一来,在“具名的函数表达式”中,既可以让函数体内的代码用到“函数名”(包括覆盖它),又不需要这个名字被“注册到”外部的(例如当前的)环境中。 关于这一部分,可以参阅《JavaScript语言精髓与编程实践》的第“5.5.2.4 函数表达式的特殊性”一节。 第2个问题。这是一个细节,因为export是语句,因此如果它用来声明一个匿名函数,那么这个匿名函数也将是“语句声明出来的”,所以它是特例。并且在真实的情况中,这样的一个“匿名函数”其实也是有名字的,它被登记在一个名为“*default*”的项中。这个在本课程的第4讲中也是讲述过了。 最后,你的问题其实还涉及“函数表达式在当前作用域中到底‘需要/不需要’有名字”的问题,以及所谓的“条件化声明(语句)“的问题,等等。这些是古老时代不同js引擎的实现带来的、与规范有差异的遗产,例如JScript 5.6及之前的版本就认为“具名的函数表达需要在当前作用域中声明一个名字”(这也称为名字泄露)。不过,这些问题中绝大多数如今已经有了定论,你可以再翻翻文章。
共 3 条评论6 - 蓝配鸡2019-11-13醍醐灌顶,但是有一些疑问: 文中说, "如果是在一门其它的(例如编译型的)语言中,“为变量 x 绑定一个初值”就可能实现为“在创建环境时将变量 x 指向一个特定的初始值”。这通常是静态语言的处理方法,然而,如前面说过的,JavaScript 是门动态的语言,所以它的“绑定初值”的行为是通过动态的执行过程来实现的,也就是赋值操作。" 为什么动态语言就不可以给变量初始化, 一定要使用动态赋值呢? 我对动态语言的理解是,变量的类型可以在运行时改变,静态语言变量的类型不可以改变。 但是这性质好像并不影响初始化?展开
作者回复: 动态语言的诸多细节,其实要到18讲之后才会讨论到。不过有个概念上的问题,并不是说有动态类型就是动态语言,或者说支持动态执行就是动态语言。有很多方面的“动态语言的特性”,这个需要详细解析。 仅是说初始化这一项,使用动态赋值的原因是因为这个值必须要到执行期环境中才能确定下来,而在静态语法分析阶段是确认不了的。——所以它不可能“先于环境”而执行。
共 2 条评论6 - 卡尔2020-06-11老师,你说let声明的变量不能在赋值之前使用。 这里说的赋值是不是说赋值操作呢? let a; console.log(a) 上面代码对a是没有赋值操作吧?
作者回复: `let a;` 是作为`let a = undefined;`处理的。 声明语句确实是在语法分析期和环境初始化阶段处理的,但当代码“执行到”相应位置时,会执行它在“运行期语义”——也就是“绑定值”。 而如上说它是作为“let a = undefined”来处理的,因此这一行代码不是“没有赋值操作”,而是“执行了初值绑定操作”。 “绑定初值”与“赋值”是语法效果上是一致的,只是概念上的不同。
共 2 条评论5 - Smallfly2020-02-24y = 100 有的地方叫赋值语句,有的地方叫赋值表达式。因为它的执行结构能在代码层面获得 x = (y = 10),所以我更倾向于认为它是表达式。 想请问下老师叫赋值语句是错误的么,还是有其它的原因?
作者回复: 在ECMAScript中,并没有“赋值语句”,它的准确说法是“赋值表达式语句”。——任何一般表达式,都可以解析为一个语句,并称为“一般表达式语句”。所以“赋值表达式(语句)”在这里并没有任何的特殊性。 “任何一般表达式”都可以通过在末尾添加一个“;”号来表明将它处理为一个语句(包括还有换行符,以及文末符等等,这些参考“自动分号插入(ASI)”这个语法特性)。 所以这里不存在是否“错误”的问题。很多地方、很多书都只是按传统的语言习惯来理解JavaScript,所以用传统的名字或概念往上面套。进一步的,也就有了很多似是而非的概念。如果真正的追求概念上的准确性,那么应该使用ECMAScript规范中的明确定义。——但是如果这样讲,那么这个课程中基础也还有稍稍有一些地方有着含混不清的概念的。 JavaScript中需要分开理解表达式和语句,它们处理机制不同,结果不同,效果也不同,应用环境还是不同。这些的入手点称为“表达式语言”,也就是所谓“函数式语言特性”。你可以尝试着将所有的语句去掉,试着用“纯粹地JavaScript表达式”来写代码,你会发现JS在这个层面上也是完备的。只有先抽离出了“表达式”,以及“表达式语言”,再反观“语句”,才能理解语句真正的用处。总而言之,语句与表达式在JavaScript的语言设计中是很精彩而又令人迷惑的。 这个课程的后面一部分会分开讲“语句执行”和“表达式执行”,慢慢看就会明白这些东西之间的区别了。
5 - 家家家家2019-12-02忘了在哪本书中还是哪篇文章中讲过,变量的生命周期:声明阶段、初始化阶段、赋值阶段。 老师这里讲的静态分析阶段就是指的变量生命周期中的声明阶段对吗? 对于var,它的声明阶段和初始化阶段是一起发生的,都在静态分析中;对于let,它的声明阶段和初始化阶段是分开的,只有声明阶段在静态分析中,是这样理解的嘛?
作者回复: 这个讲法倒也不错,比较容易理解。 不过有些不严谨的地方,比如说,“var x;”这样的声明,“声明阶段和初始化阶段一齐发生”是对的,因为var声明的时候,名字声明并初始化为undefined确实是同时发生的。但是,如果是“var x = 100;”,那么就比较容易混淆了。因为“x = 100”其实是赋值,而不是引擎层面的“初始化”。 对于let来说,“声明阶段和初始化阶段是分开”其实更不严谨。确切地说,let就没有“初始化阶段”。而用户代码“let x = 100”中的“x = 100”就是赋值阶段了。“let 没有初始化阶段”,所以才会出现它在未赋值之前不能读的现象。 用这样三个阶段来解释这件事情,跟ECMAScript中的逻辑并不矛盾(而且也可以正确解释),只是细节上需要严谨一点、留意一点就行。
共 2 条评论5 - Marvin2019-11-14相当于 var/let/const x = (y =100) 再拆就是 y=100 // 变量泄漏 var x=y // 模拟表达式返回值赋值
作者回复: 是的。简洁,正确。^^.
5 - ssala2019-11-13老师,像变量提升这种不符合直觉的设计,难道规范不能将其移除吗?不移除是为了兼容老代码吗?我想应该没有代码会依赖这个特性吧?
作者回复: 一是因为向下兼容,这是规范订制过程中面临的主要挑战之一。第二个原因,在于按照JavaScript的核心设计,在原理上这就很难绕过去。例如就包括不受兼容性影响的import,其实也是有变量提升效果的。例如下面这样的代码也是成立的: ``` console.log(x); import x from './test.js'; ```
5 - Geek__kkkkkkkk2019-11-13老师,您文章中讲到“ECMAScript 规定在这个全局对象之外再维护一个变量名列表(varNames),所有在静态语法分析期或在 eval() 中使用var声明的变量名就被放在这个列表中。然后约定,这个变量名列表中的变量是“直接声明的变量”,不能使用delete删除。”,这里面的意思我理解了 eval() 中使用var声明的变量名,不可用delete删除,但是下面的代码中eval()声明了b,configurable是true,可删除,是否矛盾了?还是我理解的不够全面?
作者回复: > 这种情况下使用var声明的变量名尽管也会添加到varNames列表,但它也可以从varNames中移除(这是唯一一种能从varNames中移除项的特例... 文章中是解释过的。可能我没有足够地强调这个特例,或者它的特殊性吧。
共 2 条评论5