阿瑞斯的BLOG

JavaScript运算符

关键词
表达式 优先级 结合性 执行顺序 求值顺序

JavaScript运算符

也叫操作符,英文:operator,关于运算符,除了都知道的优先级,还要知道Associativity

JavaScript中的运算符大多由标点符号表示,少数由关键字表示,每一个运算符具有不同的行为,运算符始终都遵循着一些固定语法。一些运算符可以作用于任何数据类型, 但仍然希望它们的操作数是指定类型的数据,并且大多数运算符返回一个特定类型的值,在下面的运算符规则表中,箭头前为运算符操作数的类型,箭头后为运算结果的类型

分类

JavaScript的运算符总共有59个,如果根据其操作数的个数进行分类,分为一元运算符,二元运算符,三元运算符。而它们的目的都是一致的: 将一个到多个表达式合并为一个表达式

优先级和结合性

运算符优先级控制着运算符的执行顺序,优先级高的运算符的执行总是先于优先级运算符低的运算符——————错误的理解

修正:
不同操作相邻出现的时候,表达式是作为左操作符的后操作数呢?还是作为右操作符的前操作数呢?=>这就是操作符优先级干的事

对优先级错误的理解:

认为拿到一行语句,先按照操作符的优先级划分,找到最高优先级的操作符,先求值。

所以就对 baz&& fn()这样的代码存在困惑,因为如果这么理解的话,fn()的优先级最高,那么无论如何都会先执行 fn()再去 && 判断,可实际的是 &&判断前面的baz是否存在而决定是否执行后面的fn(),此时发生冲突。但实际上所谓的优先级是指操作数结合操作符,优先结合。即同一个操作数被左右两个操作符共同使用的时候,此时这个操作数并不会被两个操作符共同使用,而决定它被谁使用的依据就是操作符的优先级。不是说谁的优先级高就先给谁求值。

简单一点的说,所谓的优先级就是加个括号,而优先级和结合性就是决定括号怎么加

那么何时求值?比如 d = a + (b+c)

对表达式d求值,首先对表达式a求值,返回;然后要对 + 后的一个表达式求值,此时 + 后面的表达式结果依赖于b + c,此时b + c才开始求值,因为最终的结果依赖b + c 的结果,所以才有了求值。

所以像短路运算符 &&|| 一起出现的时候,因为&&的优先级要比||高,所以&&优先结合操作数(但是并不意味着要立即求值,还是要根据从左往右的顺序,先对第一个操作数求值。剩下的,有依赖有需求才会求值。)

所以千万不要再理解成优先级高,优先求值

优先级是怎么产生的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1
var a =42, b;
b = (a++, a);
a; //43
b; //43
如果去掉括号会怎么样?
var a=42, b;
b = a++, a;
a; //43
b; //42

【1】注意: b的结果不一样了,上面代码发生了什么?说明了什么?

原因就在于,运算符的优先级比=低,当不同操作相邻(, =)出现时,表达式a++是作为=的后操作数呢?还是作为,的前操作数呢?

=>操作符的优先级工作了

=>因为=>的优先级高于,

=>原式<=>(b = a++), a;

=>此时a++后自增,先返回值,所以表达式b的值是42

1
2
3
4
简单的例2:
fn01() || fn02 && fn03();
//等价于
fn01() || (fn02 && fn03()); // && 优先级高于 || fn02结合&&
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
例3:
var d = a && b || c ? c || b ? a : c && b : a;
分析:
1.先解决优先级问题
=>哪里发生了优先级问题? a b c c b c b a (看操作数两边的符号,除了第一个操作数和最后一个操作数以及三元表达式 ? a :都会发生优先级问题)
2.再来一遍,a发生优先级问题,看表达式a的左边和右边,&& 高于 =
=> a 作为&&的前操作数
3.再看表达式b,b发生优先级问题,看表达式b的左边和右边,&& 高于 ||
=> b 作为&&的后操作数
=>var d = (a && b) || c ? c || b ? a : c && b : a;
//...重复以上步骤,得出以下结果
=>var d = ((a && b) || c) ? (c || b) ? a : (c && b) : a;
做到这一步无法往下做了,可以看成
e = ((a && b) || c)
f = (c || b)
g = (c && b)
=> e ? f ? a : g : a;
此时仅有优先级已经无法满足求值的需求了。
--------------- ----------------

Associativity(结合性)

如果所有操作符优先级一样,结合性决定了执行结果的唯一性。

结合性是怎么起作用的?

当表达式相邻的操作符具有相同的优先级时,优先级已经不能决定表达式最终结果的时候,结合性就起作用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
回到之前例子没有解决的问题:
=>var d = ((a && b) || c) ? (c || b) ? a : (c && b) : a;
e = ((a && b) || c)
f = (c || b)
g = (c && b)
=> e ? f ? a : g : a;
这里没有可能性二
//可能一
e ? (f ? a : g) : a;
true --- a
/
true --- (f ? a : g)
/ \false --- g
e
\
false --- a

结合性解决了什么问题?

当优先级一样的时候括号怎么加

1
a OP b OP c

左结合性(从左到右)意味着它被处理为(a OP b) OP c,而右结合性(从右到左)意味着它被解释为a OP (b OP c)。而对于以下代码右结合性是正确的,所以你可以写:

1
2
3
4
5
6
7
8
9
10
11
a = b = 5;
分析:
对于表达式b,左边和右边是相同的操作符,优先级一样的。所以优先级已经无法解决这种情况下的执行结果了。
显然,如果只有优先级的话,
a = b = 5;
//可能性1
(a = b) = 5;
//可能性2
a = (b = 5);
但是由于(a = b) = 5中,(a = b)的结果并不是一个变量,而是一个返回表达式a赋值后的结果2,因此它不能出现在等号的左边!所以以上最终的执行结果只有可能是可能性2

那么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
a?b:c?d:e
//可能性一
(a?b:c)?d:e
true --- d
/
true --- b
/ \
a false --- e
true --- d
/
\false --- c
\
false ---e
=> a 执行结果 d || e
//可能性二(因为三元表达式右结合)
a?b:(c?d:e)
true --- b
/
a
true --- d
\ /
false --- (c?d:e)
\
false ---e
=> a 执行结果 b || d || e
结果不唯一了,所以结合性干的事就是确保结果的唯一性。

这个要怎么算?先看优先级,两个一样。再看结合性,右结合,所以:
先算c?d:e
再算 a?b:(c?d:e) 这就是所谓右结合。如果是左结合的话 就是先算a?b:c
再算 (a?b:c)?d:e
实际上,一般结合性的问题都可以用括号来解决。

先要考虑优先级,有相同优先级的就看结合性以决定括号的添加方式。结合性决定处理相同优先级的运算符的顺序

或者说为什么结合性不同影响着执行结果???

因为表达式可能会产生副作用,比如函数调用:

1
var a = foo() && bar();

这里foo()会首先执行,它的返回结果决定了bar()是否执行。所以如果bar()在foo()之前执行,整个结果完全不同。

如何更好理解操作符的结合性???

想象一下,什么情况下 AB 不等于 BA

矩阵(不考虑单位矩阵的情况下)

执行顺序

执行顺序是不变的,总是从左到右。优先级和结合性并没有影响代码的执行顺序,也无法影响,它们做的是改变了表达式的求值顺序,注意 执行顺序 != 求值顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var a=1;
b=(a=3)+a++;
这个例子中运算顺序是这样的
1.计算b
2.a=3
3.a++(设为c)
4.计算a(这时候a变成了4已经,不是再最后才变得,但表达式使用的是a++的结果c,也就是a原来的值)
5.计算3+c
6.把3+c赋值给b
=
/ \
b +
/ \
= ++
/ \ /
a 3 a

深入浅出优先级,结合性,执行顺序,求值顺序

代码执行顺序是永远不变的,从左往右执行。

而优先级和结合性是如何添加括号的依据,表达式越先求值的说明离AST根节点越远,为什么?先求值的说明依赖少,有依赖的还得继续向下遍历。

什么是左值(Left-hand-side expressions)

Left values are the destination of an assignment.

左值指的是赋值操作的目标,也就是能出现在赋值表达式左边的表达式,自定义的函数不能返回左值是ECMAScript的规范。

那么什么类型的值可以出现在赋值表达式的左侧呢?

变量
对象属性
数组元素

运算符总结(持续更新)












































































































































































































































































































































































































































优先级 运算符 操作 结合性 类型
20 ( … ) 分组 N/A N/A
19 … . … 成员访问 l-to-r
19 … [ … ] 成员访问 l-to-r N/A
19 new … ( … ) 有参构造 N/A N/A
18 … ( … ) 函数调用 l-to-r N/A
18 new … new.target操作符检测是否用new操作符调用函数/构造函数 r-to-l new.target=>undefined/
在类构造函数中,new.target指的是直接调用的构造函数new。
17 … ++ 后自增 N/A N/A
17 … – 后自减 N/A N/A
16 ! … 逻辑非 r-to-l bool->bool
16 ~ … 按位求反 r-to-l
16 + … 一元加(注意区别于二元加) r-to-l any=>num
16 - … 一元减(注意区别于二元减) r-to-l any=>num
16 ++ … 先自增 r-to-l 属于算术运算符,操作lval
16 – … 先自减 r-to-l 属于算术运算符,操作lval
16 typeof … 判断类型 r-to-l any=>str
16 void … 返回空 r-to-l any=>undefined
16 delete … 删除属性 r-to-l lval->bool
15 幂运算 r-to-l any,any=>num,求幂前会发生类型转换num
14 … * … 求积 l-to-r any,any=>num,求积前会发生类型转换num
14 … / … 求商 l-to-r any,any=>num,求商前会发生类型转换num
14 … % … 取余 l-to-r any,any=>num,求商前会发生类型转换num
13 … + … 求和/拼接(考虑类型转换) l-to-r any,any=>any
13 … - … 求差(考虑隐式转换) l-to-r any,any=>num
12 … << … 按位左移 l-to-r
12 … >> … 按位右移 l-to-r
12 … >>> … 无符号右移 l-to-r
11 … < … 小于(会发生隐式转换) l-to-r any,any=>bool
11 … <= … 小于等于(会发生隐式转换) l-to-r any,any=>bool
11 … > … 大于(会发生隐式转换) l-to-r any,any=>bool
11 … >= … 大于等于(会发生隐式转换) l-to-r any,any=>bool
11 … in … 测试属性 l-to-r str,obj=>bool
11 … instanceof … 测试对象类(含原型链) l-to-r obj,fn=>bool
10 … == … 测试相等 l-to-r any,any=>bool
10 … != … 测试不等 l-to-r any,any=>bool
10 … === … 测试严等 l-to-r any,any=>bool
10 … !== … 测试严不等 l-to-r any,any=>bool
9 … & … 按位与 l-to-r
8 … ^ … 按位非 l-to-r
7 … | … 按位或 l-to-r
6 … && … 逻辑与(操作数选择器) l-to-r any,any=>any
5 … || … 逻辑或(操作数选择器) l-to-r any,any=>any
4 … ? … : … 三元表达式 r-to-l bool,any,any=>any
3 … = … 运算且赋值 r-to-l lval,any=>any
3 … += … 运算且赋值 r-to-l lval,any=>any
3 … -= … 运算且赋值 r-to-l lval,any=>any
3 … = … 运算且赋值 r-to-l lval,any=>any
3 = … 运算且赋值 r-to-l lval,any=>any
3 … /= … 运算且赋值 r-to-l lval,any=>any
3 … %= … 运算且赋值 r-to-l lval,any=>any
3 … <<= … 运算且赋值 r-to-l lval,any=>any
3 … >>= … 运算且赋值 r-to-l lval,any=>any
3 … >>>= … 运算且赋值 r-to-l lval,any=>any
3 … &= … 运算且赋值 r-to-l lval,any=>any
3 … ^= … 运算且赋值 r-to-l lval,any=>any
3 … |= … 运算且赋值 r-to-l lval,any=>any
2 yield … 暂停当前gen函数执行,返回表达式的值给gen函数的调用者 r-to-l any=>any
2 yield … 用于在一个gen函数中调用另一个gen函数 r-to-l AnoFn*()=>any
1 … … 展开运算符 r-to-l array=>any
0 , … 忽略第一个操作数,返回第二个操作数 r-to-l any,any=>any

注:

l-to-r : left-to-right 左结合

r-to-l : right-to-left 右结合

lval : leftValue 左值

in右侧必须是一个对象。例如,您可以指定使用String构造函数创建的字符串,但不能指定字符串文字。in操作符会检测原型链上的属性。使用delete操作符删除属性,则操作in员返回false该属性。如果将属性设置为undefined但不将其删除,则in该属性的操作符返回true。

instanceof操作符会检测原型链。

关于–,++操作符报错“ReferenceError: Invalid left-hand side expression in postfix operation” ,这里报的是引用错误,其实想想应该可以这么理解因为++ 和 –操作符不能直接操作值类型,因为不管是先自增/自减。还是后自增/自减,总要返回值。那么值往哪返?->肯定需要一个容器,这个容器就是左值.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ++ --操作符对操作数有要求吗?是不是只能操作左值?
//Invalid left-hand side expression in postfix operation
var str = 'abc';
console.log(str++); // NaN 说明可以操作字符串
//console.log(5++);//ReferenceError: Invalid left-hand side expression in postfix operation 说明不能直接操作值
//引用错误:出现这个的原因是因为++ 和 --操作符不能直接操作值类型,因为不管是先自增/自减。还是后自增/自减,总要返回值。那么值往哪放?->需要一个容器 (因为值是不可变的)
var o = {};
console.log(o++); // NaN 说明可以操作对象
var b;
console.log(b++); //NaN 说明操作数对于undefined也可以接受
var obj = null;
console.log(obj++); //0 ???why 为什么null会转换成0?
var boolF = false;
console.log(boolF++); //0 ???why
var boolT = true;
console.log(boolT++); //1 ???why

操作符对数据做了什么???

关注类型转换

参考来源:

知乎:JavaScript中运算符优先级的问题?@RednaxelaFX

解释器,树遍历解释器,表达式的求值顺序@RednaxelaFX

JavaScript yield*操作符

JavaScript 展开运算符

JavaScript剩余参数