運算符是處理數(shù)據(jù)的基本方法,用來從現(xiàn)有的值得到新的值。JavaScript 提供了多種運算符,本章逐一介紹這些運算符。
1 加法運算符
1.3 基本規(guī)則
加法運算符(+)是最常見的運算符,用來求兩個數(shù)值的和。
1 + 1 // 2
JavaScript 允許非數(shù)值的相加。
true + true // 2
1 + true // 2
說明:上面代碼中,第一行是兩個布爾值相加,第二行是數(shù)值與布爾值相加。這兩種情況,布爾值都會自動轉(zhuǎn)成數(shù)值,然后再相加。
比較特殊的是,如果是兩個字符串相加,這時加法運算符會變成連接運算符,返回一個新的字符串,將兩個原字符串連接在一起。
'a' + 'bc' // "abc"
如果一個運算子是字符串,另一個運算子是非字符串,這時非字符串會轉(zhuǎn)成字符串,再連接在一起。
1 + 'a' // "1a"
false + 'a' // "falsea"
加法運算符是在運行時決定,到底是執(zhí)行相加,還是執(zhí)行連接。也就是說,運算子的不同,導(dǎo)致了不同的語法行為,這種現(xiàn)象稱為“重載”(overload)。由于加法運算符存在重載,可能執(zhí)行兩種運算,使用的時候必須很小心。
'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"
上面代碼中,由于從左到右的運算次序,字符串的位置不同會導(dǎo)致不同的結(jié)果。
除了加法運算符,其他算術(shù)運算符(比如減法、除法和乘法)都不會發(fā)生重載。它們的規(guī)則是:所有運算子一律轉(zhuǎn)為數(shù)值,再進行相應(yīng)的數(shù)學(xué)運算。
1 - '2' // -1
1 * '2' // 2
1 / '2' // 0.5
上面代碼中,減法、除法和乘法運算符,都是將字符串自動轉(zhuǎn)為數(shù)值,然后再運算。
1.2 對象的相加
如果運算子是對象,必須先轉(zhuǎn)成原始類型的值,然后再相加。
var obj = { p: 1 };
obj + 2 // "[object Object]2"
上面代碼中,對象obj轉(zhuǎn)成原始類型的值是[object Object],再加2就得到了上面的結(jié)果。
對象轉(zhuǎn)成原始類型的值,規(guī)則如下。
首先,自動調(diào)用對象的valueOf方法。
var obj = { p: 1 };
obj.valueOf() // { p: 1 }
一般來說,對象的valueOf方法總是返回對象自身,這時再自動調(diào)用對象的toString方法,將其轉(zhuǎn)為字符串。
var obj = { p: 1 };
obj.valueOf().toString() // "[object Object]"
對象的toString方法默認返回[object Object],所以就得到了最前面那個例子的結(jié)果。
知道了這個規(guī)則以后,就可以自己定義valueOf方法或toString方法,得到想要的結(jié)果。
var obj = {
valueOf: function () {
return 1;
}
};
obj + 2 // 3
上面代碼中,我們定義obj對象的valueOf方法返回1,于是obj + 2就得到了3。這個例子中,由于valueOf方法直接返回一個原始類型的值,所以不再調(diào)用toString方法。
下面是自定義toString方法的例子。
var obj = {
toString: function () {
return 'hello';
}
};
obj + 2 // "hello2"
上面代碼中,對象obj的toString方法返回字符串hello。前面說過,只要有一個運算子是字符串,加法運算符就變成連接運算符,返回連接后的字符串。
這里有一個特例,如果運算子是一個Date對象的實例,那么會優(yōu)先執(zhí)行toString方法。
var obj = new Date();
obj.valueOf = function () { return 1 };
obj.toString = function () { return 'hello' };
obj + 2 // "hello2"
上面代碼中,對象obj是一個Date對象的實例,并且自定義了valueOf方法和toString方法,結(jié)果toString方法優(yōu)先執(zhí)行。
2 算術(shù)運算符
包括加法運算符在內(nèi),JavaScript 共提供10個算術(shù)運算符,用來完成基本的算術(shù)運算。
加法運算符:x + y
減法運算符: x - y
乘法運算符: x * y
除法運算符:x / y
指數(shù)運算符:x ** y
余數(shù)運算符:x % y
自增運算符:++x 或者 x++
自減運算符:--x 或者 x--
數(shù)值運算符: +x
負數(shù)值運算符:-x
減法、乘法、除法運算法比較好理解,就是執(zhí)行相應(yīng)的數(shù)學(xué)運算。下面說明其他幾個算術(shù)運算符。
2.1 余數(shù)運算符
余數(shù)運算符(%)返回前一個運算子被后一個運算子除,所得的余數(shù)。
12 % 5 // 2
需要注意的是,運算結(jié)果的正負號由第一個運算子的正負號決定。
-1 % 2 // -1
1 % -2 // 1
所以,為了得到負數(shù)的正確余數(shù)值,可以先使用絕對值函數(shù)。
// 錯誤的寫法
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
余數(shù)運算符還可以用于浮點數(shù)的運算。但是,由于浮點數(shù)不是精確的值,無法得到完全準確的結(jié)果。
6.5 % 2.1
// 0.19999999999999973
2.2 自增和自減運算符
自增和自減運算符,是一元運算符,只需要一個運算子。它們的作用是將運算子首先轉(zhuǎn)為數(shù)值,然后加上1或者減去1。它們會修改原始變量。
var x = 1;
++x // 2
x // 2
--x // 1
x // 1
上面代碼的變量x自增后,返回2,再進行自減,返回1。這兩種情況都會使得,原始變量x的值發(fā)生改變。
區(qū)別:自增和自減運算符有一個需要注意的地方,就是放在變量之后,會先返回變量操作前的值,再進行自增/自減操作;
放在變量之前,會先進行自增/自減操作,再返回變量操作后的值。
var x = 1;
var y = 1;
x++ // 1
++y // 2
上面代碼中,x是先返回當(dāng)前值,然后自增,所以得到1;y是先自增,然后返回新的值,所以得到2。
2.3 數(shù)值運算符,負數(shù)值運算符
數(shù)值運算符(+)同樣使用加號,但它是一元運算符(只需要一個操作數(shù)),而加法運算符是二元運算符(需要兩個操作數(shù))。
數(shù)值運算符的作用在于可以將任何值轉(zhuǎn)為數(shù)值(與Number函數(shù)的作用相同)。
+true // 1
+[] // 0
+{} // NaN
上面代碼表示,非數(shù)值經(jīng)過數(shù)值運算符以后,都變成了數(shù)值(最后一行NaN也是數(shù)值)
負數(shù)值運算符(-),也同樣具有將一個值轉(zhuǎn)為數(shù)值的功能,只不過得到的值正負相反。連用兩個負數(shù)值運算符,等同于數(shù)值運算符。
var x = 1;
-x // -1
-(-x) // 1
上面代碼最后一行的圓括號不可少,否則會變成自減運算符。
數(shù)值運算符號和負數(shù)值運算符,都會返回一個新的值,而不會改變原始變量的值。
2.4 指數(shù)運算符
指數(shù)運算符(**)完成指數(shù)運算,前一個運算子是底數(shù),后一個運算子是指數(shù)。
2 ** 4 // 16
3 賦值運算符
賦值運算符(Assignment Operators)用于給變量賦值。
最常見的賦值運算符,當(dāng)然就是等號(=)。
// 將 1 賦值給變量 x
var x = 1;
// 將變量 y 的值賦值給變量 x
var x = y;
賦值運算符還可以與其他運算符結(jié)合,形成變體。下面是與算術(shù)運算符的結(jié)合。
// 等同于 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
下面是與位運算符的結(jié)合。
// 等同于 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
這些復(fù)合的賦值運算符,都是先進行指定運算,然后將得到值返回給左邊的變量。
4 比較運算符
比較運算符用于比較兩個值的大小,然后返回一個布爾值,表示是否滿足指定的條件。
2 > 1 // true
上面代碼比較 2是否大于1,返回true。
注意,比較運算符可以比較各種類型的值,不僅僅是數(shù)值。
JavaScript 一共提供了8個比較運算符。
< 小于運算符
大于運算符
<= 小于或等于運算符
= 大于或等于運算符
== 相等運算符
=== 嚴格相等運算符
!= 不相等運算符
!== 嚴格不相等運算符
這八個比較運算符分成兩類:相等比較和非相等比較。兩者的規(guī)則是不一樣的,對于非相等的比較,算法是先看兩個運算子是否都是字符串,如果是的,就按照字典順序比較(實際上是比較 Unicode 碼點);否則,將兩個運算子都轉(zhuǎn)成數(shù)值,再比較數(shù)值的大小。
4.1 字符串的比較
字符串按照字典順序進行比較。
'cat' > 'dog' // false
'cat' > 'catalog' // false
JavaScript 引擎內(nèi)部首先比較首字符的 Unicode 碼點。如果相等,再比較第二個字符的 Unicode 碼點,以此類推。
'cat' > 'Cat' // true'
上面代碼中,小寫的c的 Unicode 碼點(99)大于大寫的C的 Unicode 碼點(67),所以返回true。
由于所有字符都有 Unicode 碼點,因此漢字也可以比較。
'大' > '小' // false
上面代碼中,“大”的 Unicode 碼點是22823,“小”是23567,因此返回false。
4.2 非字符串的比較
(1)原始類型的值
兩個原始類型的值的比較,除了相等運算符(==)和嚴格相等運算符(===),其他比較運算符都是先轉(zhuǎn)成數(shù)值再比較。
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
上面代碼中,字符串和布爾值都會先轉(zhuǎn)成數(shù)值,再進行比較。
這里有一個特殊情況,即任何值(包括NaN本身)與NaN比較,返回的都是false。
1 > NaN // false
1 <= NaN // false
'1' > NaN // false
'1' <= NaN // false
NaN > NaN // false
NaN <= NaN // false
(2)對象
如果運算子是對象,會轉(zhuǎn)為原始類型的值,再進行比較。
對象轉(zhuǎn)換成原始類型的值,算法是先調(diào)用valueOf方法;如果返回的還是對象,再接著調(diào)用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]'
注意,Date對象實例用于比較時,是先調(diào)用toString方法。如果返回的不是原始類型的值,再接著對返回值調(diào)用valueOf方法。
4.3 嚴格相等運算符
JavaScript 提供兩種相等運算符:==和===。
簡單說,它們的區(qū)別是相等運算符(==)比較兩個值是否相等,嚴格相等運算符(===)比較它們是否為“同一個值”。如果兩個值不是同一類型,嚴格相等運算符(===)直接返回false,而相等運算符(==)會將它們轉(zhuǎn)換成同一個類型,再用嚴格相等運算符進行比較。
嚴格相等運算符的算法如下。
(1)不同類型的值
如果兩個值的類型不同,直接返回false。
1 === "1" // false
true === "true" // false
上面代碼比較數(shù)值的1與字符串的“1”、布爾值的true與字符串"true",因為類型不同,結(jié)果都是false。
(2)同一類的原始類型值
同一類型的原始類型的值(數(shù)值、字符串、布爾值)比較時,值相同就返回true,值不同就返回false。
1 === 0x1 // true
上面代碼比較十進制的1與十六進制的1,因為類型和值都相同,返回true。
需要注意的是,NaN與任何值都不相等(包括自身)。另外,正0等于負0。
NaN === NaN // false
+0 === -0 // true
(3)復(fù)合類型值
兩個復(fù)合類型(對象、數(shù)組、函數(shù))的數(shù)據(jù)比較時,不是比較它們的值是否相等,而是比較它們是否指向同一個地址。
{} === {} // false
[] === [] // false
(function () {} === function () {}) // false
上面代碼分別比較兩個空對象、兩個空數(shù)組、兩個空函數(shù),結(jié)果都是不相等。原因是對于復(fù)合類型的值,嚴格相等運算比較的是,它們是否引用同一個內(nèi)存地址,而運算符兩邊的空對象、空數(shù)組、空函數(shù)的值,都存放在不同的內(nèi)存地址,結(jié)果當(dāng)然是false。
如果兩個變量引用同一個對象,則它們相等。
var v1 = {};
var v2 = v1;
v1 === v2 // true
注意,對于兩個對象的比較,嚴格相等運算符比較的是地址,而大于或小于運算符比較的是值。
new Date() > new Date() // false
new Date() < new Date() // false
new Date() === new Date() // false
上面的三個表達式,前兩個比較的是值,最后一個比較的是地址,所以都返回false。
(4)undefined 和 null
undefined和null與自身嚴格相等。
undefined === undefined // true
null === null // true
由于變量聲明后默認值是undefined,因此兩個只聲明未賦值的變量是相等的。
var v1;
var v2;
v1 === v2 // true
(5)嚴格不相等運算符
嚴格相等運算符有一個對應(yīng)的“嚴格不相等運算符”(!==),它的算法就是先求嚴格相等運算符的結(jié)果,然后返回相反值。
1 !== '1' // true
4.4 相等運算符
相等運算符用來比較相同類型的數(shù)據(jù)時,與嚴格相等運算符完全一樣。
比較不同類型的數(shù)據(jù)時,相等運算符會先將數(shù)據(jù)進行類型轉(zhuǎn)換,然后再用嚴格相等運算符比較。類型轉(zhuǎn)換規(guī)則如下。
(1)原始類型的值
原始類型的數(shù)據(jù)會轉(zhuǎn)換成數(shù)值類型再進行比較。
1 == true // true
// 等同于 1 === Number(true)
0 == false // true
// 等同于 0 === Number(false)
2 == true // false
// 等同于 2 === Number(true)
2 == false // false
// 等同于 2 === Number(false)
'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
// 因為字符串轉(zhuǎn)為數(shù)字時,省略前置和后置的空格
上面代碼將字符串和布爾值都轉(zhuǎn)為數(shù)值,然后再進行比較。
(2)對象與原始類型值比較
對象(這里指廣義的對象,包括數(shù)組和函數(shù))與原始類型的值比較時,對象轉(zhuǎn)化成原始類型的值,再進行比較。
[1] == 1 // true
// 等同于 Number([1]) == 1
[1] == '1' // true
// 等同于 Number([1]) == Number('1')
[1] == true // true
// 等同于 Number([1]) == Number(true)
上面代碼中,數(shù)組[1]與數(shù)值進行比較,會先轉(zhuǎn)成數(shù)值,再進行比較;與字符串進行比較,會先轉(zhuǎn)成數(shù)值,然后再與字符串進行比較,這時字符串也會轉(zhuǎn)成數(shù)值;與布爾值進行比較,兩個運算子都會先轉(zhuǎn)成數(shù)值,然后再進行比較。
(3)undefined 和 null
undefined和null與其他類型的值比較時,結(jié)果都為false,它們互相比較時結(jié)果為true。
false == null // false
false == undefined // false
0 == null // false
0 == undefined // false
undefined == null // true
絕大多數(shù)情況下,對象與undefined和null比較,都返回false。只有在對象轉(zhuǎn)為原始值得到undefined時,才會返回true,這種情況是非常罕見的。
(4)相等運算符的缺點
相等運算符隱藏的類型轉(zhuǎn)換,會帶來一些違反直覺的結(jié)果。
0 == '' // true
0 == '0' // true
2 == true // false
2 == false // false
false == 'false' // false
false == '0' // true
false == undefined // false
false == null // false
null == undefined // true
' \t\r\n ' == 0 // true
上面這些表達式都很容易出錯,因此不要使用相等運算符(==),最好只使用嚴格相等運算符(===)。
(5)不相等運算符
相等運算符有一個對應(yīng)的“不相等運算符”(!=),兩者的運算結(jié)果正好相反。
1 != '1' // false
5 布爾運算符
布爾運算符用于將表達式轉(zhuǎn)為布爾值,一共包含四個運算符。
取反運算符:!
且運算符:&&
或運算符:||
三元運算符:?:
5.1 取反運算符(!)
取反運算符是一個感嘆號,用于將布爾值變?yōu)橄喾粗担?strong>true變成false,false變成true。
!true // false
!false // true
對于非布爾值,取反運算符會將其轉(zhuǎn)為布爾值。可以這樣記憶,以下六個值取反后為true,其他值都為false。
undefined
null
false
0
NaN
空字符串('')
!undefined // true
!null // true
!0 // true
!NaN // true
!"" // true
!54 // false
!'hello' // false
![] // false
!{} // false
上面代碼中,不管什么類型的值,經(jīng)過取反運算后,都變成了布爾值。
如果對一個值連續(xù)做兩次取反運算,等于將其轉(zhuǎn)為對應(yīng)的布爾值,與Boolean函數(shù)的作用相同。這是一種常用的類型轉(zhuǎn)換的寫法。
!!x
// 等同于
Boolean(x)
上面代碼中,不管x是什么類型的值,經(jīng)過兩次取反運算后,變成了與Boolean函數(shù)結(jié)果相同的布爾值。所以,兩次取反就是將一個值轉(zhuǎn)為布爾值的簡便寫法。
5.2 且運算符(&&)
且運算符(&&)往往用于多個表達式的求值。
它的運算規(guī)則是:如果第一個運算子的布爾值為true,則返回第二個運算子的值(注意是值,不是布爾值);如果第一個運算子的布爾值為false,則直接返回第一個運算子的值,且不再對第二個運算子求值。
't' && '' // ""
't' && 'f' // "f"
't' && (1 + 2) // 3
'' && 'f' // ""
'' && '' // ""
var x = 1;
(1 - 1) && ( x += 1) // 0
x // 1
上面代碼的最后一個例子,由于且運算符的第一個運算子的布爾值為false,則直接返回它的值0,而不再對第二個運算子求值,所以變量x的值沒變。
這種跳過第二個運算子的機制,被稱為“短路”。有些程序員喜歡用它取代if結(jié)構(gòu),比如下面是一段if結(jié)構(gòu)的代碼,就可以用且運算符改寫。
if (i) {
doSomething();
}
// 等價于
i && doSomething();
上面代碼的兩種寫法是等價的,但是后一種不容易看出目的,也不容易除錯,建議謹慎使用。
且運算符可以多個連用,這時返回第一個布爾值為false的表達式的值。
true && 'foo' && '' && 4 && 'foo' && true
// ''
上面代碼中,第一個布爾值為false的表達式為第三個表達式,所以得到一個空字符串。
5.3 或運算符(||)
或運算符(||)也用于多個表達式的求值。它的運算規(guī)則是:如果第一個運算子的布爾值為true,則返回第一個運算子的值,且不再對第二個運算子求值;如果第一個運算子的布爾值為false,則返回第二個運算子的值。
't' || '' // "t"
't' || 'f' // "t"
'' || 'f' // "f"
'' || '' // ""
短路規(guī)則對這個運算符也適用。
var x = 1;
true || (x = 2) // true
x // 1
上面代碼中,且運算符的第一個運算子為true,所以直接返回true,不再運行第二個運算子。所以,x的值沒有改變。這種只通過第一個表達式的值,控制是否運行第二個表達式的機制,就稱為“短路”(short-cut)。
或運算符可以多個連用,這時返回第一個布爾值為true的表達式的值。
false || 0 || '' || 4 || 'foo' || true
// 4
上面代碼中第一個布爾值為true的表達式是第四個表達式,所以得到數(shù)值4。
或運算符常用于為一個變量設(shè)置默認值。
function saveText(text) {
text = text || '';
// ...
}
// 或者寫成
saveText(this.text || '')
上面代碼表示,如果函數(shù)調(diào)用時,沒有提供參數(shù),則該參數(shù)默認設(shè)置為空字符串。
5.4 三元條件運算符(?:)
三元條件運算符由問號(?)和冒號(:)組成,分隔三個表達式。它是 JavaScript 語言唯一一個需要三個運算子的運算符。如果第一個表達式的布爾值為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方法的參數(shù)必須是一個表達式,這時就只能使用三元條件表達式。如果要用if...else語句,就必須改變整個代碼寫法了。
6 位運算符
位運算符用于直接對二進制位進行計算,一共有7個。
二進制或運算符(or):符號為|,表示若兩個二進制位都為0,則結(jié)果為0,否則為1。
二進制與運算符(and):符號為&,表示若兩個二進制位都為1,則結(jié)果為1,否則為0。
二進制否運算符(not):符號為~,表示對一個二進制位取反。
異或運算符(xor):符號為^,表示若兩個二進制位不相同,則結(jié)果為1,否則為0。
左移運算符(left shift):符號為<<,詳見下文解釋。
右移運算符(right shift):符號為>>,詳見下文解釋。
帶符號位的右移運算符(zero filled right shift):符號為>>>,詳見下文解釋。
這些位運算符直接處理每一個比特位(bit),所以是非常底層的運算,好處是速度極快,缺點是很不直觀,許多場合不能使用它們,否則會使代碼難以理解和查錯。
有一點需要特別注意,位運算符只對整數(shù)起作用,如果一個運算子不是整數(shù),會自動轉(zhuǎn)為整數(shù)后再執(zhí)行。另外,雖然在 JavaScript 內(nèi)部,數(shù)值都是以64位浮點數(shù)的形式儲存,但是做位運算的時候,是以32位帶符號的整數(shù)進行運算的,并且返回值也是一個32位帶符號的整數(shù)。
i = i | 0;
上面這行代碼的意思,就是將i(不管是整數(shù)或小數(shù))轉(zhuǎn)為32位整數(shù)。
利用這個特性,可以寫出一個函數(shù),將任意數(shù)值轉(zhuǎn)為32位整數(shù)。
function toInt32(x) {
return x | 0;
}
上面這個函數(shù)將任意值與0進行一次或運算,這個位運算會自動將一個值轉(zhuǎn)為32位整數(shù)。下面是這個函數(shù)的用法。
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
上面代碼中,toInt32可以將小數(shù)轉(zhuǎn)為整數(shù)。對于一般的整數(shù),返回值不會有任何變化。對于大于2的32次方的整數(shù),大于32位的數(shù)位都會被舍去。
6.2 二進制或運算符
二進制或運算符(|)逐位比較兩個運算子,兩個二進制位之中只要有一個為1,就返回1,否則返回0。
0 | 3 // 3
上面代碼中,0和3的二進制形式分別是00和11,所以進行二進制或運算會得到11(即3)。
位運算只對整數(shù)有效,遇到小數(shù)時,會將小數(shù)部分舍去,只保留整數(shù)部分。所以,將一個小數(shù)與0進行二進制或運算,等同于對該數(shù)去除小數(shù)部分,即取整數(shù)位。
2.9 | 0 // 2
-2.9 | 0 // -2
需要注意的是,這種取整方法不適用超過32位整數(shù)最大值2147483647的數(shù)。
2147483649.4 | 0;
// -2147483647
6.3 二進制與運算符
二進制與運算符(&)的規(guī)則是逐位比較兩個運算子,兩個二進制位之中只要有一個位為0,就返回0,否則返回1。
0 & 3 // 0
上面代碼中,0(二進制00)和3(二進制11)進行二進制與運算會得到00(即0)。
6.4 二進制否運算符
二進制否運算符(~)將每個二進制位都變?yōu)橄喾粗担?strong>0變?yōu)?strong>1,1變?yōu)?strong>0)。它的返回結(jié)果有時比較難理解,因為涉及到計算機內(nèi)部的數(shù)值表示機制。
~ 3 // -4
上面表達式對3進行二進制否運算,得到-4。之所以會有這樣的結(jié)果,是因為位運算時,JavaScirpt 內(nèi)部將所有的運算子都轉(zhuǎn)為32位的二進制整數(shù)再進行運算。
3的32位整數(shù)形式是00000000000000000000000000000011,二進制否運算以后得到11111111111111111111111111111100。由于第一位(符號位)是1,所以這個數(shù)是一個負數(shù)。JavaScript 內(nèi)部采用補碼形式表示負數(shù),即需要將這個數(shù)減去1,再取一次反,然后加上負號,才能得到這個負數(shù)對應(yīng)的10進制值。這個數(shù)減去1等于11111111111111111111111111111011,再取一次反得到00000000000000000000000000000100,再加上負號就是-4。考慮到這樣的過程比較麻煩,可以簡單記憶成,一個數(shù)與自身的取反值相加,等于-1。
~ -3 // 2
上面表達式可以這樣算,-3的取反值等于-1減去-3,結(jié)果為2。
對一個整數(shù)連續(xù)兩次二進制否運算,得到它自身。
~~3 // 3
所有的位運算都只對整數(shù)有效。二進制否運算遇到小數(shù)時,也會將小數(shù)部分舍去,只保留整數(shù)部分。所以,對一個小數(shù)連續(xù)進行兩次二進制否運算,能達到取整效果。
~~2.9 // 2
~~47.11 // 47
~~1.9999 // 1
~~3 // 3
使用二進制否運算取整,是所有取整方法中最快的一種。
對字符串進行二進制否運算,JavaScript 引擎會先調(diào)用Number函數(shù),將字符串轉(zhuǎn)為數(shù)值。
// 相當(dāng)于~Number('011')
~'011' // -12
// 相當(dāng)于~Number('42 cats')
~'42 cats' // -1
// 相當(dāng)于~Number('0xcafebabe')
~'0xcafebabe' // 889275713
// 相當(dāng)于~Number('deadbeef')
~'deadbeef' // -1
對于其他類型的值,二進制否運算也是先用Number轉(zhuǎn)為數(shù)值,然后再進行處理。
// 相當(dāng)于 ~Number([])
~[] // -1
// 相當(dāng)于 ~Number(NaN)
~NaN // -1
// 相當(dāng)于 ~Number(null)
~null // -1
6.5 異或運算
異或運算(^)在兩個二進制位不同時返回1,相同時返回0。
0 ^ 3 // 3
上面表達式中,0(二進制00)與3(二進制11)進行異或運算,它們每一個二進制位都不同,所以得到11(即3)。
“異或運算”有一個特殊運用,連續(xù)對兩個數(shù)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
6.6 左移運算符
左移運算符(<<)表示將一個數(shù)的二進制值向左移動指定的位數(shù),尾部補0,即乘以2的指定次方(最高位即符號位不參與移動)。
// 4 的二進制形式為100,
// 左移一位為1000(即十進制的8)
// 相當(dāng)于乘以2的1次方
4 << 1
// 8
-4 << 1
// -8
上面代碼中,-4左移一位得到-8,是因為-4的二進制形式是11111111111111111111111111111100,左移一位后得到11111111111111111111111111111000,該數(shù)轉(zhuǎn)為十進制(減去1后取反,再加上負號)即為-8。
如果左移0位,就相當(dāng)于將該數(shù)值轉(zhuǎn)為32位整數(shù),等同于取整,對于正數(shù)和負數(shù)都有效。
13.5 << 0
// 13
-13.5 << 0
// -13
左移運算符用于二進制數(shù)值非常方便。
var color = {r: 186, g: 218, b: 85};
// RGB to HEX
// (1 << 24)的作用為保證結(jié)果是6位數(shù)
var rgb2hex = function(r, g, b) {
return '#' + ((1 << 24) + (r << 16) + (g << 8) + b)
.toString(16) // 先轉(zhuǎn)成十六進制,然后返回字符串
.substr(1); // 去除字符串的最高位,返回后面六個字符串
}
rgb2hex(color.r, color.g, color.b)
// "#bada55"
上面代碼使用左移運算符,將顏色的RGB 值轉(zhuǎn)為 HEX 值。
6.7 右移運算符
右移運算符(>>)表示將一個數(shù)的二進制值向右移動指定的位數(shù),頭部補0,即除以2的指定次方(最高位即符號位不參與移動)。
4 >> 1
// 2
/*
// 因為4的二進制形式為 00000000000000000000000000000100,
// 右移一位得到 00000000000000000000000000000010,
// 即為十進制的2
*/
-4 >> 1
// -2
/*
// 因為-4的二進制形式為 11111111111111111111111111111100,
// 右移一位,頭部補1,得到 11111111111111111111111111111110,
// 即為十進制的-2
*/
右移運算可以模擬 2 的整除運算。
5 >> 1
// 2
// 相當(dāng)于 5 / 2 = 2
21 >> 2
// 5
// 相當(dāng)于 21 / 4 = 5
21 >> 3
// 2
// 相當(dāng)于 21 / 8 = 2
21 >> 4
// 1
// 相當(dāng)于 21 / 16 = 1
6.8 帶符號位的右移運算符
帶符號位的右移運算符(>>>)表示將一個數(shù)的二進制形式向右移動,包括符號位也參與移動,頭部補0。所以,該運算總是得到正值。對于正數(shù),該運算的結(jié)果與右移運算符(>>)完全一致,區(qū)別主要在于負數(shù)。
4 >>> 1
// 2
-4 >>> 1
// 2147483646
/*
// 因為-4的二進制形式為11111111111111111111111111111100,
// 帶符號位的右移一位,得到01111111111111111111111111111110,
// 即為十進制的2147483646。
*/
這個運算實際上將一個值轉(zhuǎn)為32位無符號整數(shù)。
查看一個負整數(shù)在計算機內(nèi)部的儲存形式,最快的方法就是使用這個運算符。
-1 >>> 0 // 4294967295
上面代碼表示,-1作為32位整數(shù)時,內(nèi)部的儲存形式使用無符號整數(shù)格式解讀,值為4294967295(即(2^32)-1,等于11111111111111111111111111111111)。
6.9 開關(guān)作用
位運算符可以用作設(shè)置對象屬性的開關(guān)。
假定某個對象有四個開關(guān),每個開關(guān)都是一個變量。那么,可以設(shè)置一個四位的二進制數(shù),它的每個位對應(yīng)一個開關(guān)。
var FLAG_A = 1; // 0001
var FLAG_B = 2; // 0010
var FLAG_C = 4; // 0100
var FLAG_D = 8; // 1000
上面代碼設(shè)置 A、B、C、D 四個開關(guān),每個開關(guān)分別占有一個二進制位。
然后,就可以用二進制與運算檢驗,當(dāng)前設(shè)置是否打開了指定開關(guān)。
var flags = 5; // 二進制的0101
if (flags & FLAG_C) {
// ...
}
// 0101 & 0100 => 0100 => true
上面代碼檢驗是否打開了開關(guān)C。如果打開,會返回true,否則返回false。
現(xiàn)在假設(shè)需要打開A、B、D三個開關(guān),我們可以構(gòu)造一個掩碼變量。
var mask = FLAG_A | FLAG_B | FLAG_D;
// 0001 | 0010 | 1000 => 1011
上面代碼對A、B、D三個變量進行二進制或運算,得到掩碼值為二進制的1011。
有了掩碼,二進制或運算可以確保打開指定的開關(guān)。
flags = flags | mask;
二進制與運算可以將當(dāng)前設(shè)置中凡是與開關(guān)設(shè)置不一樣的項,全部關(guān)閉。
flags = flags & mask;
異或運算可以切換(toggle)當(dāng)前設(shè)置,即第一次執(zhí)行可以得到當(dāng)前設(shè)置的相反值,再執(zhí)行一次又得到原來的值。
flags = flags ^ mask;
二進制否運算可以翻轉(zhuǎn)當(dāng)前設(shè)置,即原設(shè)置為0,運算后變?yōu)?strong>1;原設(shè)置為1,運算后變?yōu)?strong>0。
flags = ~flags;
7其他運算符
7.1 void 運算符
void運算符的作用是執(zhí)行一個表達式,然后不返回任何值,或者說返回undefined。
void 0 // undefined
void(0) // undefined
上面是void運算符的兩種寫法,都正確。建議采用后一種形式,即總是使用圓括號。因為void運算符的優(yōu)先性很高,如果不使用括號,容易造成錯誤的結(jié)果。比如,void 4 + 7實際上等同于(void 4) + 7。
下面是void運算符的一個例子。
var x = 3;
void (x = 5) //undefined
x // 5
這個運算符的主要用途是瀏覽器的書簽工具(bookmarklet),以及在超級鏈接中插入代碼防止網(wǎng)頁跳轉(zhuǎn),請看下面的代碼。
<script>
function f() {
console.log('Hello World');
}
</script>
<a onclick="f(); return false;">點擊</a>
上面代碼中,點擊鏈接后,會先執(zhí)行onclick的代碼,由于onclick返回false,所以瀏覽器不會跳轉(zhuǎn)到 example.com。
void運算符可以取代上面的寫法。
<a href="javascript: void(f())">文字</a>
下面是一個更實際的例子,用戶點擊鏈接提交表單,但是不產(chǎn)生頁面跳轉(zhuǎn)。
<a href="javascript: void(document.form.submit())">
提交
</a>
7.2 逗號運算符
逗號運算符用于對兩個表達式求值,并返回后一個表達式的值。
'a', 'b' // "b"
var x = 0;
var y = (x++, 10);
x // 1
y // 10
上面代碼中,逗號運算符返回后一個表達式的值。
8 運算順序
8.1 優(yōu)先級
JavaScript 各種運算符的優(yōu)先級別(Operator Precedence)是不一樣的。優(yōu)先級高的運算符先執(zhí)行,優(yōu)先級低的運算符后執(zhí)行。
4 + 5 * 6 // 34
上面的代碼中,乘法運算符(*)的優(yōu)先性高于加法運算符(+),所以先執(zhí)行乘法,再執(zhí)行加法,相當(dāng)于下面這樣。
4 + (5 * 6) // 34
如果多個運算符混寫在一起,常常會導(dǎo)致令人困惑的代碼。
var x = 1;
var arr = [];
var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0];
上面代碼中,變量y的值就很難看出來,因為這個表達式涉及5個運算符,到底誰的優(yōu)先級最高,實在不容易記住。
根據(jù)語言規(guī)格,這五個運算符的優(yōu)先級從高到低依次為:小于等于(<=)、嚴格相等(===)、或(||)、三元(?:)、等號(=)。因此上面的表達式,實際的運算順序如下。
var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0];
記住所有運算符的優(yōu)先級,是非常難的,也是沒有必要的。
8.2 圓括號的作用
圓括號(())可以用來提高運算的優(yōu)先級,因為它的優(yōu)先級是最高的,即圓括號中的表達式會第一個運算。
(4 + 5) * 6 // 54
上面代碼中,由于使用了圓括號,加法會先于乘法執(zhí)行。
運算符的優(yōu)先級別十分繁雜,且都是硬性規(guī)定,因此建議總是使用圓括號,保證運算順序清晰可讀,這對代碼的維護和除錯至關(guān)重要。
順便說一下,圓括號不是運算符,而是一種語法結(jié)構(gòu)。它一共有兩種用法:一種是把表達式放在圓括號之中,提升運算的優(yōu)先級;另一種是跟在函數(shù)的后面,作用是調(diào)用函數(shù)。
注意,因為圓括號不是運算符,所以不具有求值作用,只改變運算的優(yōu)先級。
var x = 1;
(x) = 2;
上面代碼的第二行,如果圓括號具有求值作用,那么就會變成1 = 2,這是會報錯了。但是,上面的代碼可以運行,這驗證了圓括號只改變優(yōu)先級,不會求值。
這也意味著,如果整個表達式都放在圓括號之中,那么不會有任何效果。
(exprssion)
// 等同于
expression
函數(shù)放在圓括號中,會返回函數(shù)本身。如果圓括號緊跟在函數(shù)的后面,就表示調(diào)用函數(shù)。
function f() {
return 1;
}
(f) // function f(){return 1;}
f() // 1
上面代碼中,函數(shù)放在圓括號之中會返回函數(shù)本身,圓括號跟在函數(shù)后面則是調(diào)用函數(shù)。
圓括號之中,只能放置表達式,如果將語句放在圓括號之中,就會報錯。
(var a = 1)
// SyntaxError: Unexpected token var
8.3 左結(jié)合與右結(jié)合
對于優(yōu)先級別相同的運算符,大多數(shù)情況,計算順序總是從左到右,這叫做運算符的“左結(jié)合”(left-to-right associativity),即從左邊開始計算。
x + y + z
上面代碼先計算最左邊的x與y的和,然后再計算與z的和。
但是少數(shù)運算符的計算順序是從右到左,即從右邊開始計算,這叫做運算符的“右結(jié)合”(right-to-left associativity)。其中,最主要的是賦值運算符(=)和三元條件運算符(?:)。
w = x = y = z;
q = a ? b : c ? d : e ? f : g;
上面代碼的運算結(jié)果,相當(dāng)于下面的樣子。
w = (x = (y = z));
q = a ? b : (c ? d : (e ? f : g));
上面的兩行代碼,各有三個等號運算符和三個三元運算符,都是先計算最右邊的那個運算符。
本文大量參考借鑒了阮一峰老師的博客《JavaScript 標(biāo)準參考教程》,他的文章使我受益匪淺,在此表示由衷的感謝!