JavaScript-修炼之路第三层

三、运算符

1,算术运算符

运算符是处理数据的基本方法,用来从现有的值得到新的值。JavaScript 提供了多种运算符,覆盖 了所有主要的运算。

JavaScript 共提供10个算术运算符,用来完成基本的算术运算。

        加法运算符: x + y

        减法运算符: x - y

        乘法运算符: x * y

        除法运算符: x / y

        指数运算符: x ** y

        余数运算符: x % y

        自增运算符: ++x 或者 x++

        自减运算符: --x 或者 x--

        数值运算符: +x

        负数值运算符: -x

减法、乘法、除法运算法比较单一,就是执行相应的数学运算。下面介绍其他几个算术运算符,重点是 加法运算符。

加法运算符

加法运算符( + )是最常见的运算符,用来求两个数值的和。

1. 1 + 1 // 2

JavaScript 允许非数值的相加。

1. true + true // 2
2. 1 + true // 2

上面代码中,第一行是两个布尔值相加,第二行是数值与布尔值相加。这两种情况,布尔值都会自动转 成数值,然后再相加。

比较特殊的是,如果是两个字符串相加,这时加法运算符会变成连接运算符,返回一个新的字符串,将 两个原字符串连接在一起。

1. 'a' + 'bc' // "abc"

如果一个运算子是字符串,另一个运算子是非字符串,这时非字符串会转成字符串,再连接在一起。

1. 1 + 'a' // "1a"
2. false + 'a' // "falsea"

加法运算符是在运行时决定,到底是执行相加,还是执行连接。也就是说,运算子的不同,导致了不同 的语法行为,这种现象称为“重载”(overload)。由于加法运算符存在重载,可能执行两种运算,使 用的时候必须很小心。

1. '3' + 4 + 5 // "345"
2. 3 + 4 + '5' // "75"

上面代码中,由于从左到右的运算次序,字符串的位置不同会导致不同的结果。 除了加法运算符,其他算术运算符(比如减法、除法和乘法)都不会发生重载。它们的规则是:所有运 算子一律转为数值,再进行相应的数学运算。

1. 1 - '2' // -1
2. 1 * '2' // 2
3. 1 / '2' // 0.5

上面代码中,减法、除法和乘法运算符,都是将字符串自动转为数值,然后再运算。

对象的相加

如果运算子是对象,必须先转成原始类型的值,然后再相加。

1. var obj = { p: 1 };
2. obj + 2 // "[object Object]2"

上面代码中,对象 obj 转成原始类型的值是 [object Object] ,再加 2 就得到了上面的结果。 对象转成原始类型的值,规则如下。 首先,自动调用对象的 valueOf 方法。

1. var obj = { p: 1 };
2. obj.valueOf() // { p: 1 }

一般来说,对象的 valueOf 方法总是返回对象自身,这时再自动调用对象的 toString 方法,将其 转为字符串。

1. var obj = { p: 1 };
2. obj.valueOf().toString() // "[object Object]"

对象的 toString 方法默认返回 [object Object] ,所以就得到了最前面那个例子的结果。 知道了这个规则以后,就可以自己定义 valueOf 方法或 toString 方法,得到想要的结果。

1. var obj = {
2.     valueOf: function () {
3.         return 1;
4.     }
5. };
6.
7. obj + 2 // 3

上面代码中,我们定义 obj 对象的 valueOf 方法返回 1 ,于是 obj + 2 就得到了 3 。这 个例子中,由于 valueOf 方法直接返回一个原始类型的值,所以不再调用 toString 方法。 下面是自定义 toString 方法的例子。

1. var obj = {
2.     toString: function () {
3.         return 'hello';
4.     }
5. };
6.
7. obj + 2 // "hello2"

上面代码中,对象 obj 的 toString 方法返回字符串 hello 。前面说过,只要有一个运算子是 字符串,加法运算符就变成连接运算符,返回连接后的字符串。 这里有一个特例,如果运算子是一个 Date 对象的实例,那么会优先执行 toString 方法。

1. var obj = new Date();
2. obj.valueOf = function () { return 1 };
3. obj.toString = function () { return 'hello' };
4.
5. obj + 2 // "hello2"

上面代码中,对象 obj 是一个 Date 对象的实例,并且自定义了 valueOf 方法 和 toString 方法,结果 toString 方法优先执行。

余数运算符

余数运算符( % )返回前一个运算子被后一个运算子除,所得的余数。

1. 12 % 5 // 2

需要注意的是,运算结果的正负号由第一个运算子的正负号决定。

1. -1 % 2 // -1
2. 1 % -2 // 1

所以,为了得到负数的正确余数值,可以先使用绝对值函数。

1. // 错误的写法
2. function isOdd(n) {
3.     return n % 2 === 1;
4. }
5. isOdd(-5) // false
6. isOdd(-4) // false
7.
8. // 正确的写法
9. function isOdd(n) {
10.     return Math.abs(n % 2) === 1;
11. }
12. isOdd(-5) // true
13. isOdd(-4) // false

余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果。

1. 6.5 % 2.1
2. // 0.19999999999999973

自增和自减运算符

自增和自减运算符,是一元运算符,只需要一个运算子。它们的作用是将运算子首先转为数值,然后加上1或者减去1。它们会修改原始变量。

1. var x = 1;
2. ++x // 2
3. x // 2
4.
5. --x // 1
6. x // 1

上面代码的变量 x 自增后,返回 2 ,再进行自减,返回 1 。这两种情况都会使得,原始变 量 x 的值发生改变。 运算之后,变量的值发生变化,这种效应叫做运算的副作用(side effect)。自增和自减运算符是 仅有的两个具有副作用的运算符,其他运算符都不会改变变量的值。 自增和自减运算符有一个需要注意的地方,就是放在变量之后,会先返回变量操作前的值,再进行自 增/自减操作;放在变量之前,会先进行自增/自减操作,再返回变量操作后的值。

1. var x = 1;
2. var y = 1;
3.
4. x++ // 1
5. ++y // 2

上面代码中, x 是先返回当前值,然后自增,所以得到 1 ; y 是先自增,然后返回新的值,所 以得到 2 。

数值运算符,负数值运算符

数值运算符( + )同样使用加号,但它是一元运算符(只需要一个操作数),而加法运算符是二元运 算符(需要两个操作数)。 数值运算符的作用:可以将任何值转为数值(与 Number 函数的作用相同)。

1. +true // 1
2. +[] // 0
3. +{} // NaN

上面代码表示,非数值经过数值运算符以后,都变成了数值(最后一行 NaN 也是数值)。负数值运算符( - ),也同样具有将一个值转为数值的功能,只不过得到的值正负相反。连用两个负数值运算符,等同于数值运算符。

1. var x = 1;
2. -x // -1
3. -(-x) // 1

数值运算符号和负数值运算符,都会返回一个新的值,而不会改变原始变量的值。

指数运算符

指数运算符( ** )完成指数运算,前一个运算子是底数,后一个运算子是指数。

1. 2 ** 4 // 16

注意,指数运算符是右结合,而不是左结合。即多个指数运算符连用时,先进行最右边的计算。

1. // 相当于 2 ** (3 ** 2)
2. 2 ** 3 ** 2
3. // 512

上面代码中,由于指数运算符是右结合,所以先计算第二个指数运算符,而不是第一个。

赋值运算符

赋值运算符(Assignment Operators)用于给变量赋值。 最常见的赋值运算符,当然就是等号( = )。

1. // 将 1 赋值给变量 x
2. var x = 1;
3.
4. // 将变量 y 的值赋值给变量 x
5. var x = y;

赋值运算符还可以与其他运算符结合,形成变体。下面是与算术运算符的结合。

1. // 等同于 x = x + y
2. x += y
3.
4. // 等同于 x = x - y
5. x -= y
6.
7. // 等同于 x = x * y
8. x *= y
9.
10. // 等同于 x = x / y
11. x /= y
12.
13. // 等同于 x = x % y
14. x %= y
15.
16. // 等同于 x = x ** y
17. x **= y

下面是与位运算符的结合。

1. // 等同于 x = x >> y
2. x >>= y
3.
4. // 等同于 x = x << y
5. x <<= y
6.
7. // 等同于 x = x >>> y
8. x >>>= y
9.
10. // 等同于 x = x & y
11. x &= y
12.
13. // 等同于 x = x | y
14. x |= y
15.
16. // 等同于 x = x ^ y
17. x ^= y

这些复合的赋值运算符,都是先进行指定运算,然后将得到值返回给左边的变量。

2,比较运算符

比较运算符用于比较两个值的大小,然后返回一个布尔值,表示是否满足指定的条件。

1. 2 > 1 // true

注意,比较运算符可以比较各种类型的值,不仅仅是数值。

JavaScript 一共提供了8个比较运算符。

        > 大于运算符

        < 小于运算符

        = 大于或等于运算符

        == 相等运算符

        === 严格相等运算符

        != 不相等运算符

        !== 严格不相等运算符

这八个比较运算符分成两类:相等比较和非相等比较。两者的规则是不一样的,对于非相等的比较,算 法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码 点);否则,将两个运算子都转成数值,再比较数值的大小。

非相等运算符:字符串的比较

字符串按照字典顺序进行比较。

1. 'cat' > 'dog' // false
2. 'cat' > 'catalog' // false

JavaScript 引擎内部首先比较首字符的 Unicode 码点。如果相等,再比较第二个字符的 Unicode 码点,以此类推。

1. 'cat' > 'Cat' // true'

上面代码中,小写的 c 的 Unicode 码点( 99 )大于大写的 C 的 Unicode 码点 ( 67 ),所以返回 true 。 由于所有字符都有 Unicode 码点,因此汉字也可以比较。

1. '大' > '小' // false

上面代码中,“大”的 Unicode 码点是22823,“小”是23567,因此返回 false 。

非相等运算符:非字符串的比较

如果两个运算子之中,至少有一个不是字符串,需要分成以下两种情况。

(1)原始类型值

如果两个运算子都是原始类型的值,则是先转成数值再比较。

1. 5 > '4' // true
2. // 等同于 5 > Number('4')
3. // 即 5 > 4
4.
5. true > false // true
6. // 等同于 Number(true) > Number(false)
7. // 即 1 > 0
8.
9. 2 > true // true
10. // 等同于 2 > Number(true)
11. // 即 2 > 1

上面代码中,字符串和布尔值都会先转成数值,再进行比较。 这里需要注意与 NaN 的比较。任何值(包括 NaN 本身)与 NaN 比较,返回的都是 false 。

1. 1 > NaN // false
2. 1 <= NaN // false
3. '1' > NaN // false
4. '1' <= NaN // false
5. NaN > NaN // false
6. NaN <= NaN // false

(2)对象

如果运算子是对象,会转为原始类型的值,再进行比较。 对象转换成原始类型的值,算法是先调用 valueOf 方法;如果返回的还是对象,再接着调 用 toString 方法。

1. var x = [2];
2. x > '11' // true
3. // 等同于 [2].valueOf().toString() > '11'
4. // 即 '2' > '11'
5.
6. x.valueOf = function () { return '1' };
7. x > '11' // false
8. // 等同于 [2].valueOf() > '11'
9. // 即 '1' > '11'

两个对象之间的比较也是如此。

1. [2] > [1] // true
2. // 等同于 [2].valueOf().toString() > [1].valueOf().toString()
3. // 即 '2' > '1'
4.
5. [2] > [11] // true
6. // 等同于 [2].valueOf().toString() > [11].valueOf().toString()
7. // 即 '2' > '11'
8.
9. { x: 2 } >= { x: 1 } // true
10. // 等同于 { x: 2 }.valueOf().toString() >= { x: 1 }.valueOf().toString()
11. // 即 '[object Object]' >= '[object Object]'

严格相等运算符

JavaScript 提供两种相等运算符: == 和 === 。 简单说,它们的区别是相等运算符( == )比较两个值是否相等,严格相等运算符( === )比较它 们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符( === )直接返回 false ,而 相等运算符( == )会将它们转换成同一个类型,再用严格相等运算符进行比较。 本节介绍严格相等运算符的算法。

(1)不同类型的值

如果两个值的类型不同,直接返回 false 。

1. 1 === "1" // false
2. true === "true" // false

上面代码比较数值的 1 与字符串的“1”、布尔值的 true 与字符串 "true" ,因为类型不同,结果都是 false 。

(2)同一类的原始类型值

同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回 true ,值不同就返 回 false 。

1. 1 === 0x1 // true

上面代码比较十进制的 1 与十六进制的 1 ,因为类型和值都相同,返回 true 。 需要注意的是, NaN 与任何值都不相等(包括自身)。正 0 等于负 0 。

1. NaN === NaN // false
2. +0 === -0 // true

(3)复合类型值

两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指 向同一个地址。

1. {} === {} // false
2. [] === [] // false
3. (function () {} === function () {}) // false

上面代码分别比较两个空对象、两个空数组、两个空函数,结果都是不相等。原因是对于复合类型的 值,严格相等运算比较的是,它们是否引用同一个内存地址,而运算符两边的空对象、空数组、空函数 的值,都存放在不同的内存地址,结果当然是 false 。

如果两个变量引用同一个对象,则它们相等。

1. var v1 = {};
2. var v2 = v1;
3. v1 === v2 // true

注意,对于两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值。

1. var obj1 = {};
2. var obj2 = {};
3.
4. obj1 > obj2 // false
5. obj1 < obj2 // false
6. obj1 === obj2 // false

上面的三个比较,前两个比较的是值,最后一个比较的是地址,所以都返回 false 。

(4)undefined 和 null

undefined 和 null 与自身严格相等。

1. undefined === undefined // true
2. null === null // true

由于变量声明后默认值是 undefined ,因此两个只声明未赋值的变量是相等的。

1. var v1;
2. var v2;
3. v1 === v2 // true

严格不相等运算符

严格相等运算符有一个对应的“严格不相等运算符”( !== ),它的算法就是先求严格相等运算符的 结果,然后返回相反值。

1. 1 !== '1' // true
2. // 等同于
3. !(1 === '1')

上面代码中,感叹号 ! 是求出后面表达式的相反值。

相等运算符

相等运算符用来比较相同类型的数据时,与严格相等运算符完全一样。

1. 1 == 1.0
2. // 等同于
3. 1 === 1.0

比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。下面分 成四种情况,讨论不同类型的值互相比较的规则。

(1)原始类型值

原始类型的值会转换成数值再进行比较。

1. 1 == true // true
2. // 等同于 1 === Number(true)
3.
4. 0 == false // true
5. // 等同于 0 === Number(false)
6.
7. 2 == true // false
8. // 等同于 2 === Number(true)
9.
10. 2 == false // false
11. // 等同于 2 === Number(false)
12.
13. 'true' == true // false
14. // 等同于 Number('true') === Number(true)
15. // 等同于 NaN === 1
16.
17. '' == 0 // true
18. // 等同于 Number('') === 0
19. // 等同于 0 === 0
20.
21. '' == false // true
22. // 等同于 Number('') === Number(false)
23. // 等同于 0 === 0
24.
25. '1' == true // true
26. // 等同于 Number('1') === Number(true)
27. // 等同于 1 === 1
28.
29. '\n 123 \t' == 123 // true
30. // 因为字符串转为数字时,省略前置和后置的空格

上面代码将字符串和布尔值都转为数值,然后再进行比较。

(2)对象与原始类型值比较

对象(这里指广义的对象,包括数组和函数)与原始类型的值比较时,对象转换成原始类型的值,再进 行比较。

1. // 对象与数值比较时,对象转为数值
2. [1] == 1 // true
3. // 等同于 Number([1]) == 1
4.
5. // 对象与字符串比较时,对象转为字符串
6. [1] == '1' // true
7. // 等同于 String([1]) == '1'
8. [1, 2] == '1,2' // true
9. // 等同于 String([1, 2]) == '1,2'
10.
11. // 对象与布尔值比较时,两边都转为数值
12. [1] == true // true
13. // 等同于 Number([1]) == Number(true)
14. [2] == true // false
15. // 等同于 Number([2]) == Number(true)

上面代码中,数组 [1] 与数值进行比较,会先转成数值,再进行比较;与字符串进行比较,会先转成 字符串,再进行比较;与布尔值进行比较,对象和布尔值都会先转成数值,再进行比较。

(3)undefined 和 null

undefined 和 null 与其他类型的值比较时,结果都为 false ,它们互相比较时结果 为 true 。

1. false == null // false
2. false == undefined // false
3.
4. 0 == null // false
5. 0 == undefined // false
6.
7. undefined == null // true

(4)相等运算符的缺点

相等运算符隐藏的类型转换,会带来一些违反直觉的结果。

1. 0 == '' // true
2. 0 == '0' // true
3.
4. 2 == true // false
5. 2 == false // false
6.
7. false == 'false' // false
8. false == '0' // true
9.
10. false == undefined // false
11. false == null // false
12. null == undefined // true
13.
14. ' \t\r\n ' == 0 // true

上面这些表达式都不同于直觉,很容易出错。因此建议不要使用相等运算符( == ),最好只使用严 格相等运算符( === )。

不相等运算符

相等运算符有一个对应的“不相等运算符”( != ),它的算法就是先求相等运算符的结果,然后返回 相反值。

1. 1 != '1' // false
2.
3. // 等同于
4. !(1 == '1')

3,布尔运算符

布尔运算符用于将表达式转为布尔值,一共包含四个运算符。

        取反运算符: !

        且运算符: &&

        或运算符: ||

        三元运算符: ?:

取反运算符(!)

取反运算符是一个感叹号,用于将布尔值变为相反值,即 true 变成 false , false 变 成 true 。

1. !true // false
2. !false // true

对于非布尔值,取反运算符会将其转为布尔值。以下六个值取反后为 true ,其他值 都为 false 。

        undefined

        null

        false

        0

        NaN

        空字符串( '' )

1. !undefined // true
2. !null // true
3. !0 // true
4. !NaN // true
5. !"" // true
6.
7. !54 // false
8. !'hello' // false
9. ![] // false
10. !{} // false

上面代码中,不管什么类型的值,经过取反运算后,都变成了布尔值。 如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与 Boolean 函数的作用相同。这是 一种常用的类型转换的写法。

1. !!x
2. // 等同于
3. Boolean(x)

上面代码中,不管 x 是什么类型的值,经过两次取反运算后,变成了与 Boolean 函数结果相同的 布尔值。所以,两次取反就是将一个值转为布尔值的简便写法。

且运算符(&&)

且运算符( && )往往用于多个表达式的求值。 它的运算规则是:如果第一个运算子的布尔值为 true ,则返回第二个运算子的值(注意是值,不是 布尔值);如果第一个运算子的布尔值为 false ,则直接返回第一个运算子的值,且不再对第二个运 算子求值。

1. 't' && '' // ""
2. 't' && 'f' // "f"
3. 't' && (1 + 2) // 3
4. '' && 'f' // ""
5. '' && '' // ""
6.
7. var x = 1;
8. (1 - 1) && ( x += 1) // 0
9. x // 1

上面代码的最后一个例子,由于且运算符的第一个运算子的布尔值为 false ,则直接返回它的 值 0 ,而不再对第二个运算子求值,所以变量 x 的值没变。 这种跳过第二个运算子的机制,被称为“短路”。有些程序员喜欢用它取代 if 结构,比如下面是一 段 if 结构的代码,就可以用且运算符改写。

1. if (i) {
2. doSomething();
3. }
4.
5. // 等价于
6.
7. i && doSomething();

上面代码的两种写法是等价的,但是后一种不容易看出目的,也不容易除错,建议谨慎使用。 且运算符可以多个连用,这时返回第一个布尔值为 false 的表达式的值。如果所有表达式的布尔值都 为 true ,则返回最后一个表达式的值。

1. true && 'foo' && '' && 4 && 'foo' && true
2. // ''
3.
4. 1 && 2 && 3
5. // 3

上面代码中,例一里面,第一个布尔值为 false 的表达式为第三个表达式,所以得到一个空字符串。 例二里面,所有表达式的布尔值都是 true ,所以返回最后一个表达式的值 3 。

或运算符(||)

或运算符( || )也用于多个表达式的求值。它的运算规则是:如果第一个运算子的布尔值 为 true ,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值 为 false ,则返回第二个运算子的值。

1. 't' || '' // "t"
2. 't' || 'f' // "t"
3. '' || 'f' // "f"
4. '' || '' // ""

短路规则对这个运算符也适用。

1. var x = 1;
2. true || (x = 2) // true
3. x // 1

上面代码中,或运算符的第一个运算子为 true ,所以直接返回 true ,不再运行第二个运算子。 所以, x 的值没有改变。这种只通过第一个表达式的值,控制是否运行第二个表达式的机制,就称 为“短路”(short-cut)。 或运算符可以多个连用,这时返回第一个布尔值为 true 的表达式的值。如果所有表达式都 为 false ,则返回最后一个表达式的值。

1. false || 0 || '' || 4 || 'foo' || true
2. // 4
3.
4. false || 0 || ''
5. // ''

上面代码中,例一里面,第一个布尔值为 true 的表达式是第四个表达式,所以得到数值4。例二里 面,所有表达式的布尔值都为 false ,所以返回最后一个表达式的值。 或运算符常用于为一个变量设置默认值。

1. function saveText(text) {
2.     text = text || '';
3.     // ...
4. }
5.
6. // 或者写成
7. saveText(this.text || '')

上面代码表示,如果函数调用时,没有提供参数,则该参数默认设置为空字符串。

三元条件运算符(?:)

三元条件运算符由问号(?)和冒号(:)组成,分隔三个表达式。它是 JavaScript 语言唯一一个需 要三个运算子的运算符。如果第一个表达式的布尔值为 true ,则返回第二个表达式的值,否则返回 第三个表达式的值。

1. 't' ? 'hello' : 'world' // "hello"
2. 0 ? 'hello' : 'world' // "world"

上面代码的 t 和 0 的布尔值分别为 true 和 false ,所以分别返回第二个和第三个表达式的 值。 通常来说,三元条件表达式与 if...else 语句具有同样表达效果,前者可以表达的,后者也能表 达。但是两者具有一个重大差别, if...else 是语句,没有返回值;三元条件表达式是表达式,具 有返回值。所以,在需要返回值的场合,只能使用三元条件表达式,而不能使用 if..else 。

1. console.log(true ? 'T' : 'F');

上面代码中, console.log 方法的参数必须是一个表达式,这时就只能使用三元条件表达式。

4,二进制位运算符

二进制位运算符用于直接对二进制位进行计算,一共有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位带符号的整数。

1. i = i | 0;

上面这行代码的意思,就是将 i (不管是整数或小数)转为32位整数。 利用这个特性,可以写出一个函数,将任意数值转为32位整数。

1. function toInt32(x) {
2.     return x | 0;
3. }

上面这个函数将任意值与 0 进行一次或运算,这个位运算会自动将一个值转为32位整数。下面是这 个函数的用法。

1. toInt32(1.001) // 1
2. toInt32(1.999) // 1
3. toInt32(1) // 1
4. toInt32(-1) // -1
5. toInt32(Math.pow(2, 32) + 1) // 1
6. toInt32(Math.pow(2, 32) - 1) // -1

上面代码中, toInt32 可以将小数转为整数。对于一般的整数,返回值不会有任何变化。对于大于或 等于2的32次方的整数,大于32位的数位都会被舍去。

二进制或运算符

二进制或运算符( | )逐位比较两个运算子,两个二进制位之中只要有一个为 1 ,就返回 1 , 否则返回 0 。

1. 0 | 3 // 3

上面代码中, 0 和 3 的二进制形式分别是 00 和 11 ,所以进行二进制或运算会得 到 11 (即 3 )。 位运算只对整数有效,遇到小数时,会将小数部分舍去,只保留整数部分。所以,将一个小数与 0 进 行二进制或运算,等同于对该数去除小数部分,即取整数位。

1. 2.9 | 0 // 2
2. -2.9 | 0 // -2

需要注意的是,这种取整方法不适用超过32位整数最大值 2147483647 的数。

1. 2147483649.4 | 0;
2. // -2147483647

二进制与运算符

二进制与运算符( & )的规则是逐位比较两个运算子,两个二进制位之中只要有一个位为 0 ,就 返回 0 ,否则返回 1 。

1. 0 & 3 // 0

上面代码中,0(二进制 00 )和3(二进制 11 )进行二进制与运算会得到 00 (即 0 )。

二进制否运算符

二进制否运算符( ~ )将每个二进制位都变为相反值( 0 变为 1 , 1 变为 0 )。它的返回 结果有时比较难理解,因为涉及到计算机内部的数值表示机制。

1. ~ 3 // -4

上面表达式对 3 进行二进制否运算,得到 -4 。之所以会有这样的结果,是因为位运算时, JavaScript 内部将所有的运算子都转为32位的二进制整数再进行运算。 3 的32位整数形式是 00000000000000000000000000000011 ,二进制否运算以后得 到 11111111111111111111111111111100 。由于第一位(符号位)是1,所以这个数是一个负数。 JavaScript 内部采用补码形式表示负数,即需要将这个数减去1,再取一次反,然后加上负号,才能 得到这个负数对应的10进制值。这个数减去1等于 11111111111111111111111111111011 ,再取一次 反得到 00000000000000000000000000000100 ,再加上负号就是 -4 。考虑到这样的过程比较麻 烦,可以简单记忆成,一个数与自身的取反值相加,等于-1。

1. ~ -3 // 2

上面表达式可以这样算, -3 的取反值等于 -1 减去 -3 ,结果为 2 。 对一个整数连续两次二进制否运算,得到它自身。

1. ~~3 // 3

所有的位运算都只对整数有效。二进制否运算遇到小数时,也会将小数部分舍去,只保留整数部分。所以,对一个小数连续进行两次二进制否运算,能达到取整效果。

1. ~~2.9 // 2
2. ~~47.11 // 47
3. ~~1.9999 // 1
4. ~~3 // 3

使用二进制否运算取整,是所有取整方法中最快的一种。 对字符串进行二进制否运算,JavaScript 引擎会先调用 Number 函数,将字符串转为数值。

1. // 相当于~Number('011')
2. ~'011' // -12
3.
4. // 相当于~Number('42 cats')
5. ~'42 cats' // -1
6.
7. // 相当于~Number('0xcafebabe')
8. ~'0xcafebabe' // 889275713
9.
10. // 相当于~Number('deadbeef')
11. ~'deadbeef' // -1

对于其他类型的值,二进制否运算也是先用 Number 转为数值,然后再进行处理。

1. // 相当于 ~Number([])
2. ~[] // -1
3.
4. // 相当于 ~Number(NaN)
5. ~NaN // -1
6.
7. // 相当于 ~Number(null)
8. ~null // -1

异或运算符

异或运算( ^ )在两个二进制位不同时返回 1 ,相同时返回 0 。

1. 0 ^ 3 // 3

上面表达式中, 0 (二进制 00 )与 3 (二进制 11 )进行异或运算,它们每一个二进制位都 不同,所以得到 11 (即 3 )。 “异或运算”有一个特殊运用,连续对两个数 a 和 b 进行三次异或运算, a^=b; b^=a; a^=b; ,可以互换它们的值。这意味着,使用“异或运算”可以在不引入临时变量的前提下,互换两个 变量的值。

1. var a = 10;
2. var b = 99;
3.
4. a ^= b, b ^= a, a ^= b;
5.
6. a // 99
7. b // 10

这是互换两个变量的值的最快方法。 异或运算也可以用来取整。

1. 12.9 ^ 0 // 12

左移运算符

左移运算符( << )表示将一个数的二进制值向左移动指定的位数,尾部补 0 ,即乘以 2 的指定 次方。向左移动的时候,最高位的符号位是一起移动的。

1. // 4 的二进制形式为100,
2. // 左移一位为1000(即十进制的8)
3. // 相当于乘以2的1次方
4. 4 << 1
5. // 8
6.
7. -4 << 1
8. // -8

上面代码中, -4 左移一位得到 -8 ,是因为 -4 的二进制形式 是 11111111111111111111111111111100 ,左移一位后得 到 11111111111111111111111111111000 ,该数转为十进制(减去1后取反,再加上负号)即 为 -8 。 如果左移0位,就相当于将该数值转为32位整数,等同于取整,对于正数和负数都有效。

1. 13.5 << 0
2. // 13
3.
4. -13.5 << 0
5. // -13

左移运算符用于二进制数值非常方便。

1. var color = {r: 186, g: 218, b: 85};
2.
3. // RGB to HEX
4. // (1 << 24)的作用为保证结果是6位数
5. var rgb2hex = function(r, g, b) {
6. return '#' + ((1 << 24) + (r << 16) + (g << 8) + b)
7. .toString(16) // 先转成十六进制,然后返回字符串
8. .substr(1); // 去除字符串的最高位,返回后面六个字符串
9. }
10.
11. rgb2hex(color.r, color.g, color.b)
12. // "#bada55"

上面代码使用左移运算符,将颜色的 RGB 值转为 HEX 值。

右移运算符

右移运算符( >> )表示将一个数的二进制值向右移动指定的位数。如果是正数,头部全部补 0 ; 如果是负数,头部全部补 1 。右移运算符基本上相当于除以 2 的指定次方(最高位即符号位参与 移动)。

1. 4 >> 1
2. // 2
3. /*
4. // 因为4的二进制形式为 00000000000000000000000000000100,
5. // 右移一位得到 00000000000000000000000000000010,
6. // 即为十进制的2
7. */
8.
9. -4 >> 1
10. // -2
11. /*
12. // 因为-4的二进制形式为 11111111111111111111111111111100,
13. // 右移一位,头部补1,得到 11111111111111111111111111111110,
14. // 即为十进制的-2
15. */

右移运算可以模拟 2 的整除运算。

1. 5 >> 1
2. // 2
3. // 相当于 5 / 2 = 2
4.
5. 21 >> 2
6. // 5
7. // 相当于 21 / 4 = 5
8.
9. 21 >> 3
10. // 2
11. // 相当于 21 / 8 = 2
12.
13. 21 >> 4
14. // 1
15. // 相当于 21 / 16 = 1

头部补零的右移运算符

头部补零的右移运算符( >>> )与右移运算符( >> )只有一个差别,就是一个数的二进制形式向 右移动时,头部一律补零,而不考虑符号位。所以,该运算总是得到正值。对于正数,该运算的结果与 右移运算符( >> )完全一致,区别主要在于负数。

1. 4 >>> 1
2. // 2
3.
4. -4 >>> 1
5. // 2147483646
6. /*
7. // 因为-4的二进制形式为11111111111111111111111111111100,
8. // 带符号位的右移一位,得到01111111111111111111111111111110,
9. // 即为十进制的2147483646。
10. */

这个运算实际上将一个值转为32位无符号整数。 查看一个负整数在计算机内部的储存形式,最快的方法就是使用这个运算符。

1. -1 >>> 0 // 4294967295

上面代码表示, -1 作为32位整数时,内部的储存形式使用无符号整数格式解读,值为 4294967295(即 (2^32)-1 ,等于 11111111111111111111111111111111 )。

开关作用

位运算符可以用作设置对象属性的开关。 假定某个对象有四个开关,每个开关都是一个变量。那么,可以设置一个四位的二进制数,它的每个位 对应一个开关。

1. var FLAG_A = 1; // 0001
2. var FLAG_B = 2; // 0010
3. var FLAG_C = 4; // 0100
4. var FLAG_D = 8; // 1000

上面代码设置 A、B、C、D 四个开关,每个开关分别占有一个二进制位。 然后,就可以用二进制与运算,检查当前设置是否打开了指定开关。

1. var flags = 5; // 二进制的0101
2.
3. if (flags & FLAG_C) {
4. // ...
5. }
6. // 0101 & 0100 => 0100 => true

上面代码检验是否打开了开关 C 。如果打开,会返回 true ,否则返回 false 。 现在假设需要打开 A 、 B 、 D 三个开关,我们可以构造一个掩码变量。

1. var mask = FLAG_A | FLAG_B | FLAG_D;
2. // 0001 | 0010 | 1000 => 1011

上面代码对 A 、 B 、 D 三个变量进行二进制或运算,得到掩码值为二进制的 1011 。 有了掩码,二进制或运算可以确保打开指定的开关。

1. flags = flags | mask;

上面代码中,计算后得到的 flags 变量,代表三个开关的二进制位都打开了。 二进制与运算可以将当前设置中凡是与开关设置不一样的项,全部关闭。

1. flags = flags & mask;

异或运算可以切换(toggle)当前设置,即第一次执行可以得到当前设置的相反值,再执行一次又得 到原来的值。

1. flags = flags ^ mask;

二进制否运算可以翻转当前设置,即原设置为 0 ,运算后变为 1 ;原设置为 1 ,运算后变 为 0 。

1. flags = ~flags;

5,其他运算符,运算顺序

void 运算符

void 运算符的作用是执行一个表达式,然后不返回任何值,或者说返回 undefined 。

1. void 0 // undefined
2. void(0) // undefined

上面是 void 运算符的两种写法,都正确。建议采用后一种形式,即总是使用圆括号。因 为 void 运算符的优先性很高,如果不使用括号,容易造成错误的结果。比如, void 4 + 7 实际 上等同于 (void 4) + 7 。 下面是 void 运算符的一个例子。

1. var x = 3;
2. void (x = 5) //undefined
3. x // 5

这个运算符的主要用途是浏览器的书签工具(Bookmarklet),以及在超级链接中插入代码防止网页 跳转。 请看下面的代码。

1. <script>
2. function f() {
3.     console.log('Hello World');
4. }
5. </script>
6. <a href="http://example.com" onclick="f(); return false;">点击</a>

上面代码中,点击链接后,会先执行 onclick 的代码,由于 onclick 返回 false ,所以浏览器 不会跳转到 example.com。 void 运算符可以取代上面的写法。

1. <a href="javascript: void(f())">文字</a>

下面是一个更实际的例子,用户点击链接提交表单,但是不产生页面跳转。

1. <a href="javascript: void(document.form.submit())">
2. 提交
3. </a>

逗号运算符

逗号运算符用于对两个表达式求值,并返回后一个表达式的值。

1. 'a', 'b' // "b"
2.
3. var x = 0;
4. var y = (x++, 10);
5. x // 1
6. y // 10

上面代码中,逗号运算符返回后一个表达式的值。 逗号运算符的一个用途是,在返回一个值之前,进行一些辅助操作。

1. var value = (console.log('Hi!'), true);
2. // Hi!
3.
4. value // true

上面代码中,先执行逗号之前的操作,然后返回逗号后面的值。

运算顺序

优先级:JavaScript 各种运算符的优先级别(Operator Precedence)是不一样的。优先级高的运算符先 执行,优先级低的运算符后执行。

1. 4 + 5 * 6 // 34

上面的代码中,乘法运算符( * )的优先性高于加法运算符( + ),所以先执行乘法,再执行加 法,相当于下面这样。

1. 4 + (5 * 6) // 34
1. var x = 1;
2. var arr = [];
3.
4. var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0];

上面代码中,变量 y 的值就很难看出来,因为这个表达式涉及5个运算符,到底谁的优先级最高,实 在不容易记住。 根据语言规格,这五个运算符的优先级从高到低依次为:小于等于( <= )、严格相等( === )、 或( || )、三元( ?: )、等号( = )。因此上面的表达式,实际的运算顺序如下。

1. var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0];

圆括号的作用

圆括号( () )可以用来提高运算的优先级,因为它的优先级是最高的,即圆括号中的表达式会第一 个运算。

1. (4 + 5) * 6 // 54

上面代码中,由于使用了圆括号,加法会先于乘法执行。 运算符的优先级别十分繁杂,且都是硬性规定,因此建议总是使用圆括号,保证运算顺序清晰可读,这 对代码的维护和除错至关重要。 顺便说一下,圆括号不是运算符,而是一种语法结构。它一共有两种用法:一种是把表达式放在圆括号 之中,提升运算的优先级;另一种是跟在函数的后面,作用是调用函数。 注意,因为圆括号不是运算符,所以不具有求值作用,只改变运算的优先级。

1. var x = 1;
2. (x) = 2;

上面代码的第二行,如果圆括号具有求值作用,那么就会变成 1 = 2 ,这是会报错了。但是,上面的 代码可以运行,这验证了圆括号只改变优先级,不会求值。 这也意味着,如果整个表达式都放在圆括号之中,那么不会有任何效果。

1. (expression)
2. // 等同于
3. expression

函数放在圆括号中,会返回函数本身。如果圆括号紧跟在函数的后面,就表示调用函数。

1. function f() {
2.     return 1;
3. }
4.
5. (f) // function f(){return 1;}
6. f() // 1

上面代码中,函数放在圆括号之中会返回函数本身,圆括号跟在函数后面则是调用函数。 圆括号之中,只能放置表达式,如果将语句放在圆括号之中,就会报错。

1. (var a = 1)
2. // SyntaxError: Unexpected token var

左结合与右结合

对于优先级别相同的运算符,同时出现的时候,就会有计算顺序的问题。

1. a OP b OP c

上面代码中, OP 表示运算符。它可以有两种解释方式。

1. // 方式一
2. (a OP b) OP c
3.
4. // 方式二
5. a OP (b OP c)

上面的两种方式,得到的计算结果往往是不一样的。方式一是将左侧两个运算数结合在一起,采用这种 解释方式的运算符,称为“左结合”(left-to-right associativity)运算符;方式二是将右侧两 个运算数结合在一起,这样的运算符称为“右结合”运算符(right-to-left associativity)。

JavaScript 语言的大多数运算符是“左结合”,请看下面加法运算符的例子。

1. x + y + z
2.
3. // 引擎解释如下
4. (x + y) + z

上面代码中, x 与 y 结合在一起,它们的预算结果再与 z 进行运算。 少数运算符是“右结合”,其中最主要的是赋值运算符( = )和三元条件运算符( ?: )。

1. w = x = y = z;
2. q = a ? b : c ? d : e ? f : g;

上面代码的解释方式如下。

1. w = (x = (y = z));
2. q = a ? b : (c ? d : (e ? f : g));

上面的两行代码,都是右侧的运算数结合在一起。 另外,指数运算符( ** )也是右结合。

1. 2 ** 3 ** 2
2. // 相当于 2 ** (3 ** 2)
3. // 512

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值