ECMA-262描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。ECMAScript中的操作符是独特的,因为它们可用于各种值,包括字符串、数值、布尔值,甚至还有对象。在应用给对象时,操作符通常会调用valueOf()和/或toString()方法来取得可以计算的值。
1 一元操作符
只操作一个值的操作符叫一元操作符(unary operator)。一元操作符是ECMAScript中最简单的操作符。
1.1 递增/递减操作符
有两个版本:前缀版和后缀版。顾名思义,前缀版就是位于要操作的变量前头,后缀版就是位于要操作的变量后头。
前++/前--
let age = 29;
++age; // 30
// 等价于
age = aga +1;
无论使用前缀递增还是前缀递减操作符,变量的值都会在语句被求值之前改变。
let age = 30;
let anotherAge = --age + 2;
console.log(age) // 29
console.log(anotherAge) //31
变量anotherAge以age减1后的值再加2进行初始化。因为递减操作先发生,所以age的值先变成29,然后再加2,结果是31。
后++/后--
后缀版递增和递减在语句被求值后才发生。在某些情况下,这种差异没什么影响:
let age = 29;
age++; // 30
// 等价于
age = aga +1;
可是,在跟其他操作混合时,差异就会变明显
let age = 30;
let anotherAge = age-- + 2;
console.log(age) // 29
console.log(anotherAge) //32
变量anotherAge先以age的值加2进行初始化(因为后缀版递增和递减在语句被求值后才发生),所以变量anotherAge = 30 +2 = 32
这4个操作符可以作用于任何值,不限于整数——字符串、布尔值、浮点值,甚至对象都可以。递增和递减操作符遵循如下规则:
- 对于字符串,如果字符串是一个数字形式的字符串(比如 let a = "123"),则会先转换为对应的数值,在进行对应的操作,并且操作后变量类型就变为数值型了
let a = "12";
console.log(typeof a); // string
a++;
console.log(a); // 13
// 操作后变量类型就变为数值型了
console.log(typeof a); //number
- 对于字符串,如果字符串不是一个数字形式的字符串(比如 let a = "hello"),则变量会变成NaN,并且操作后变量类型就变为数值型了
let a = "hello";
console.log(typeof a); // string
a++;
console.log(a); // NaN
// 操作后变量类型就变为数值型了
console.log(typeof a); //number
- 对于布尔值,如果是false就变成0在进行对应的操作,true就变为1进行对应的操作,并且操作后变量类型就变为数值型了
let a = true;
console.log(typeof a); // boolean
a++;
console.log(a); // 2
console.log(typeof a); //number
-
浮点值,和整形没有区别,都是数值的加减操作
-
如果是对象,就会先调用其valueOf方法取得可以操作的的值,进行操作。如果vauleOf返回是NaN,再调用其toString方法,再次尝试获取可以操作的值
2 位操作符
用于数值的底层操作,也就是操作内存中表示数据的比特(位)。ECMAScript中的所有数值都以IEEE 754 64位格式存储,但位操作并不直接应用到64位表示,而是先把值转换为32位整数,再进行位操作,之后再把结果转换为64位。
有符号整数使用32位的前31位表示整数值。第32位表示数值的符号,如0表示正,1表示负。这一位称为符号位(sign bit),它的值决定了数值其余部分的格式。
正值以真正的二进制格式存储,即31位中的每一位都代表2的幂。第一位(称为第0位)表示2的0次方,第二位表示2的1次方,依此类推。如果一个位是空的,则以0填充,相当于忽略不计。
数值18的二进制格式为00000000000000000000000000010010,或更精简的10010。后者是用到的5个有效位,决定了实际的值
负值以一种称为二补数(或补码)的二进制编码存储。一个数值的二补数通过如下3个步骤计算得到
-
确定绝对值的二进制表示(如,对于-18,先确定18的二进制表示);
-
找到数值的一补数(或反码),换句话说,就是每个0都变成1,每个1都变成0;
-
给结果加1。
eg
// 1:18的二进制32位表示
0000 0000 0000 0000 0000 0000 0001 0010
// 2 计算一补数,即反转每一位的二进制值:
1111 1111 1111 1111 1111 1111 1110 1101
// 3 最后,给一补数加1:
1111 1111 1111 1111 1111 1111 1110 1110
18的二进制表示就是11111111111111111111111111101110。要注意的是,在处理有符号整数时,我们无法访问第31位(符号位)。
在把负值输出为一个二进制字符串时,我们会得到一个前面加了减号的绝对值
let num = -18;
console.log(num.toString(2)); // -10010
默认情况下,ECMAScript中的所有整数都表示为有符号数。不过,确实存在无符号整数。对无符号整数来说,第32位不表示符号,因为只有正值。无符号整数比有符号整数的范围更大,因为符号位被用来表示数值了。
在对ECMAScript中的数值应用位操作符时,后台会发生转换:64位数值会转换为32位数值,然后执行位操作,最后再把结果从32位转换为64位存储起来。整个过程就像处理32位数值一样,这让二进制操作变得与其他语言中类似。但这个转换也导致了一个奇特的副作用,即特殊值NaN和Infinity在位操作中都会被当成0处理。
如果将位操作符应用到非数值,那么首先会使用Number()函数将该值转换为数值(这个过程是自动的),然后再应用位操作。最终结果是数值。
2.1 按位非
按位非操作符用波浪符(~)表示,它的作用是返回数值的一补数。按位非是ECMAScript中为数不多的几个二进制数学操作符之一
let num1 = 25; //二进制00000000000000000000000000011001
let num2 = ~num1; //二进制11111111111111111111111111100110
console.log(num2); // -26
按位非操作符作用到了数值25,得到的结果是-26。由此可以看出,按位非的最终效果是对数值取反并减1,就像执行如下操作的结果一样:
let num1 = 25;
let num2 = -num1 - 1;
console.log(num2); // "-26"
尽管两者返回的结果一样,但位操作的速度快得多。这是因为位操作是在数值的底层表示上完成的。
2.2 按位与
按位与操作符用和号(&)表示,有两个操作数。本质上,按位与就是将两个数的每一个位对齐,然后基于真值表中的规则,对每一位执行相应的与操作。
let result = 25 & 3;
console.log(result); // 1
25 = 0000 0000 0000 0000 0000 0000 0001 1001
3 = 0000 0000 0000 0000 0000 0000 0000 0011
---------------------------------------------
AND = 0000 0000 0000 0000 0000 0000 0000 0001
2.3 按位或
按位或操作符用管道符(|)表示,按位或操作在至少一位是1时返回1,两位都是0时返回0。
2.4 按位异或
按位异或用脱字符(^)表示, 按位异或与按位或的区别是,它只在一位上是1的时候返回1(两位都是1或0,则返回0)。
2.5 左移
左移操作符用两个小于号(<<)表示,会按照指定的位数将数值的所有位向左移动。比如,如果数值2(二进制10)向左移5位,就会得到64(二进制1000000)
let oldValue = 2; //等于二进制10
let newValue = oldValue << 5; //等于二进制1000000,即十进制64
注意
- 在移位后,数值右端会空出5位。左移会以0填充这些空位,让结果是完整的32位数值
- 左移会保留它所操作数值的符号。比如,如果-2左移5位,将得到-64,而不是正64。
2.6 有符号右移
有符号右移由两个大于号(>>)表示,会将数值的所有32位都向右移,同时保留符号(正或负)。有符号右移实际上是左移的逆运算。比如,如果将64右移5位,那就是2
同样,移位后就会出现空位。不过,右移后空位会出现在左侧,且在符号位之后。ECMAScript会用符号位的值来填充这些空位,以得到完整的数值。
2.7 无符号右移
无符号右移用3个大于号表示(>>>),会将数值的所有32位都向右移。对于正数,无符号右移与有符号右移结果相同。仍然以前面有符号右移的例子为例,64向右移动5位,会变成2
对于负数,有时候差异会非常大。与有符号右移不同,无符号右移会给空位补0,而不管符号位是什么。
- 对正数来说,这跟有符号右移效果相同。
- 但对负数来说,结果就差太多了。无符号右移操作符将负数的二进制表示当成正数的二进制表示来处理。因为负数是其绝对值的二补数,所以右移之后结果变得非常之大
比如:64的二进制表示是11111111111111111111111111000000,无符号右移却将它当成正值,也就是4 294 967 232。把这个值右移5位后,结果是00000111111111111111111111111110,即134 217 726。
let oldValue = -64; //等于二进制11111111111111111111111111000000
let newValue = oldValue >>> 5; //等于十进制134217726
3 布尔操作符
布尔操作符一共有3个:逻辑非、逻辑与和逻辑或。
3.1 逻辑非
逻辑非操作符由一个叹号(!)表示,可应用给ECMAScript中的任何值。这个操作符始终返回布尔值,无论应用到的是什么数据类型。逻辑非操作符首先将操作数转换为布尔值,然后再对其取反。
- 如果操作数是对象,返回false
- 如果操作数是空字符串,返回true
- 如果操作数是非空字符串,返回false
- 如果操作数是0,返回true
- 如果操作数是非0,返回false
- 如果操作数是Null,返回true
- 如果操作数是NaN,返回true
- 如果操作数是undefined,返回true
console.log(!false); // true
console.log(!"blue"); // false
console.log(!0); // true
console.log(!NaN); // true
console.log(!""); // true
console.log(!12345); // false
tips : 逻辑非操作符也可以用于把任意值转换为布尔值。同时使用两个叹号(!!),相当于调用了转型函数Boolean()。无论操作数是什么类型,第一个叹号总会返回布尔值。第二个叹号对该布尔值取反,从而给出变量真正对应的布尔值。结果与对同一个值使用Boolean()函数
3.2 逻辑与
逻辑与操作符由两个和号(&&)表示
逻辑与操作符可用于任何类型的操作数,不限于布尔值。如果有操作数不是布尔值,则逻辑与并不一定会返回布尔值
- 如果第一个操作数是对象,返回第二个操作数
- 如果第二个操作数是对象,则只有第一个操作数求值为true时,才返回true
- 如果第两个操作数是对象,返回第二个操作数
- 如果一个操作数是Null,返回null
- 如果一个操作数是NaN,返回NaN
- 如果一个操作数是undefined,返回undefined
逻辑与操作符是一种短路操作符,意思就是如果第一个操作数决定了结果,那么永远不会对第二个操作数求值。
对逻辑与操作符来说,如果第一个操作数是false,那么无论第二个操作数是什么值,结果也不可能等于true。
下面这个例子说明一下:逻辑与操作符是一种短路操作符
let found = true;
let result = (found && someUndeclaredVariable); //这里会出错
console.log(result); //不会执行这一行
因为someUndeclaredVariable没有事先声明,所以当逻辑与操作符对它求值时就会报错。 假如变量found的值是false,那么就不会报错了
let found = false;
let result = (found && someUndeclaredVariable); //不会出错
console.log(result); //会执行
console.log会成功执行。即使变量someUndeclaredVariable没有定义,由于第一个操作数是false,逻辑与操作符也不会对它求值,因为此时对&&右边的操作数求值是没有意义的。
3.3 逻辑或
逻辑或操作符由两个管道符(||)表示
- 如果第一个操作数是对象,返回第一个操作数
- 如果第一个操作数求值是false,返回第二个操作数
- 如果第两个操作数是对象,返回第一个操作数
- 如果一个操作数是Null,返回null
- 如果一个操作数是NaN,返回NaN
- 如果一个操作数是undefined,返回undefined
逻辑或操作符也具有短路的特性。只不过对逻辑或而言,第一个操作数求值为true,第二个操作数就不会再被求值了
let found = true;
let result = (found || someUndeclaredVariable); //不会出错
console.log(result); //会执行
变量someUndeclaredVariable也没有定义。但是,因为变量found的值为true,所以逻辑或操作符不会对变量someUndeclaredVariable求值,而直接返回true。假如把found的值改为false,那就会报错了
利用这个行为,可以避免给变量赋值null或undefined
let myObject = preferredObject || backupObject;
在这个例子中,变量myObject会被赋予两个值中的一个。其中,preferredObject变量包含首选的值,backupObject变量包含备用的值。如果preferredObject不是null,则它的值就会赋给myObject;如果preferredObject是null,则backupObject的值就会赋给myObject。
4 乘/除/取余操作符
ECMAScript定义了3个乘性操作符:乘法、除法和取模。这些操作符跟它们在Java、C语言及Perl中对应的操作符作用一样,但在处理非数值时,它们也会包含一些自动的类型转换。
4.1 乘法操作符
乘法操作符由一个星号(*)表示,可以用于计算两个数值的乘积。
- 如果操作数都是数值型,那就那按照数学的乘法计算
- 如果有任何一个数是NaN,则返回NaN
- 如果是Infinity 乘以0,则返回NaN
- 如果是Infinity 乘以非0的有限数值,则根据第二个操作数的符号返回Infinity或者-Infinity
- Infinity 乘以 Infinity,则Infinity
- 如果不是数值型,则先调用Number函数,转换为数值,然后应用上述规则
4.2 除法操作符
除法操作符由一个斜杠(/)表示,用于计算第一个操作数除以第二个操作数的商
- 如果操作数都是数值型,那就那按照数学的除法计算
- 如果有任何一个数是NaN,则返回NaN
- Infinity 除以 Infinity,则Infinity
- 0 除以 0,则Infinity
- 如果是非0的有限数值除以0,则根据第一个操作数的符号返回Infinity或者-Infinity
- 如果是Infinity值除以有限数值,则根据第二个操作数的符号返回Infinity或者-Infinity
- 如果不是数值型,则先调用Number函数,转换为数值,然后应用上述规则
4.3 取模操作符
取模(余数)操作符由一个百分比符号(%)表示
- 如果操作数都是数值型,那就那按照数学的取余计算
- 如果被除数是无限值,除数是有限值,返回NaN
- 如果被除数是有限值,除数是0,返回NaN
- 如果被除数是有限值,除数是无限值,返回被除数
- Infinity % Infinity,则返回NaN
- 如果被除数是0,除数不是0,返回0
- 如果不是数值型,则先调用Number函数,转换为数值,然后应用上述规则
5 指数操作符
ECMAScript 7新增了指数操作符,Math.pow()现在有了自己的操作符**
console.log(Math.pow(3, 2); // 9
console.log(3 ** 2); // 9
console.log(Math.pow(16, 0.5); // 4
console.log(16** 0.5); // 4
指数操作符也有自己的指数赋值操作符*=
let squared = 3;
squared **= 2;
console.log(squared); // 9
let sqrt = 16;
sqrt **= 0.5;
console.log(sqrt); // 4
6 加/减操作符
6.1 加法操作符
如果两个操作数都是数值,加法操作符执行加法运算并根据如下规则返回结果:
- 如果有一个操作数是NaN,怎返回NaN
- Infinity + Infinity,则返回Infinity
- -Infinity + -Infinity,则返回-Infinity
- -Infinity + Infinity,怎返回NaN
如果有一个操作数是字符串,则要应用如下规则:
- 如果两个都是字符串,就是字符串拼接
- 如果一个是字符串,那就将另一个转为字符串,再进行拼接
- 如果有任一操作数是对象、数值或布尔值,则调用它们的toString()方法以获取字符串,然后再应用前面的关于字符串的规则。对于undefined和null,则调用String()函数,分别获取"undefined"和"null"。
let result1 = 5 + 5; //两个数值
console.log(result1); // 10
let result2 = 5 + "5"; //一个数值和一个字符串
console.log(result2); // "55"
6.2 减法操作符
如果两个操作数都是数值,操作符执行减法运算并根据如下规则返回结果:
- 如果有一个操作数是NaN,返回NaN
- Infinity - Infinity,则返回NaN
- -Infinity - -Infinity,则返回NaN
- Infinity - -Infinity,返回Infinity
- -Infinity - Infinity,返回-Infinity
let result1 = 5 - true; // true被转换为1,所以结果是4
let result2 = NaN - 1; // NaN
let result3 = 5 - 3; // 2
let result4 = 5 - ""; // ""被转换为0,所以结果是5
let result5 = 5 - "2"; // "2"被转换为2,所以结果是3
let result6 = 5 - null; // null被转换为0,所以结果是5
7 比较操作符
执行比较两个值的操作,包括小于(<)、大于(>)、小于等于(<=)和大于等于(>=),用法跟数学课上学的一样。这几个操作符都返回布尔值
在使用关系操作符比较两个字符串时,会发生一个有趣的现象。很多人认为小于意味着“字母顺序靠前”,而大于意味着“字母顺序靠后”,实际上不是这么回事。对字符串而言,关系操作符会比较字符串中对应字符的编码,而这些编码是数值。比较完之后,会返回布尔值。问题的关键在于,大写字母的编码都小于小写字母的编码,因此以下这种情况就会发生:
let result = "Brick" < "alphabet"; // true
另一个奇怪的现象是在比较两个数值字符串的时候,比如下面这个例子:
let result = "23" < "3"; // true
这里在比较字符串"23"和"3"时返回true。因为两个操作数都是字符串,所以会逐个比较它们的字符编码(字符"2"的编码是50,而字符"3"的编码是51)。
如果有一个操作数是数值,那么比较的结果: 这次会将字符串"23"转换为数值23,然后再跟3比较
let result = "23" < 3; // false
只要是数值和字符串比较,字符串就会先被转换为数值,然后进行数值比较。对于数值字符串而言,这样能保证结果正确。但如果字符串不能转换成数值呢:
let result = "a" < 3; //因为"a"会转换为NaN,所以结果是false
因为字符"a"不能转换成任何有意义的数值,所以只能转换为NaN。这里有一个规则,即任何关系操作符在涉及比较NaN时都返回false。
在大多数比较的场景中,如果一个值不小于另一个值,那就一定大于或等于它。但在比较NaN时,无论是小于还是大于等于,比较的结果都会返回false。
let result1 = NaN < 3; // false
let result2 = NaN >= 3; // false
8 相等操作符
ECMAScript中的相等和不相等操作符,原本在比较之前会执行类型转换,但很快就有人质疑这种转换是否应该发生。最终,ECMAScript提供了两组操作符。第一组是等于和不等于,它们在比较之前执行转换。第二组是全等和不全等,它们在比较之前不执行转换。
8.1 等于和不等于
ECMAScript中的等于操作符用两个等于号(==)表示,如果操作数相等,则会返回true。不等于操作符用叹号和等于号(!=)表示,如果两个操作数不相等,则会返回true。这两个操作符都会先进行类型转换(通常称为强制类型转换)再确定操作数是否相等。
特殊:
- 当一个对象和一个其他类型比较时候,会先调用对象的valueOf方法,在进行比较
- undefined和null相等
- 只要有一个操作数是NaN,则返回false;NaN != NaN
- 如果两个都是对象比较的就是,二者的地址值
8.2 全等和不全等
全等和不全等操作符与相等和不相等操作符类似,只不过它们在比较相等时不转换操作数。全等操作符由3个等于号(===)表示,只有两个操作数在不转换的前提下相等才返回true
let result1 = ("55" == 55); // true,转换后相等
let result2 = ("55" === 55); // false,不相等,因为数据类型不同
不全等操作符用一个叹号和两个等于号(!==)表示,只有两个操作数在不转换的前提下不相等才返回true。
let result1 = ("55" != 55); // false,转换后相等
let result2 = ("55" !== 55); // true,不相等,因为数据类型不同
虽然null == undefined是true(因为这两个值类似),但null === undefined是false,因为它们不是相同的数据类型。