表达式与操作符
表达式是一个可以被求值并产生一个值的 JavaScript 短语。直接嵌入在程序中的常量是最简单的表达式。变量名也是简单的表达式,可以求值为之前赋给它的值。
复杂表达式有简单表达式构成。比如,数组访问表达式由一个求值为数组的表达式,一个左方括号、一个求值为整数的表达式和一个右方括号构成。
这个新的更复杂的表达式求值为保存在指定数组的指定索引位置的值。类似地,函数调用表达式由一个求值为函数对象的表达式和零或多个作为函数参数的表达式构成。
基于简单表达式构建复杂表达式最常见的方式是使用操作符。操作符以某种方式组合其操作数的值(通常有两个),然后求值为一个新值。
以乘法操作符*
为例。表达式x * y
求值为表达式 x 和 y 值的积。为简单起见,有时也说操作符返回值,而不是“求值为”一个值。
主表达式
最简单的的表达式称为主表达式(primary expression),即那些独立存在,不再包含更简单表达式的表达式。
JavaScript 中的主表达式包括常量或字面量值、某些语言关键字和变量引用。
字面量是可以直接嵌入在程序中的常量值,例如:
1 2 3 |
|
JavaScript 的一些保留字也是主表达式:
1 2 3 4 |
|
第三种主表达式是变量、常量或全局对象属性的引用:
1 2 3 |
|
当程序中出现任何独立的标识符时,JavaScript 假设它是一个变量或常量或全局对象的属性,并查询它的值。
如果不存在该名字的变量,则求值不存在的变量会导致抛出 ReferenceError
对象和数组初始化程序
对象和数组初始化程序也是一种表达式,其值为新创建的对象或数组。这些初始化程序表达式有时候也被称为对象字面量和数组字面量。
但与真正的字面量不同,它们不是主表达式,因为它们包含用于指定属性或元素值的子表达式。
数组初始化程序的语法稍微简单一点,我们先来介绍它。
数组初始化程序是一个包含在方括号内的逗号分隔的表达式列表。数组初始化程序的值是新创建的数组。
这个新数组的元素被初始化为逗号分隔的表达式的值:
1 2 |
|
数组初始化程序中的元素表达式本身也可以是数组初始化程序,这意味着以下表达式可以创建嵌套数组:
1 |
|
数组初始化程序中的元素表达式在每次数组初始化程序被求值时也会被求值。这意味着数组初始化程序表达式每次求值的结果可能不一样。
在数组字面量中省略逗号间的值可以包含未定义元素。例如,以下数组包含 5 个元素,其中有 3 个未定义元素:
1 |
|
数组初始化程序的最后一个表达式后面可以再跟一个逗号,而且这个逗号不会创建未定义元素。
不过,通过数组访问表达式访问最后一个表达式后面的索引一定会求值为 undefined
对象初始化程序表达式与数组初始化表达式类似,但方括号变成了花括号,且每个子表达式前面多了一个属性名和冒号:
1 2 3 |
|
在 ES6 中,对象字面量拥有了更丰富的语法。对象字面量可以嵌套。例如:
1 2 3 4 |
|
函数定义表达式
函数定义表达式定义 JavaScript 函数,其值为新定义的函数。某种意义上说,函数定义表达式也是“函数字面量”,就像对象初始化程序是“对象字面量”一样。
函数定义表达式通常由关键字 function,位于括号中的逗号分隔的零或多个标识符(参数名),以及一个位于花括号中的 JavaScript 代码块(函数体)构成。例如:
1 2 |
|
函数定义表达式也可以包含函数的名字。函数也可以使用函数语句而非函数表达式来定义。
在 ES6 即之后的版本中,函数表达式可以使用更简洁的“箭头函数”语法。
属性访问表达式
属性访问表达式求值为对象属性或数组元素的值。JavaScript 定义了两种访问属性的语法:
1 2 |
|
第一种属性访问语法是表达式后跟一个句点和一个标识符。其中,表达式指定对象。标识符指定属性名。
第二种属性访问语法是表达式(对象或数组)后跟另一个位于方括号中的表达式。这第二个表达式指定属性名或数组元素的索引。下面是几个具体的例子:
1 2 3 4 5 6 7 8 |
|
无论哪种属性访问表达式,位于 .
或 [
前面的表达式都会先求值。如果求值结果为 null 或 undefined,则表达式会抛出 TypeError,因为它们是 JavaScript 中不能有属性的两个值。
如果对象表达式后跟一个点和一个标识符,则会对以该标识符为名字的属性求值,且该值会成为整个表达式的值。
如果对象表达式后跟位于方括号中的另一个表达式,则第二个表达式会被求值并转换为字符串。整个表达式的值就是名字为该字符串的属性的值。
任何一种情况下,如果指定名字的属性不存在,则属性访问表达式的值是 undefined。
在两种属性访问表达式中,加标识符的语法更简单,但通过它访问的属性的名字必须是合法的标识符,而且在写代码时已经知道了这个名字。
如果属性名中包含空格或标点字符,或者是一个数值(对于数组而言),则必须使用方括号语法。方括号也可以用来访问非静态属性名,即属性本身是计算结果
条件式访问属性
ES2020 增加了两个新的属性访问表达式:
1 2 |
|
在 JavaScript 中,null 和 undefined 是唯一两个没有属性的值,在使用普通的属性访问表达式时,如果.
或[]
左侧的表达式求值为 null 或 undefined,会报 TypeError。
可以使用 ?.
或 ?.[]
语法防止这种错误发生
比如表达式 a?.b
,如果 a 是 null 或 undefined,那么整个表达式求值结果为 undefined,不会尝试访问属性 b。
如果 a 是其他值,则 a?.b
求值为 a.b
的值(如果 a 没有名为 b 的属性,则整个表达式的值还是 undefined)
这种形式的属性访问表达式有时候也被称为“可选链接”,因为它也适用于下面这样更长的属性访问表达式链条:
1 2 |
|
a 是个对象,因此 a.b 是有效的属性访问表达式。但 a.b 的值是 null,因此 a.b.c 会抛出 TypeError。但通过使用 ?.
而非 .
就可以避免这个 TypeError,最终 a.b?.c
求值为 undefined。
这意味着 (a.b?.c).d
也会抛出 TypeError,因为这个表达式尝试访问 undefined 值的属性。
但如果没有括号,即 a.b?.c.d
(这种形式是 "可选链接" 的重要特征)就会直接为 undefined 而不会抛出错误。
这是因为通过 ?.
访问属性是 "短路操作": 如果 ?.
左侧的子表达式求值为 null 或 undefined,那么整个表达式立即求值为 undefined,不会再进一步尝试访问属性。
当然,如果 a.b 是对象,且这个对象没有名为 c 的属性,则 a.b?.c.d
仍然会抛出 TypeError。此时应该再加一个条件式属性访问:
1 2 |
|
条件式属性访问也可以让我们使用 ?.[]
而非 []
。在表达式 a?.[b][c]
中,如果 a 的值是 null 或 unfefined,则整个表达式立即求值为 unfefined,子表达式 b 和 c 不会被求值。
换句话说,如果 a 没有定义,那么 b 和 c 无论谁有副效应(side effect),这个副效应都不会发生:
1 2 3 4 5 6 7 8 9 10 |
|
使用 ?.
和 ?.[]
的条件式属性访问是 JavaScript 最新的特性之一。在 2020 年初,多数主流浏览器的当前版本或预览版已经支持这个新语法。
调用表达式
调用表达式是 JavaScript 中调用(或执行)函数或方法的一种语法。这种表达式开发是一个表示要调用函数的函数表达式。
函数表达式后面跟着左圆括号、逗号分隔的零或多个参数表达式的列表和右圆括号。看几个例子:
1 2 3 |
|
求值调用表达式时,首先求值函数表达式,然后求值参数表达式以产生参数值的列表。如果函数表达式的值不是函数,则抛出 TypeError。
然后,按照函数定义时参数的顺序给参数赋值,之后再执行函数体。如果函数使用了 return 语句返回一个值,则该值就成为调用表达式的值。否则,调用表达式的值是 undefined。
关于函数调用的完整细节,包括在参数表达式个数与函数定义的参数个数不匹配时会发生什么
每个调用表达式都包含一对圆括号和左圆括号前面的表达式。如果该表达式是属性访问表达式,则这种调用被称为方法调用。
在方法调用中,作为属性访问主体的对象或数组在执行函数体时会变成 this 关键字的值。这样就可以支持面向对象的编程范式,即函数(这样使用时我们称其为“方法”)会附着在其所属对象上来执行操作
条件式调用
在 ES2020 中,可以使用 ?.()
而非 ()
来调用函数。正常情况下,我们调用函数时,如果圆括号左侧的表达式是 null 或 undefined 或任何其他非函数值,都会抛出 TypeError。
而使用 ?.()
调用语法,如果 ?
左侧的表达式求值为 null 或 undefined,则整个表达式求值为 unfefined,不会抛出异常。
数组对象有一个 sort() 方法,接收一个可选的函数参数,用来定义对数组元素排序的规则。
在 ES2020 之前,如果想写一个类似 sort() 的这种接收可选函数参数的方法,通常需要在函数内使用 if 语句检查该函数参数是否有定义,然后再调用:
1 2 3 4 5 6 |
|
但有了 ES2020 的条件式调用语法,可以简单地使用 ?.()
来调用这个可选的函数,只有在函数有定义时才会真正调用:
1 2 3 4 |
|
不过要注意,?.()
只会检查左侧的值是不是 null 或 undefined,不会验证该值是不是函数。因此,这个例子中的 square() 函数在接收到两个数值时仍然会抛出异常。
与条件式属性访问表达式类似,使用 ?.()
进行函数调用也是短路操作:
如果 ?.
左侧的值是 null 或 undefined,则圆括号中的任何参数表达式都不会被求值:
1 2 3 4 5 6 7 8 |
|
使用 ?.()
的条件式调用表达式既适用于函数调用,也适用于方法调用。
因为方法调用又涉及属性访问,所以有必要花时间确认一下自己是否理解下列表达式的区别:
1 2 3 |
|
第一个表达式,o 必须是一个对象且必须有一个 m 属性,且该属性的值必须是函数。
第二个表达式中,如果 o 是 null 或 undefined,则表达式求值为 undefined。但如果 o 是任何其他值,则它必须有一个值为函数的属性 m。
第三个表达式中,o 必须不是 null 或 undefined。如果它没有属性 m 或属性 m 的值是 null,则整个表达式求值为 undefined
使用 ?.()
的条件式调用是 JavaScript 最新的特性之一。在 2020 年初,多数主流浏览器的当前版本或预览版已经支持这个新语法
对象创建表达式
对象创建表达式创建一个新对象并调用一个函数(称为构造函数)来初始化这个新对象。对象创建表达式类似于调用表达式,区别在于前面多了一个关键字 new:
1 2 |
|
如果在对象创建表达式中不会给构造函数传参,则可以省略圆括号:
1 2 |
|
对象创建表达式的值是新创建的对象
操作符概述
操作符在 JavaScript 中用于算术表达式,比较表达式、逻辑表达式、赋值表达式等。
注意,多数操作符都以 + 和 = 这样的标点符号表示。不过,有一些也以 delete 和 instanceof 这样的关键字表示。
关键字操作符也是常规操作符,与标点符号表示的操作符一样,只不过它们的语法没那么简短而已。
下表按操作符优先级组织。换句话说,表格前面的操作符比后面的操作符优先级更高。横线分隔的操作符具有不同优先级。
"结合性" 中的 "左" 表示 "从左到右","右" 表示 "从右到左"。"操作数"表示操作数的个数。
"类型" 表示操作数的类型,以及操作符的结果类型(->后面)。
操作符 | 操作 | 结合性 | 操作数 | 类型 |
---|---|---|---|---|
++ | 先或后递增 | 右 | 1 | lval -> num |
-- | 先或后递减 | 右 | 1 | lval -> num |
- | 负值 | 右 | 1 | num -> num |
+ | 转换为数值 | 右 | 1 | any -> num |
~ |
反转二进制位 | 右 | 1 | int -> int |
! |
反转布尔值 | 右 | 1 | bool -> bool |
delete | 删除属性 | 右 | 1 | lval -> bool |
typeof | 确定操作数类型 | 右 | 1 | ans -> str |
void | 返回 undefined | 右 | 1 | any -> undef |
** |
幂 | 右 | 2 | num, num -> num |
*、/、% |
乘、除、取余 | 左 | 2 | num,num -> num |
+、- |
加、减 | 左 | 2 | num,num -> num |
+ | 拼接字符串 | 左 | 2 | str,str -> str |
<< |
左移位 | 左 | 2 | int,int -> int |
>> |
右移位以富豪填充 | 左 | 2 | int,int -> int |
>>> |
右移位以零填充 | 左 | 2 | int,int -> int |
<、<=、>、>= |
按数值顺序比较 | 左 | 2 | num,num -> bool |
<、<=、>、>= |
按字母表顺序比较 | 左 | 2 | str,str -> bool |
instanceof |
测试对象类 | 左 | 2 | obj,func -> bool |
in | 测试属性是否存在 | 左 | 2 | any,obj -> bool |
== | 非严格相等测试 | 左 | 2 | any,any -> bool |
!= | 非严格不相等测试 | 左 | 2 | any,any -> bool |
=== | 严格相等测试 | 左 | 2 | any,any -> bool |
!== | 严格不相等测试 | 左 | 2 | any,any -> bool |
& |
计算按位与 | 左 | 2 | int,int -> int |
^ |
计算按位异或 | 左 | 2 | int,int -> int |
| |
计算按位或 | 左 | 2 | int,int -> int |
&& |
计算逻辑与 | 左 | 2 | any,any -> any |
|| |
计算逻辑或 | 左 | 2 | any,any -> any |
?? |
选择第一个有定义的操作数 | 左 | 2 | any,any -> any |
?: |
选择第二或第三个操作数 | 右 | 3 | bool,any,any -> any |
=、**=、*=、/=、%=、+=、-=、&=、^=、|=、<<=、>>=、>>>= |
为变量或属性赋值操作并赋值 | 右 | 2 | lval,any -> any |
, | 丢弃第一个操作数,返回第二个 | 左 | 2 | any,any -> any |
操作数个数
操作符可以按照它们期待的操作数个数(参数数量)来分类。多数 JavaScript 操作符(如乘法操作符 *
)都是二元操作符,可以将两个表达式组合成一个更复杂的表达式。
换句话说,这些操作符期待两个操作数。JavaScript 也支持一些一元操作符,这些操作符将一个表达式转换为另一个更复杂的表达式。
表达式 -x
中的操作符 -
就是一元操作符,用于对操作数 x 进行负值操作。最后,JavaScript 也支持一个三元操作符,即条件操作符 ?:
,用于将三个表达式组合为一个表达式。
操作数与结果类型
有些操作符适用于任何类型的值,但多数操作符期待自己的操作数是某种特定类型,而且多数操作符也返回(或求值为)特定类型的值。
JavaScript 操作符通常会按照需要转换操作数的类型。比如,乘法操作符*
期待数值参数,而表达式 "3" * "5"
之所以合法,是因为 JavaScript 可以把操作数转换为数值。
因为这个表达式的值是数值 15,而非字符串 "15"。也要记住,每个 JavaScript 值要么是 "真值" 要么是 "假值",因此期待布尔值操作数的操作符可以用于任何类型的操作数。
有些操作符的行为会因为操作数类型的不同而不同。最明显的,+ 操作符可以把数值加起来,也可以拼接字符串。
类似地,比较操作符(如<
)根据操作数类型会按照数值顺序或字母表顺序比较。
注意,上面表中列出的赋值操作符和少数其他操作符期待操作数类型为 lval。lval 即 lvalue(左值),是一个历史悠久的术语,意思是 "一个可以合法地出现在赋值表达式左侧的表达式"。
在 JavaScript 中,变量、对象属性和数组元素都是 "左值"
操作符副效应
对类似 2 * 3
这样的简单表达式求值不会影响程序状态,程序后续的任何计算也不会被这个求值所影响。
但有些表达式是有副效应的,即对它们求值可能影响将来求值的结果。赋值操作符就是明显的例子: 把一个值赋给变量或属性,会改变后续使用该变量或属性的表达式的值。
类似地,递增和递减操作符 ++ 和 -- 也有副效应,因为它们会执行隐式赋值。同样,delete 操作符也有副效应,因为删除属性类似于(但不同于)给属性赋值 undefined
其他 JavaScript 操作符都没有副效应,但函数调用和对象创建表达式是否有副效应,取决于函数或构造函数体内是否使用了有副效应的操作符。
关系表达式
in 操作符
in 操作符期待左侧操作符是字符串、符号或可以转换为字符串的值,期待右侧操作数是对象。如果左侧的值是右侧的对象的属性名,则 in 返回 true。例如:
1 2 3 4 5 6 7 8 9 |
|
instanceof 操作符
instanceof 操作符期待左侧操作数是对象,右侧操作数是对象类的标识。这个操作符在左侧对象是右侧类的实例时求值为 true,否则求值为 false。
在 JavaScript 中,对象类是通过初始化它们的构造函数定义的。因而,instanceof 的右侧操作数应该是一个函数。下面看几个例子:
1 2 3 4 5 6 7 8 |
|
注意,所有对象都是 Object 的实例。instanceof 在确定对象是不是某个类的实例时会考虑“超类”。如果 instanceof 的左侧操作数不是对象,它会返回 false。如果右侧操作数不是对象的类,它会抛出 TypeError
要理解 instanceof 的工作原理,必须理解“原型链”。原型链是 JavaScript 的继承机制。为了对表达式 o instanceof f
求值,JavaScript 会求值 F.prototype,然后在 o 的原型链上查找这个值。
如果找到了,则 o 是 f(或 f 的子类)的实例,instanceof 返回 true。如果 f.prototype 不是 o 原型链上的一个值,则 o 不是 f 的实例,instanceof 返回 false
求值表达式
与很多解释型语言一样,JavaScript 有能力解释 JavaScript 源代码字符串,对它们求值以产生一个值。JavaScript 是通过全局函数 eval() 来对源代码字符串求值的:
1 |
|
对源代码字符串的动态求值是一个强大的语言特性,但这种特性在实际项目当中几乎用不到。如果你发现自己在使用 eval(),那应该好好思考一下到底是不是真需要使用它。
如果你发现自己在使用 eval(),那应该好好思考一下到底是不是真需要使用它。特别地,eval() 可能会成为安全漏洞,为此永远不要把来自用户输入的字符串交给它执行。
对于像 JavaScript 这么复杂的语言,无法对用户输入脱敏,因此无法保证在 eval() 中安全地使用。由于这些安全问题,某些 Web 服务器使用 HTTP 的 "Content-Security-Policy" 头部对整个网站禁用 eval()
eval() 是函数还是操作符?
eval() 是一个函数,但之所以在讲表达式时介绍它,是因为它其实应该是个操作符。
JavaScript 语言最初的版本定义了一个 eval() 函数,而从那时起,语言设计者和解释器开发者一致对它加以限制,导致它越来越像操作符。
现代 JavaScript 解释器会执行大量代码分析和优化。一般来说,如果一个函数调用 eval(),则解释器讲无法再优化该函数。把 eval() 定义为函数的问题在于可以给它起不同的名字:
1 2 |
|
如果可以这样,那么解释器无法确定哪个函数会调用 eval(),也就无法激进优化。假如 eval() 是个操作符(即保留字),那这个问题就可以避免。
eval()
eval() 期待一个参数。如果给它传入任何非字符串值,它会简单地返回这个值。如果传入字符串,它会尝试把这个字符串当成 JavaScript 代码来解析,解析失败会抛出 SyntaxError。
如果解析字符串成功,它会求值代码并返回该字符串中最后一个表达式或语句的值;如果最后一个表达式或语句没有值则返回 undefined。如果求值字符串抛出异常,该异常会从调用 eval() 的地方传播出来。
对于 eval()(在像这样调用时),关键在于它使用调用它的代码的变量环境。也就是说,它会像本地代码一样查找变量的值、定义新变量和函数。
如果一个函数定义了一个局部变量 x,然后调用了 eval("x")
,那它会取得这个局部变量的值。如果这个函数调用了 eval("var y = 3;")
,则会声明一个新局部变量 y。
另外,如果被求值的字符串使用了 let 或 const,则声明的变量或常量会被限制在求值的局部作用域内,不会定义到调用环境中。
类似地,函数也可以像下面这样声明一个局部函数:
1 |
|
如果在顶级代码中调用 eval(),则它操作的一定是全局变量和全局函数。