跳到主要内容

操作符

操作符可以用于算术、比较、逻辑、赋值等表达式中。除了标点符号外,也有 deleteinstanceof 这样的关键字表示。具体操作符表格见书 P66 页或 运算符优先级 - MDN

操作数有分一元、二元、三元。?: 是唯一的三元操作符。

操作符会根据需要转换操作数类型,比如 "3" * "5" 结果为 15 。同时,即便是使用相同的操作数,可能会有不同的结果。比如 + 可能用于数值相加或字符串拼接。

操作符存在 优先级 ,一元操作符的优先级最高,而赋值操作符优先级很低。如果你不确定操作符的优先级,请使用圆括号括起来。

有些新增的操作符在优先级上没有明确定义。比如 ** 幂操作符,他与一元负值操作符 - 的优先级大小没有明确定义,所以应当加括号。比如对于 -3 ** 2 ,他可能意味着 -(3 ** 2)(-3) ** 2 。同样的还有 ??|| or &&

操作符中存在 结合性 ,它规定了相同优先级下操作符的执行顺序。比如四则运算均为左结合性(从左往右),而幂、一元、赋值、三元操作符均为右结合性:

2 ** 2 ** 2     // 等价于 2 ** (2 ** 2)

优先级和结合性共同规定了复杂表达式中操作的执行顺序。在复杂表达式中建议不使用 ++-- 操作符,它的自增自减副作用会对表达式求值带来逻辑上的不确定性。

算术表达式

基本的算术操作符有 +-*/% 以及 ES2016 新增的 **

幂操作会优先于乘、除、取模操作。

除法操作得到的是浮点数 ,被 0 除得到的是正或负无穷,零除以零则为 NaN

5 / 2           // => 2.5
-10 / 0 // => -Infinity
0 / 0 // => NaN
1 / Infinity // => 0

对于取模运算,求得的余数的符号与被除数一致(包括 -0 ),浮点数也可以取模:

-5 % 2          // => -1
-10 % 5 // => -0
6.5 % 2.1 // => 0.2

+ 操作符

严格来讲,+ 操作符的行为如下:

  1. 如果存在操作数为对象,则先根据无偏好算法转为原始值;

    Date 对象会使用 toString() 方法转换,而其它类先使用 valueOf() 方法转换,如果得到的不是原始值或为空值,则用 valueOf() 转换。由于大部分对象没有 valueOf() 方法,所以也会通过 toString() 方法转换。

  2. 将对象转为原始值后,如果存在操作数为字符串,则将另一个操作数也转为字符串,然后进行拼接;

  3. 否则,两个操作数都被转为数值,进行加法计算。

1 + 2         // => 3
"1" + 2 // => "12"
1 + {} // => "1[object Object]"
true + true // => 2
2 + null // => 2

多个 + 操作符用在字符串和数值时,请注意结合性:

1 + 2 + " people"   // => "3 people" 而非 "12 people"

一元算术操作符

  • 一元加 +

    会将操作数转为数值,然后返回。如果原本是数值则什么都不做。不能对 BigInt 值使用一元加操作符(会抛出错误:不能将大数值转为数值)。

  • 一元减 -

    先将操作数转为数值,然后更改符号。

  • 自增 ++ & 自减 --

    操作数应当是个变量。操作的返回值取决于操作符的位置:

    let i = 1, j = i++    // => i = 2, j = 2
    let a = 1, b = ++a // => a = 2, b = 1

位操作符

位操作符有 &|^~<<>>>>>

位操作会强制将数值当作 32 位而非 64 位浮点值处理,且右操作数必须为 [0, 32) 区间内整数。而 NaN±Infinity 作为位操作数时会转为 0 。

>>> 为无符号右移。在左移中,最低位始终补 0 ,而有符号右移则需要看符号位,正数补 0 ,负数补 1 。而无符号右移则均补 0 。

除了 >>> ,其余操作符都可用在大数值上(因为大数值在右移时不需要高位填充),且两个操作数应当均为大数,否则会抛出错误。

关系表达式

相等 & 严格相等

=== 用于严格比较两个操作数是否 完全相同 ,而 == 会先尝试类型转换再进行比较。== 是早期 JS 的语法,现在应当坚持使用 === 而非 ==

一个对象只与自己严格相等,与其他任何对象都不相等,即使两者的表现完全一致。因为每一个对象都有独一无二的内存地址。

nullundefined 不严格相等,但是相等。NaN 与任何值不等,包括自身。0 和 -0 严格相等。

两个字符串应当包含完全相同的 16 位值,否则即便看起来完全相同,也会判为不等。因为 JS 不会执行 Unicode 归一化操作。

如果将数值和字符串 == 比较,则会将字符串先转为数值。如果有布尔值,则 true 转为 1 ,false 转为 0 ,再进行比较。

"1" == true     // => true

== 比较中如果有对象,则使用无偏好算法。

比较操作符

比较操作符有 <><=>= 。比较和转换规则如下:

  • 如果有操作数为对象,则采用无偏好算法转为原始值;

  • 字符串之间的比较会根据字母表顺序,即 16 位 Unicode 值的数值顺序;

  • 如果存在一个操作数不是字符串,则会将两个操作数转为数值再进行比较。

"11" < "3"    // => true
"11" < 3 // => false

in 操作符

in 操作符用来判断左侧操作符是否为右侧操作符的属性:

let points = { x: 1, y: 2 }
"x" in points // => true
"z" in points // => false

对于数组要注意的是,它们的索引下标也是属性:

let data = [5, 6, 7, 8]

data[0] // => 5
data["2"] // => 7
data.0 // ! SyntaxError

0 in data // => true
"0" in data // => true

instanceof 操作符

instanceof 操作符的左操作数应当为一个对象,右侧为一个对象类型(比如 Object、Array )。它会判断对象的原型链中是否存在该对象类型的原型。

左操作数如果是原始值则会返回 false ,因为原始值不是对象,也就无法跟对象类型比较。右操作数如果不是对象类型,则会抛出 TypeError 。

逻辑表达式

逻辑操作符会进行布尔代数运算,有 &&||!

所有 JS 值要么是真性值,要么是假性值。假值仅有 falsenullundefined、±0、NaN、"" ,其余均为真值。

逻辑与 &&

逻辑与 && 要求两个操作数都为真值才会为真。当然,当左操作数是假值时,则不会对右操作数进行求值。

若左操作数为真值,整个表达式的值为右操作数:

10 && "hello"       // => "hello"
10 && null // => null

if (a === b) { stop() }
(a === b) && stop() // 与上条等效

逻辑或 ||

逻辑或 || 要求两个操作数只需有一真值则为真。

和逻辑与类似,会返回第一个为真值的操作数,并不再处理后面的操作数;若一个都没有,则返回最后一个操作数。这也是逻辑或的习惯性用法:在一系列备选项中选择第一个真值。

逻辑非 !

逻辑非 ! 会对操作数取反,如操作数不是布尔值,会先转换为布尔值。

如果想将一个值转为布尔值,可以用两个叹号 !!

!"hello"    // => false
!!"" // => false

赋值表达式

= 操作符会将右操作数赋值给左操作数,且整个表达式会返回该值,左操作数应当是个左值(即变量)。

另外,算术及位操作符支持简写,比如:

let a = 1, b = 2, c = 3
a += b // => 3,等价于 a = a + b
c <<= b // => 12,等价于 c = c << b

但对于这样的带副效应的操作(比如自增自减),则不等价:

data[i++] *= 2            // 与下条表达式不等价
data[i++] = data[i++] * 2

求值表达式

可以将一个表达式字符串传给全局函数 eval() 来求得表达式的值:

eval("3 + 2")     // => 5

这是个强大的语言特性,但是 不应当使用 这个函数:一来,几乎没有对该特性的需求场景;二来,该特性会有安全隐患(永远不要将用户的输入交给 eval() 执行);三来,会干扰 JS 的优化程序,因为它能够修改局部变量。

传入的表达式应当能作为一个合法的脚本运行(比如 eval("return") 是没有意义的)。如果解析失败,则会抛出 SyntaxError ;解析成功会返回最后一个表达式的值。

如果表达式中使用 letconst ,则声明的变量会被限制在求值的局部作用域中;而 var 会泄露出一个局部变量。如果表达式以 "use strict" 开头(即在严格模式下),则不能在其局部作用域中定义变量或函数。

全局 eval()

JS 规范中指明,如果 eval() 以别名身份调用,则应当将表达式字符串当作全局代码来求值。这样表达式会作用于全局对象,而不会妨碍局部优化:

const geval = eval
let x = "Hello"
function f() {
let x = "Hello"
geval("x += 'Talaxy'")
return x
}
console.log(f(), x) // => "Hello HelloTalaxy"

如果 f() 函数中直接使用 eval() ,则打印结果为 "HelloTalaxy Hello"

其他操作符

条件操作符 ?:

条件操作符是唯一一个三目运算符,语法为 a ? b : c 。当 a 为真时,返回 b ,否则返回 c 。

空值合并 ??

对于 a ?? b ,若 a 不为空(即不为 null or undefined )返回 a ,否则返回 b 。这等价于:

(a !== null && a !== undefined) ? a : b

若与 && or || 混用,必须使用圆括号表明优先级。

typeof 操作符

typeof 会简单返回该值的类型的字符串。以下是可能的结果:

xtypeof x
undefined"undefined"
true or false"boolean"
任意数值、NaN"number"
任意大数值"bigint"
任意字符串"string"
任意符号"symbol"
任意函数"function"
任意非函数对象、null"object"
typeof "hello"      // => "string"

delete 操作符

delete 会尝试删除指定的对象属性或数组元素,删除成功则返回 true 。并非所有属性都能删除:不可配置属性就无法删除。如果尝试删除变量、函数或函数参数也会失败,并返回 false

let obj = { x: 0, y: 0 }
delete obj.x // => true
"x" in obj // => false

如果删除数组元素,则会在数组中留下一个 "坑" ,并不会改变数组长度:

let arr = [1, 2, 3, 4]
delete arr[3] // => true
3 in arr // => false
arr // => [1, 2, 3, 空]

在严格模式下,delete 不能删除变量、函数或函数参数,否则抛出 SyntaxError 。此时只能作用于属性访问表达式。同时,如果 delete 尝试删除不可配置属性,则会抛出 TypeError 。

await 操作符

该操作符用于 Promise 的异步等待。

void 操作符

用于丢弃表达式所返回的值:

void 1 + 2    // => undefined

let counter = 0
const add = () => void counter++
add() // => undefined
counter // => 1

如果没有使用 void 操作符,add() 则会返回 0 。

逗号表达式

逗号 , 操作符会返回右操作数,且为左结合性:

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

let 语句中的逗号为其语法的一部分,而非逗号表达式。

参考

表达式和运算符 - MDN