Skip to content

表达式与操作符

1. 主表达式

  • 最简单的表达式称为主表达式,JavaScript 中主表达式包括 字面量值某些关键字常量变量引用
  • 字面量是可以直接嵌入在程序中的常量值。
  • 一些保留字也是主表达式,如 truefalsenullthis 等。
  • 全局对象属性的引用(如 undefined)也是主表达式。
javascript
1.23  // 数值字面量
"hello" // 字符串字面量
/pattern/ // 正则表达式字面量

2. 对象和数组初始化程序

对象和数组初始化程序也是一种表达式,值为新创建的对象或数组。这些初始化程序也叫对象字面量或数组字面量,但不是真正的字面量,因为它们不是主表达式,内部可以包含子表达式。

数组初始化程序中元素表达式本身也可以是数组初始化程序,因此可以创建嵌套数组:

javascript
let matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

数组字面量中省略逗号中的值可以包含未定义元素:

javascript
let sparseArray = [1, , , , 5] // 包含三个未定义元素

对象初始化程序和数组初始化程序类似,不过方括号变成了花括号,同时元素变成了键值对:

javascript
let p = { x: 2.3, y: -1.2 }

3. 函数定义表达式

函数定义表达式通过由关键字 function、位于括号中的参数名以及花括号中的代码块构成:

javascript
const square = function(x) {
  return x * x
}

函数定义表达式也可以包括函数的名字:

javascript
function square(x) {
  return x * x
}

ES6 之后,函数表达式可以用更简洁的“箭头函数”语法。

4. 属性访问表达式

属性访问表达式求值为对象属性或数组元素,有两种语法:

  1. 表达式后跟一个句点和一个标识符,只能用于对象。
  2. 达式后跟一个位于方括号中的表达式,可用于对象或数组。
javascript
let o = { x: 1, y: { z: 3 } }
let a = [o, 4, [5, 6]]
o.x // 1
o.y.z // 3
o['x'] // 1
a[1] // 4
a[0].x // 1

注意

  • 位于 .[ 前面的表达式会先求值,如果为 nullundefined,会报 TypeError。
  • 任何一种情况下,指定名字的属性不存在,属性访问表达式的值都为 undefined
  • 属性名中如果包含空格或标点字符,或者是一个数值,则必须使用方括号语法。

4.1 条件式属性访问

ES2020 新增了两种条件式属性访问语法:

  1. expression?.identifier
  2. expression?.[expression]

使用 ?.?.[] 可以防止对 nullundefined 取属性时的报错,比如表达式 a?.b,如果 a 是 null 或者 undefined,整个表达式的值为 undefined

这种条件式属性访问语法也称“可选链式”访问,这种访问是短路操作,如果问号前表达式为 undefinednull,那么整个表达式返回 undefined

5. 调用表达式

调用表达式是 JavaScript 中调用函数或方法的一种语法。

javascript
f(0) // f 是函数表达式,0 是参数
Math.max(x, y , z) // Math.max 是函数表达式,x y z 是参数

如果函数使用 return 返回了一个值,该值就是调用表达式的结果,否则,调用表达式的结果是 undefined

5.1 条件式调用

在 ES2020 中,可使用 ?.() 而不是 () 调用函数,如果问号前表达式为 undefinednull,那么整个表达式返回 undefined,而不会抛出异常。

注意

  • ?.() 只会检测左侧是否是 nullundefined,不会验证是否是函数,如果左侧不是函数,仍会抛错。
  • 与条件式属性访问表达式类似,条件式调用也是“短路”操作,如果问号前表达式为 undefinednull,则圆括号中任何参数表达式均不会求值。
javascript
let f = null, x = 0
try {
  f(x++) // 抛 TypeError,参数列表仍会计算
} catch (e) {
  x // 1,抛出错误前 x 发生了递增
}
f?.(x++)  // undefined
x // 1,短路操作,参数列表不进行计算

6. 对象创建表达式

对象创建表达式可以创建一个对象,并调用构造函数初始化对象:

javascript
new Object()
new Point(2, 3)

如果在对象创建表达式中不给构造函数传参,则可以省略圆括号:

javascript
new Object
new Date

7. 操作符概述

操作符用于算术表达式、比较表达式、赋值表达式等。

多数操作符用标点符号表示,也有一些以关键字表示,如 deleteinstanceof

7.1 操作数个数

操作符可以按期望的操作数个数分类:

  1. 二元操作符: +- 等。
  2. 一元操作符:如 -x
  3. 三元操作符:?:

7.2 操作数与结果类型

操作符通常会按照需要转换操作数的类型:

javascript
5 * 3 // 数值15
'5' * '3' // 数值15,操作数被转为数值类型

有的操作符行为会跟操作数类型不同而不同,如 + 操作符会把数值相加,也会把字符串拼接。

7.3 操作符副效应

有些表达式有副效应,即求值结果可能影响将来求值结果,比如赋值操作符,将一个值赋值给变量或属性,会改变后续使用该变量或属性的值。

类似的,递增和递减操作符、delete 操作符都有副效应。

7.4 操作符优先级

操作符优先级不建议硬背,实际使用中不确定的优先级可以通过圆括号显式改写。

最重要的规则:乘数优先于加减,赋值优先级很低,几乎都是最后执行。

7.5 操作符结合性

操作符结合性规定了相同优先级操作符的执行顺序,左结合表示操作从左到右执行,右结合表示操作从右到左执行。

幂、一元、赋值和三元条件操作符具有右结合性。

7.6 求值顺序

操作符优先级和结合性规定了操作的执行顺序,但没有规定子表达式的求值顺序。

JavaScript 始终按照从左至右的顺序对表达式求值,如 w = x + y * z,表达式 w 首先被求值,再对 x、y、z求值,然后将 y 和 z 相乘,加到 x 上,再把结果赋值给 w。

8. 算术表达式

基本算术操作符:***/%+-

基本操作符都会对操作数进行求值,必要时转为数值类型,无法转为数值的则转为 NaN

JavaScript 中所有数值都是浮点数,因此 / 的结果是浮点数。

被 0 除为正无穷或负无穷,0/0NaN

8.1 + 操作符

+ 操作符的行为如下:

  • 如果有一个操作数是对象,则使用无偏好算法将该操作数转为原始值。
  • 完成对象到原始值的转换后,如果有操作数是字符串,则都转为字符串并拼接。
  • 否则,两个操作数都被转为数值(或 NaN)并相加。

8.2 一元算术操作符

  • +:一元加,将操作数转为数值或 NaN,不能用于 BigInt
  • -:一元减,将操作数转为数值,并改变符号。
  • ++:递增,操作数必须是左值(变量、对象属性、数组元素,即能合法出现在赋值操作符左侧的表达式);不能进行字符串拼接,始终会将操作数转为数值。
  • --:递减,与递增操作符类似,也期待左值操作数。

8.3 位操作符

JavaScript 中位操作符期待整数操作数,并当成 32 位整数处理,必要时会强制转换为 32 位整数,NaNInfinity-Infinity 会转为 0。

  • &:按位与。
  • |:按位或。
  • ^:按位异或,同为 0,异为 1。
  • ~:按位取反。
  • <<:左移,低位补 0,左移一位相当于乘 2,移两位乘 4。
  • >>:有符号右移,正值高位补 0,负值高位补 1。
  • >>>:零填充右移,不管正负,高位均补 0,这是唯一不能用于 BigInt 的位操作符。

9. 关系表达式

9.1 相等和不相等操作符

== 是相等操作符,=== 是严格相等操作符,!=!== 刚好相反。

实践中判断相等应坚持使用 === 而不是 ==

严格相等判定规则:

  • 两个值类型不同,则不相等。
  • 两个值都是 null 或都是 undefined 才相等。
  • 两个值都是布尔值的 truefalse 才相等。
  • 如果有一个值为 NaN,则不相等。
  • 0 和 -0 不相等。
  • 两个值都是字符串且相同位置包含完全相同的 16 位值,则相等。JavaScript 不会主动进行 Unicode 归一化,因此可能有的字符串看起来一样但不相等。
  • 两个值引用同一个对象、数组或函数,则相等。不是同一引用则不相等。

基于类型转换的相等:

  • nullundefined 相等。
  • 一个是数值,一个是字符串,则把字符串转为数值再比较。
  • 若有一个值为 true 则转为 1,false 转为 0,再比较。
  • 如果一个值是对象,另一个值是数值或字符串,则将对象用无偏好算法转为原始值再比较。

9.2 比较操作符

比较操作符转换规则:

  • 如果有操作数值为对象,则按无偏好算法转为原始值再比较。
  • 如果两个都为字符串,则使用字母表顺序(Unicode 16位值的数值顺序)比较两个字符串。
  • 如果不都为字符串,则将原始值转为数值进行比较。
javascript
11 < 3 // false
"11" < "3" // ture
"11" < 3 // false
"one" < 3 // false,one 转为 NaN

9.3 in 操作符

in 操作符期待左侧是字符串、Symbol 和 可转为字符串的值,右侧是对象。如果左侧是对象的属性返回 true,否则返回 false

9.4 instanceof 操作符

  • instanceof 期待左侧是对象,右侧是对象类标识,左侧对象是右侧类的实例返回 true,否则返回 false
  • JavaScript 中对象通过构造函数创建,因此 instanceof 右侧应该是一个函数,否则报错;左侧如果不是对象,则返回 false
  • instanceof 的工作原理基于原型链,会在左侧对象的原型链上查找,如果右侧的 prototype 能在原型链上找到,返回 true

10. 逻辑表达式

  • JavaScript 的值要么是“真值”,要么是“假值”。
  • 假值有 falsenullundefined0-0NaN"",其它都为真值。

10.1 逻辑与

&& 执行短路操作,如果左侧为假,不会计算右侧值直接返回 false

10.2 逻辑或

|| 同样执行短路操作,左侧值为真,则不会计算右侧值。

10.3 逻辑非

想要取得任何值的布尔值表示,只要应用这个操作符两次即可:!!x

11. 赋值表达式

  • 赋值操作符期待左值是一个变量,对象属性或数组元素,右值是任意类型的任意值。
  • 赋值表达式的值是右侧操作数的值。
  • 赋值表达式具有右结合性,一个表达式中出现多个赋值操作,会从右向左求值。
javascript
let a = 0, b = 1
(a = b) === 1 //  true,(a = b) 的值就是b的值

i = j = k = 0 // 三个变量都初始为0

11.1 通过操作赋值

形如 a op= b,其中 op 是操作符,如 +=-= 等。

+= 操作符可以处理字符串和数值。对数值执行加法再赋值,对字符串进行拼接再赋值。

多数情况下,a op= b 都等价于 a = a op b,但如果 a 包含副效应,二者会有区别:

javascript
let i = 0
data[i++] *= 2 // data[0] = data[0] * 2
data[i++] = data[i++] * 2 // data[0] = data[1] * 2

12. 求值表达式

12.1 eval()

  • 全局函数 eval() 可以对源代码字符串求值。
  • eval() 期待一个参数,如果传入任何非字符串的值,会直接返回该值。
  • eval() 会使用调用它的代码的变量环境,即会像本地代码一样查找变量的值,定义新变量和函数,类似于代码块。
  • 如果一个函数内定义了变量 x,然后调用 eval('x'),则会取得这个局部变量的值。
  • 如果调用 eval('var y = 3;'),则会声明一个新的局部变量 y。
  • 如果被求值的字符串内使用 letconst,则声明的变量或常量会被限制在求值的局部作用域内,不会定义到调用环境。
  • 传给 eval 的字符串必须从语法上说得通,只要这个字符串本身可以作为独立的脚本运行,就可以合法地传给 eval,否则会报错,如 eval('return;') 就会报错。

12.2 全局 eval()

  • 可以给 eval 起别名,如果通过别名调用 eval 叫做间接调用,否则叫直接调用。
  • 间接调用时将字符串当作顶级全局代码来求值,被求值的代码可能定义新的顶级变量和顶级函数,可能修改顶级变量,但不会再使用或修改调用函数内的局部变量。
  • 直接调用 eval 使用的是调用上下文的变量环境,任何其它调用方式,都是使用全局对象(globalThis)作为变量环境。
  • 无论直接调用还是间接调用,都只在通过 var 定义新变量时才能修改调用或全局环境,通过 letconst 定义的被限制在求值局部作用域内。
  • eval 全局求值的能力是一种有用的特性,如果必须使用 eval,那很可能就是为了使用它的全局求值。
javascript
const gaval = eval;
let x = 'global', y = 'global' // 全局变量,node 环境需去掉 let,否则只是模块作用域

function f() {
  let x = 'local';
  eval('x += "changed";');
  return x
}

function g() {
  let y = 'local';
  geval('y += "changed";');
  return y
}

console.log(f(), x); // localchanged global
console.log(g(), y); // local globalchanged

12.3 严格 eval()

  • 严格模式下,eval()(直接调用)会基于一个私有变量环境进行局部求值,即被求值的代码可以查询和设置局部变量,但不能在局部作用域(调用 eval 的上下文环境)中定义新变量和函数。
  • 严格模式下,eval 不能被重写,即不能用 eval 关键字声明变量、函数、函数参数或捕获块参数。

13. 其它操作符v

13.1 条件操作符(?:)

  • 唯一的三元操作符,形式:操作数1 ? 操作数2 : 操作数3
  • 如果操作数 1 为真,则执行操作数 2,否则执行操作数 3。

13.2 先定义(??)

  • 先定义操作符 ?? 求值其先定义的操作数,如果左边操作数不是 nullundefined,则返回左值,否则返回右值,执行短路操作。
  • ?? 是对 || 的有效替代,特别对于 0、空字符串或者 false 的情况,?? 会认为有定义并直接返回。

13.3 typeof 操作符

  • typeof 是一元操作符,操作数可以是任意类型,返回字符串。
  • 如果操作数是 nulltypeof 结果是 "object"。
  • 对除函数外的所有对象或数组,typeof 求值结果都是 "object"。
xtypeof x
undefined"undefined"
null"object"
true or false"boolean"
任意数值或 NaN"number"
任意 Bigint"biting"
任意字符串"string"
任意符号"symbol"
任意函数"function"
任意非函数对象"object"

13.4 delete 操作符

  • delete 是一元操作符,尝试删除对象属性或数组元素。期待左值,对非左值不做任何操作,但返回 true。特别的,数组元素删除后就是稀疏数组。
  • 在严格模式下,delete 操作符只能作用于属性访问表达式,删除变量、函数、函数参数或不可删除属性就会报错,非严格模式下不报错,但返回 false,表示不可删除。
javascript
let o = { x: 1, y: 2 }
delete o.x // 删除成功,返回 true
typeof o.x // 属性不存在,返回 "undefined"
delete o.x // 删除不存在的属性,返回 true
delete 1 // 啥操作都不做,返回 true
delete o // 不能删除变量,返回 false,严格模式下报错
delete Object.prototype // 不可删除属性,返回 false,严格模式下报错

13.5 await 操作符

  • await 期待一个 Promise 对象作为其唯一操作数,可以让代码看起来在等待异步计算完成(实际上不会阻塞主进程,不会妨碍其它异步操作执行)。
  • await 操作符的值就是 Promise 对象的兑现值。
  • await 只能出现在通过 async 关键字声明为异步的函数中。

13.6 void 操作符

  • void 是一元操作符,操作数可以是任意类型。
  • void 可以求值自己的操作数,并丢弃,只有在操作数有副效应时才有必要使用 void
javascript
let counter = 0
const increment = () => void counter++
increment() // undefined
counter // 1

13.7 逗号操作符

  • 逗号操作符是二元操作符,操作数可以是任意类型。
  • 该操作符会求值左右操作数,但只返回右操作数的值,也意味着只有左操作数有副效应时才有必要使用逗号操作符。
  • 常见使用场景就是有多个循环变量的 for 循环。
javascript
for (let i = 0, j = 10; i < j; i++, j--) {
  console.log(i + j)
}

14. 小结

  • 任何表达式都可以求值为 JavaScript 中的一个值。
  • + 操作符既可以用于数值加法,也可以用于字符串拼接。
  • 逻辑操作符 &&|| 具有短路行为。