表达式与操作符
1. 主表达式
- 最简单的表达式称为主表达式,JavaScript 中主表达式包括 字面量值、某些关键字、常量 和 变量引用。
- 字面量是可以直接嵌入在程序中的常量值。
- 一些保留字也是主表达式,如
true
、false
、null
、this
等。 - 全局对象属性的引用(如 undefined)也是主表达式。
1.23 // 数值字面量
"hello" // 字符串字面量
/pattern/ // 正则表达式字面量
2. 对象和数组初始化程序
对象和数组初始化程序也是一种表达式,值为新创建的对象或数组。这些初始化程序也叫对象字面量或数组字面量,但不是真正的字面量,因为它们不是主表达式,内部可以包含子表达式。
数组初始化程序中元素表达式本身也可以是数组初始化程序,因此可以创建嵌套数组:
let matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
数组字面量中省略逗号中的值可以包含未定义元素:
let sparseArray = [1, , , , 5] // 包含三个未定义元素
对象初始化程序和数组初始化程序类似,不过方括号变成了花括号,同时元素变成了键值对:
let p = { x: 2.3, y: -1.2 }
3. 函数定义表达式
函数定义表达式通过由关键字 function、位于括号中的参数名以及花括号中的代码块构成:
const square = function(x) {
return x * x
}
函数定义表达式也可以包括函数的名字:
function square(x) {
return x * x
}
ES6 之后,函数表达式可以用更简洁的“箭头函数”语法。
4. 属性访问表达式
属性访问表达式求值为对象属性或数组元素,有两种语法:
- 表达式后跟一个句点和一个标识符,只能用于对象。
- 达式后跟一个位于方括号中的表达式,可用于对象或数组。
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
注意
- 位于
.
或[
前面的表达式会先求值,如果为null
或undefined
,会报 TypeError。 - 任何一种情况下,指定名字的属性不存在,属性访问表达式的值都为
undefined
。 - 属性名中如果包含空格或标点字符,或者是一个数值,则必须使用方括号语法。
4.1 条件式属性访问
ES2020 新增了两种条件式属性访问语法:
expression?.identifier
expression?.[expression]
使用 ?.
或 ?.[]
可以防止对 null
或 undefined
取属性时的报错,比如表达式 a?.b
,如果 a 是 null
或者 undefined
,整个表达式的值为 undefined
。
这种条件式属性访问语法也称“可选链式”访问,这种访问是短路操作,如果问号前表达式为 undefined
或 null
,那么整个表达式返回 undefined
。
5. 调用表达式
调用表达式是 JavaScript 中调用函数或方法的一种语法。
f(0) // f 是函数表达式,0 是参数
Math.max(x, y , z) // Math.max 是函数表达式,x y z 是参数
如果函数使用 return
返回了一个值,该值就是调用表达式的结果,否则,调用表达式的结果是 undefined
。
5.1 条件式调用
在 ES2020 中,可使用 ?.()
而不是 ()
调用函数,如果问号前表达式为 undefined
或 null
,那么整个表达式返回 undefined
,而不会抛出异常。
注意
?.()
只会检测左侧是否是null
或undefined
,不会验证是否是函数,如果左侧不是函数,仍会抛错。- 与条件式属性访问表达式类似,条件式调用也是“短路”操作,如果问号前表达式为
undefined
或null
,则圆括号中任何参数表达式均不会求值。
let f = null, x = 0
try {
f(x++) // 抛 TypeError,参数列表仍会计算
} catch (e) {
x // 1,抛出错误前 x 发生了递增
}
f?.(x++) // undefined
x // 1,短路操作,参数列表不进行计算
6. 对象创建表达式
对象创建表达式可以创建一个对象,并调用构造函数初始化对象:
new Object()
new Point(2, 3)
如果在对象创建表达式中不给构造函数传参,则可以省略圆括号:
new Object
new Date
7. 操作符概述
操作符用于算术表达式、比较表达式、赋值表达式等。
多数操作符用标点符号表示,也有一些以关键字表示,如 delete
和 instanceof
。
7.1 操作数个数
操作符可以按期望的操作数个数分类:
- 二元操作符:
+
、-
等。 - 一元操作符:如
-x
。 - 三元操作符:
?:
。
7.2 操作数与结果类型
操作符通常会按照需要转换操作数的类型:
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/0
为 NaN
。
8.1 + 操作符
+
操作符的行为如下:
- 如果有一个操作数是对象,则使用无偏好算法将该操作数转为原始值。
- 完成对象到原始值的转换后,如果有操作数是字符串,则都转为字符串并拼接。
- 否则,两个操作数都被转为数值(或
NaN
)并相加。
8.2 一元算术操作符
+
:一元加,将操作数转为数值或NaN
,不能用于BigInt
。-
:一元减,将操作数转为数值,并改变符号。++
:递增,操作数必须是左值(变量、对象属性、数组元素,即能合法出现在赋值操作符左侧的表达式);不能进行字符串拼接,始终会将操作数转为数值。--
:递减,与递增操作符类似,也期待左值操作数。
8.3 位操作符
JavaScript 中位操作符期待整数操作数,并当成 32 位整数处理,必要时会强制转换为 32 位整数,NaN
、Infinity
、-Infinity
会转为 0。
&
:按位与。|
:按位或。^
:按位异或,同为 0,异为 1。~
:按位取反。<<
:左移,低位补 0,左移一位相当于乘 2,移两位乘 4。>>
:有符号右移,正值高位补 0,负值高位补 1。>>>
:零填充右移,不管正负,高位均补 0,这是唯一不能用于BigInt
的位操作符。
9. 关系表达式
9.1 相等和不相等操作符
==
是相等操作符,===
是严格相等操作符,!=
和 !==
刚好相反。
实践中判断相等应坚持使用 ===
而不是 ==
。
严格相等判定规则:
- 两个值类型不同,则不相等。
- 两个值都是
null
或都是undefined
才相等。 - 两个值都是布尔值的
true
或false
才相等。 - 如果有一个值为
NaN
,则不相等。 - 0 和 -0 不相等。
- 两个值都是字符串且相同位置包含完全相同的 16 位值,则相等。JavaScript 不会主动进行 Unicode 归一化,因此可能有的字符串看起来一样但不相等。
- 两个值引用同一个对象、数组或函数,则相等。不是同一引用则不相等。
基于类型转换的相等:
null
与undefined
相等。- 一个是数值,一个是字符串,则把字符串转为数值再比较。
- 若有一个值为
true
则转为 1,false
转为 0,再比较。 - 如果一个值是对象,另一个值是数值或字符串,则将对象用无偏好算法转为原始值再比较。
9.2 比较操作符
比较操作符转换规则:
- 如果有操作数值为对象,则按无偏好算法转为原始值再比较。
- 如果两个都为字符串,则使用字母表顺序(Unicode 16位值的数值顺序)比较两个字符串。
- 如果不都为字符串,则将原始值转为数值进行比较。
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 的值要么是“真值”,要么是“假值”。
- 假值有
false
、null
、undefined
、0
、-0
、NaN
和""
,其它都为真值。
10.1 逻辑与
&&
执行短路操作,如果左侧为假,不会计算右侧值直接返回 false
。
10.2 逻辑或
||
同样执行短路操作,左侧值为真,则不会计算右侧值。
10.3 逻辑非
想要取得任何值的布尔值表示,只要应用这个操作符两次即可:!!x
。
11. 赋值表达式
- 赋值操作符期待左值是一个变量,对象属性或数组元素,右值是任意类型的任意值。
- 赋值表达式的值是右侧操作数的值。
- 赋值表达式具有右结合性,一个表达式中出现多个赋值操作,会从右向左求值。
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 包含副效应,二者会有区别:
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。 - 如果被求值的字符串内使用
let
或const
,则声明的变量或常量会被限制在求值的局部作用域内,不会定义到调用环境。 - 传给 eval 的字符串必须从语法上说得通,只要这个字符串本身可以作为独立的脚本运行,就可以合法地传给 eval,否则会报错,如
eval('return;')
就会报错。
12.2 全局 eval()
- 可以给
eval
起别名,如果通过别名调用eval
叫做间接调用,否则叫直接调用。 - 间接调用时将字符串当作顶级全局代码来求值,被求值的代码可能定义新的顶级变量和顶级函数,可能修改顶级变量,但不会再使用或修改调用函数内的局部变量。
- 直接调用
eval
使用的是调用上下文的变量环境,任何其它调用方式,都是使用全局对象(globalThis)作为变量环境。 - 无论直接调用还是间接调用,都只在通过
var
定义新变量时才能修改调用或全局环境,通过let
或const
定义的被限制在求值局部作用域内。 eval
全局求值的能力是一种有用的特性,如果必须使用eval
,那很可能就是为了使用它的全局求值。
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 先定义(??)
- 先定义操作符
??
求值其先定义的操作数,如果左边操作数不是null
或undefined
,则返回左值,否则返回右值,执行短路操作。 ??
是对||
的有效替代,特别对于 0、空字符串或者 false 的情况,??
会认为有定义并直接返回。
13.3 typeof 操作符
typeof
是一元操作符,操作数可以是任意类型,返回字符串。- 如果操作数是
null
,typeof
结果是 "object"。 - 对除函数外的所有对象或数组,
typeof
求值结果都是 "object"。
x | typeof x |
---|---|
undefined | "undefined" |
null | "object" |
true or false | "boolean" |
任意数值或 NaN | "number" |
任意 Bigint | "biting" |
任意字符串 | "string" |
任意符号 | "symbol" |
任意函数 | "function" |
任意非函数对象 | "object" |
13.4 delete 操作符
delete
是一元操作符,尝试删除对象属性或数组元素。期待左值,对非左值不做任何操作,但返回true
。特别的,数组元素删除后就是稀疏数组。- 在严格模式下,
delete
操作符只能作用于属性访问表达式,删除变量、函数、函数参数或不可删除属性就会报错,非严格模式下不报错,但返回false
,表示不可删除。
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
。
let counter = 0
const increment = () => void counter++
increment() // undefined
counter // 1
13.7 逗号操作符
- 逗号操作符是二元操作符,操作数可以是任意类型。
- 该操作符会求值左右操作数,但只返回右操作数的值,也意味着只有左操作数有副效应时才有必要使用逗号操作符。
- 常见使用场景就是有多个循环变量的
for
循环。
for (let i = 0, j = 10; i < j; i++, j--) {
console.log(i + j)
}
14. 小结
- 任何表达式都可以求值为 JavaScript 中的一个值。
+
操作符既可以用于数值加法,也可以用于字符串拼接。- 逻辑操作符
&&
和||
具有短路行为。