0%

JavaScript 权威指南 04:表达式与操作符

这篇文章讲述 JavaScript 表达式和用于构建各种表达式的操作符。表达式是一个可以被求值并产生一个值的 JavaScript 短语。基于简单表达式构建复杂表达式最常见的方式是使用操作符。操作符以某种方式组合其操作数的值​,然后求值为一个新值。

主表达式

主表达式(primary expression),即那些独立存在,不再包含更简单表达式的表达式。常量或字面量值、某些语言关键字(null、this 等)和变量引用等都是主表达式。

当程序中出现任何独立的标识符时,JavaScript 假设它是一个变量或常量或全局对象的属性,并查询它的值。如果不存在该名字的变量,则求值不存在的变量会导致抛出 ReferenceError。

对象和数组初始化表达式

对象和数组初始化表达式也是一种表达式,其值为新创建的对象或数组。这些初始化表达式有时候也称为 对象字面量 或者 数组字面量。但与真正的字面量不同,它们不是主表达式,因为它们包含用于指定属性或元素值的子表达式

数组初始化表达式是一个包含在方括号内的逗号分隔的表达式列表,其中的元素表达式本身也可以是数组初始化表达式,从而创建嵌套数组:

1
2
3
> []
> [1 + 2, 3 + 4]
> [[ 1, 2, 3 ], [4, 5 6]]

在数组字面量中,可以通过省略逗号间的值来包含未定义元素。初始化表达式中的最后一个元素表达式后面可以再跟一个逗号,而且这个逗号不会创建未定义元素。

1
2
3
4
5
6
> let s = [1,,,,5]
> s.length
5
> let s2 = [1,,,,5,]
> s2.length
5

对象初始化表达式使用花括号包含多个子表达式,每个子表达式的格式为 属性名:属性值,子表达式之间以逗号隔开。对象字面量也可以嵌套。

1
2
3
let p = {Name:"jack", Age:20};
let q = {};
let x = {p: p, q: q}:

函数定义表达式

函数定义表达式定义 JavaScript 函数,其值为新定义的函数。函数定义表达式也是 函数字面量

  • 函数定义表达式通常由关键字 function、位于括号中的逗号分隔的零或多个标识符(参数名)​,以及一个位于花括号中的 JavaScript 代码块(函数体)构成
  • 函数定义表达式也可以包含函数的名字
1
2
3
4
5
6
7
8
> let square = function(x) {return x * x};
> square(10)
100
> let f2 = function test(x) {return x * x};
> f2(10)
100
> test(10)
Uncaught ReferenceError: test is not defined

函数也可以使用函数语句而非函数表达式来定义。在 ES6 及之后的版本中,函数表达式可以使用更简洁的 箭头函数 语法。

属性访问表达式

属性访问表达式求值为对象属性或数组元素的值。JavaScript 定义了两种访问属性的语法:

1
2
expression.identifier
expression[expression]
  • expression.identifier 中,expression 指定对象,标识符指定属性名
  • expression[expression] 中,第一个 expression 为对象或者数组,第二个 expression 指定属性名或数组元素索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> let o = {x:1, y:{z:3}}
> o.x
1
> o["x"]
> o.y.z
3

> let a = [o, 4, [5, 6]]
> a[1]
4
> a[2][1]
6
> a[2]["1"]
6
> a[0].x
1

无论哪种属性访问表达式,位于 .[ 前面的表达式都会先求值。如果求值结果为 null 或 undefined,则表达式会抛出 TypeError,因为它们是 JavaScript 中不能有属性的两个值。

  • 如果对象表达式后跟一个点和一个标识符,则会对以该标识符为名字的属性求值
  • 如果对象表达式后跟位于方括号中的另一个表达式,则第二个表达式会被求值并转换为字符串。整个表达式的值就是名字为该字符串的属性的值
  • 任何一种情况下,如果指定名字的属性不存在,则属性访问表达式的值是 undefined

expression.identifier 语法更加简单,但通过它访问的属性的名字必须是合法的标识符,而且在写代码时已经知道了这个名字。如果属性名中包含空格或标点字符,或者是一个数值(对于数组而言)​,则必须使用方括号语法。方括号也可以用来访问非静态属性名。

条件式属性访问

ES2020 增加了两个新的属性访问表达式:

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

由于 null 和 undefined 没有属性,使用普通的属性访问表达式时会出现错误。可以使用 ?. 或者 ?.[] 来防止这种错误发生。例如对于 a?.b

  • 如果 a 是 null 或者 undefined,则整个表达式的值为 undefined,不会尝试访问属性 b
  • 如果 a 是其他值,则等效于 a.b

它的典型应用场景是较长的属性访问表达式链条:

1
2
let a = {b: null};
a.b?.c.d

一定要理解 ?. 以及 ?.[] 是短路操作。​如果它们左侧的子表达式求值为 null 或 undefined,那么整个表达式立即求值为undefined,不会再进一步尝试访问属性。

调用表达式

调用表达式是 JavaScript 中调用(或执行)函数或方法的一种语法。调用表达式由 表示要调用函数的函数表达式 开头,函数表达式后面跟着左圆括号、逗号分隔的零或多个参数表达式的列表和右圆括号。

1
2
3
> f(0)
> Math.max(x, y, z)
> a.sort()
  • 求值调用表达式时,首先求值函数表达式,然后求值参数表达式以产生参数值的列表
  • 如果函数表达式的值不是函数,则抛出TypeError
  • 然后按照函数定义时参数的顺序给参数赋值
  • 之后再执行函数体
  • 如果函数使用了 return 语句返回一个值,则该值就成为调用表达式的值。否则,调用表达式的值是 undefined

如果函数表达式是属性访问表达式(例如 a.sort() 中的 a.sort),则这种调用被称为方法调用。在方法调用中,作为属性访问主体的对象或数组在执行函数体时会变成 this 关键字的值。这样就可以支持面向对象的编程范式。

条件式调用

在 ES2020 中,可以使用 ?.() 而非 () 来调用函数。正常情况下,我们调用函数时,如果圆括号左侧的表达式是 null 或 undefined 或任何其他非函数值,都会抛出 TypeError。而使用 ?.() 调用语法,如果左侧的表达式求值 为 null 或 undefined,则整个调用表达式求值为 undefined,不会抛出异常。

与条件式属性访问表达式类似,使用 ?.() 进行函数调用也是短路操作:如果 ?. 左侧的值是 null 或 undefined,则圆括号中的任何参数表达式都不会被求值。

1
2
3
4
5
6
7
8
9
10
let f = null, x = 0;

try {
f(x++);
} catch(e) {
console.log(x); // => 1
}

f?.(x++)
console.log(x) // x => 1

使用 ?.() 的条件式调用表达式既适用于函数调用,也适用于方法调用。因为方法调用又涉及属性访问,所以需要理解下面调用的差异:

1
2
3
4
> o.m()
> o?.m()
> o.m?.()
> o?.m?.()

对象创建表达式

对象创建表达式创建一个新对象并调用一个函数(称为构造函数)来初始化这个新对象。对象创建表达式类似于调用表达式,只不过前面多了一个关键字 new:

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

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

1
2
new Object
new Date

对象创建表达式的值是新创建的对象

操作符概述

操作符在 JavaScript 中用于算术表达式、比较表达式、逻辑表达式、赋值表达式等。绝大多数操作符都是标点符号,但也有一些以关键字表示的操作符,例如 delete, typeof 等。

  • JavaScript 操作符通常会按照需要转换操作数的类型
  • 有些操作符的行为会因为操作数类型的不同而不同。最明显的,+ 操作符可以把数值加起来,也可以拼接字符串
  • 赋值操作符及少数其他操作符(例如 ++--)期待操作数类型为 lval,lval 即 lvalue(左值),表示一个可以合法地出现在赋值表达式左侧的表达式。变量、对象属性和数组元素都是 左值

操作符优先级控制操作符被执行的顺序。优先级高的操作符先于优先级低的操作符执行。操作符优先级可以通过圆括号显式改写。实践中,如果你完全不确定自己所用操作符的优先级,最简单的办法是使用圆括号明确求值顺序。

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

操作符的优先级和结合性规定了复杂表达式中操作的执行顺序,但它们没有规定子表达式的求值顺序。JavaScript始终严格按照从左到右的顺序对表达式求值。例如对于表达式 w = x + y * z

  • 子表达式 w 首先被求值
  • 再对 x、y、z 分别求值
  • 然后再将 y 和 z 的值相乘,加到 x 上,再把结果赋值给表达式 w 表示的变量或属性

圆括号只能改变操作符的执行顺序,但不会改变从左到右的求值顺序。求值顺序只在一种情况下会造成差异,即被求值的表达式具有副效应,这会影响其他表达式的求值。

1
2
3
4
5
6
7
8
> let a = [1, 2, 3]
> let x = 0
> a[x] = 1 + (a[++x] * x)
3
> a
[ 3, 2, 3 ]
> x
1

算术表达式

接下来介绍可以对操作数执行算术或其他数值操作的操作符。基本的算术操作符包括 +-*/%**。其中 + 操作符比较特殊,需要单独讨论。另外5个基本操作符都会对自己的操作数进行求值,必要时将操作数转换为数值,然后在执行计算。无法转换为数值的操作数则转换为 NaN。

  • ** 优先级最高,为右结合。另外 -3**2 是有歧义的,因为 JavaScript 没有规定一元 -** 的优先级,此时需要用圆括号
  • / 操作符用第二个操作数除第一个操作数。在JavaScript中,所有数值都是浮点数,因此所有除法操作得到的都是浮点数
  • % 操作符计算第一个操作数对第二个操作数取模的结果,它返回第一个操作数被第二个操作数整除之后的余数,它返回第一个操作数被第二个操作数整除之后的余数。% 操作符也可以用于浮点数

+ 可以执行字符串的拼接操作或数值的加法运算

  • 如果两个操作数都是字符串,则执行字符串拼接
  • 如果两个操作数都是数值,则执行数值加法运算
  • 以上情况之外,都会涉及类型转换。+ 优先执行字符串拼接:只要有操作数是字符串或可以转换为字符串的对象,另一个操作数也会被转换为字符串并执行拼接操作。只有任何操作数都不是字符串或类字符串值时才会执行加法操作。严格来说,其行为如下:
    • 如果有一个操作数是对象,则 + 操作符首先使用 对象到原始值的算法 把该操作数转换为原始值。Date 对象用toString()方法来转换(Data 类型实现了偏字符串算法),其他所有对象通过 valueOf() 转换(其他类型实现了偏数值算法)。但是由于多数对象并没有 valueOf() 方法,因此它们也会通过 toString() 方法转换
    • 完成对象到原始值的转换后,如果有操作数是字符串,另一个操作数也会被转换为字符串并进行拼接
    • 否则,两个操作数都被转换为数值(或NaN)​,计算加法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
> 1 + 2
3
> "1" + "2"
'12'
> 2 + "1"
'21'
> "1" + 2
'12'
> 1 + {}
'1[object Object]'
> true + true
2
> 1 + null
1
> 1 + undefined
NaN

> 1 + 2 + "test"
'3test'
> 1 + (2 + "test")
'12test'

算术一元操作符(+、-、++和–)都在必要时将自己唯一的操作数转换为数值:

  • ++ 这个操作符将其操作数转换为数值,在这个数值上加 1,然后将递增后的数值再赋值回这个变量、元素或属性。因此其操作数必须是一个 lval
  • ++ 操作符的本身返回值取决于它与操作数的相对位置。如果位于操作数前面,即先递增操作数,再求值为该操作数递增后的值。如果位于操作数后面,它也会递增操作数,但仍然求值为该操作数未递增的值
1
2
3
4
5
6
7
> let i = 1, j = ++i;
> [i, j]
[ 2, 2 ]

> let n = 1, m = n++;
> [n, m]
[ 2, 1 ]
  • -- 操作符和 ++ 类似,只不过它将操作数的数值减 1。其他行为都是类似的

位操作符对数值的二进制表示执行低级位操作。位操作符期待整数操作数,而且将它们当成 32 位整数而非 64 位浮点值。这些操作符必要时将它们的操作数转换为数值,然后再将得到的数值强制转换为 32 位整数,即丢弃小数部分和第 32 位以外的部分。位操作符包括:&|^~<<>>(有符号右移)、>>>(零填充右移)。移位操作符的第二个操作数应该是介于 0 到 31 之间的整数。

关系表达式

关系操作符测试两个值之间的关系,并依据相应关系是否存在返回 true 或 false。

相等和不相等操作符

===== 分别用两个不同的标准来检查相等性。!=!== 操作符测试的关系与 ===== 恰好相反。

  • === 操作符被称为严格相等操作符(或者全等操作符),它根据 严格相同 的定义检查两个操作数是否完全相同
  • == 操作符被称为相等操作符,它根据更宽松的(允许类型转换的)相同定义检查两个操作数是否相等

== 操作符是 JavaScript 早期的特性,被普遍认为是个隐患。因此实践中应该坚持使用 === 而不使用 ==,使用 !== 而不使用 !=

JavaScript 对象是按引用而不是按值比较的。对象与自己相等,但与其他任何对象都不相等。即使两个对象有同样多的属性,每个属性的名字和值也相同,它们也不相等。数组也是类似的,两个数组即使元素相同、顺序相同,它们也不相等。

1
2
3
4
5
6
7
8
> {} == {}
false
> {} === {}
false
> [] == []
false
> [] === []
false

严格相等操作符 === 求值其操作数,然后按下列步骤比较两个值,不做类型转换:

  • 如果两个值类型不同,则不相等
  • 如果两个值都是 null 或都是 undefined,则相等
  • 如果两个值都是布尔值 true 或都是布尔值 false,则相等
  • 如果一个或两个值是 NaN,则不相等(NaN 不等于任何值,包括自己)
  • 如果两个值都是数值且值相同,则相等。如果一个值是 0 而另一个是 -0,则也相等
  • 如果两个值都是字符串且相同位置包含完全相同的 16 位值​,则相等,否则不相等。两个字符串有可能看起来相同,也表示同样的意思,但底层编码却使用不同的 16 位值序列,JavaScript 是不会执行 Unicode 归一化操作
  • 如果两个值引用同一个对象、数组或函数,则相等。如果它们引用不同的对象,即使两个对象有完全相同的属性,也不相等
1
2
3
4
5
6
> null === undefined
false
> NaN === NaN
false
> 0 === -0
true

相等操作符 == 是基于类型转换的宽松相等性测试。相等操作符 == 与严格相等类似,但没那么严格。如果两个操作数的值类型不同,它会尝试做类型转换,然后再比较

  • 如果两个值类型相同,则按照前面的规则测试它们是否严格相等。如果严格相等,则相等。如果不严格相等,则不相等
  • 如果两个值类型不同,== 操作符仍然可能认为它们相等。此时它会使用以下规则,基于类型转换来判定相等关系
    • 如果一个值是 null,另一个值是 undefined,则相等
    • 如果一个值是数值,另一个值是字符串,把字符串转换为数值,再比较转换后的数值
    • 如果有一个值为 true,把它转换为 1,再比较。如果有一个值为 false,把它转换为0,再比较
    • 如果一个值是对象,另一个值是数值或字符串,那么尝试把对象转换为原始值再进行比较(使用无偏好算法)
    • 其他任何值的组合都不相等
1
2
3
4
5
6
7
8
> null == undefined
true
> 1 == " 1 "
true
> true == " 1 "
true
> true == 1
true

比较操作符

比较操作符测试操作数的相对顺序(数值或字母表顺序)​,包括 <<=>>=。这几个比较操作符的操作数可能是任何类型。但比较只能针对数值和字符串,因此不是数值或字符串的操作数会被转换类型

  • 如果操作数有对象,尝试将对象转换为原始值(使用偏数值算法)
    • 所以对于 Date 类型,无偏好算法会将其转换为字符串,但偏数值算法则可以将其转换为数值。对于 Date 类型,字符串比较是无意义的,而数值的比较才是预期行为。因此 Date 类型使用比较运算符会得到预期结果
  • 如果在完成对象到原始值的转换后两个操作数都是字符串,则使用字母表顺序比较这两个字符串。字母表顺序就是字符的 16 位值顺序
  • 如果在完成对象到原始值的转换后至少有一个操作数不是字符串,则两个操作数都会被转换为数值并按照数值顺序来比较
  • 只有有任意操作数(或转换后)为 NaN,都会返回 false
1
2
3
4
5
6
7
8
> let d1 = new Date()
> let d2 = new Date()
> d1 < d2
true
> d1.valueOf()
1736949603846
> d2.valueOf()
1736949609116
1
2
3
4
5
6
> Infinity > -Infinity
true
> NaN > NaN
false
> NaN > 1
false

+ 操作符和比较操作符同样都会对数值和字符串操作数区别对待。+ 偏向字符串,即只要有一个操作数是字符串,它就会执行拼接操作。而比较操作符偏向数值,只有两个操作数均为字符串时才按字符串处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
> 1 + 2
3
> "1" + "2"
'12'
> 2 + "1"
'21'

> 11 > 2
true
> 11 > "2"
true
> "11" > "2"
false

in 操作符

in 操作符期待左侧操作数是字符串、符号或可以转换为字符串的值,期待右侧操作数是对象。如果左侧的值是右侧的对象的属性名,则 in 返回 true。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> let p = {x:1, y:2}
> "x" in p
true
> "y" in p
true
> "valueOf" in p
true

> "0" in t
true
> 0 in t
true
> 3 in t
false

instanceof 操作符

instanceof操作符期待左侧操作数是对象,右侧操作数是对象类的标识。这个操作符在左侧对象是右侧类的实例时求值为 true,否则求值为 false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> let d = new Date()
> let o = {}
> let arr = []

> d instanceof Date
true
> o instanceof Date
false
> d instanceof Object
true
> o instanceof Object
true

> arr instanceof Array
true
> arr instanceof Object
true
> arr instanceof Date
false

所有对象都是 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

逻辑表达式

逻辑运算符包括 &&||!,可以执行布尔运算。

  • && 的操作数可以是非布尔值,如果两个操作数都是真值,&& 返回真值,否则返回假值
    • 首先对第一个操作数即它左边的表达式求值,如果左边的值是假值,直接返回它左边的值,不再求值它右侧的表达式。
    • 如果左边的值为真值,&& 操作符求值并返回它右侧的值
    • && 可能会(也可能不会)对其右侧操作数求值,它具有短路求值的特性
1
2
3
4
5
6
> let o = {x: 0}
> let p = null
> o && o.x
0
> p && p.x
null
  • || 执行布尔或操作,如果有一个操作数是真值,这个操作符就返回真值,否则返回假值
    • 它首先会对第一个操作数,即它左侧的表达式求值,如果坐标的值是真值,直接返回它左边的值,不再求值它右侧表达式的值
    • 而如果第一个操作数的值是假值,则会求值第二个操作数并返回
    • || 同样具有短路求值的特性

|| 的典型用法是在一系列备选项中选择第一个真值

1
let max = maxWidth || preference.maxWidth || 500;
  • ! 用于反转其操作数的布尔值,即如果 x 是个真值,!x 返回 false,如果 x 是个假值,!x 返回 true
    • 与 &&、|| 不同,! 操作符将其操作数转换为布尔值,然后再反转得到的布尔值
    • 因此 ! 操作符总是返回布尔值,!!x 就可以返回 x 对应的布尔值

赋值表达式

JavaScript 使用 = 操作符为变量或属性赋值。= 操作符期待其左侧操作数是一个左值,即变量或对象属性或数组元素,右操作数可以是任意类型的任意值。赋值表达式本身的值是其右侧的值,所以如下语句是合法的,它的作用是在同一个表达式中赋值并测试该值:

1
if (a = b) {}

但上述语句很可能的真实意图是 if (a == b),这类错误很难察觉。这正式因为赋值操作符是表达式,而不是语句(这一点 Go 就很好)。

赋值操作符具有右结合性,这意味着如果一个表达式中出现多个赋值操作符,它们会从右向左求值。

1
a = b = c = 0;

除了常规的 = 赋值操作符,JavaScript 还支持其他一些赋值操作符,这些操作符通过组合赋值和其他操作符提供了快捷操作,例如 +=-=*= 等等。

多数情况下,a op= b 等价于 a = a op b。但是 a op= b 中,a 表达式只会求值一次,而 a = a op b 中 a 会求值两次。这在 a 表达式具有副效应时会有区别:

1
2
data[i++] *= 2;
data[i++] = data[i++] * 2;

求值表达式

与很多解释型语言一样,JavaScript 有能力解释 JavaScript 源代码字符串,对它们求值以产生一个值。JavaScript 是通过全局函数 eval() 来对源代码字符串求值的:

1
2
> eval("3 + 2")
5

eval() 可能会成为安全漏洞,为此永远不要把来自用户输入的字符串交给它执行。对于像 JavaScript 这么复杂的语言,无法对用户输入脱敏,因此无法保证在 eval() 中安全地使用。

eval() 是一个函数,它其实应该是一个操作符。语言设计者和解释器开发者一直对它加以限制,导致它越来越像操作符。

eval() 期待一个参数:

  • 如果给它传入任何非字符串值,它会简单地返回这个值
  • 如果传入字符串,它会尝试把这个字符串当成 JavaScript 代码来解析,解析失败会抛出 SyntaxError
  • 如果解析字符串成功,它会求值代码并返回该字符串中最后一个表达式或语句的值。如果最后一个表达式或语句没有值则返回 undefined
  • eval() 会使用调用它的代码的变量环境,它会像本地代码一样查找变量的值、定义新变量和函数。但是如果被求值的字符串使用了 let 或 const,则声明的变量或常量会被限制在求值的局部作用域内,不会定义到调用环境中。
  • 如果在顶级代码中调用eval(),则它操作的一定是全局变量和全局函数。
  • 传给 eval() 的代码字符串本身必须从语法上说得通:不能使用它向函数中粘贴代码片段。例如在 eval("return") 就是无意义的,因为 return 只在函数中有效。传递给 eval() 的字符串是作为独立的脚本运行(虽然会使用调用它的代码的变量环境)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
> eval(true)
true

> eval({})
{}

> eval("not code")
Uncaught SyntaxError: Unexpected identifier

> eval("1 + 1; 2 + 2;")
4

> x = 20
20
> eval("x")
20

> eval("var m = 20")
> m
20

> eval("let n = 20")
> n
Uncaught ReferenceError: n is not defined

正是因为 eval() 会修改局部变量,因此它会干扰 JavaScript 的优化程序。因此解释器不会过多的优化调用 eval() 的函数。但是 eval 函数可以赋值给其他标识符,并通过其他标识符来调用。JavaScript规范中说,如 果 eval() 被以 eval 之外的其他名字调用时,它应该把字符串当成顶级全局代码来求值。被求值的代码可能定义新全局变量或全局函数,可能修改全局变量,但它不会再使用或修改调用函数的局部变量。因此也就不会妨碍局部优化。

因此,直接的 eval() 调用使用的是调用上下文的变量环境,而其他调用方式,都使用全局对象作为变量环境,因而不能读、写或定义局部变量或函数。

如下是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const geval = eval;
let x = "global", y = "global";

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); // v18.16.0 出现错误:ReferenceError: y is not defined

严格模对 eval() 函数增加了更多限制,甚至对标识符 eval的使用也进行了限制:

  • 当我们在严格模式下调用 eval() 时,或者当被求值的代码字符串以 use strict 指令开头时,eval() 会基于一个私有变量环境进行局部求值。因此被求值的代码可以查询和设置局部变量,但不能在局部作用域中定义新变量或函数
  • 严格模式让 eval() 变得更像操作符,因为 eval 在严格模式下会变成保留字。此时不能再使用新值来重 eval() 函数

其他操作符

条件操作符

条件操作符 ?: 是 JavaScript 唯一一个三元操作符。条件操作符的操作数可以是任意类型。第一个操作数被求值并解释为一个布尔值,如果为真,那么就求值第二个操作数并返回它的值,否则求值第三个操作数并返回它的值。

1
greeting = "hello " + (username ? username : "there")

可以使用 if 语句实现类似的结果,但是 ?: 操作符更简洁。

先定义

先定义(first-defined)操作符 ?? 求值其先定义的操作数,如果其左操作数不是 null 或 undefined,就返回该值。否则,它会返回右操作数的值。?? 也是短路的,只有第一个操作数求值为 null 或 undefined 时才会求值第二个操作数。

如果表达式 a 没有副效应,那么 a ?? b 等价于 (a != null && a != undefined) ? a : b。?? 也是对 || 的一个有用的替代,适合选择先定义的操作数,而不是第一个为真值的操作数

?? 操作符与 && 和 || 操作符类似,但优先级并不比它们更高或更低。如果表达式中混用了 ?? 和它们中的任何一个,必须使用圆括号说明先执行哪个操作。

typeof 操作符

typeof 操作符求值为一个字符串,表示其操作数的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
> typeof undefined
'undefined'
> typeof true
'boolean'
> typeof 1
'number'
> typeof "test"
'string'
> typeof function(){}
'function'
> typeof 1n
'bigint'
> typeof Symbol("test")
'symbol'
> typeof {}
'object'
> typeof {}
'object'
> typeof []
'object'

注意如果操作数的值是 null,typeof 返回 object​。如果想区分 null 和对象,必须显式测试这个特殊值。由于对于所有对象和数组,typeof 返回的都是 object​,如果要区分不同对象的类,必须使用其他方法。

delete 操作符

delete 操作符尝试删除其操作数指定的对象属性或数组元素,当删除一个属性时,这个属性就不存在了(可以通过 in 操作符检查时返回 false),而不只是将其设置为 undefined。删除某个数组元素会在数组中留下一个 ​,并不改变数组的长度。结果数组是一个稀疏数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
> let o = {x: 10, y:20}
> delete o.x
true
> o.x
undefined
> "x" in o
false
> delete o.x
true

> o.y = undefined
undefined
> o.y
undefined
> "y" in o
true

> let a = [1, 2, 3]
> delete a[0]
true
> "0" in a
false

delete 期待它的操作数是个左值:

  • 如果操作数不是左值,delete 什么也不做,且返回 true
  • 否则 delete 尝试删除指定的左值,删除成功则返回 true,
  • 但是并非所有属性都是可以删除的:不可配置属性就无法删除

在严格模式下:

  • delete 的操作数如果是未限定标识符,比如变量、函数或函数参数,就会导致 SyntaxError。此时,delete 操作符只能作用于属性访问表达式)
  • 严格模式也会在 delete 尝试删除不可配置(即不可删除)属性时抛出 TypeError

但在严格模式之外,这两种情况都不会发生异常,delete 只是简单地返回false,表示不能删除操作数。

await 操作符

await 是 ES2017 增加的,用于让 JavaScript 中的异步编程更自然。await 期待一个 Promise 对象(表示异步计算)作为其唯一操作数,可以让代码看起来像是在等待异步计算完成(但实际上它不会阻塞主线程,不会妨碍其他异步操作进行)​。

await 操作符的值是 Promise 对象的兑现值。关键在于,await 只能出现在已经通过 async 关键字声明为异步的函数中。

void 操作符

void 操作符求值自己的操作数,然后丢弃这个值并返回 undefined。由于操作数的值会被丢弃,只有在操作数有副效应时才有必要使用 void 操作符。

如下是一个例子,使用了箭头函数的简写语法,其函数体会被求值并返回表达式,这里通过 void 将返回值丢弃:

1
2
let counter = 0;
const increment = () => void counter++;

逗号操作符

逗号操作符是二元操作符,其操作数可以是任意类型。这个操作符会求值其左操作数,求值其右操作数,然后返回右操作数的值。

1
2
> i = 0, j = 1, k = 2
2