JavaScript核心原理解析
周爱民
《JavaScript语言精髓与编程实践》作者,南潮科技(Ruff)首席架构师
立即订阅
3529 人已学习
课程目录
已更新 16 讲 / 共 21 讲
0/3登录后,你可以任选3讲全文学习。
开篇词 (1讲)
开篇词 | 如何解决语言问题?
免费
从零开始:JavaScript语言是如何构建起来的 (5讲)
01 | delete 0:JavaScript中到底有什么是可以销毁的
02 | var x = y = 100:声明语句与语法改变了JavaScript语言核心性质
03 | a.x = a = {n:2}:一道被无数人无数次地解释过的经典面试题
04 | export default function() {}:你无法导出一个匿名函数表达式
05 | for (let x of [1,2,3]) ...:for循环并不比使用函数递归节省开销
从表达式到执行引擎:JavaScript是如何运行的 (6讲)
06 | x: break x; 搞懂如何在循环外使用break,方知语句执行真解
07 | `${1}`:详解JavaScript中特殊的可执行结构
08 | x => x:函数式语言的核心抽象:函数与表达式的同一性
09 | (...x):不是表达式、语句、函数,但它却能执行
10 | x = yield x:迭代过程的“函数式化”
11 | throw 1;:它在“最简单语法榜”上排名第三
从原型到类:JavaScript是如何一步步走向应用编程语言的 (1讲)
12 | 1 in 1..constructor:这行代码的结果值,既可能是true,也可能是false
不定期加餐 (3讲)
加餐 | 捡豆吃豆的学问(上):这门课讲的是什么?
免费
加餐 | 捡豆吃豆的学问(下):这门课该怎么学?
免费
加餐 | 让JavaScript运行起来
免费
JavaScript核心原理解析
登录|注册

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

周爱民 2019-11-13
你好,我是周爱民。
如果你听过上一讲的内容,心里应该会有一个问题,那就是——在规范中存在的“引用”到底有什么用?它对我们的编程有什么实际的影响呢?
当然,除了已经提及过的delete 0obj.x之外,在今后的课程中,我还会与你讨论这个“引用”的其它应用场景。而今天的内容,就从标题来看,若是我要说与这个“引用”有关,你说不定得跳起来说我无知;但若说是跟“引用”无关的话呢,我觉得又不能把标题中的这一行代码解释清楚。
为什么这行代码看起来与规范类型中的“引用”无关呢?因为这行代码出现的时候,连 ECMAScript 这个规范都不存在。
我大概是在 JavaScript 1.2 左右的时代就接触到这门语言,在我写的最早的一些代码中就使用过它,并且——没错,你一定知道的:它能执行!
有很多的原因会促使你在 JavaScript 写出表达式连等这样的代码。从 C/C++ 走过来的程序员对这样的一行代码是不会陌生的。它能用,而且结果也与你的预期会一致,例如:
var x = y = 100;
console.log(x); // 100
console.log(y); // 100
它既没错、又好用,还很酷,你说我们为什么不用它呢?然而很不幸,这行代码可能是 JavaScript 中最复杂和最容易错用的表达式了。
所以今天我要与你一起好好地盘盘它。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《JavaScript核心原理解析》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(30)

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

    作者回复: +1 ^^.

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

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

    2019-11-13
    10
  • 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
    7
  • 海绵薇薇
    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
    5
  • 蓝配鸡
    醍醐灌顶,但是有一些疑问:
    文中说,
    "如果是在一门其它的(例如编译型的)语言中,“为变量 x 绑定一个初值”就可能实现为“在创建环境时将变量 x 指向一个特定的初始值”。这通常是静态语言的处理方法,然而,如前面说过的,JavaScript 是门动态的语言,所以它的“绑定初值”的行为是通过动态的执行过程来实现的,也就是赋值操作。"

    为什么动态语言就不可以给变量初始化, 一定要使用动态赋值呢?
    我对动态语言的理解是,变量的类型可以在运行时改变,静态语言变量的类型不可以改变。 但是这性质好像并不影响初始化?

    作者回复: 动态语言的诸多细节,其实要到18讲之后才会讨论到。不过有个概念上的问题,并不是说有动态类型就是动态语言,或者说支持动态执行就是动态语言。有很多方面的“动态语言的特性”,这个需要详细解析。

    仅是说初始化这一项,使用动态赋值的原因是因为这个值必须要到执行期环境中才能确定下来,而在静态语法分析阶段是确认不了的。——所以它不可能“先于环境”而执行。

    2019-11-13
    1
    3
  • Marvin
    相当于
    var/let/const x = (y =100)
    再拆就是
    y=100 // 变量泄漏
    var x=y // 模拟表达式返回值赋值

    作者回复: 是的。简洁,正确。^^.

    2019-11-14
    2
  • 佳民
    思考题:var声明会声明提升,在语法解析(静态分析)阶段进行,不是在运行阶段执行,这样理解对吗?

    作者回复: 是的。不过所有的6种声明都是如此,非独var声明。

    2019-11-13
    1
    2
  • Geek__kkkkkkkk
    老师,您文章中讲到“ECMAScript 规定在这个全局对象之外再维护一个变量名列表(varNames),所有在静态语法分析期或在 eval() 中使用var声明的变量名就被放在这个列表中。然后约定,这个变量名列表中的变量是“直接声明的变量”,不能使用delete删除。”,这里面的意思我理解了 eval() 中使用var声明的变量名,不可用delete删除,但是下面的代码中eval()声明了b,configurable是true,可删除,是否矛盾了?还是我理解的不够全面?

    作者回复: > 这种情况下使用var声明的变量名尽管也会添加到varNames列表,但它也可以从varNames中移除(这是唯一一种能从varNames中移除项的特例...

    文章中是解释过的。可能我没有足够地强调这个特例,或者它的特殊性吧。

    2019-11-13
    1
    2
  • Makus
    附「语句和声明」的链接:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements
    严格意义上来说,除了本篇中出现的六种和链接中的五大类都不能算作语句吧。
    例如 :标题中的 y = 100
    麻烦老师指正一下,不太懂

    作者回复: 不着急。会慢慢讲到的。^^.

    还有,我所讲的有些会与MDN有出入,这个你可以先存疑。不必急着得到“YES/NO”这样的答案,慢慢地听到后面就会知道我为什么这么讲了。

    2019-11-13
    2
  • 家家家家
    忘了在哪本书中还是哪篇文章中讲过,变量的生命周期:声明阶段、初始化阶段、赋值阶段。
    老师这里讲的静态分析阶段就是指的变量生命周期中的声明阶段对吗?
    对于var,它的声明阶段和初始化阶段是一起发生的,都在静态分析中;对于let,它的声明阶段和初始化阶段是分开的,只有声明阶段在静态分析中,是这样理解的嘛?

    作者回复: 这个讲法倒也不错,比较容易理解。
    不过有些不严谨的地方,比如说,“var x;”这样的声明,“声明阶段和初始化阶段一齐发生”是对的,因为var声明的时候,名字声明并初始化为undefined确实是同时发生的。但是,如果是“var x = 100;”,那么就比较容易混淆了。因为“x = 100”其实是赋值,而不是引擎层面的“初始化”。

    对于let来说,“声明阶段和初始化阶段是分开”其实更不严谨。确切地说,let就没有“初始化阶段”。而用户代码“let x = 100”中的“x = 100”就是赋值阶段了。“let 没有初始化阶段”,所以才会出现它在未赋值之前不能读的现象。

    用这样三个阶段来解释这件事情,跟ECMAScript中的逻辑并不矛盾(而且也可以正确解释),只是细节上需要严谨一点、留意一点就行。

    2019-12-02
    1
  • 授人以摸鱼
    发现我好像对全局作用域的理解有一些偏差:
    var,或者没有声明直接赋值,这样的创建标识符(引用)是作为global对象的字段存在的,可以用Object.getOwnPropertyDescriptor从global上读到。
    全局作用域里用let,const创建的变量,虽然也是全局可见,但它并没有创建在global上,而是创建在了另一个地方。从作用域链的视角来看的话,这个作用域要比global低一级这样子。

    作者回复: 你是对的。在JavaScript中,“全局环境”里面的var与let/const是用了两个东西来管理的,所以他们也确实是创建在不同的地方。
    但是从“作用域链”的角度上来说,它们并没有级别高低(也就是parent没有相互指向)。使得它们存取的效果有差别的,是因为“全局环境”采用了词法环境优先(也就是let/const声明)的顺序。

    2019-11-22
    1
  • 陆昱嘉
    老师,一个赋值表达式的左边和右边其实“都是”表达式,那么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

    2019-11-17
    1
  • blockman-luz
    这一节不知主题是什么?这样讲会造成知识点东一快西一快没有发挥系统学习的效果,更像很多很散的集合

    作者回复: 有主题的哦~ ^^.

    目录中对这一个段落的定义是“JavaScript语言是如何构建起来的”。基本上是讲静态语言语义,以及它在引擎级别上的实现。5个小节,从值、引用等基础,到模块和语句,基本上覆盖构成语言的核心组件。

    可不是在乱讲哦。^^.

    2019-11-15
    1
    1
  • ssala
    老师,像变量提升这种不符合直觉的设计,难道规范不能将其移除吗?不移除是为了兼容老代码吗?我想应该没有代码会依赖这个特性吧?

    作者回复: 一是因为向下兼容,这是规范订制过程中面临的主要挑战之一。第二个原因,在于按照JavaScript的核心设计,在原理上这就很难绕过去。例如就包括不受兼容性影响的import,其实也是有变量提升效果的。例如下面这样的代码也是成立的:

    ```
    console.log(x);
    import x from './test.js';
    ```

    2019-11-13
    1
  • 凯文1985
    对于js在词法分析和语法分析中发生的事情还不够了解 希望也能借这个课程讲解一下 :)
    2019-11-13
    1
  • Smallfly
    老师文中说变量声明分为两步,静态分析和动态绑定,JS 是在进入每个作用域后,执行前进行词法分析的么?

    作者回复: 是在加载.js文件之后,执行第一行用户代码之前,就已经完成了全部的词法分析(当然,除了eval执行的)。

    2019-11-13
    1
  • 旺旺
    简单语法语句背后还有这么多门道.
    声明不是语句,声明只是在告诉编译器,并没有对运行产生影响,是这样吗?

    作者回复: 是的。

    2019-11-13
    1
  • 麦田里的守望者
    为了兼容旧的 JavaScript 语言设计,现在的 JavaScript 环境仍然是通过将全局对象初始化为这样的一个全局闭包来实现的。但是为了得到一个“尽可能”与其它变量环境相似的声明效果(varDecls),ECMAScript 规定在这个全局对象之外再维护一个变量名列表(varNames),所有在静态语法分析期或在 eval() 中使用var声明的变量名就被放在这个列表中。然后约定,这个变量名列表中的变量是“直接声明的变量”,不能使用delete删除。
    这段话的最后几句,是不是写错了

    作者回复: 这里没错的。不过因果是说反了,varNames并不“决定”列表中的变量不能用delete删除。但是由于delete global.x无效时,总是不会删除varNames中的项,所以表现起来,也是它里面的项不能删除。

    其实严格来说,并没有其它地方的逻辑需要特别地使用到varNames,它非常没存在感。只是从引擎的角度上来说,如果没有varNames,它也不能在运行期识别哪个全局变量是从var声明来的,而哪个又是直接在global上创建的属性。

    2019-12-01
  • 穿秋裤的男孩
    一天只敢看一章,看多了cpu受不了了。。。这一章看了第三遍,总算看懂百分之80了。。
    有些名次听过,但为啥就是讲不上了是个啥。。比如语句,表达式什么的。。
    2019-11-28
  • 因为有你心存感激
    var x = y =100 中的y 是可以通过delete y 删除,老师又说 delete 只删除引用不删除值,在这里y是引用吗?还有对于 js 为什不能 变量名 = 值 这样赋值呢?

    作者回复: 1. y是属性(也就是一个属性引用)。y是泄露到全局的变量,他事实上变成了global.y。
    2. js能“变量名=值”这样赋值,这称为赋值表达式,但“var 变量名=值”是一个声明语句,其中的“=值”称为初始器,他只是跟赋值表达式“形似”而已,在语法和语义上都不是同一个东西。

    2019-11-27
收起评论
30
返回
顶部