表达式与操作符
基本(primary)表达式
指最基本的、无法再分的表达式,包含三种:
- 字面量:数值字面量、字符串字面量等等
- 一部分保留字:true、false、null、undefined等
- 变量、常量和全局对象的引用:arr、i、global object
对象、数组初始化表达式
又称为对象字面量、数组字面量
const obj = {};
const obj2 = {
name: '',
value: 1,
};const arr = [];
const arr2 = [1, 2, , , 3];函数定义表达式
又称为函数字面量
const add = function (a, b) {
return a + b;
};属性访问表达式
包含两种形式,分别是静态的和动态的
expression.identifierexpression[expression]
形式一更加简洁,但是在需要事先知道要访问属性的标识符。形式二要访问的属性则是动态计算的,并且方括号中的表达一定会被解释为字符串
条件式属性访问
又称为可选链
expression?.identifierexpression?.[expression]
在JS中,null和undefined是唯二没有属性的值,那么如果变量为其中一个,在访问属性时会抛出TypeError,但是可选链可以防止这种错误发生
支持引用或者函数
const a = { b: null };
console.log(a.b?.c);
console.log(a.b?.['c']);
// output: undefineda.func?.(...args);WARNING
注意这里只会检测fun是否为null和undefined,并不会检查它是否是一个函数。
在访问值可能为null和undefined的变量属性时,添加可选链是一个最佳实践。
可选链表达式等价于
a.b === null || a.b === undefined ? undefined : a.b.c;可以将可选链表达式理解为一个电路通道,当短路时返回undefined,反之则正常通过(即正常访问属性)
调用表达式
指调用函数或方法的语法
fun(1);
[(1, 2, 3)].sort();调用表达式时,执行流程如下:
- 首先求值函数表达式,然后求值参数列表。如果函数表达式的值不是函数,则抛出TypeError。
- 接着按照参数列表的顺序给参数赋值
- 之后执行函数体
如果函数体中return了值,则执行结果为这个值。反之则为undefined
INFO
- 附着在其附属对象上调用时,我们称其为“方法”,例如面向对象编程时
- 直接调用则称为“函数”
WARNING
注意使用了可选链调用的函数或方法与不使用的区别如下:
根据调用表达式的执行步骤可知:当非可选链调用时,计算出函数表达式和参数列表表达式的值之后才会执行函数体,那么即使函数表达式最终计算出的值并不是函数类型,参数列表中的表达式也已经执行了
而可选链调用时,如果它的值为null和undefined则会短路掉,便不会再去执行参数列表中的表达式。
let fun = null,
x = 0;
try {
fun(x++);
} catch (e) {
console.log(x);
}
fun?.(x++);
console.log(x);
// output:
// 1
// 1实例化表达式
new Object();如果实例化时,不需要传递任何参数,则可以省略圆括号
new Object();INFO
虽然省略圆括号也可以实例化,但是最好带上圆括号保持格式统一
操作符概述


操作数个数
可以根据操作数个数进行分类:假如需要的操作数个数为n,则称该操作符为n元操作符。
例如:
- -x只有一个操作符(取x的负值),因此为一元操作符
- *是二元操作符
- ?:为三元操作符
操作数与结果类型
有些操作符适用于任何类型的值,但是多数操作符期待自己的操作数是某种特定类型,也期待结果是某种特定类型。表4-1中的类型一栏:
输入类型 -> 输出类型
例如:
num -> num 表示:期待输入类型是number类型,输出也是number类型在对操作数进行操作符对应的运算之前,首先会根据操作符期待的输入类型进行数据转换,转换规则如下:

当然也有写操作符会根据操作数的类型不同而不同,例如:+操作符(既可以拼接字符串,又可以进行数值加减)、<操作符(可以根据数值大小排序,也可以通过字符顺序排序)
// 拼接字符串
'3' + '3';
// output: '33'
// 数值加减
3 + 3;
// output: 6
// 字符串与数值
3 + '3';
// output:'33'INFO
其中lval表示左值表达式,即可以合法的出现在赋值表达式(即=)左侧的表达式。
在js中,变量、对象属性和数组元素都是左值
操作符副效应
副效应(side effect):操作符对应的运算可能影响将来的求值。例如:赋值(=)、递增(++)、递减(--)、delete操作符
其他操作符则没有负效应,但是函数调用和对象创建表达式是否有副效应,取决与函数或构造函数内部是否使用了有副效应的操作符。
优先级
4-1表格按照优先级从高到低排列,而且用横线对相同优先级的操作符进行了分组
但是操作符的优先级可以通过圆括号()改变:
(1 + 2) * 3;求值顺序
求值顺序只会在一种情况下有差异:操作符有副效应,例如递增、递减
算术操作数
包含**、*、+、-、/、%等6种基本操作符。
在必要时会将输入值转换为数值类型,再进行操作。如果无法转换,则输出NaN。而且如果操作数为NaN,结果几乎都是NaN
+操作符
二元+操作符用于计算数值操作数的和或者拼接字符串操作数
对于两个相同类型的操作数比较简单,但是对于两个不同类型的操作数一般都伴随着类型转换:
- 获取原始值
- Date调用toString方法获取原始值,其他对象调用valueOf获取原始值
- 如果没有valueOf方法,则调用toString方法获取
- 计算
如果其中一个操作数为字符串类型,那么则将另一个操作数转换为字符串类型进行拼接
- 否则两个操作数都转换为数值(或NaN),计算加法
需要注意的是当混合字符串和数值使用二元+操作符时:
1 + 2 + 'hello world'; // '3hello world'
1 + (2 + 'hello world'); // '12hello world'可以这样理解:如果二元+操作符运算时,只要在运算过程中碰到一次运算结果为string类型,则后续运算结果都是string类型
一元操作符
+、-、++、--都在必要时将自己唯一的操作符转换为数值类型
也就意味着这些一元操作符具有隐式转换,可以利用这个性质简化类型转换操作,例如:一元操作符+
const a = '1';
console.log(typeof +a); // number关系表达式
包括<、>、<=、>=、==、===
INFO
虽然比较操作符支持比较引用数据类型,但是不建议这样做
in操作符
in操作符期望左侧操作数为string或symbol,右侧操作数为对象
instanceOf操作符
instanceOf操作符期望左侧操作数为对象实例,右侧操作数为对象标识符。
它本质是基于原型链查询,对于o instanceOf f,JS则是先取得f.prototype,并在o的原型链上查找这个值。如果找到了,则返回true,反之则返回false
逻辑表达式
包含3种:逻辑与(&&)、逻辑或(||)、逻辑非(!)
- 逻辑与(&&):如果左侧操作数为假值(falsy)则短路
- 逻辑或(||):如果左侧操作数为值(falsy)则短路
- 逻辑非(!)
德摩根定律
!(p && q) === !p || !q;
!(p || q) === !p && !q;赋值操作符
使用=赋值,但是当与===或==赋值时需要注意顺序
(a = b) === 0;除了常规的赋值操作符外,JS还提供了其他赋值操作符:

多数情况下a op= b等价于a = a op b,例如
a += 1;
// 等价于
a = a + 1;但是要注意特殊情况
data[i++] *= 2;
// 等价于
data[i++] = data[i++] * 2;求值表达式
与许多解释型语言一样,JS有能力解释JS源代码字符串,并对它们求值以产生一个值。
'### eval中的this,执行上下文'
eval('2 + 3');虽然eval是一个函数,但是它看起来更像是一个表达式
如果不希望用户在控制台中输入执行eval,可以使用在HTTP头部设置Content-Security-Policy来禁用它。
eval()期望入参是一个字符串,如果:
- 入参不是字符串,则简单返回这个值
- 是字符
- 如果可以正常解析并执行,则返回最有一个表达式或语句的值
- 反之,则抛出SyntaxError
// 入参不是字符串
const ctx = { name: 'xiaoming' };
const res = eval(ctx);
console.log(res); // { name: 'xiaoming' }// 入参是字符串,并且可以正常解析
const ctx = '1 + 2';
const res = eval(ctx);
console.log(res); // 3// 入参是字符串,但是无法解析
const ctx = '1 + ';
const res = eval(ctx);
console.log(res); // SyntaxError: Unexpected end of inputeval求值时会像本地代码执行的那样去查找变量:首先在作用域找,如果找不到则去上级作用域找,例如:在函数中执行eval('a')求值,则是在当前函数作用域中查找变量a的
let a = 20;
function test() {
console.log(eval('a += 1'));
}
test();全局eval()
eval()有个特点,如果使用将它赋值给另外一个变量名称,例如geval,则它的执行上下文是全局的
const geval = eval;
let a = 10;
function test() {
let a = 20;
eval('a += 1');
// ctx;
geval('a += 1');
return a;
}
console.log(test(), a); // 21 11WARNING
注意执行该段代码时,需要新建一个html文件并放入script标签中,在浏览器控制台(可能开启了严格模式)和在node中的执行效果是不同的
根据此特性,我们可以控制eval中代码片段的执行环境
严格eval
- 不再支持重命名eval来控制
eval中代码片段的执行环境 - 不支持在局部作用域中定义新变量或函数
其他操作符
条件操作符(?:)
又称为三元表达式,可用于简化if条件判断
先定义(??)
first-defined操作符,又称为缺值合并(nullish coalescing),它是ES2020提供的新操作符,等价于
a ?? b;
// 等价于
a !== null && a !== undefined ? a : b;它可以作为||的替代用法,例如:
let max = maxWidth || preferences.maxWidth || 500;我们经常使用这种方式来检查前面值是否有值,如果有值,则取前面的值。但是如果maxWidth = 0时,我们应该取maxWidth的值,但是仍没有短路掉
如果将上述代码改成
let max = maxWidth ?? preferences.maxWidth ?? 500;这样更符合我们的需求,只去判断前值是否为null或undefined
当与||、&&同时使用时,需要使用圆括号改变优先级:
(a ?? b) || c; // 先执行??,后执行||
a ?? (b || c); // 先执行||,后执行??
a ?? b || c; // syntaxErrortypeOf
typeOf判断变量类型,注意如果它的判断结果并不准确,例如:
console.log(typeof null); // objectINFO
面试题:为什么typeOf null会判定为object?
历史遗留问题,在JS设计初期使用0000来表示变量类型,但是null全为0,所以会被判定为object
delete
delete是一元操作数,尝试删除其操作数指定的对象属性或数组元素。具有副作用(side effect)
let o = { x: 1, y: 2 };
delete o.x;
console.log('x' in o); // false
let b = [1, 2, 3];
delete b[0];
console.log(0 in b); // false
console.log(b.length); // 3如果操作数是数组,通过delete删除指定索引的元素时,数组则变成稀疏数组,可以形象表示为数组中多个坑:
[, 2, 3];在严格模式下
- 如果delete的操作数是未限定标识符,例如:变量、函数或函数参数,则抛出SyntaxError
- 删除configurable为false的属性时,抛出TypeError
'use strict';
const o = { x: 1, y: 2 };
Object.defineProperty(o, 'z', {
configurable: false,
value: 3,
});
console.log(o.z); // 3
console.log(delete o.z); // TypeError: Cannot delete property 'z' of #<Object>await
ES2017增加,用于让JS异步编程更加自然,并且只能出现在async标识的函数中
void
逗号操作符(,)
let a = 1,
b = 1,
c = 1;
for (let i = 0; i < 10; i++) {
console.log(i);
}
