03 | a.x = a = {n:2}:一道被无数人无数次地解释过的经典面试题
03 | a.x = a = {n:2}:一道被无数人无数次地解释过的经典面试题
讲述:周爱民
时长25:24大小23.26M
与声明语句的不同之处
来自《JavaScript 权威指南》的解释
两个连续赋值的表达式
复现现场
回到标题中的示例
知识回顾
复习题
赞 8
提建议
精选留言(58)
- blacknhole2019-11-15从内容上其实已经说清楚了,不过在内容表达上还是会让人产生困惑,我觉得问题是出在“当前上下文中的那个变量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过去的值”那个对象已经不再被任何变量持有,所以它已经无法被访问到了,它“跑丢了”。 是这样吧?展开
作者回复: 赞的!就是这个意思。呵呵~
共 16 条评论103 - 青史成灰2019-11-16老师上面引用《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 = ...
共 4 条评论13 - 新哥2020-06-14画个图最好说明问题了,a和ref 指向同一块内存地址,保存的数据是{n:1}; 执行第二行的时候,a下移指向新的内存地址,保存的数据是{n:2}; 且第一块内存空间 添加新的属性x,因为ref.x被赋值a,所以ref.x指向新的刚添加的那个地址,数据为{n:2}; 这样ref指向原始的内存地址,a指向新的内存地址;
作者回复: 是的。谢谢~ ^^.
12 - 天方夜2019-11-181. with ({x:100}) delete x 中 delete 删除的是对象的成员,即 property x; 2. (0, eval) 之中有一步逗号运算; 3. 表达式 (a.x) 的计算结果是 a 对象的属性 x 这个引用,所以可行; 4. with 只指定属性查找的优先级,所以 with 里面 x = 100 还是会泄漏到全局。
作者回复: 第2个不太完整。不过总体满分😃 第二个涉及的问题到20讲才开讲呢^_^
共 2 条评论11 - 红白十万一只2020-02-25老师这题我看过别的文章,不过是与运算符优先级解释。 按照运算符优先级的思路: var a={n1} a.x=a={n:1} =的关联性是从右到左,优先级是3,赋值运算符的返回结果是右边的值 .(成员访问)的关联性是从右到左,优先级是19 a.x的赋值等于a={n:1},而a的赋值等于{n:1}。 按照顺序会先计算a={n:1}的值,但是a.x是成员访问优先级是19。 所以会先进行a.x的解析,解析结果就是变量a对象的引用(引用地址#001)并创建了a.x这个属性,引用被暂存。 这是表达式就是:#001.x=a={n:1} a={n:1}时修改了变量a(例#001)的引用地址为{n:1}(例#002)。 表达式就是#001.x={n:1}(例#002) 也就是#001这个引用地址中x的值被修改为了{n:1} #001这个引用地址的值也就是 { n:1, x:{n:1} } 但是这个引用已经没有任何变量、属性持有了 而变量a的值就是 {n:1} 关于这种解释有没有什么问题,麻烦老师解释一下。展开
作者回复: 这种解释是对的。并且跟这一讲的解释是同义的。只是由于两个解释的侧重点不同,所以貌似有不同而已。 这个解释中也引入了一个#001来说明,这个在本讲中被称为“原始的a”,又或者说是“原始的a的一个引用”。其实都是相同的意思,你按照这种关联来对照着看,就明白了。但是本讲侧重于说明表达式和引用,所以是更强调基于“引用(规范类型)”的解释过程。 我刻意没有讨论优先级的问题。在课后的留言评论中提到过按优先级来演算的过程,但也不如你这里的细致。优先级是运算规则的很重要的组成部分,在设计表达式语法时也很重要,但是我们的课程并不特别关注这个部分,所以我是有意不从这个角度入手来讲的。
共 2 条评论9 - weineel2019-11-15老是您好:我理解的指针和引用是,指针是存储的地址,引用是存储的别名。 在 js 中的“引用”与传统语言中的“指针”有哪些根本性的区别。
作者回复: 其实我早期也是这么理解的。好象大家理解事物的方式都差不多,就是从相似性出发,从差异性辨别。 但是我后来发现,与其如此,不如为新东西建个体系,然后在新体系中来看待这个新事物。这一下子就不同了。 以至于我现在对引用的认识,就不太依赖与比较或比拟。引用就是引用,它就是一个计算的结果,它存放结果中包括的那几个东西。它是一个数据结构,用在引擎层面来存储计算过程的中间信息,以及在连续计算中传递这些信息。
共 3 条评论9 - 🇧🇪 Hazard�...2020-04-15老师你好,我有一些关于词法环境规范的疑问,可能跟这一讲的内容有点出入,希望能得到您的解答。 1. 环境记录规范有 5 种,但是我没有找到什么资料去告诉我,什么声明会把标识符binding到具体哪个EnvironmentRecord中;还有就是全局变量会放在哪里? 2. ECMAScript中关于环境记录与标识符喜欢用 binding 这个词,我不知道是什么意思?这个变量是存储在环境记录规范中的吗?还是存储在别的地方?在执行上下文的结构中有一个叫 Realms 的东西,不知道是不是跟这个有关。 3. EnvironmentRecord的内部结构其实是怎样的?感觉听到了很多术语,但还是感觉很抽象。 我现在看到了第9讲,发现越来越有点看不懂,于是从头开始学,希望能得到老师的解答,如果解答起来比较复杂,能否提供一些其他资料链接。 谢谢!展开
作者回复: 在去年的D2上,我专门讲过一讲《JS 语言在引擎级别的执行过程》,对你提到的问题大都有涉及。并且,有丰富的图示讲解。所以我建议你先听听视频,或者你的许多问题就有解了。 在这里: https://v.youku.com/v_show/id_XNDUwNTc3MjUzMg==.html PPT在这里找: https://github.com/d2forum/14th/tree/master/PPT 还有文字版,在“2020前端工程师必读手册”里面有收录。你搜搜~
8 - 授人以摸鱼2019-11-24所以我现在这么理解js中的“值”和“引用”这两个概念了: “引用”保存了两个信息:对象的地址,和要查询的属性名(字符串或symbol) “值”只保存了一个信息:原始值本身,或一个地址 从引用中获取值这个操作是惰性的,只有真正要使用值的时候才会执行getvalue
作者回复: 是的。都对!赞!
9 - 蓝配鸡2019-11-18不明白为什么a.x 这个表达式的result是一个a的引用呢? 不应该是 undefined吗? 没明白...
作者回复: Result是引用。 value是undefined。 value = GetValue(Result)
共 3 条评论8 - 海绵薇薇2019-11-20hello 老师好: 一开始我不明白为啥要称 var a = 1; 是值绑定操作,看了几遍之后应该理解了,var 是一个申明,等号左边不是表达式。而赋值操作等号左边是一个表达式结果是引用,右边是值,这样完成的赋值操作。但是var 右边等号左边不是一个表达式所以不是赋值,换了名字叫绑定。
作者回复: YES! 这回侬对了。^^.
7 - Lambert2019-11-15“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
共 3 条评论7 - HoSalt2020-05-08老师 (test.fn)()和test.fn()的调用this都只想test,为什么前面的括号里面的内容没有返回值而是返回了引用
作者回复: 一对括号,亦即是所谓分组表达式`()`,这个东西是在JS中极其罕见的在执行中“返回结果(result)”的表达式。因为通常的表达式是“返回值(value)”的,这甚至包括返回所谓ECMAScript规范引用,这也是作为value来返回的。——另一个如此有趣的东西是表达式作为函数体的箭头函数。 在分组表达式中,“返回结果(result)”而不是“返回值(value)”其实是有着非常大的、非常有魔力的不同的。例如说: > (test.fn) 在这个表达式里面,`test.fn`的Result是一个ECMAScript规范中的引用,因此这个引用就被返回了,因此`(test.fn)()`这个函数调用中,fn()就能得到this。也因此(eval)与直接的eval没有区别,都是eval引用。 但是你注意看, > (test.fn) 分组表达式操作的“里面的表达式`test.fn`”是一个属性存取表达式,它返回“ECMAScript规范的引用”。而下面: > (0, test.fn) 代码中在“里面的表达式`0, test.fn`”是什么呢?是用`,`号分隔开的所谓的“连续运算表达式”,而这个连续运算表达式的第二个子表达式,才是`test.fn`,对吧? 连续运算表达式返回什么呢?很不幸,连续运算表达式返回“最后一个子表达式的值(value)”——我们前面说过,所有表达式中目前只有两个是直接返回Result的。其它的情况下,其实都会返回value,包括以value定义的规范类型,或者GetValue(result)。 所以说,事实上 > (0, test.fn) 表达式的返回的是连续运算的最后一个表达式的value,也就是test.fn的getValue(result),也就是fn这个函数。因此,再调用 > (0, test.fn)() 的时候,就丢失掉了test这个对象引用了。
共 4 条评论6 - GitHubGanKai2020-01-11老师你好,有个问题想要请教一下你,就是MDN中:typeof 操作符返回一个字符串,表示未经计算的操作数的类型。那么这句话中的 ‘未经计算的操作数’是什么意思呢?这个‘未经计算的操作数’有哪些类型呢?而且这个typeof的返回值,返回的应该不是一种类型吧!因为用typeof检测类型的时候可能返回 'function',但是function又不属于数据类型,是不是有点矛盾呢?
作者回复: 我之前并没有听过关于这个 ‘未经计算的操作数’的说法。因此我特地地看了一下MDN中的相关说明。 ‘未经计算的操作数(unevaluated operand)’这个,其实也并没有特别的难解。例如有一个值是2的常量x,对于这个`x`,如果它“计算了”,那结果当然就是2,对吧。那么这种情况下,“未经计算时的x”是什么呢? 这个其实还是我们在文章中说的“引用(规范类型)”。“引用(规范类型)”作为左手端时,只是引用,并不求值,这种情况下它就是`unevaluated operand`。所以,一个错误的、根本不存在的引用也可以被typeof操作,因为这个“错误的、根本不存在的”并没有被“计算”,所以也就不会抛出错误。例如你试试: ``` typeof adfasdljkfla; // <- 随便一个变量名 ``` 之所以没有异常发生,就是后面的`adfasdljkfla`这个东东“未经计算”。同样的,如果我们尝试下面的代码: ``` typeof(adfasdljkfla); // 同上例 ``` 这里其实多了一个操作符,就是一对括号表示的“分组运算符(grouping)”,这个运算符也是“返回未经运算的结果”。所以同样,不会出错。——在我们这个系列的文章中,这种情况称为“引用(规范类型)”,或者一个“(未决定操作手性rhs/lhs的)结果Result”。 还有你的另一个问题: > 而且这个typeof的返回值,返回的应该不是一种类型吧! 这个是其它类型的语言来理解函数式语言的一个常见误区。尤其是,如果你以传统的(经典的)数据结构的知识为基础,那么更是会有误解。 在JavaScript中,以及在函数式语言中 ,“函数”的确是一种数据类型。它可以作为值(在函数界面上)传递,也可以作为结果(在函数返回中)传出,还可以查看类型,还可以与其它数据进行运算(例如 1 + (function(){})),那么它为什么不是“一种数据类型”呢? 函数既是数据,也是运算,这个是函数式语言的核心概念。
5 - Wiggle Wiggle2019-11-15那么“引用”这个数据结构究竟是什么样子呢?在引擎内部是如何实现的呢?老师可否讲一下或者给个链接?
作者回复: https://tc39.es/ecma262/#sec-reference-specification-type ^^.
5 - itgou2019-11-29书读百遍,其义自见,在听读了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),那么它的确会被废弃。你从数据结构的角度上思考一下就明白了,没有办法回溯,也没有别的东西来引用任何一个“向前的”结点,只会有最后一个结点被引用(从而不被废弃)。
4 - Geek_8d73e32020-08-06老师,那我还有一个疑惑 既然let x 为词法声明,词法声明不会初始化绑定一个undefined,而且js引擎拒绝访问未初始化的词法声明 那如何解释以下代码 let x; console.log(x) //这里输出undefined展开
作者回复: let x; 这是声明没错,但它有“执行期语义(Runtime Semantics)”。对于LetOrConst来说,这个执行期语义就是“绑定初始值”。 简单地说,就是“执行到这一行就初始化了”。 参见这里: https://tc39.es/ecma262/#sec-let-and-const-declarations-runtime-semantics-evaluation
3 - Chor2020-02-21老师您好,我想问一下: 1. “这个被赋值的引用“a.x”其实是一个未创建的属性,赋值操作将使得那个“原始的变量a”具有一个新属性” 这句话是不是说,x这个本来不存在的属性仅在第二次赋值操作的时候才会被创建? 2. a.x 这个表达式计算的结果(Result)是一个引用,是否可以把这个引用看作一个“容器”,这个“容器”包含着原始的a的信息?还是说这个引用就是原始的a本身? 3.一开始程序在分析 a.x 的时候(第二次赋值发生之前),这个表达式的Result中是否包含相关的x的信息?还是说这时候x只是一个暂时不存在、等待创建的东西?展开
作者回复: 1. 是的。 2. 是的。“引用(规范类型)”可以看作原始a的容器,包含原始a的信息。 3. "表达式的Result中是否包含相关的x的信息",是的,是包含着x的相关信息。 “引用(规范类型)”是一个结构,通常有三个成员,base、name和strict。所以,无论`x`属性是否存在,`a.x`都被表达为{"base": a, "name": "x", ...},这个结构就是`a.x`的引用,或者说是Result。直到需要读写它的时候(例如作为rhs),才会去检索a["x"]是否真实存在,并决定后续操作。 关于“引用(规范类型)”的一些细节,你可以看看这个: https://github.com/d2forum/14th/ 在《JS 语言在引擎级别的执行过程》中专门有一部分是讲述它的。视频在这里: https://v.youku.com/v_show/id_XNDUwNTc3MjUzMg==.html
共 2 条评论3 - Ming2019-11-16反反复复看了几遍,留言区里帮我屡清了思路。 第一句: 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”导致栈异常之外,这个好象不太对。其它应该没什么问题了。
3 - Smallfly2019-11-15文章读起来挺吃力的,可能是 JS 很多设计跟固有思维不一致,也可能是对 EMACScript 规范不了解,老师能否考虑下放文章中涉及到的规范地址?
作者回复: 好主意!我问问编辑能怎么改。 后面的内容我尽量都加上。多谢提议!
共 2 条评论3 - 卡尔2020-06-11如果a(或elemData)总是被重写的旧的变量,那么如下代码: 老师,这话是什么意思?
作者回复: 在下面的示例代码中: node.next中的`node`,就是这个”总是重写的旧的变量“;而, node.index中的`node`则是”新的变量“。 所以,”a(或elemData)总是被重写“,意味着在建立链表的过程中它是可以用来”暂存上一个节点(node)“的。
2