# JavaScript 基础之运算符

# 算术运算符

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

  • 加法运算符:x + y
  • 减法运算符:x - y
  • 乘法运算符:x * y
  • 除法运算符:x / y
  • 指数运算符:x ** y
  • 余数运算符:x % y
  • 自增运算符:++x 或者 x++
  • 自减运算符:--x 或者 x--
  • 数值运算符: +x
  • 负数值运算符:-x

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

# 加法运算符

JavaScript 允许非数值的两个操作数相加。比如两个布尔值相加,或者是数值与布尔值相加,布尔值都会自动转成数值,然后再相加。

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

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

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

如果一个操作数是字符串,另一个操作数是非字符串,这时非字符串会转成字符串,再连接在一起。

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

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

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

上面代码中,由于从左到右的运算次序,字符串的位置不同会导致不同的结果。

如果操作数中含有对象/数组/函数,会先将复合类型的值转为原始类型的值,然后再相加。

复合类型的值转换成原始类型的值,算法是先调用 valueOf 方法;如果返回的还是对象,再接着调用 toString 方法,详细解释参见《数据类型转换》。

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

# 余数运算符

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

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

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

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

// 错误的写法
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
2
3
4
5
6
7
8
9
10
11
12
13

# 自增和自减运算符

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

var a = '10';
var b = '10';
a = a + 1;
b++;
console.log(a);	// '101'
console.log(b);	// 11
1
2
3
4
5
6

运算之后,变量的值发生变化,这种效应叫做运算的副作用(side effect)。自增和自减运算符是仅有的两个具有副作用的运算符,其他运算符都不会改变变量的值。

自增和自减运算符有一个需要注意的地方,就是放在变量之后,会先返回变量操作前的值,再进行自增/自减操作;放在变量之前,会先进行自增/自减操作,再返回变量操作后的值

var x = 1;
var y = 1;

console.log(x++); // 1
console.log(++y); // 2
1
2
3
4
5

# 指数运算符

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

2 ** 4 // 16
1

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

2 ** 3 ** 2 // 相当于 2 ** (3 ** 2),即2的9次幂
1

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

数值运算符(+)同样使用加号,但它是一元运算符(只需要一个操作数),而加法运算符是二元运算符(需要两个操作数)。

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

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

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

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

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

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

# 比较运算符

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

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

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

这八个比较运算符分成两类:相等比较和非相等比较。两者的规则是不一样的,对于非相等的比较,算法是如果两个操作数都是字符串,则比较 Unicode 码点的大小;否则,将两个操作数都转成数值,再比较数值的大小。

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

JavaScript 引擎内部首先比较首字符的 Unicode 码点。如果相等,再比较第二个字符的 Unicode 码点,以此类推。由于所有字符都有 Unicode 码点,因此汉字也可以比较。

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

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

这里需要注意:任何值(包括 NaN 本身)与 NaN 比较,返回的都是 false

如果操作数是对象,会转为原始类型的值,再进行比较。对象转换成原始类型的值,算法是先调用 valueOf 方法;如果返回的还是对象,再接着调用 toString 方法,详细解释参见《数据类型转换》。

# 3、严格相等运算符

下面分成四种情况,讨论严格相等运算符的比较规则。

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

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

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

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

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

上面例子中分别比较两个空对象、两个空数组、两个空函数,结果都是不相等。因为运算符两边的空对象、空数组、空函数的值,都存放在不同的内存地址,结果当然是 false

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

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

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

(4)undefinednull 与自身严格相等。

# 4、严格不相等运算符

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

# 5、相等运算符

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

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

(1)原始类型的值进行比较时,会转换成数值再进行比较

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

数组[1]与数值进行比较,会先转成数值,再进行比较;与字符串进行比较,会先转成字符串,再进行比较;与布尔值进行比较,数组和布尔值都会先转成数值,再进行比较

(3)undefinednull 与其他类型的值比较时,结果都为 false ,它们互相比较时结果为 true

# 6、不相等运算符

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

# 逻辑运算符

# 1、与(&&

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

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

't' && ''       // ""
't' && 'f'      // "f"
't' && (1 + 2)  // 3
'' && 'f'       // ""
'' && ''        // ""

// 由于第一个表达式的布尔值为 false,则直接返回它的值 0
// 而不再对第二个表达式求值,所以变量 x 的值没变
var x = 1;
(1 - 1) && ( x += 1) // 0
x // 1
1
2
3
4
5
6
7
8
9
10
11

上面最后一个例子中这种通过第一个表达式的值,控制是否运行第二个表达式的机制,被称为“短路(short-cut)”。有些程序员喜欢用它取代 if 结构,比如下面是一段if结构的代码,就可以用且运算符改写。

if (i) {
  doSomething();
}

// 等价于

i && doSomething();
1
2
3
4
5
6
7

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

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

// 第一个布尔值为 false 的表达式为第三个表达式
// 所以返回一个空字符串
true && 'foo' && '' && 4 && 'foo' && true

// 所有表达式的布尔值都是 true,所以返回最后一个表达式的值 3
1 && 2 && 3
1
2
3
4
5
6

# 2、或(||

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

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

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

// 第一个表达式为 true,所以直接返回 true
// 不再运行第二个表达式,所以变量x的值没变
var x = 1;
true || (x = 2) // true
x // 1
1
2
3
4
5

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

// 第一个布尔值为 true 的表达式是第四个表达式,所以得到数值 4
false || 0 || '' || 4 || 'foo' || true

// 所有表达式的布尔值都为false
// 所以返回最后一个表达式的值空字符串
false || 0 || ''
1
2
3
4
5
6

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

function saveText(text) {
  // 如果函数调用时,没有提供参数,则该参数默认设置为空字符串
  text = text || '';
}

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

# 3、非(!

非运算符(!)用于将布尔值变为相反值,即true变成false,false变成true。

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

  • undefined
  • null
  • false
  • +0-0
  • NaN
  • ''(空字符串)

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

!!x
// 两次取反就是将一个值转为布尔值的简便写法,等同于
Boolean(x)
1
2
3

# 位运算符

二进制位运算符用于直接对二进制位进行计算,一共有7个。

  • 二进制或运算符(or):符号为 | ,表示若两个二进制位都为0,则结果为0,否则为1。
  • 二进制与运算符(and):符号为 & ,表示若两个二进制位都为1,则结果为1,否则为0。
  • 二进制否运算符(not):符号为 ~ ,表示对一个二进制位取反。
  • 异或运算符(xor):符号为 ^ ,表示若两个二进制位不相同,则结果为1,否则为0。
  • 左移运算符(left shift):符号为 << ,详见参考链接。
  • 右移运算符(right shift):符号为 >> ,详见参考链接。
  • 头部补零的右移运算符(zero filled right shift):符号为 >>> ,详见参考链接。

参考链接

# 赋值运算符

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

最常见的赋值运算符,当然就是等号(=)。赋值运算符还可以与其他运算符结合,形成变体。

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
1
2
3
4
5
6

# 三目运算符

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

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

上面代码的 t0 的布尔值分别为 truefalse ,所以分别返回第二个和第三个表达式的值。

通常来说,三目运算符与 if...else 语句具有同样表达效果,但是两者具有一个重大差别,if...else 是语句,没有返回值;三目运算符是表达式,具有返回值。所以,在需要返回值的场合,只能使用三目运算符,而不能使用 if..else

如果三元运算符中的某一部分不需要做任何的处理,我们用 null / undeifned / void 0 占位即可。

var num = 12;
num>10 ? num++ : null;
1
2

如果需要执行多项操作,我们把其用小括号包裹起来,每条操作语句用逗号分隔。

var num=10;
num>=10 ? (num++, num*=10) : null;
1
2

# instanceof 运算符

instanceof 运算符返回一个布尔值,用来验证一个对象是否为指定构造函数的实例。obj instanceof Object 返回 true ,就表示 obj 对象是 Object 的实例。

var v = new Vehicle();
v instanceof Vehicle // true
1
2

instanceof运算符的左边是实例对象,右边是构造函数。instanceof的原理是检查右边构建函数的原型对象(prototype),是否在左边对象的原型链上。因此,下面两种写法是等价的。

v instanceof Vehicle
// 等同于
Vehicle.prototype.isPrototypeOf(v)
1
2
3

由于instanceof检查整个原型链,因此同一个实例对象,可能会对多个构造函数都返回true

var d = new Date();
d instanceof Date // true
d instanceof Object // true
1
2
3

上面代码中,d同时是DateObject的实例,因此对这两个构造函数都返回true

由于任意对象(除了null)都是Object的实例,所以instanceof运算符可以判断一个值是否为非null的对象。

var obj = { foo: 123 };
obj instanceof Object // true

null instanceof Object // false
1
2
3
4

有一种特殊情况,就是左边对象的原型链上,只有null对象。这时,instanceof判断会失真。

var obj = Object.create(null);
typeof obj // "object"
Object.create(null) instanceof Object // false
1
2
3

上面代码中,Object.create(null)返回一个新对象obj,它的原型是null。右边的构造函数Objectprototype属性,不在左边的原型链上,因此instanceof就认为obj不是Object的实例。但是,只要一个对象的原型不是nullinstanceof运算符的判断就不会失真。

instanceof运算符的一个用处,是判断值的类型。

var x = [1, 2, 3];
var y = {};
x instanceof Array // true
y instanceof Object // true
1
2
3
4

上面代码中,instanceof运算符判断,变量x是数组,变量y是对象。

注意,instanceof运算符只能用于对象,不适用原始类型的值。

var s = 'hello';
s instanceof String // false
1
2

上面代码中,字符串不是String对象的实例(因为字符串不是对象),所以返回false

此外,对于undefinednullinstanceof运算符总是返回false

undefined instanceof Object // false
null instanceof Object // false
1
2

利用instanceof运算符,还可以巧妙地解决,调用构造函数时,忘了加new命令的问题。

function Fubar (foo, bar) {
  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  } else {
    return new Fubar(foo, bar);
  }
}
1
2
3
4
5
6
7
8

上面代码使用instanceof运算符,在函数体内部判断this关键字是否为构造函数Fubar的实例。如果不是,就表明忘了加new命令。

# 运算顺序

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

# 圆括号的作用

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

运算符的优先级别十分繁杂,且都是硬性规定,因此建议总是使用圆括号,保证运算顺序清晰可读,这对代码的维护和除错至关重要。

圆括号不是运算符,而是一种语法结构。它一共有两种用法:一种是把表达式放在圆括号之中,提升运算的优先级;另一种是跟在函数的后面,作用是调用函数。

注意,因为圆括号不是运算符,所以不具有求值作用,只改变运算的优先级。

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

function f() {
  return 1;
}

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

圆括号之中,只能放置表达式,如果将语句放在圆括号之中,就会报错。

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

# 左结合与右结合

对于优先级别相同的运算符,大多数情况,计算顺序总是从左到右,这叫做运算符的“左结合”(left-to-right associativity),即从左边开始计算。

x + y + z // 先计算最左边的x与y的和,然后再计算与z的和
1

但是少数运算符的计算顺序是从右到左,即从右边开始计算,这叫做运算符的“右结合”(right-to-left associativity)。其中,最主要的是赋值运算符(=)和三元条件运算符(?:)。

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

// 相当于

w = (x = (y = z));
q = a ? b : (c ? d : (e ? f : g));
1
2
3
4
5
6
7

指数运算符(**)也是右结合的。

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

逻辑与的优先级高于逻辑或:

0 || 2 && false || 3

// 相当于
0 || (2 && false) || 3

// 相当于
0 || false || 3
1
2
3
4
5
6
7
上次更新: 2020年6月18日星期四 14:37