1.加法运算符
加法运算符(+)是最常见的运算符之一,但是使用规则却相对复杂。因为在JavaScript语言里面,这个运算符可以完成两种运算,既可以处理算术的加法,也可以用作字符串连接,它们都写成+
示例:
// 加法
1 + 1 // 2
true + true // 2
1 + true // 2
// 字符串连接
'1' + '1' // "11"
'1.1' + '1.1' // "1.11.1"
它的算法步骤如下。
如果运算子是对象,先自动转成原始类型的值(即先执行该对象的valueOf方法,如果结果还不是原始类型的值,再执行toString方法;如果对象是Date实例,则先执行toString方法)。
两个运算子都是原始类型的值以后,只要有一个运算子是字符串,则两个运算子都转为字符串,执行字符串连接运算。
否则,两个运算子都转为数值,执行加法运算
加法运算符会将其他类型的值,自动转为字符串,然后再执行连接运算
示例:
[1, 2] + [3]
// "1,23"
// 等同于
String([1, 2]) + String([3])
// '1,2' + '3'
上面代码中,两个数组相加,会先转成字符串,然后再连接
加法运算符一定有左右两个运算子,如果只有右边一个运算子,就是另一个运算符,叫做“数值运算符”
示例:
+ - 3 // 等同于 +(-3)
+ 1 + 2 // 等同于 +(1 + 2)
+ '1' // 1
上面代码中,数值运算符用于返回右边运算子的数值形式,详细解释见下文。
你可能会问,如果只有左边一个运算子,会出现什么情况?答案是会报错
1 +
// SyntaxError: Unexpected end of input
加法运算符以外的其他算术运算符(比如减法、除法和乘法),都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算
示例:
1 - '2' // -1
1 * '2' // 2
1 / '2' // 0.5
上面代码中,减法、除法和乘法运算符,都是将字符串自动转为数值,然后再运算。
由于加法运算符与其他算术运算符的这种差异,会导致一些意想不到的结果,计算时要小心
var now = new Date();
typeof (now + 1) // "string"
typeof (now - 1) // "number"
上面代码中,now是一个Date对象的实例。加法运算时,得到的是一个字符串;减法运算时,得到却是一个数值
2.算术运算符
加法运算符(Addition):x + y
减法运算符(Subtraction): x - y
乘法运算符(Multiplication): x * y
除法运算符(Division):x / y
余数运算符(Remainder):x % y
自增运算符(Increment):++x 或者 x++
自减运算符(Decrement):--x 或者 x--
数值运算符(Convert to number): +x
负数值运算符(Negate):-x
余数运算符,余数运算符(%)返回前一个运算子被后一个运算子除,所得的余数
12 % 5 // 2
需要注意的是,运算结果的正负号由第一个运算子的正负号决定
-1 % 2 // -1
1 % -2 // 1
为了得到正确的负数的余数值,需要先使用绝对值函数
// 错误的写法
function isOdd(n) {
return n % 2 === 1;
}
isOdd(-5) // false
isOdd(-4) // false
// 正确的写法
function isOdd(n) {
return Math.abs(n % 2) === 1;
}
isOdd(-5) // true
isOdd(-4) // false
自增和自减运算符,是一元运算符,只需要一个运算子。它们的作用是将运算子首先转为数值,然后加上1或者减去1。它们会修改原始变量
示例:
var x = 1;
++x // 2
x // 2
--x // 1
x // 1
自增和自减运算符有一个需要注意的地方,就是放在变量之后,会先返回变量操作前的值,再进行自增/自减操作;放在变量之前,会先进行自增/自减操作,再返回变量操作后的值
示例:
var x = 1;
var y = 1;
x++ // 1
++y // 2
上面代码中,x是先返回当前值,然后自增,所以得到1;y是先自增,然后返回新的值,所以得到2
数值运算符(+)同样使用加号,但是加法运算符是二元运算符(需要两个操作数),它是一元运算符(只需要一个操作数)。
数值运算符的作用在于可以将任何值转为数值(与Number函数的作用相同)
示例:
+true // 1
+[] // 0
+{} // NaN
上面代码表示,非数值类型的值经过数值运算符以后,都变成了数值(最后一行NaN也是数值)
负数值运算符(-),也同样具有将一个值转为数值的功能,只不过得到的值正负相反。连用两个负数值运算符,等同于数值运算符
示例:
var x = 1;
-x // -1
-(-x) // 1
上面代码最后一行的圆括号不可少,否则会变成递减运算符。
数值运算符号和负数值运算符,都会返回一个新的值,而不会改变原始变量的值
赋值运算符,用于给变量赋值
示例:
x += y // 等同于 x = x + y
x -= y // 等同于 x = x - y
x *= y // 等同于 x = x * y
x /= y // 等同于 x = x / y
x %= y // 等同于 x = x % y
x >>= y // 等同于 x = x >> y
x <<= y // 等同于 x = x << y
x >>>= y // 等同于 x = x >>> y
x &= y // 等同于 x = x & y
x |= y // 等同于 x = x | y
x ^= y // 等同于 x = x ^ y
比较运算符,用于比较两个值,然后返回一个布尔值,表示是否满足比较条件
示例:
2 > 1 // true
上面代码比较2是否大于1,返回true。
JavaScript一共提供了8个比较运算符。
== 相等
=== 严格相等
!= 不相等
!== 严格不相等
< 小于
<= 小于或等于
> 大于
>= 大于或等于
比较运算符的算法
比较运算符可以比较各种类型的值,不仅仅是数值。
除了相等运算符号和精确相等运算符,其他比较运算符的算法如下。
如果两个运算子都是字符串,则按照字典顺序比较(实际上是比较Unicode码点)。
否则,将两个运算子都转成数值,再进行比较(等同于先调用Number函数)。
下面的例子是两个原始类型的值之间的比较
示例:
5 > '4' // true
// 等同于 5 > Number('4')
// 即 5 > 4
true > false // true
// 等同于 Number(true) > Number(false)
// 即 1 > 0
2 > true // true
// 等同于 2 > Number(true)
// 即 2 > 1
上面代码中,字符串和布尔值都会先转成数值,再进行比较。
如果运算子是对象,必须先将其转为原始类型的值,即先调用valueOf方法,如果返回的还是对象,再接着调用toString方法
示例:
var x = [2];
x > '11' // true
// 等同于 [2].valueOf().toString() > '11'
// 即 '2' > '11'
x.valueOf = function () { return '1' };
x > '11' // false
// 等同于 [2].valueOf() > '11'
// 即 '1' > '11'
两个对象之间的比较也是如此
[2] > [1] // true
// 等同于 [2].valueOf().toString() > [1].valueOf().toString()
// 即 '2' > '1'
[2] > [11] // true
// 等同于 [2].valueOf().toString() > [11].valueOf().toString()
// 即 '2' > '11'
{x: 2} >= {x: 1} // true
// 等同于 {x: 2}.valueOf().toString() >= {x: 1}.valueOf().toString()
// 即 '[object Object]' >= '[object Object]'
字符串的比较,字符串将按照字典顺序进行比较
示例:
'cat' > 'dog' // false
'cat' > 'catalog' // false
JavaScript 引擎内部首先比较首字符的 Unicode 码点,如果相等,再比较第二个字符的 Unicode 码点,以此类推
3.严格相等运算符
== === 它们的区别是相等运算符(==)比较两个值是否相等,严格相等运算符(===)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(===)直接返回false,而相等运算符(==)会将它们转化成同一个类型,再用严格相等运算符进行比较
不同类型的值
示例:
1 === "1" // false
true === "true" // false
同一类型的原始类型值
1 === 0x1 // true
同一类型的复合类型值
{} === {} // false
[] === [] // false
(function (){} === function (){}) // false
上面代码分别比较两个空对象、两个空数组、两个空函数,结果都是不相等。原因是对于复合类型的值,严格相等运算比较的是,它们是否引用同一个内存地址,而运算符两边的空对象、空数组、空函数的值,都存放在不同的内存地址,结果当然是false
如果两个变量引用同一对象,则它们相等
var v1 = {};
var v2 = v1;
v1 === v2 // true
undefined和null
undefined和null与自身严格相等
示例:
undefined === undefined // true
null === null // true
严格不相等运算符
严格相等运算符有一个对应的“严格不相等运算符”(!==),两者的运算结果正好相反
1 !== '1' // true
4.相等运算符
原始类型的数据会转换成数值类型再进行比较
1 == true // true
// 等同于 1 === 1
0 == false // true
// 等同于 0 === 0
2 == true // false
// 等同于 2 === 1
2 == false // false
// 等同于 2 === 0
'true' == true // false
// 等同于 Number('true') === Number(true)
// 等同于 NaN === 1
'' == 0 // true
// 等同于 Number('') === 0
// 等同于 0 === 0
'' == false // true
// 等同于 Number('') === Number(false)
// 等同于 0 === 0
'1' == true // true
// 等同于 Number('1') === Number(true)
// 等同于 1 === 1
'\n 123 \t' == 123 // true
// 因为字符串转为数字时,省略前置和后置的空格
对象与原始类型值比较
[1] == 1 // true
// 等同于 Number([1]) == 1
[1] == '1' // true
// 等同于 String([1]) == Number('1')
[1] == true // true
// 等同于 Number([1]) == Number(true)
上面代码中,数组[1]分别与数值、字符串和布尔值进行比较,会先转成字符串或数值,再进行比较。比如,与数值1比较时,数组[1]会被自动转换成数值1,因此得到true
undefined和null
undefined和null与其他类型的值比较时,结果都为false,它们互相比较时结果为true
示例:
false == null // false
false == undefined // false
0 == null // false
0 == undefined // false
undefined == null // true
5.布尔运算符
取反运算符:!
且运算符:&&
或运算符:||
三元运算符:?:
取反运算符
!true // false
!false // true
且运算符
't' && '' // ""
't' && 'f' // "f"
't' && (1 + 2) // 3
'' && 'f' // ""
'' && '' // ""
var x = 1;
(1 - 1) && ( x += 1) // 0
x // 1
或运算符
't' || '' // "t"
't' || 'f' // "t"
'' || 'f' // "f"
'' || '' // ""
6.三元运算符
三元条件运算符用问号(?)和冒号(:),分隔三个表达式。如果第一个表达式的布尔值为true,则返回第二个表达式的值,否则返回第三个表达式的值
't' ? 'hello' : 'world' // "hello"
0 ? 'hello' : 'world' // "world"
上面代码的t和0的布尔值分别为true和false,所以分别返回第二个和第三个表达式的值。
通常来说,三元条件表达式与if...else语句具有同样表达效果,前者可以表达的,后者也能表达。但是两者具有一个重大差别,if...else是语句,没有返回值;三元条件表达式是表达式,具有返回值。所以,在需要返回值的场合,只能使用三元条件表达式,而不能使用if..else
console.log(true ? 'T' : 'F');
上面代码中,console.log方法的参数必须是一个表达式,这时就只能使用三元条件表达式。如果要用if...else语句,就必须改变整个代码写法了
7.位运算符
或运算(or):符号为|,表示若两个二进制位都为0,则结果为0,否则为1。
与运算(and):符号为&,表示若两个二进制位都为1,则结果为1,否则为0。
否运算(not):符号为~,表示对一个二进制位取反。
异或运算(xor):符号为^,表示若两个二进制位不相同,则结果为1,否则为0。
左移运算(left shift):符号为<<,详见下文解释。
右移运算(right shift):符号为>>,详见下文解释。
带符号位的右移运算(zero filled right shift):符号为>>>
这些位运算符直接处理每一个比特位(bit),所以是非常底层的运算,好处是速度极快,缺点是很不直观,许多场合不能使用它们,否则会使代码难以理解和查错。
有一点需要特别注意,位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再执行。另外,虽然在JavaScript内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数
i = i | 0;
上面这行代码的意思,就是将i(不管是整数或小数)转为32位整数。
利用这个特性,可以写出一个函数,将任意数值转为32位整数
function toInt32(x) {
return x | 0;
}
toInt32(1.001) // 1
toInt32(1.999) // 1
toInt32(1) // 1
toInt32(-1) // -1
toInt32(Math.pow(2, 32) + 1) // 1
toInt32(Math.pow(2, 32) - 1) // -1
上面代码中,最后两行得到1和-1,是因为一个整数大于32位的数位都会
被舍去
或运算,与运算
这两种运算比较容易理解,就是逐位比较两个运算子。“或运算”的规则是,两个二进制位之中只要有一个为1,就返回1,否则返回0。“与运算”的规则是,两个二进制位之中只要有一个位为0,就返回0,否则返回1
0 | 3 // 3
0 & 3 // 0
位运算只对整数有效,遇到小数时,会将小数部分舍去,只保留整数部分。所以,将一个小数与0进行或运算,等同于对该数去除小数部分,即取整数位
2.9 | 0 // 2
-2.9 | 0 // -2
否运算
“否运算”将每个二进制位都变为相反值(0变为1,1变为0)。它的返回结果有时比较难理解,因为涉及到计算机内部的数值表示机制
~ 3 // -4
上面表达式对3进行“否运算”,得到-4。之所以会有这样的结果,是因为位运算时,JavaScirpt内部将所有的运算子都转为32位的二进制整数再进行运算。3在JavaScript内部是00000000000000000000000000000011,否运算以后得到11111111111111111111111111111100,由于第一位是1,所以这个数是一个负数。JavaScript内部采用补码形式表示负数,即需要将这个数减去1,再取一次反,然后加上负号,才能得到这个负数对应的10进制值。这个数减去1等于11111111111111111111111111111011,再取一次反得到00000000000000000000000000000100,再加上负号就是-4。考虑到这样的过程比较麻烦,可以简单记忆成,一个数与自身的取反值相加,等于-1
~ -3 // 2
异或运算
“异或运算”在两个二进制位不同时返回1,相同时返回0
0 ^ 3 // 3
上面表达式中,0的二进制形式是00,3的二进制形式是11,它们每一个二进制位都不同,所以得到11(即3)
“异或运算”有一个特殊运用,连续对两个数a和b进行三次异或运算,aˆ=b, bˆ=a, aˆ=b,可以互换它们的值
示例:
var a = 10;
var b = 99;
a ^= b, b ^= a, a ^= b;
a // 99
b // 10
这是互换两个变量的值的最快方法
异或运算也可以用来取整
12.9 ^ 0 // 12
左移运算符<<
左移运算符表示将一个数的二进制值向左移动指定的位数,尾部补0,即乘以2的指定次方(最高位即符号位不参与移动)
示例:
// 4 的二进制形式为100,
// 左移一位为1000(即十进制的8)
// 相当于乘以2的1次方
4 << 1
// 8
-4 << 1
// -8
上面代码中,-4左移一位得到-8,是因为-4的二进制形式是11111111111111111111111111111100,左移一位后得到11111111111111111111111111111000,该数转为十进制(减去1后取反,再加上负号)即为-8
如果左移0位,就相当于将该数值转为32位整数,等同于取整,对于正数和负数都有效
13.5 << 0
// 13
-13.5 << 0
// -13
右移运算符>>
右移运算符表示将一个数的二进制值向右移动指定的位数,头部补0,即除以2的指定次方(最高位即符号位不参与移动)
示例:
4 >> 1
// 2
/*
// 因为4的二进制形式为00000000000000000000000000000100,
// 右移一位得到00000000000000000000000000000010,
// 即为十进制的2
*/
-4 >> 1
// -2
/*
// 因为-4的二进制形式为11111111111111111111111111111100,
// 右移一位,头部补1,得到11111111111111111111111111111110,
// 即为十进制的-2
*/
右移运算可以模拟2的整除运算
示例:
5 >> 1
// 相当于 5 / 2 = 2
21 >> 2
// 相当于 21 / 4 = 5
21 >> 3
// 相当于 21 / 8 = 2
21 >> 4
// 相当于 21 / 16 = 1
带符号位的右移运算符>>>
该运算符表示将一个数的二进制形式向右移动,包括符号位也参与移动,头部补0。所以,该运算总是得到正值。对于正数,该运算的结果与右移运算符(»)完全一致,区别主要在于负数
示例:
4 >>> 1
// 2
-4 >>> 1
// 2147483646
/*
// 因为-4的二进制形式为11111111111111111111111111111100,
// 带符号位的右移一位,得到01111111111111111111111111111110,
// 即为十进制的2147483646。
*/
这个运算实际上将一个值转为32位无符号整数
8.开关作用
位运算符可以用作设置对象属性的开关
假定某个对象有四个开关,每个开关都是一个变量。那么,可以设置一个四位的二进制数,它的每个位对应一个开关
示例:
var FLAG_A = 1; // 0001
var FLAG_B = 2; // 0010
var FLAG_C = 4; // 0100
var FLAG_D = 8; // 1000
上面代码设置A、B、C、D四个开关,每个开关分别占有一个二进制位。
然后,就可以用“与运算”检验,当前设置是否打开了指定开关
var flags = 5; // 二进制的0101
if (flags & FLAG_C) {
// ...
}
// 0101 & 0100 => 0100 => true
上面代码检验是否打开了开关C。如果打开,会返回true,否则返回false
9.其他运算符
void运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined
示例:
void 0 // undefined
void(0) // undefined
上面是void运算符的两种写法,都正确。建议采用后一种形式,即总是使用括号。因为void运算符的优先性很高,如果不使用括号,容易造成错误的结果。比如,void 4 + 7实际上等同于(void 4) + 7
逗号运算符
逗号运算符用于对两个表达式求值,并返回后一个表达式的值
示例:
'a', 'b' // "b"
var x = 0;
var y = (x++, 10);
x // 1
y // 10
10.运算顺序
优先级,优先级高的运算符先执行,优先级低的运算符后执行
4 + 5 * 6 // 34
如果多个运算符混写在一起,常常会导致令人困惑的代码
示例:
var x = 1;
var arr = [];
var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0];
上面代码中,变量y的值就很难看出来,因为这个表达式涉及5个运算符,到底谁的优先级最高,实在不容易记住。
根据语言规格,这五个运算符的优先级从高到低依次为:小于等于(<=)、严格相等(===)、或(||)、三元(?:)、等号(=)。因此上面的表达式,实际的运算顺序如下
var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0];
圆括号的作用,可以用来提高运算的优先级
(4+5)*6//54
圆括号不是运算符,而是一种语法结构。它一共有两种用法:一种是把表达式放在圆括号之中,提升运算的优先级;另一种是跟在函数的后面,作用是调用函数
左结合右结合
对于优先级别相同的运算符,大多数情况,计算顺序总是从左到右,这叫做运算符的“左结合”(left-to-right associativity),即从左边开始计算
x + y + z
上面代码先计算最左边的x与y的和,然后再计算与z的和
但是少数运算符的计算顺序是从右到左,即从右边开始计算,这叫做运算符的“右结合”(right-to-left associativity)。其中,最主要的是赋值运算符(=)和三元条件运算符(?:)
示例:
q = a ? b : c ? d : e ? f : g;
上面代码的运算结果,相当于下面的样子
w = (x = (y = z));
q = a ? b : (c ? d : (e ? f : g));
上面的两行代码,各有三个等号运算符和三个三元运算符,都是先计算最右边的那个运算符