JavaScript 核心原理解析
周爱民
《JavaScript 语言精髓与编程实践》作者,南潮科技(Ruff)首席架构师
32699 人已学习
新⼈⾸单¥59
登录后,你可以任选3讲全文学习
课程目录
已完结/共 28 讲
开篇词 (1讲)
JavaScript 核心原理解析
15
15
1.0x
00:00/00:00
登录|注册

02 | var x = y = 100:声明语句与语法改变了JavaScript语言核心性质

表达式的结果
变量声明的处理
动态添加的var声明
全局变量管理方式
let/const vs. var
变量提升问题
静态语义
try...catch
for
import
class
function
var
const
let
标识符的创建
静态语法分析
声明的特性
赋值操作
变量泄漏问题
读取值的差异
声明的处理
潜在的声明能力
变量声明
声明的特性
思考题
向一个不存在的变量赋值
从读取值到赋值
声明语句与语法
JavaScript核心性质的改变

该思维导图由 AI 生成,仅供参考

你好,我是周爱民。
如果你听过上一讲的内容,心里应该会有一个问题,那就是——在规范中存在的“引用”到底有什么用?它对我们的编程有什么实际的影响呢?
当然,除了已经提及过的delete 0obj.x之外,在今后的课程中,我还会与你讨论这个“引用”的其它应用场景。而今天的内容,就从标题来看,若是我要说与这个“引用”有关,你说不定得跳起来说我无知;但若说是跟“引用”无关的话呢,我觉得又不能把标题中的这一行代码解释清楚。
为什么这行代码看起来与规范类型中的“引用”无关呢?因为这行代码出现的时候,连 ECMAScript 这个规范都不存在。
我大概是在 JavaScript 1.2 左右的时代就接触到这门语言,在我写的最早的一些代码中就使用过它,并且——没错,你一定知道的:它能执行!
有很多的原因会促使你在 JavaScript 写出表达式连等这样的代码。从 C/C++ 走过来的程序员对这样的一行代码是不会陌生的。它能用,而且结果也与你的预期会一致,例如:
var x = y = 100;
console.log(x); // 100
console.log(y); // 100
它既没错、又好用,还很酷,你说我们为什么不用它呢?然而很不幸,这行代码可能是 JavaScript 中最复杂和最容易错用的表达式了。
所以今天我要和你一起好好地盘盘它。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

JavaScript中的声明语句和语法的改变对其核心特性产生了重大影响。本文深入探讨了JavaScript中的六种声明语句,包括变量和常量的声明方式,并强调了声明在静态语法分析中的重要性以及变量提升的问题。文章还讨论了变量和常量的读取和赋值过程,以及在ECMAScript 6之前和之后的差异。特别强调了JavaScript中赋值操作的语法特点,以及全局变量管理方式对“向一个不存在的变量赋值”所导致的变量泄漏的不可避免性。此外,文章还介绍了全局对象的属性表动态添加和变量名列表的维护,以及对变量声明和赋值过程的详细解析。通过本文,读者可以深入了解JavaScript中声明和赋值的机制,以及语法变化对其核心特性的影响。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《JavaScript 核心原理解析》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(63)

  • 最新
  • 精选
  • fatme
    声明和语句的区别在于发生的时间点不同,声明发生在编译期,语句发生在运行期。声明发生在编译期,由编译器为所声明的变量在相应的变量表,增加一个名字。语句是要在运行时执行的程序代码。因此,如果声明不带初始化,那么可以完全由编译器完成,不会产生运行时执行的代码。

    作者回复: +1 ^^.

    2019-11-14
    97
  • 海绵薇薇
    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-19
    3
    22
  • Ming
    〈以下是小生愚见〉 概念纷繁,建议老师将讲解重心放到这门语言的现有特性,贯之历史脉络,是否(怎样)解决了某种设计缺陷。这样,知识纵深感更强,并可指导实际工作以避免踩坑。适当穿插示例代码和图文更佳。

    作者回复: 多谢。我在后面的课程中尽量注意这个 ^^.

    2019-11-13
    2
    20
  • 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-02
    2
    18
  • 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-13
    13
  • 孜孜
    今天写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-11
    2
    10
  • 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-23
    10
  • 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-28
    9
  • 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-14
    9
  • Geek_baa4ad
    var 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-04
    8
收起评论
显示
设置
留言
63
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部