在前端的日常开发工作中,除了 cv 大法外,最常用的应该就是对于后端数据根据业务需要进行处理了,处理中充斥着各种操作符,比如三元(?...:...)(4
),逻辑与或(&& ||)(7
/6
),计算(+-*/)(14
/15
)...
当一个处理过于复杂,需要许多运算组合起来时,常规开发者会基于易读性而选择分段写;非要写在一行的时候,也会尽量选择用括号(21
)来区分每一个部分。这除了增加易读性以外,最大的作用就是防止js编译时操作符的处理顺序与我们想要的有冲突,也就是运算符优先级,一个你可能未曾在意过的小知识(比如上面括号中的数字就是运算符对应的优先级
)。
下面让我们来感受下操作符优先级的有趣之处
在 js 规范中,包括当前最新的可选链(?.),目前的运算符优先级已有22级(0-21),以下我将重点聊聊常用的运算符以及关于他们的有意思的问题。
1 运算语句是怎么执行的
对于每个 statement ,可以将其中的元素分为两类,操作对象与操作符。当扫描执行每个 statement 时,会从左至右依次将操作对象(o)以及操作符(c)分别放入两个栈中,简称为对象栈(s1)和操作栈(s2),并且遵循以下规则:
- 1 如果遇到 o ,则将 o 放入 s1 ;
- 2 如果遇到 c ,则需要和 s2 栈顶的 c2 进行优先级比较:
- 2.1 如果 c > c2 ,则将 c 放入 s2 ;
- 2.2 如果 c <= c2 ,则将 c2 与 s1 栈顶的两个元素一同退栈,计算结束后,将结果放入 s1 ,c 放入 s2(此动作相当于再次执行步骤2);
例1: a = 1 + 2 * (3 - 4) / 5;
- 有很多操作符有着自己的特性,比如上面的
()
,虽然-
的优先级小于()
,但是当-
入栈时并没有直接运算优先级更高的()
。
这是(
的特性:在他之后的操作符会直接入栈,不做判断,用以实现()
本来想要实现的功能。
像这样有自己特性的操作符还有很多,并不是所有操作符都会完全遵循以上规范。
2 最高的优先级与最低的优先级是谁
优先级最高的操作符是谁,显而易见,是不清楚优先级时就会用到的括号 ()
,优先级高达21
。
例2.1:(1 + 2) * 3 ==> 3 * 3 ==> 9
高达21的优先级,显然不止是为了在四则运算这种简单的场景下来使用,而是为了对于高优先级的操作符也能起到作用。
例2.2:new (ClassA || ClassB).toString() ==> new ClassA.toString()
- 在超高优先级的 new 与 . 操作符下,也能使低优先级的 || 先执行
而对应的,低达0
优先级的操作符,则是最不起眼的逗号 ,
,作为用来间隔语句的操作符,你不会希望他还会误入你的处理中去的。
3 一人之下万人之上的操作符以及关联性
除去括号外,还有一批优先级为20
的操作符,其中最重要的当属成员访问
(a.b
, a['b']
, a?.b
)以及函数调用
(func(a, b)
)。
他们都是与两个或多个操作对象参与计算的,又该怎么决定多个操作对象是如何围绕操作符执行的呢?
这就要引申出操作符与操作对象怎样执行的概念——关联性了。
无关联
('()', '++', '--', '...'):此类操作符不会对左右内容进行关联,只会执行完自身后返回结果。从左至右
(成员访问,函数调用等):此类操作符会从自身的左侧开始计算。从右至左
(各类赋值等):与从左至右相反。
4 一符多用的操作符
并不是每个操作符的优先级是唯一的,有些操作符会因使用的场景而导致优先级发生变化。
4.1 new
关键字 new
是大家很熟悉的一个了,不仅是面向对象的原因,还有那道最常见的面试题(关键字 new 分别做了几件事),然而 new 的优先级却有两个(20
/19
),并且其原因与某个重要的操作符相关。
回到上面的例2.2: new ClassA.toString()
,他的下一步将先执行哪个呢('new' or '.' or '()')?
最后的顺序当然是先指向 ClassA.toString ,然后创建这个类函数的实例。
虽然上述的顺序和我们预期的一样,但三个操作符的优先级都是20
,按照关联性应该是新建 ClassA 的实例,然后调用实例上的 toString 方法才对,为什么会先执行成员访问呢?
因为 js 规范分别为 new 设计了两种优先级:
new(带参数列表)
:new ... (...),优先级为20new(无参数列表)
: new ...,优先级为19
以此方法来规避 new 和成员访问造成的各种冲突
4.2 递增&递减
递增递减操作大家很熟悉,并且其分为前置后置两种,但是这两种的优先级又是多少,谁高谁低呢?
可能根据以下例子观察:
var a = 1;
console.log(1 + ++a); // 输出3
var b = 1;
console.log(1 + b++); // 输出2
前置的优先级貌似更高,但是实际上却是后置优先级18
,前置与逻辑非('!'), typeof 等操作符一样优先级17
,如何证明呢?
由于递增递减只能作用在变量上,所以可以利用 ++a++ 的报错来判断他们的优先级,由下图可见是后置优先级更高。
可是为什么在运算中,明明后置优先级那么高,但是运算时却没有体现出来呢?
原来是因为后置时,操作符将 "a++" 替换成 a 原来的值,而不再是 a 的引用,然后对 a 进行递增。
5 逻辑运算
在数据处理中躲不掉的一定是逻辑运算符,当你厌倦了整篇的 if-else
, while
, switch
时,他就是条件语句的替代,但是逻辑运算的优先级也有不同。
17
: 逻辑非(!
)
12
: 小于(<
),小于等于(<=
),大于(>
),大于等于(>=
)
11
: 等于(==
),不等于(!=
),全等于(===
),不全等于(!==
)
7
: 逻辑与(&&
)
6
: 逻辑或(||
)
4
: 条件语句(... ? ... : ...
)
知道这些操作符的优先级后,你应该能轻松的答出以下语句的输出结果了(数字计算的操作符优先级在14
-16
之间)。
// 例5
alert(1 + 2 === 3 ? 4 && 0 || 5 : 0 === 6); // 输出?
6 关于优先级的有趣题目
6.1 请写出下列输出结果
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a); // ?
console.log(b); // ?
console.log(a.x); // ?
- 如果第三行换成
a = a.x = {n: 2};
答案又是什么呢?
6.2 请写出下列输出结果
function Foo() {
getName = function () {
console.log(1);
};
return this;
}
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}
Foo.getName(); // ?
getName(); // ?
Foo().getName(); // ?
getName(); // ?
new Foo.getName(); // ?
new Foo().getName(); // ?
new new Foo().getName(); // ?
小结
操作符是编程语言极重要组成部分,了解操作符不意味着一定要运用其中的一些规则去完成自我认同的奇技淫巧。
了解它,你也许会在复杂的逻辑判断时更好地组织语句,也许会在遇到奇怪的语法错误时准确地抓住重点,无论如何,感谢你能读到这里。
备注
- 计算,赋值,位运算等其他的操作符未提及,可查看优先级完整表