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)); 上面的两行代码,各有三个等号运算符和三个三元运算符,都是先计算最右边的那个运算符