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核心原理解析
登录|注册

03 | a.x = a = {n:2}:一道被无数人无数次地解释过的经典面试题

周爱民 2019-11-15
你好,我是周爱民。
在前端的历史中,有很多人都曾经因为同一道面试题而彻夜不眠。这道题出现在 9 年之前,它的提出者“蔡 mc(蔡美纯)”曾是 JQuery 的提交者之一,如今已经隐去多年,不复现身于前端。然而这道经典面试题仍然多年长挂于各大论坛,被众多后来者一遍又一遍地分析。
在 2010 年 10 月,Snandy于 iteye/cnblogs 上发起对这个话题的讨论之后,淘宝的玉伯(lifesinger)也随即成为这个问题早期的讨论者之一,并写了一篇“a.x = a = { }, 深入理解赋值表达式”来专门讨论它。再后来,随着它在各种面试题集中频繁出现,这个问题也就顺利登上了知乎,成为一桩很有历史的悬案。
蔡 mc 最初提出这个问题时用的标題是“赋值运算符:"=", 写了 10 年 javascript 未必全了解的"="”,原本的示例代码如下:
var c = {};
c.a = c = [];
alert(c.a); //c.a是什么?
蔡 mc 是在阅读 JQuery 代码的过程中发现了这一使用模式:
elemData = {}
...
elemData.events = elemData = function(){};
elemData.events = {};
并置疑,为什么elemData.events需要连续两次赋值。而 Snandy 在转述的时候,换了一个更经典和更有迷惑性的示例:
var a = {n:1};
a.x = a = {n:2};
alert(a.x); // --> undefined
Okay,这就是今天的主题。
接下来,我就为你解释一下,为什么在第二行代码之后a.x成了 undefined 值。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《JavaScript核心原理解析》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(22)

  • blacknhole
    从内容上其实已经说清楚了,不过在内容表达上还是会让人产生困惑,我觉得问题是出在“当前上下文中的那个变量a”和“原始的变量a”这样的表述方式上。或许如下表述在语意上会更加清晰:

    1,这里其实只有一个变量,就是a,不存在那个变量a和这个变量a之分,有分别的其实是变量a的值,即“变量a过去的值”和“变量a现在的值”。

    2,当发生第一次赋值时,“左操作数a作为一个引用被覆盖”,此时变量a产生了新的值。

    3,第二次赋值时,“整个语句行的最左侧‘空悬’了一个已经求值过的‘a.x’”,这是一个表达式结果,这个结果以及其中保留的“a”(即“变量a过去的值”)与变量a已经没有关系了,因为变量a已经有了新的值,即“变量a现在的值”。

    4,第二次赋值其实是,在“变量a过去的值”那个对象上,创建一个新属性x,x的值为变量a的值,即“变量a现在的值”。

    5,在第二次赋值后,因为“变量a过去的值”那个对象已经不再被任何变量持有,所以它已经无法被访问到了,它“跑丢了”。

    是这样吧?

    作者回复: 赞的!就是这个意思。呵呵~

    2019-11-15
    4
    22
  • 蓝配鸡
    不明白为什么a.x 这个表达式的result是一个a的引用呢?

    不应该是 undefined吗?

    没明白...

    作者回复: Result是引用。
    value是undefined。

    value = GetValue(Result)

    2019-11-18
    1
    3
  • sprinty
    老是您好:我理解的指针和引用是,指针是存储的地址,引用是存储的别名。

    在 js 中的“引用”与传统语言中的“指针”有哪些根本性的区别。

    作者回复: 其实我早期也是这么理解的。好象大家理解事物的方式都差不多,就是从相似性出发,从差异性辨别。

    但是我后来发现,与其如此,不如为新东西建个体系,然后在新体系中来看待这个新事物。这一下子就不同了。

    以至于我现在对引用的认识,就不太依赖与比较或比拟。引用就是引用,它就是一个计算的结果,它存放结果中包括的那几个东西。它是一个数据结构,用在引擎层面来存储计算过程的中间信息,以及在连续计算中传递这些信息。

    2019-11-15
    1
    3
  • 天方夜
    1. with ({x:100}) delete x 中 delete 删除的是对象的成员,即 property x;
    2. (0, eval) 之中有一步逗号运算;
    3. 表达式 (a.x) 的计算结果是 a 对象的属性 x 这个引用,所以可行;
    4. with 只指定属性查找的优先级,所以 with 里面 x = 100 还是会泄漏到全局。

    作者回复: 第2个不太完整。不过总体满分😃
    第二个涉及的问题到20讲才开讲呢^_^

    2019-11-18
    2
  • 青史成灰
    老师上面引用《JavaScript权威指南》中说“JavaScript总是严格按照从左到右的顺序计算表达式”,那为什么下文的2次赋值操作`a.x = a = {n:2}`,是先赋值`a={n:2}`,然后才是`a.x = a`呢

    作者回复: 这个顺序是这样来读的(你仔细看看顺序是不是从左至右):


    第一次
    ======
    a.x = a = {n:2}
    ^1 ^2

    第二次
    ======
    a = { n: 2 }
    ^3 ^4

    第三次
    ======
    { n: 2 }
    ^5 ^6

    第四次(以下求值然后回传)
    ======
    求值传回(4)
    @4 <= ^5, ^6

    第五次
    ======
    求值回传(3)
    @3 = (^4 <= ^5, ^6)

    第六次
    ======
    求值回传(2)
    a = @3 = (^4 <= ^5, ^6)

    第七次
    ======
    求值回传(1)
    a.x = a = @3 = ...

    2019-11-16
    1
    2
  • Lambert
    “a.x”这个表达式的语义是:
    计算单值表达式a,得到a的引用;
    将右侧的名字x理解为一个标识符,并作为“.”运算的右操作数;
    计算“a.x”表达式的结果(Result)。
    老师请问一下 这个时候 的 Result 是 undefined吗? 因为还没有进行赋值

    作者回复: 这个时候的Result是一个“引用(Reference)”。

    如果它在后续运算中被作为lhs,例如 a.x = ...,那么它就是作为“引用”来使用,这样就可以访问到`x`这个属性,并置值;如果它在后续运算中被作为rhs,例如console.log(a.x),那么它就会被GetValue()取值(并作为值来使用),于是console.log()就能打印出它的值来。

    a.x整体被作为“一个操作数”,它的用法与它被使用的位置是有关的。但是“得到它(亦即是对a.x这个表达式求Result)”的过程并没有什么不同。

    你可以读一下这个“.”操作在ECMAScript中的规范:
    https://tc39.es/ecma262/#sec-property-accessors-runtime-semantics-evaluation

    2019-11-15
    2
    2
  • Summer
    有的地方描述有点晕,看了好几遍才明白表述的意思,要是有一些动态的图演示的话可能效果更好点

    作者回复: 这个……确实实现起来有难度。我通常在做讲演稿的时候才会用这种方式,但讲演稿的讲法,跟这里的课程的讲课方法区别还是很大的。

    当然,即使不用动态的图,使用流程图或框线图其实也挺好的。不过,总之,以极客时间的“语音课程”来说,很难讲。——话说回来,如果是需要更深的阅读,以及更丰富的图例,以及表格等表现形式,那么可以看我的书哦。《JavaScript语言精髓与编程实践》这本书的第三版……快要出版了吔~ ^^.

    2019-11-15
    2
  • 反反复复看了几遍,留言区里帮我屡清了思路。

    第一句:
    var a = {n : 1};
    // 变量声明,变量a作为引用,最终指向了等号右侧表达式的计算结果,即一个对象{n : 1}

    第二句:
    a.x = a = {m : 2};
    // 两个等号划分了3个表达式(宏观上);
    // a.x... 要为a添加x属性的蠢蠢欲动,缓存a,a = {n : 1};
    // a.x = a... 没有做赋值操作!如果代码写到这截止,事实上会报一种错,叫Error: Maximum call stack size exceeded
    // a.x = a = {m : 2}; 做了两次赋值操作,首先后半段先做赋值操作,a的引用指向了新的对象{m : 2},第二次赋值操作完成了为之前缓存的a添加x属性的如愿已久,x的引用指向后面的这个完成了初始化的a。现在,我们去使用a,实际上使用的是后面的这个a,a = {m : 2},那之前缓存的那个a呢?被引擎吃掉了,无法访问到。那它指向哪个对象呢?{n : 1, x : {m : 2}},理由是一次初始化和一次属性拓展。

    作者回复: 除了“a.x = a”导致栈异常之外,这个好象不太对。其它应该没什么问题了。

    2019-11-16
    1
  • 旺旺
    JavaScript果然太灵活,然后感觉好难啊
    2019-11-15
    1
  • 许童童
    老师讲得真细啊,学到了很多,谢谢老师。
    2019-11-15
    1
  • Wiggle Wiggle
    那么“引用”这个数据结构究竟是什么样子呢?在引擎内部是如何实现的呢?老师可否讲一下或者给个链接?

    作者回复: https://tc39.es/ecma262/#sec-reference-specification-type

    ^^.

    2019-11-15
    1
  • Smallfly
    文章读起来挺吃力的,可能是 JS 很多设计跟固有思维不一致,也可能是对 EMACScript 规范不了解,老师能否考虑下放文章中涉及到的规范地址?

    作者回复: 好主意!我问问编辑能怎么改。
    后面的内容我尽量都加上。多谢提议!

    2019-11-15
    1
    1
  • qqq
    1. x 就是一个引用,没有异常发生,返回 true
    2. 一个是引用求值,一个是表达式求值
    3. a.x 就是一个引用,可以赋值
    4. 添加到全局对象了

    作者回复: 最后一个答案有点草率了,其它的都很简洁地指到关键处了。可以+2分~ 哈哈~

    2019-12-03
  • 穿秋裤的男孩
    老师,假如换个位置如下:
    var a = { n: 1 };
    a = a.x = { n: 2 }; // 之前为a.x = a = { n: 2 };
    a.x的赋值是在第二次(从左往右执行),也就是a = ...是第一次赋值,为什么a.x还是undefined呢?
    难道第二次赋值的结果是{ n: 2 },并且第二次赋值完成之后,会把结果再赋值给第一次a = ...吗?这边的赋值过程能帮忙解惑吗?
    非常感谢!

    作者回复: “第二次赋值的结果是{ n: 2 },并且第二次赋值完成之后,会把结果再赋值给第一次a = ...”

    ^^,答案正确。

    2019-12-02
  • itgou

    书读百遍,其义自见,在听读了n遍之后,终于理解了标题中的代码,但是看到链表代码,又有点晕了,亲老师解答一下。问题如下
    var i = 10, root = {index: "NONE"}, node = root;


    while (i > 0) {
      node.next = node = new Object; //本行开头的node.next未被丢弃,是因为这里大括号里面是一个闭包,而外层node=root对这里有引用吗?
      node.index = i--;
    }

    // 测试
    node = root;
    while (node = node.next) {
      console.log(node.index);
    }

    问题写在了while循环当中,请老师回答一下。

    作者回复: 闭包这个概念是与函数相关的(当然对象闭包则与with相关),所以这里不适合用“闭包”这个词。
    在大括号内的是一个块级作用域,你也可以叫“词法的块级作用域”或者直接叫“作用域”。

    当一个“单向链表”处于系统中时,如果链表首(root)没有被引用的话,你是找不到这个完整的链表的。——很明显,你没有办法反向地检索。所以会有外层的node = root。当然,从引擎的角度上来说,如果是这样的一个链表(没有变量来引用root),那么它的确会被废弃。你从数据结构的角度上思考一下就明白了,没有办法回溯,也没有别的东西来引用任何一个“向前的”结点,只会有最后一个结点被引用(从而不被废弃)。

    2019-11-29
  • Ronnie
    a.x= ...., 这个是不是可以这样理解, 在求值a.x=..., 时候,由于x 在原来变量里不存在,所以js engine,首先会create 一个新的属性x,然后把这个x 的引用作为赋值的左操作数,然后依据右边的操作数完成赋值,但右边的a={n: 2}是一个表达式,赋值完成后,返回的是a 的新值{n: 2}, 这样赋值后 x 就等于{n: 2}, 由于x所在的对象{n: 1, x: {n: 2}} 现在没有引用(很快会被回收)也就没法访问了。

    作者回复: “所以js engine,首先会create 一个新的属性x”。——这个操作是不会发生的。如果这样做,那么意味着“访问不存在的属性”是一个“创建属性”的行为,这样一来,系统的负担/开销就大得不得了了。

    所以我在讲述这里的时候,说的是“空悬”了一个引用。就是这个意思。除了没有“创建属性”这样的隐式行为之外,你其它的描述是没什么问题的。

    2019-11-26
    1
  • 南墙的树
    看不懂,多看几遍
    2019-11-25
  • 授人以摸鱼
    所以我现在这么理解js中的“值”和“引用”这两个概念了:
    “引用”保存了两个信息:对象的地址,和要查询的属性名(字符串或symbol)
    “值”只保存了一个信息:原始值本身,或一个地址
    从引用中获取值这个操作是惰性的,只有真正要使用值的时候才会执行getvalue

    作者回复: 是的。都对!赞!

    2019-11-24
  • 简<单
    文中老师说a.x = a = { n: 2 }的执行是从左向右的,但是第一次赋值操作是 a = { n: 2},这一点说说自己的理解,希望老师确认一下:
    1、从左向右,执行表达式a.x = a = { n: 2 } , 先执行a.x
    2、然后执行表达式 a.x = 「 a= { n :2} 」(这里加「」是为了表达清晰),由于后面a={ n: 2}也是个表达式,得继续求值之后,才能执行第一个 = 赋值操作
    3、执行a = { n :2 } ,这里同理,先a,再{ n: 2}
    4、这个时候 = 两边的表达式已经求值完成,可以执行赋值操作了( a = { n: 2} ),所以说第一次赋值是a = { n: 2} ,同时,这个赋值表达式的求值结果是{ n: 2}
    5、这时候第一个 = 两边表达式的求值都已经完成(分别是a.x 和 { n: 2}),这时候执行第二次赋值操作,a.x = { n: 2 }
    以上是我自己的理解,希望老师批改一下~

    顺便对于复习题第三道“设“a.x === 0”,试说明“(a.x) = 1”为什么可行”,没有看懂题意😢,这难道不是说a.x先是0,然后再给它赋值为1嘛?为什么会有可行不可行一说?对a.x加了括号也是一个表达式,它的求值结果也就是a.x引用,对它赋值1和直接操作a.x=1应该都是一样的吧?我已经绕晕了T_T

    作者回复: 前面部分的问题,在留言中有一个给“青史成灰”的回复,应该是一样的,你先看看。

    后面这个问题,你的答案是对的,“对它赋值1和直接操作a.x=1应该都是一样”。但是这个问题的另一个“扩展版本”是,如果“如何该赋值是成立的,那么为什么(0, a.x) = 1”不成立。^^.

    :)

    2019-11-20
  • 海绵薇薇
    hello 老师好:
    一开始我不明白为啥要称 var a = 1; 是值绑定操作,看了几遍之后应该理解了,var 是一个申明,等号左边不是表达式。而赋值操作等号左边是一个表达式结果是引用,右边是值,这样完成的赋值操作。但是var 右边等号左边不是一个表达式所以不是赋值,换了名字叫绑定。

    作者回复: YES! 这回侬对了。^^.

    2019-11-20
收起评论
22
返回
顶部