02 | var x = y = 100:声明语句与语法改变了JavaScript语言核心性质
该思维导图由 AI 生成,仅供参考
- 深入了解
- 翻译
- 解释
- 总结
JavaScript中的声明语句和语法的改变对其核心特性产生了重大影响。本文深入探讨了JavaScript中的六种声明语句,包括变量和常量的声明方式,并强调了声明在静态语法分析中的重要性以及变量提升的问题。文章还讨论了变量和常量的读取和赋值过程,以及在ECMAScript 6之前和之后的差异。特别强调了JavaScript中赋值操作的语法特点,以及全局变量管理方式对“向一个不存在的变量赋值”所导致的变量泄漏的不可避免性。此外,文章还介绍了全局对象的属性表动态添加和变量名列表的维护,以及对变量声明和赋值过程的详细解析。通过本文,读者可以深入了解JavaScript中声明和赋值的机制,以及语法变化对其核心特性的影响。
《JavaScript 核心原理解析》,新⼈⾸单¥59
全部留言(63)
- 最新
- 精选
- fatme声明和语句的区别在于发生的时间点不同,声明发生在编译期,语句发生在运行期。声明发生在编译期,由编译器为所声明的变量在相应的变量表,增加一个名字。语句是要在运行时执行的程序代码。因此,如果声明不带初始化,那么可以完全由编译器完成,不会产生运行时执行的代码。
作者回复: +1 ^^.
2019-11-1497 - 海绵薇薇hello,老师好啊,研读完文章和评论后还存在如下疑问: 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讲的样子。但编辑同学不允许我公布后续内容的计划(呜……),所以……我只能告诉你,在下一个课程里面,才会讲到闭包。
2019-11-19322 - Ming〈以下是小生愚见〉 概念纷繁,建议老师将讲解重心放到这门语言的现有特性,贯之历史脉络,是否(怎样)解决了某种设计缺陷。这样,知识纵深感更强,并可指导实际工作以避免踩坑。适当穿插示例代码和图文更佳。
作者回复: 多谢。我在后面的课程中尽量注意这个 ^^.
2019-11-13220 - Ppei老师你好,词法声明会有提升吗? 一些书里面会说不存在变量提升,但是文中说,是拒绝访问。 我是不是该从编译期跟运行期去理解?
作者回复: @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一样也有“提升效果”,则是“第三种提升”,机制上也是有所不同的,这个我在本专栏中略有讲到,但并不详细。
2020-05-02218 - Mr_Liu思考题: 小白的我,没有太明确的答案,暂时还不能明确自己理解究竟是否正确,希望听老师后续的课程能够明白 读完今天的这篇理解了昨天的提问,为什么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而已。
2019-11-1313 - 孜孜今天写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规范的)引用”。
2020-07-11210 - Elmer文中提到: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 ```
2019-12-2310 - Isaac「一个赋值表达式操作本身也是有“结果(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 ```
2020-06-289 - Zheng老师,我用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
2020-01-149 - Geek_baa4advar 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 } ``` 好运。:)
2020-05-048