出现在赋值表达式左侧的变量一定是引用吗
语法篇旨在帮助读者了解语法构成的方式,以及如何通过规范去探索更多的“未知”,从而自身形成如何系统去学习编程语言的能力,而不仅仅是局限在javascript这一门语言上,抑或是停留在使用api的层面上。在这一篇中我也会继续沿用最少知识原则:用到什么就学什么,减少无关语法和新概念带来的知识混乱。
在开始正文之前我先说明一些前置概念,js的数据类型可以归为两类分别是:基本数据类型(也可以称为值类型)和引用类型,这一分类是从值的存储角度去描述的。我们接下来所要讨论的主题虽然和其听起来十分相似,却是截然不同的概念。它们是从操作值的行为(也可以说用处)去分类的:引用和值。这两种类型是相互排斥的,当出现它们中的一个就无须再考虑另一个,比如当我们需要用到一个变量的值,那么它的引用就无需考虑,因为接下来它的所有的操作都是基于值而不会对对它的引用产生操作。
要搞清楚标题的问题是如何产生的,我们先来学一样东西:赋值表达式。你肯定会觉得这过于简单了。若我换个问法:赋值的合法性是什么,怕是很多读者就会一脸茫然了,或者脑中飘过的就是赋值给一个变量,这么说确实也没有什么大错,毕竟最常用的可不就是变量赋值吗。接下来我们就来好好聊一聊这个变量到底是什么。
顺便提一下规范中的语法描述是采用上下文无关文法来描述的,当文法不足以描述时会使用静态语义加以补充,现在读者只要知道这两个概念就行了,后面用到的时候我会加以说明的,先来看一下一般形式的变量赋值是如何定义的
LeftHandSideExpression = AssignmentExpression
x = y
如果你看过《你不知道的javascript》那么你一定对LeftHandSide和rightHandSide的概念有所了解了。LeftHandSide可以是我们常用的标识符引用(IdentifierReference)或解构赋值模式(AssignmentPattern),对于上面的赋值表达式这是符合我们的知觉的:将y(rhs)的值赋值给x(lhs)的引用,
NOTE 在javascript语法规范中是允许无目标赋值的也就是说(a || b)这种短路运算也是赋值即使它没有lhs,这里就不详细介绍了
现在回到本节的主题:出现在赋值表达式左侧的变量一定是引用吗,答案不然这取决于使用方式,先让我们看一下规范中允许的语法形式
//13.3 Left-Hand-Side Expressions Syntax CallExpression: CoverCallExpressionAndAsyncArrowHead SuperCall ImportCall CallExpression Arguments CallExpression [Expression] CallExpression . IdentifierName CallExpression TemplateLiteral[?Yield, ?Await, +Tagged] CallExpression . PrivateIdentifier
现在我们通过CallExpression Arguments去构造下面的代码片段,它仅仅是一个简单的函数调用:
let name = 'js'
function outer(){
function f(){
console.log('f被调用了')
return name
}
f() = 'php'
}
//outer()
console.log('执行了')
这在编译期是可以通过的(毕竟我们就是根据它的语法规则写的嘛),当我们尝试调用outer会出现以下错误:
Uncaught ReferenceError: Invalid left-hand side in assignment
让我们来分析一下:首先在语法层面分析是允许的,因为Left-Hand-Side Expressions的定义是允许出现CallExpression的。其内部函数f返回了外部定义的变量name,我们的期望是将php赋值给这个变量name,并且f()这个函数也如期的被调用执行了,并将name返回。到这里我们已经将错误的范围缩小到了name上,如果它作为一个ref去返回那就不应该出现任何错误,但实际情况与我们的想法相悖,它是作为一个val被返回的所以导致了上面的错误。这里我们通过一个有意为之的语法将lhsExp成功转化成了一个rhsExp,JavaScript标准是允许我们这么做的,但为了指出这样的错误,又给lhs加了静态语义的规则,通过AssignmentTargetType(lhs的类型)静态语义指出这一违规操作:
//13.15.1 Static Semantics: Early Errors It is a Syntax Error if AssignmentTargetType of LeftHandSideExpression is not simple.
我们来翻译一下:如果LeftHandSideExpression的AssignmentTargetType类型不是simple就会产生错误。下面再让我们看看AssignmentTargetType的计算方式:
//8.6.4 Static Semantics: AssignmentTargetType CallExpression : CallExpression [ Expression ] CallExpression . IdentifierName CallExpression . PrivateIdentifier return simple CallExpression : CoverCallExpressionAndAsyncArrowHead SuperCall ImportCall CallExpression Arguments CallExpression TemplateLiteral Return invalid.
我们的语法符合CallExpression Arguments所以是一个无效的表达式,原因在于你永远无法从函数中返回一个引用(lhs),因为在return的运行时语义中限定了返回的是一个值GetValue(exprRef)。这是符合函数式编程的特性的:函数可以接受函数当作输入(参数)和输出(返回值),无论哪种情况我们需要的都是值而非引用。
现在让我们对程序进行一些改造,让它可以支持函数调用作为左侧。在静态语义中AssignmentTargetType类型不是simple会产生错误,所以我们只要将它转化成返回simple的形式就可以了。改造后的代码如下
let name = {type: "javascript"}
function outer(){
function f(){
console.log('f被调用了')
return name
}
f()['type'] = 'php'
}
outer()
console.log('执行了')
这里的函数依旧是返回一个“值”,唯一不同的是我们通过操作这个值并获得了这个值上的一个引用“type”,至此就符合了赋值语句的合法性。
小结
这一小节的内容并不多,全篇围绕变量的“值”与“引用”两个形态展开讨论,为大家揭示编译器在对待变量的处理方式。同时也简明扼要的提及了上下无关文法和静态语义,希望对大家对编程有一个新的认识,不再基于文档和直觉编程。本系列文章面向有一年javascript使用经验的读者,为了突出主题文中不会对语法进行过多的介绍。另外本文中所引用的规范全部出自最新版的ECMAScript标准,并标明了具体章节,限于篇幅不便展开讨论,大家可以基于此文中提到的内容自行查阅文档
版权归原作者 qq_30066883 所有, 如有侵权,请联系我们删除。