从规范看赋值表达式的解析

栏目: JavaScript · 发布时间: 5年前

内容简介:从一道常见的面试题开始:显然,关键点在于最后一个语句的执行。这个语句的执行主要涉及了规范中规定了三种形式的赋值表达式:

从一道常见的面试题开始:

var a = {n: 1};
var b = a;
a.x = a = a.y = {n: 2};
console.log(a.x);
console.log(b.y);
复制代码

显然,关键点在于最后一个语句的执行。这个语句的执行主要涉及了 属性获取表达式赋值表达式 ,先去规范里看对于这两种语法及其执行的规定。

1. 赋值表达式

规范中规定了三种形式的赋值表达式:

AssignmentExpression : 
	ConditionalExpression
	LeftHandSideExpression = AssignmentExpression 
	LeftHandSideExpression AssignmentOperator AssignmentExpression
复制代码

a.x = a = a.y = {n: 2}; 是其中的第二种形式 (第三种形式中的 AssignmentOperator 在规范中是复合赋值符号,即 += 等等). 有的同学说,js中 = 是从右向左执行的。对于语句的执行,规范中写道:

The source text of an ECMAScript program is first converted into a sequence of input elements, which are tokens, line terminators, comments, or white space. The source text is scanned from left to right, repeatedly taking the longest possible sequence of characters as the next input element.

也就是说,源代码被转换为一系列的输入单元(输入单元的类型包括token,行结束符,注释和空白符); 然后从左到右进行解析,重复以最长子序列作为下一个输入单元。除此之外,规范规定了每种类型语句的执行流程,却并没有地方提到 = 要从右向左执行。造成这种广泛的误解的,可能是类似MDN 在语句优先级的地方提到了 =Associativity 是从右到左,但其实这个 Associativity 并不是执行流程。

规范中规定了表达式 AssignmentExpression : LeftHandSideExpression = AssignmentExpression 的执行流程(11.13.1节中),我们把这个流程命名为 parseAssignment, 后面会以 parseAssignment(n)来指代执行这里的第n步:

The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:
1. Let lref be the result of evaluating LeftHandSideExpression.
2. Let rref be the result of evaluating AssignmentExpression.
3. Let rval be GetValue(rref).
4. Throw a SyntaxError exception if the following conditions are all true:
  Type(lref) is Reference is true
  IsStrictReference(lref) is true
  Type(GetBase(lref)) is Environment Record
  GetReferencedName(lref) is either "eval" or "arguments"
5. Call PutValue(lref, rval).
6. Return rval.
复制代码

显然,第一步是 evaluating LeftHandSideExpression ,将结果赋给变量 lref 。然后是 evaluating AssignmentExpression , 将结果付给 rref 。那么在表达式 a.x = a = a.y = {n: 2} 中 哪一部分是 LeftHandSideExpression , 哪一部分是 rref 有没有疑问呢?会不会 a.x = a 或者 a.x = a = a.yLeftHandSideExpression 呢?

再来看 LeftHandSideExpression 的语法:

LeftHandSideExpression : 
	NewExpression 
	CallExpression
复制代码

只有这两种形式,它们具体的语法定义我们就不翻了,不然可能会翻出10多层(事实上,规范中正是通过这种嵌套的表达式语法定义,规定了其优先级)。总之没有赋值表达式,并没有涉及到 = 语法。

且规范中规定了 语句解析顺序 是从左到右(Chapter 7),所以 a.x = a = a.y = {n: 2}; 中的 LeftHandSideExpression 就是 a.x

再仔细思考 AssignmentExpression : LeftHandSideExpression = AssignmentExpression , 把最后的 AssignmentExpression 置换为左边的 AssignmentExpression ,就得到了我们使用的这个表达式 : AssignmentExpression : LeftHandSideExpression = (LeftHandSideExpression = AssignmentExpression) 。从这里我们也能看出,对于 a.x = a = a.y = {n: 2}; 的执行来说,是 先把 a.x 当作 LeftHandSideExpression ,把 a = a.y = {n: 2} 当作 AssignmentExpression ;执行到 evaluating AssignmentExpression 时,再把 a 当作 LeftHandSideExpressiona.y = {n: 2} 作为 AssignmentExpression 。直到最后以 a.y 作为 LeftHandSideExpression , 以 {n: 2} 作为 AssignmentExpression ( AssignmentExpression 的第一种形式 ConditionalExpression 是允许为 对象字面量 的)。

按照这样的执行步骤,第一步就是把 执行 a.x 的结果赋给 lrefa.x 是一个 属性读取表达式,我们再来看它的执行流程。

【Note】
规范中并没有对优先级进行规定,只是通过设置语句的解析规则,形成了事实上的优先级。
读者可以试试这段代码的结果:
	var a = "a"
	console.log(a) // 'a'
	true ? a : a = 'c'
	console.log(a) // 'a'
	false ? a : a = "c"
	console.log(a) // 'c'
若按照优先级规定,条件表达式的优先级高于赋值表达式;
那么语句应该按照 先执行条件表达式,后执行赋值表达式的顺序执行,第二个输出就应该是'c'了。
但事实上是'a'。
这是因为按照规范的表达式解析规则,=的左边总是被解析为 LeftHandSideExpression,
而条件表达式并不在它的语法形式之中。
所以按照最大可解析长度的原则,上式被解析为了true ? a : (a = 'c'),
所以只有在最后 a 才会被改写为'c'。
复制代码

2. 属性获取表达式

规范中在 LeftHandSideExpression 相关 Property Accessors (11.2.1节) 中规定了其执行流程,我们把这个流程命名为 parseMember, 后面会以 parseMember(n)来指代执行这里的第n步:

The production MemberExpression : MemberExpression [ Expression ] is evaluated as follows:
1. Let baseReference be the result of evaluating MemberExpression.
2. Let baseValue be GetValue(baseReference).
3. Let propertyNameReference be the result of evaluating Expression.
4. Let propertyNameValue be GetValue(propertyNameReference).
5. Call CheckObjectCoercible(baseValue).
6. Let propertyNameString be ToString(propertyNameValue).
7. If the syntactic production that is being evaluated is contained in strict mode code, let strict be true, else let
strict be false.
8. Return a value of type Reference whose base value is baseValue and whose referenced name is
propertyNameString, and whose strict mode flag is strict.
复制代码

我们看到这个流程大概是,从 MemberExpression (即这里的 a ) 得到 baseValue , 从 Expression (即这里的字符串 x )得到 propertyNameString ,然后返回以它们组成的 Reference 。 我们先去了解下 Reference

3. Reference

规范的第8章 Types 中, 将类型分为两大类: 一是 语言类型 ,也就是提供给开发者的 Undefined, Null, Boolean, String, Number, and Object ;另一类是 规范类型 ,它们不会提供给开发者,也不一定对应到一个es实现中的数据结构,只是用来描述规范中的算法和刚才提到的 语言类型 ,可以理解为是用来描述算法和数据结构的抽象。 Reference 就是 规范类型 的一种。

规范的8.7节中这样写到:

A Reference is a resolved name binding. A Reference consists of three components, the base value, the referenced name and the Boolean valued strict reference flag. The base value is either undefined, an Object, a Boolean, a String, a Number, or an environment record (10.2.1). A base value of undefined indicates that the reference could not be resolved to a binding. The referenced name is a String.

意即, Reference 是一个 已解析的命名绑定 。所谓 命名绑定 ,就是说它用来用一个 命名 找到对应的某个内部数值/数据;所谓 已解析 ,就是说这个 命名 到 数据 的绑定关系是确定的。好比我们在面对函数中的某个变量,想要知道它的确切值是多少,就是想确定它的命名绑定。 简而言之, Reference 就是一个表示 引用类型 或者 环境对象 的抽象。一个 Reference 由三个部分组成: basereference namestrict flag

base 可以看作是就是引用的实体或作宿主,好比 a.x 就是一个引用,它的 base value 就是 areference name 则是字符串 x 。而在如下函数 func 中:

function func() {
  var a = 'a';
}
复制代码

a 也是一个引用,它的 basefunc 函数对应的执行环境的环境记录( Enviroment Record ); reference name 则是字符串 'a'

前述表达式的执行流程中还用到了 ReferenceGetValue 方法。我们看它的执行过程:

GetValue (V)
1. If Type(V) is not Reference, return V.
2. Let base be the result of calling GetBase(V). // 获取 Reference 的 base component
3. If IsUnresolvableReference(V), throw a ReferenceError exception.
4. If IsPropertyReference(V), then
a. If HasPrimitiveBase(V) is false, then let get be the [[Get]] internal method of base, otherwise let get be the special [[Get]] internal method defined below.
b. Return the result of calling the get internal method using base as its this value, and passing GetReferencedName(V) for the argument.
5. Else, base must be an environment record.
a. Return the result of calling the GetBindingValue (see 10.2.1) concrete method of base passing
GetReferencedName(V) and IsStrictReference(V) as arguments.
复制代码

即,如果参数 V 不是一个 Reference 类型,那么直接返回;否则在 base 上取出对应 reference name 的值并返回。

4. 题目分析

有了这些基础,我们可以来分析面试题中的表达式了。步骤如下:

  1. 执行parseAssignment(1), 即执行 a.x 表达式,将得到的 Reference 类型值赋给 lrefa.x 是一个 Property Accessor ,我们来按照规范解析它的执行:

    1.1 parseMember(1). MemberExpression 是 a。这是表达式 PrimaryExpression 的 Identifier 类型,它会返回一个 Reference 类型的值: base 是全局环境变量(global enviroment record),reference name是'a',strict flag是false。 
     1.2 parseMember(2). 对全局环境变量调用 GetBindingValue('a')方法,在变量对象中找到对应的值,即 a 所引用的 对象字面量 {n: 1}。
     1.3 parseMember(3). Let propertyNameReference  = 'x'
     1.4 parseMember(4). Let propertyNameValue = 'x'
     1.5 parseMember(5). 检查是否可以1.2中的返回值是否可以转为 Object, {n: 1}本就是对象类型,返回true
     1.6 parseMember(6). 获取property name string,即'x'
     1.7 parseMember(7). 设置 strict flag 为false
     1.8 parseMember(8). 返回一个 Reference 类型的值,base 是 {n: 1}, reference name是'x', strict flag 是 false。
    复制代码

    这里第一步执行完得到的 lref 就是1.8中返回的值。

  2. parseAssignment(2). 执行 a = a.y = {n: 2},将返回值赋给 rref。它的执行如下:

    2.1 执行 a。它返回一个 Reference 类型的值,base 是 全局环境变量,refrence name是'a', strict flag是false。我们姑且称这一步的lref为 lref2.1。
    
     2.2 执行 a.y = {n: 2}。它也是一个赋值表达式,执行如下:
     	2.2.1 执行 a.y 。这里又涉及到了对 a 的解析,前面的操作并没有改变 a 的引用,所以到现在为止,a 仍然会被解析为全局环境变量上的一个命名绑定。所以对 a.y 的解析所返回的 Reference 中,base 组件是就是lref中的base。 我们姑且称这一步的lref为 lref2.2.1,它的组成: base 是 {n: 1},refrence name是'y', strict flag是false。(注意 lref2.2.1 的 base 与 lref 的 base, 是同一个对象。因为 a 都会解析为 全局环境变量 上对应属性'a'的对象。)
     	2.2.2 parseAssignment(2). 这里右边是一个 对象初始化表达式,返回一个对象类型的值 {n: 2}。
     	2.2.3 parseAssignment(3). 对上一步中的返回值执行 GetValue(rref),结果仍然是 {n: 2}, 赋给 rval2.2.3。
     	2.2.4 parseAssignment(4). 判断是否抛异常,这里不会。
     	2.2.5 parseAssignment(5). 调用 PutValue(lref2.2.1, rval2.2.3),结果是lref2.2.1 的base增加了一个属性,此时变为了 {n: 1, y: {n: 2}} // 这里的 base 与 lref 中的 base 仍然是同一个对象
     	2.2.6 parseAssignment(6). 返回 rval2.2.3。
     
     所以这一步返回 rval2.2.3。
    
     2.3 parseAssignment(3). 对2.2返回的值进行 GetValue(rref), 仍然是 rval2.2.3
     2.4 parseAssignment(4). 判断是否要抛异常,这里不会。
     2.5 parseAssignment(5). 调用 PutValue(lref2.1, rval2.2.3),lref2.1 的base是 全局环境变量,这里修改了其中变量 a 的引用,指向新的对象 rval2.2.3
     2.6 parseAssignment(6). 返回 rval2.2.3。
    复制代码

    这一步的返回仍然是对象 rval2.2.3。

  3. parseAssignment(3). 将 rval 设为上一步的返回即 rval2.2.3。

  4. parseAssignment(4). 判断是否要抛异常,这里不会。

  5. parseAssignment(5). 调用 PutValue(lref, rval),lref 的base 增加了一个属性,此时变为了 {n: 1, y: {n: 2}, x: {n: 2}}

  6. Return rval.

所以执行完后,变量 a 所引用的对象是 {n: 2}。 而它之前指向的对象,也即这时变量 b 指向的对象( b 的指向未改变过),变为了 {n: 1, y: {n: 2}, x: {n: 2}} 。可以用 JSON.stringify 验证下b。而且这时候 b.xb.y 和 a 指向同一个对象。

其实这里的关键点就是,赋值表达式要先对左边的表达进行引用确定,再进行赋值。

这样走完一遍,应该再也不怕面试官问你赋值表达式了吧~

PS: 文中对于符号优先级的阐述,完全出于自己对规范的理解,欢迎小伙伴们指正:D


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

浴缸里的惊叹

浴缸里的惊叹

顾森 / 人民邮电出版社 / 2014-7 / 49.00元

《浴缸里的惊叹》是一本趣题集,里面的题目全部来自于作者顾森十余年来的精心收集,包括几何、组合、行程、数字、概率、逻辑、博弈、策略等诸多类别,其中既有小学奥数当中的经典题目,又有世界级的著名难题,但它们无一例外都是作者心目中的“好题”:题目本身简单而不容易,答案出人意料却又在情理之中,解法优雅精巧令人拍案叫绝。作者还有意设置了语言和情境两个类别的问题,希望让完全没有数学背景的读者也能体会到解题的乐趣......一起来看看 《浴缸里的惊叹》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具