JS学习(5)运算符

89 阅读20分钟

前言

a在什么情况下能打印1

if (a == 1 && a == 2 && a == 3) {
  console.log(1)
}

算数运算符

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

  • 加法运算符:x + y
  • 减法运算符:x - y
  • 乘法运算符:x * y
  • 除法运算符:x / y
  • 求余运算符:x % y
  • 自增运算符:++xx++
  • 自减运算符:--xx--
  • 正号运算符:+x
  • 负号运算符:-x
  • 指数运算符:x ** y(ES2016引入)

注意,上述运算符大多数可以用于非数字类型,这时会自动将不同类型转换为数字(自动类型转换),然后再进行运算。

加法运算符

加法运算符可以在数字之间求和,也可以在字符串之间拼接。

'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"

上述代码中,第一个例子是两个字符串拼接,等于字符串"345";第二个例子是数字34相加,等于数字7,然后再与字符串"5"拼接,等于字符串"75"

这是因为加法运算符的规则是:

如果两个运算子都是数字,则执行加法运算;

如果两个运算子都是字符串,则执行字符串拼接运算;

如果只有一个运算子是字符串,则将另一个运算子转换为字符串,然后再执行拼接运算。

注意,对于非字符串类型,加法运算符会先转成数值,再执行加法运算。

true + true // 2
true + false // 1

上述代码中,truefalse都会被转为数值,然后相加,所以结果是1

对于nullundefined,则直接转为数值,所以null + null等于0undefined + undefined等于NaN

null + null // 0
undefined + undefined // NaN

对于nullundefined与字符串的加法,则直接转为字符串,所以null + "abc"等于"nullabc"undefined + "abc"等于"undefinedabc"

null + 'abc' // 'nullabc'
undefined + 'abc' // 'undefinedabc'

对于对象,会调用对象的valueOf方法,得到原始类型的值,然后再进行加法运算。

var obj = {
  valueOf: function () {
    return 2;
  }
};
obj + 1 // 3

上述代码中,对象obj转为原始类型的值是2,所以加法运算的结果是3

注意,对于没有valueOf方法或valueOf方法返回的不是原始类型的对象,加法运算符会调用对象的toString方法,将对象转为字符串,然后再执行字符串拼接运算。

var obj = {
  toString: function () {
    return 'hello';
  }
};
obj + 1 // 'hello1'

上述代码中,对象obj转为原始类型的值是字符串"hello",所以加法运算的结果是字符串"hello1"

有一个特例,如果一个运算子是一个Date对象的实例,那么会优先调用该对象的toString方法,然后再执行加法运算。

var obj = new Date();
obj.valueOf = function () {
  return 1;
};
obj.toString = function () {
  return 2
}
obj + 1 // 3

四则运算中的 - * / 都是转成数字,如果无法转成数字,则返回NaN

求余运算符

求余运算符%返回前一个运算子除以后一个运算子的余数。

5 % 2 // 1

上述代码中,5除以2的余数是1

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

-5 % 2 // -1
5 % -2 // 1

上述代码中,第一个运算子是负数,所以余数是负数;第二个运算子是负数,所以余数是正数。

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

function isOdd(n) {
  return Math.abs(n % 2) === 1;
}
isOdd(-5) // true
isOdd(-4) // false

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

6.5 % 2.1 // 0.19999999999999973

自增运算符、自减运算符

需要注意的就是运算符再前还是在后的区别

在前:先做自增或自减,然后再参与运算

在后:先做运算,然后再自增或者自减

let a = 1;
let b = ++a + 1;
console.log(a); // 2
console.log(b); // 3

let c = 1;
let d = c++ + 1;
console.log(c); // 2
console.log(d); // 2

正号运算符、负号运算符

正号运算符的作用在于可以将任何值转为数值(与Number函数的作用相同)

+true // 1
+[] // 0
+{} // NaN

上面代码表示,非数值经过数值运算符以后,都变成了数值(最后一行NaN也是数值)。具体的类型转换规则,见上一讲JS学习(4)类型转换

负号运算符也同样具有将一个值转为数值的功能,只不过得到的值正负相反。连用两个负号运算符,等同于正号运算符。

var a = 1
-a // -1
-(-a) // 1

上面代码最后一行的圆括号不可少,否则会变成自减运算符。

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

指数运算符

指数运算符(**)返回基数指数次幂的值。

2 ** 2 // 4
3 ** 3 // 27

之前的写法是Math.pow(2, 3)

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

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

上面代码中,先计算右边的3 ** 2,得到9,然后再计算2 ** 9,得到512

赋值运算符

赋值运算符(Assignment Operators)用于给变量赋值。

最常见的赋值运算符,当然就是等号(=)。

var a = 1;
var b = a;

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

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
x |= y // x = x | y

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

比较运算符

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

2 > 1 // true

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

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

  • > 大于运算符
  • < 小于运算符
  • >= 大于或等于运算符
  • <= 小于或等于运算符
  • == 相等运算符
  • === 严格相等运算符
  • != 不相等运算符
  • !== 严格不相等运算符

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

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

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

'Beijing' < 'Shanghai' // true

上面代码中,“Beijing”在字典里比“Shanghai”靠前,所以返回true

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

需要注意的是,这种比较方式是区分大小写的,大写的字母会排在小写字母前面。

'B' < 'a' // false

上面代码中,由于“B”的码点是66,“a”的码点是97,因此返回false


由于所有字符都有Unicode码点,因此汉字也可以比较。

```js
'大' > '小' // false

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

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

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

  1. 原始类型值

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

5 > '4' // true
// 相当于 5 > Number('4')
// 即 5 > 4

true > false // true
//  相当于 Number(true) > Number(false)
// 即 1 > 0

上面代码中,字符串'4'转成数值4,所以返回true

这里需要注意与NaN的比较。任何值(包括NaN本身)与NaN使用非相等运算符进行比较,返回的都是false。

1 > NaN // false
1 < NaN // false
1 >= NaN // false
1 <= NaN // false
NaN > NaN // false
NaN < NaN // false
NaN >= NaN // false
NaN <= NaN // false
  1. 对象

如果运算子是对象,会转为原始类型的值,再进行比较。

对象转换成原始类型的值,算法是先调用valueOf方法;如果返回的还是对象,再接着调用toString方法,详细解释参见JS学习(4)类型转换

var a = [2]
a > '10' // true
// 相当于 [2].valueOf().toString() > '10'
// 即 '2' > '10'

[1] > [2] // false
// 相当于 [1].valueOf().toString() > [2].valueOf().toString()
// 即 '1' > '2'

[] > [ NaN ] // false
// 相当于 [].valueOf().toString() > [NaN].valueOf().toString()
// 即 '' > 'NaN'

a.valueOf = function () { return '1' }
a > '10' // false
// 相当于 a.valueOf() > '10'
// 即 '1' > '10'

严格相等运算符

JavaScript提供两种相等运算符: == 和 ===

简单说,它们的区别是相等运算符(==)比较两个值是否相等,严格相等运算符(===)比较它们是否为"同一个值"。如果两个值不是同一类型,严格相等运算符(===)直接返回false,而相等运算符(==)会将它们转换成同一个类型,再用严格相等运算符进行比较。

  1. 不同类型的值

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

1 === '1' // false
true === 1 // false
  1. 同类型的原始类型值

同类型的原始类型值(比如两个数值或两个字符串),只要它们的值相等,严格相等运算符就返回true。

'1' === '1' // true
1 === 0x1 // true

需要注意的是,NaN与任何值都不相等(包括自身)。另外,正0等于负0。

NaN === NaN // false
0 === -0 // true
  1. 复合类型值

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

var obj1 = { name: 'zhangsan' }
var obj2 = { name: 'zhangsan' }
obj1 === obj2 // false

上面代码中,obj1obj2都是对象,但它们是两个不同的对象,尽管值相同,也是不相等的。

需要注意的是,对于两个不同的对象,即使它们的内容相同,也是不相等的。

var obj1 = { name: 'zhangsan' }
var obj2 = { name: 'zhangsan' }
obj1 === obj2 // false

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

var obj1 = {}
var obj2 = {}
obj1 > obj2 // false
obj1 < obj2 // false
obj1 === obj2 // false

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

  1. null和undefined
null == undefined // true
null === undefined // false

严格不相等运算符

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

3 !== '3' // true
// 相当于 !(3 === '3')

相等运算符

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

1 == 1.0
// 相当于 1 === 1.0

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

  1. 原始类型值

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

1 == true // true
// 相当于 1 === Number(true)
// 即 1 === 1

0 == false // true
// 相当于 0 === Number(false)
// 即 0 === 0

'1' == true // true
// 相当于 Number('1') === Number(true)
// 即 1 === 1

'1' == 1 // true
// 相当于 Number('1') === 1
// 即 1 === 1

'' == 0 // true
// 相当于 Number('') === 0
// 即 0 === 0

'' == false // true
// 相当于 Number('') === Number(false)
// 即 0 === 0

'0' == false // true
// 相当于 Number('0') === Number(false)
// 即 0 === 0

'0' == 0 // true
// 相当于 Number('0') === 0
// 即 0 === 0

上面代码中,所有等于运算符的比较,都是转换成数值以后再进行比较。

  1. 对象和原始类型比较

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

具体来说,先调用对象的valueOf()方法,如果得到原始类型的值,就按照上一小节的规则,互相比较;如果得到的还是对象,则再调用toString()方法,得到字符串形式,再进行比较。

[1] == 1 // true

[1] == '1' // true

[1, 2] == '1,2' // true

[1] == true // true

上面例子中,JavaScript引擎会先对数组[1]调用数组的valueOf()方法,由于返回的还是一个数组,所以会接着调用数组的toString()方法,得到字符串形式,再按照上一小节的规则进行比较。

var obj = {
  valueOf: function () {
    return obj
  },
  toString: function () {
    return '2'
  }
}
obj == 2 // true

上面例子中,自定义的obj对象,valueOf()方法返回的还是自身,因此接着调用toString()方法,得到字符串形式,再与数值2进行比较。

  1. null和undefined

undefined和null只有与自身比较,或者互相比较时,才会返回true;与其他类型的值比较时,结果都为false。

null == undefined // true
false == null // false
  1. 相等运算符的缺点

相等运算符隐藏的类型转换,会带来一些违反直觉的结果。因此建议不要使用相等运算符(==)最好只使用严格相等运算符(===)。

不相等运算符

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

1 != '1'
// 相当于 1 == '1' 的相反值,即 false

布尔运算符(逻辑运算符)

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

  • ! 取反运算符
  • && 逻辑与运算符
  • || 逻辑或运算符
  • ?: 三元运算符

取反运算符

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

!true // false
!false // true

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

undefined
null
0
''
NaN
false

如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与Boolean函数的作用相同。这是一种常用的类型转换的写法。

!!x
// 相当于
Boolean(x)

逻辑与运算符

逻辑与运算符(&&)往往用于多个表达式的求值。

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

'a' && '' // ""
'a' && 'b' // "b"
'a' && (1 + 2) // 3
'' && 'b' // ""
false && 'b' // false
'a' && NaN // NaN

这种跳过第二个运算子的机制,被称为"短路"。有些程序员喜欢用它取代if结构,比如下面是段if结构的代码,就可以用且运算符改写。

if (a) {
  func()
}

可以改写为

a && func()

上面代码的两种写法是等价的,但后一种不容易看出目的,也不容易除错,建议谨慎使用。

且运算符可以多个连用,这时返回第一个布尔值为false的表达式的值。如果所有表达式的布尔值都为true,则返回最后一个表达式的值。

true && 'a' && '' && 'b' // ""

1 && 2 && 3 // 3

逻辑或运算符

逻辑或运算符(||)也用于多个表达式的求值。

逻辑或运算符(||)的运算规则是:如果第一个运算子的布尔值为true,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为false,则返回第二个运算子的值。

't' || '' // "t"
't' || 'f' // "t"
'' || 'f' // "f"
false || 'f' // "f"

逻辑或运算符可以多个连用,这时返回第一个布尔值为true的表达式的值。如果所有表达式的布尔值都为false,则返回最后一个表达式的值。

false || 0 || '' || 2 || 'a' // "2"

或运算符常用于为一个变量设置默认值。

function saveText(text) {
  text = text || 'default'
  // ...
}

上面代码含义是,如果函数saveText()的参数text没有赋值,则默认使用字符串"default"。

需要注意的是,逻辑或运算符在比较时,会将其两边的值都转为布尔值,然后再返回。

三元运算符

三元运算符(?)是JavaScript唯一的一个运算符,需要三个运算子,语法如下。

condition ? expr1 : expr2

上面代码中,condition是一个表达式,它返回一个布尔值。如果为true,则返回expr1的值,否则返回expr2的值。

var even = (n % 2 === 0) ? true : false;

位运算符

按位运算符是将操作数换算成32位的二进制整数,然后按每一位来进行运输。例如:

5 的 32 位是:

00000000000000000000000000000101

按位非

按位非运算符~会把数字转为32位二进制整数,然后反转每一位。所有的1变为0,所有的0变为1。

例如:

~5的32位为

11111111111111111111111111111010

转换出来为-6

按位非,实质上是对操作数求负,然后减去1。

按位与

按位与运算符&会把两个数字转为32位二进制整数,并对两个数的每一位执行按位与运算。按位与的规则如下表:

第一个数字第二个数字结果
111
100
010
000

例如:

12 & 10 // 8

12的32位二进制表示为:1100 10的32位二进制表示为:1010

按位与的结果为:1000

按位或

按位或运算符|会把两个数字转为32位二进制整数,并对两个数的每一位执行按位或运算。按位或的规则如下表:

第一个数字第二个数字结果
111
101
011
000

例如:

12 | 10 // 14

12的32位二进制表示为:1100 10的32位二进制表示为:1010

按位或的结果为:1110

按位异或

按位异或运算符^会把两个数字转为32位二进制整数,并对两个数的每一位执行按位异或运算。按位异或的规则如下表:

运算规则为两位不同返回1,两位相同返回0。

第一个数字第二个数字结果
110
101
011
000

例如:

12 ^ 10 // 6

12的32位二进制表示为:1100 10的32位二进制表示为:1010

按位异或的结果为:0110

按位异或如果是非整数值,如果两个操作数中只有一个为真,就返回1,如果两个操作数都是真,或者都是假,就返回0。 示例如下:

true ^ 'abc' // 1
true ^ false // 1
false ^ true // 1
false ^ false // 0

这里'abc'被转换为了NaN

移位运算符

按位移位运算符<<和>>会将所有位向左或者向右移动指定的数量,实际上就是高效率地将数字乘以或者除以2的指定数的次方。

<< :乘以2的指定数的次方

例如:

12 << 2 // 48

12的32位二进制表示为:1100

左移2位的结果为:110000

转换成10进制为48

>> : 除以2的指定数的次方

例如:

12 >> 2 // 3

12的32位二进制表示为:1100

右移2位的结果为:11

转换成10进制为3

其他运算符

void运算符

void运算符的作用是执行一个表达式,然后返回undefined。

void 0 // undefined
void 1 + 2 // NaN
void (2 + 2) // undefined

上面是void运算符的两种写法,都正确。建议采用后一种形式,即总是使用圆括号。因为void运算符的优先性很高,如果不使用括号,容易造成错误的结果。

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

<script>
function f() {
  console.log('Hello World');
}
</script>
<a href="javascript:void(f())">Click here</a>

上面代码中,点击链接后,浏览器不会跳转,而是执行函数f。

逗号运算符

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

let x = 1;
x = (x++, x);
// 等同于
x = x++;

运算顺序

优先级

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

4 + 5 * 6 // 34

上面代码中,乘法运算符(* )的优先级高于加法运算符(+),所以先执行乘法,再执行加法,得到答案34。

圆括号的作用

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

(4 + 5) * 6 // 54

上面代码中,由于圆括号的优先级最高,所以先运算括号里面的加法,然后再运算括号外面的乘法。

结合性

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

a OP b OP c

上面代码中,如果OP表示同一种运算符,那么它究竟应该先运算a OP b,还是先运算b OP c,取决于OP的运算符结合性。

运算符的结合性有两种,分别是左结合和右结合。

  • 左结合:多数运算符都是左结合的,即从左到右依次运算,比如a + b + c会先运算a + b,然后再运算c

  • 右结合:即从右到左依次运算,比如a OP b OP c会先运算b OP c,然后再运算a OP (b OP c)

少数运算符是"右结合",其中最主要的是赋值运算符(=)和三元条件运算符(?:)。

a = b = c = d
// 等同于
a = (b = (c = d))

q = a ? b : c ? d : e
// 等同于
q = a ? b : (c ? d : e)

另外,指数运算符(**)也是右结合。

优先级表

JavaScript所有运算符的优先级,可以参考MDN的文档

总结

a在什么情况下能打印1

if (a == 1 && a == 2 && a == 3) {
  console.log(1)
}

方法一:利用toString()方法

var a = {
  i: 1,
  toString: function() {
    return a.i++;
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log(1)
}

方法二:利用valueOf()方法

var a = {
  i: 1,
  valueOf: function() {
    return a.i++;
  }
}
if (a == 1 && a == 2 && a == 3) {
  console.log(1)
}