这篇文章继续分享第三章的内容。
3.5 操作符
ECMA-262描述了一组可用于操作数据值的操作符,包括数学操作符(如加、减)、位操作符、关系操作符和相等操作符等。ECMAScript中的操作符是独特的,因为它们可用于各种值,包括字符串、数值、布尔值,甚至还有对象。在应用给对象时,操作符通常会调用 valueOf() 和 / 或 toString() 方法来取得可以计算的值。
3.5.1 一元操作符
只操作一个值的操作符叫一元操作符(unary operator)。这是ECMAScript中最简单的操作符。
1. 递增/递减操作符(++/--)
递增和递减操作符直接照搬自C语言,但有两个版本:前缀版和后缀版。顾名思义,前缀版就是位于要操作的变量前头,后缀版就是位于要操作的变量后头。
无论使用前缀递增还是前缀递减操作符,变量的值都会在语句被求值之前改变。(在计算机科学中,这通常被称为具有副作用。)(我是这么理解的,主作用是运行这个语句,运行的过程中,变量的值改变了,就是副作用)
let age = 29;
let anotherAge = --age + 2;
console.log(age); // 28
console.log(anotherAge); // 30
前缀递增和递减在语句中的优先级是相等的,因此会从左到右依次求值。
递增和递减的后缀版语法一样(分别是 ++ 和 -- ),只不过要放在变量后面。后缀版与前缀版的主要区别在于,后缀版递增和递减在语句被求值后才发生。如果递增是唯一的操作,那么这个差异并不明显。可是,在跟其他操作混合时,差异就会变明显。
(这里的--是后缀版的,所以在计算num3的时候,用到的是递减前的值,所以结果是2+20=22)
let num1 = 2;
let num2 = 20;
let num3 = num1-- + num2;
let num4 = num1 + num2;
console.log(num3); // 22
console.log(num4); // 21
这4个操作符可以作用于任何值,意思是不限于整数——字符串、布尔值、浮点值,甚至对象都可以。递增和递减操作符遵循如下规则。
对于字符串,如果是有效的数值形式,则转换为数值再应用改变。变量类型从字符串变成数值。
对于字符串,如果不是有效的数值形式,则将变量的值设置为 NaN 。变量类型从字符串变成数值。
对于布尔值,如果是 false ,则转换为0再应用改变。变量类型从布尔值变成数值。
对于布尔值,如果是 true ,则转换为1再应用改变。变量类型从布尔值变成数值。
对于浮点值,加1或减1。
如果是对象,则调用其valueOf() 方法取得可以操作的值。对得到的值应用上述规则。如果是 NaN ,则调用 toString() 并再次应用其他规则。变量类型从对象变成数值。
这里规则的例子:
let s1 = "2";
let s2 = "z";
let b = false;
let f = 1.1;
let o = {
valueOf() {
return -1;
}
};
s1++; // 值变成数值3
s2++; // 值变成NaN
b++; // 值变成数值1
f--; // 值变成0.10000000000000009(因为浮点数不精确)
o--; // 值变成-2
2. 一元加和减
一元加和减操作符对大多数开发者来说并不陌生,它们在ECMAScript中跟在数学中的用途一样。
一元加由一个加号( + )表示,放在变量前头,对数值没有任何影响。
如果将一元减应用到非数值,则会执行与使用 Number() 转型函数一样的类型转换:布尔值 false 和 true 转换为0和1,字符串根据特殊规则进行解析,对象会调用它们的 valueOf() 和/
或 toString() 方法以得到可以转换的值。(一元加会进行隐式类型转换,把其他类型转成数字类型,是转成数字类型最方便的方法了)
let s1 = "01";
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
valueOf() {
return -1;
}
};
s1 = +s1; // 值变成数值1
s2 = +s2; // 值变成数值1.1
s3 = +s3; // 值变成NaN
b = +b; // 值变成数值0
f = +f; // 不变,还是1.1
o = +o; // 值变成数值-1
一元减由一个减号( - )表示,放在变量前头,主要用于把数值变成负值,如把1转换为-1。
对数值使用一元减会将其变成相应的负值。在应用到非数值时,一元减会遵循与一元加同样的规则,先对它们进行转换,然后再取负值。
let s1 = "01";
let s2 = "1.1";
let s3 = "z";
let b = false;
let f = 1.1;
let o = {
valueOf() {
return -1;
}
};
s1 = -s1; // 值变成数值-1
s2 = -s2; // 值变成数值-1.1
s3 = -s3; // 值变成NaN
b = -b; // 值变成数值0
f = -f; // 变成-1.1
o = -o; // 值变成数值1
一元加和减操作符主要用于基本的算术,但也可以像上面的例子那样,用于数据类型转换。
3.5.2 位操作符
位操作符用于数值的底层操作,操作内存中表示数据的比特(位)。ECMAScript中的所有数值都以IEEE 754 64位格式存储,但位操作并不直接应用到64位表示,而是先把值转换为32
位整数,再进行位操作,之后再把结果转换为64位。对开发者而言,就好像只有32位整数一样,因为64位整数存储格式是不可见的。既然知道了这些,就只需要考虑32位整数即可。
有符号整数使用32位的前31位表示整数值。第32位表示数值的符号,如0表示正,1表示负。这一位称为符号位(sign bit),它的值决定了数值其余部分的格式。正值以真正的二进制格式存储,即31位中的每一位都代表2的幂。第一位(称为第0位)表示2的0次方,第二位表示2的1次方,依此类推。如果一个位是空的,则以0填充,相当于忽略不计。
负值以一种称为二补数的二进制编码存储。一个数值的二补数通过如下3个步骤计算得到:
(1) 确定绝对值的二进制表示(如,对于-18,先确定18的二进制表示);
(2) 找到数值的一补数,换句话说,就是每个0都变成1,每个1都变成0;
(3) 给结果加1。
比如,-18的编码:
首先从18的二进制表示开始:
0000 0000 0000 0000 0000 0000 0001 0010
然后,计算一补数,即反转每一位的二进制值:
1111 1111 1111 1111 1111 1111 1110 1101
给一补数加1:(最后一位加上1后进位了,最后两位变成了“10”)
1111 1111 1111 1111 1111 1111 1110 1110
要注意的是,在处理有符号整数时,我们无法访问第31位。(第31位指的是最左边的符号位)ECMAScript会帮我们记录这些信息。在把负值输出为一个二进制字符串时,我们会得到一个前面加了减号的绝对值
let num = -18;
console.log(num.toString(2)); // "-10010"负号后面的是18的二进制
注意 默认情况下,ECMAScript中的所有整数都表示为有符号数。不过,确实存在无符号整数。对无符号整数来说,第32位不表示符号,因为只有正值。无符号整数比有符号整数的范围更大,因为符号位被用来表示数值了。
在对ECMAScript中的数值应用位操作符时,后台会发生转换:64位数值会转换为32位数值,然后执行位操作,最后再把结果从32位转换为64位存储起来。整个过程就像处理32位数值一样,这让二进制操作变得与其他语言中类似。但这个转换也导致了一个奇特的副作用,即特殊值 NaN 和 Infinity 在位操作中都会被当成0处理。
如果将位操作符应用到非数值,那么首先会使用 Number() 函数将该值转换为数值(这个过程是自动的),然后再应用位操作。最终结果是数值。
1. 按位非
按位非操作符用波浪符( ~ )表示,它的作用是返回数值的一补数(把每一位都取反,0变成1,1变成0)。按位非是ECMAScript中为数不多的几个二进制数学操作符之一。
let num1 = 25; // 二进制 00000000000000000000000000011001
let num2 = ~num1; // 二进制 11111111111111111111111111100110
console.log(num2); // -26
(~的结果是数字,所以输出num2,得到的是数字-26。)
因为负数的编码是“绝对值每个位都取反后加1”得到的,那么如果没有加1,得到的值是“它的相反数减1”,所以按位非得到的结果就是“取反后减1”
注意,“取反再减1“这个效果也可以通过数字的计算来得到,但位操作的速度快得多。这是因为位操作是在数值的底层表示上完成的。