运算符优先级
运算符优先级
参考:运算符优先级(MDN)
优先级和结合性
示例如下,其中,OP1
和 OP2
都是操作符的占位符。
a OP1 b OP2 c
;
如果 OP1
和 OP2
具有不同的优先级(具体可以参考下一个章节 运算符优先级汇总表)。
console.log(3 + 10 * 2); // 输出 23console.log(3 + 10 * 2); // 输出 23 因为这里的括号是多余的console.log((3 + 10) * 2); // 输出 26 因为括号改变了优先级
那什么是结合性呢?
左结合(左到右)相当于把左边的子表达式加上小括号 (a OP b) OP c
,右结合(右到左)相当于 a OP (b OP c)
。赋值运算符是右结合的,所以你可以这么写:
a = b = 5; // 相当于 a = (b = 5);
预期结果是 a
和 b
的值都会成为 5
。这是因为赋值运算符的返回结果就是赋值运算符右边的那个值。
具体过程是:首先 b
被赋值为 5
,然后 a
也被赋值为 b = 5
的返回值,也就是 5
。
当多个运算符优先级相等时,这个时候就可以通过结合性来判断表达式的执行顺序了。
比如:
6 / 3 / 2
与 (6 / 3) / 2
是相同的,因为除法是左结合(左到右)的。而幂运算符是右结合(右到左)的,所以 2 ** 3 ** 2
与 2 ** (3 ** 2)
是相同的。
因此,(2 ** 3) ** 2
会更改执行顺序,它的执行结果也和原表达式2 ** 3 ** 2
不同。前者是8 ** 2 === 64
,后者是2 ** 9 === 512
。
判断表达式的执行顺序还有一个规则,那就是运算符的优先级在结合性之前,比如:
求幂会先于除法,2 ** 3 / 3 ** 2
的结果是 0.8888888888888888
,因为它相当于 (2 ** 3) / (3 ** 2)
。
运算符优先级汇总表
下面的表格将所有运算符按照优先级的不同从高(19
)到低(1
)排列。
请注意,下表中故意不包含展开语法(Spread syntax) —— 原因可以引用Stack Overflow 上的一个回答,“展开语法不是一个运算符,因此没有优先级。它是数组字面量和函数调用(和对象字面量)语法的一部分。”
优先级 | 运算符类型 | 结合性 | 运算符 |
---|---|---|---|
19 | 分组 | n/a(不相关) | ( … ) |
18 | 成员访问 | 从左到右 | … . … |
需计算的成员访问 | 从左到右 | … [ … ] |
|
new (带参数列表) |
n/a | new … ( … ) |
|
函数调用 | 从左到右 | … ( … ) |
|
可选链(Optional chaining) | 从左到右 | ?. |
|
17 | new (无参数列表) |
从右到左 | new … |
16 | 后置递增 | n/a | … ++ |
后置递减 | … -- |
||
15 | 逻辑非 (!) | 从右到左 | ! … |
按位非 (~) | ~ … |
||
一元加法 (+) | + … |
||
一元减法 (-) | - … |
||
前置递增 | ++ … |
||
前置递减 | -- … |
||
typeof |
typeof … |
||
void |
void … |
||
delete |
delete … |
||
await |
await … |
||
14 | 幂 (**) | 从右到左 | … ** … |
13 | 乘法 (*) | 从左到右 | … * … |
除法 (/) | … / … |
||
取余 (%) | … % … |
||
12 | 加法 (+) | 从左到右 | … + … |
减法 (-) | … - … |
||
11 | 按位左移 (<<) | 从左到右 | … << … |
按位右移 (>>) | … >> … |
||
无符号右移 (>>>) | … >>> … |
||
10 | 小于 (<) | 从左到右 | … < … |
小于等于 (<=) | … <= … |
||
大于 (>) | … > … |
||
大于等于 (>=) | … >= … |
||
in |
… in … |
||
instanceof |
… instanceof … |
||
9 | 相等 (==) | 从左到右 | … == … |
不相等 (!=) | … != … |
||
一致/严格相等 (===) | … === … |
||
不一致/严格不相等 (!==) | … !== … |
||
8 | 按位与 (&) | 从左到右 | … & … |
7 | 按位异或 (^) | 从左到右 | … ^ … |
6 | 按位或 (|) | 从左到右 | … | … |
5 | 逻辑与 (&&) | 从左到右 | … && … |
4 | 逻辑或 (||) | 从左到右 | … || … |
空值合并 (??) | 从左到右 | … ?? … |
|
3 | 条件(三元)运算符 | 从右到左 | … ? … : … |
2 | 赋值 | 从右到左 | … = … |
… += … |
|||
… -= … |
|||
… **= … |
|||
… *= … |
|||
… /= … |
|||
… %= … |
|||
… <<= … |
|||
… >>= … |
|||
… >>>= … |
|||
… &= … |
|||
… ^= … |
|||
… |= … |
|||
… &&= … |
|||
… ||= … |
|||
… ??= … |
|||
1 | 逗号 / 序列 | 从左到右 | … , … |
new 带参数列表和不带参数列表
根据运算符优先级汇总表我们得知,new ...(...)
带参数列表,比new ...
不带参数的列表的优先级更高,这两者到底如何区分呢?
function Person() {}// new ... 不带参数列表const person1 = new Person();// new ...(...) 带参数列表const person2 = new Person();
Person.prototype.run = function () {};
// new ... 不带参数列表new Person.run(); // --> new (Person.run)
// new ...(...) 带参数列表new Person.run(); // --> (new (Person.run)()) 这里要注意的是 run 后面的括号不能看作函数调用,而要看作 new ... (...) 带参数
因此这里我们总结一下,什么叫new ...(...)
带参数列表呢?
只要new
后面的表达式有一个括号()
,那么这里就能看作new ...(...)
带参数列表。
如果new
后面的表达式没有括号()
,那么这里就能看作new ...
不带参数列表。
其中带参数列表的优先级比不带参数列表的优先级更高。
之前看过别的文章,有的说new Person.run()
应该看作new ...
不带参数列表,因此按照运算符的优先级划分,'new ...
不带参数列表的优先级比 ... . ...
访问对象属性, ...(...)
函数调用都要低。
因此上述表达式等同于:new ((Person.run)())
。
上述表达式执行顺序会划分为如下三步:
const run = Person.run
;const res = run()
;new res --> new undefined
;
这里浏览器的报错就会变为:Uncaught TypeError: undefined is not a constructor
。
但是你去实际运行一下new Person.run()
就会发现实际报错是:
Uncaught TypeError: Person.run is not a constructor
。
这里我们反推一下执行顺序就能知道这里的new
应该看作new ...(...)
带参数列表。
参考运算符优先级对照表得知,new ...(...)
带参数列表的优先级和 ... . ...
访问对象属性, ...(...)
函数调用相同,那么这里就需要判断结合性。
首先得知new ...(...)
带参数的结合性是n\a
,也就是直接将其当作一个整体即可:
new Person.run() --> (new Person.run())
;
也就是说这里最后的括号不能看作...(...)
函数调用,而应该看作new ...(...)
带参数。
然后... . ...
访问对象属性的结合性是从左到右,因此可以看作:
(new Person.run()) --> (new (Person.run)())
;
最终我们推测出该表达式的执行顺序为:
const run = Person.run
;new run()
因此报错就为Uncaught TypeError: Person.run is not a constructor
。