操作符
操作符可以用于算术、比较、逻辑、赋值等表达式中。除了标点符号外,也有 delete
、instanceof
这样的关键字表示。具体操作符表格见书 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
+ 操作符
严格来讲,+
操作符的行为如下:
如果存在操作数为对象,则先根据无偏好算法转为原始值;
Date 对象会使用
toString()
方法转换,而其它类先使用valueOf()
方法转换,如果得到的不是原始值或为空值,则用valueOf()
转换。由于大部分对象没有valueOf()
方法,所以也会通过toString()
方法转换。将对象转为原始值后,如果存在操作数为字符串,则将另一个操作数也转为字符串,然后进行拼接;
否则,两个操作数都被转为数值,进行加法计算。
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 的语法,现在应当坚持使用 ===
而非 ==
。
一个对象只与自己严格相等,与其他任何对象都不相等,即使两者的表现完全一致。因为每一个对象都有独一无二的内存地址。
null
和 undefined
不严格相等,但是相等。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 值要么是真性值,要么是假性值。假值仅有 false
、null
、undefined
、±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 ;解析成功会返回最后一个表达式的值。
如果表达式中使用 let
或 const
,则声明的变量会被限制在求值的局部作用域中;而 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
会简单返回该值的类型的字符串。以下是可能的结果:
x | typeof 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
语句中的逗号为其语法的一部分,而非逗号表达式。