• js提供了指数运算符:* 指数运算符x ** y

加法运算符

基本规则
  • js允许非数值的相加:布尔值自动转化为数值
true + true // 2
1 + true // 2
  • 两个字符串相加,变成连接运算符
'a' + 'bc' // "abc"
  • 字符串+非字符串:非字符串转为字符串,在连接在一起
1 + 'a' // "1a"
false + 'a' // "falsea"
  • 运算子的不同,导致了相加和连接的行为不同,这种现象称为重载。注意,由于从左到右的运算顺序,字符串位置不同,运算结果不同
'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"

除了加法运算符,其他算术运算符(比如减法、除法和乘法)都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。

对象的相加

当运算子为对象时,必须先转换成原始类型的值,然后再相加

var obj = { p: 1 };
obj + 2 // "[object Object]2"
对象转换成原始类型的值的规则
  • 首先调用对象的valueOf方法,返回对象自身
var obj = { p: 1 };
obj.valueOf() // { p: 1 }
  • 然后调用对象的toString方法,将其转为字符串。注意,对象的toString方法默认返回[object Object],所以就得到了最前面那个例子的结果。
var obj = { p: 1 ;}
obj.valueOf().toString() // "[object Object]"
得到规则之后,我们就可以自己定义valueOf方法或toString方法,得到想要的结果。
  • 自定义valueOf
var obj = {
  valueOf: function () {
    return 1;
  }
};

obj + 2 // 3

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

  • 自定义toString
var obj = {
  toString: function () {
    return 'hello';
  }
};

obj + 2 // "hello2"

上面代码中,对象objtoString方法返回字符串hello。前面说过,只要有一个运算子是字符串,加法运算符就变成连接运算符,返回连接后的字符串。

  • 这里有一个特例,如果运算子是一个Date对象的实例,那么会优先执行toString方法。
var obj = new Date();
obj.valueOf = function () { return 1 };
obj.toString = function () { return 'hello' };

obj + 2 // "hello2"

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

余数运算符

  • 运算结果的正负号由第一个运算子正负号决定,不遵循算数法则
-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

数值运算符、负数值运算符

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

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

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

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

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

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

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

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

指数运算符

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

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

比较运算符

比较运算符可以分为相等比较和非相等比较。对于非相等比较,先看两个运算子是否都是字符串,如果是,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算子都转成数值,再比较数值的大小

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

若两个都是字符串,按照字典顺序进行比较,即比较Unicode码点

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

只要两个运算子有一个不是字符串,就属于非字符串比较

  • 若都是原始类型,(字母、字符串、布尔值)先转换成数值再比较。这里注意的是,任何值与NaN比较,返回的都是false(包括NaN本身)
  • 对象会先调用valueOf方法;如果返回的还是对象,再接着调用toString方法,最终转换为原始类型的值。在进行比较

严格相等运算符

严格相等运算符(===)先比较类型,后比较值,只要一个不相等就返回false;而相等运算符当类型不同时,会先转换成相同类型,再比较

  • NaN与任何值都不相等(包括本身),+0=-0
  • 复合类型的数据比较时,不是比较值是否相等,而是比较是否指向同一个地址

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

var v1 = {};
var v2 = v1;
v1 === v2 // true

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

var obj1 = {};
var obj2 = {};

obj1 > obj2 // false
obj1 < obj2 // false
obj1 === obj2 // false

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

  • undefined和null与自身严格相等。另外,由于变量声明后默认值为undefined,所以两个只声明未赋值的变量是相等的
var v1;
var v2;
v1 === v2 // true

相等运算符

  • 原始类型的值转换成数值再比较
  • 对象转换成原始函数的值再比较
  • undefined和null相互比较为true,与其他比较为false

布尔运算符:将表达式转为布尔值

!取反运算符

除了以下6个值取反后为true,其余都是false:

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

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

&&且运算符

运算规则:若第一个为true,则返回第二个的值;若第一个false,直接返回第一个的值,不再对第二个求值

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

var x = 1;
(1 - 1) && ( x += 1) // 0
x // 1

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

if (i) {
  doSomething();
}

// 等价于

i && doSomething();

且运算符可以多个连用,结果会返回第一个为false的值;若所有表达式都为false,则返回最后一个表达式的值

||或运算符

运算规则:
如果第一个运算子的布尔值为true,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为false,则返回第二个运算子的值。

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

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

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

// 或者写成
saveText(this.text || '')

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

二进制运算符

位运算符只对整数起作用,若运算子不是整数,会自动转换为整数后再执行

i = i | 0;

上面这行代码的意思,就是将i(不管是整数或小数)转为32位整数

二进制或运算符|

  • 将小数与0进行二进制或运算,可用于去除小数的小数部分

二进制与运算符&

二进制否运算符~

~ 3 // -4
  • 返回结果有时难以理解,因为涉及到就三级内部的数值表示机制

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

  • 对一个整数连续两次二进制否运算,得到本身。对小数连续两次二进制否运算,达到取整效果
  • 当对字符串等其他类型的值进行二进制否运算时,js会先调用Number函数,将其转化为数值

异或运算符

  • “异或运算”有一个特殊运用,连续对两个数ab进行三次异或运算,a^=b; b^=a; a^=b;,可以互换它们的值。这意味着,使用“异或运算”可以在不引入临时变量的前提下,互换两个变量的值。
  • 异或运算也可以用来取整。
12.9 ^ 0 // 12

应用:开关作用

位运算符可以用作设置对象属性的开关。

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

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

现在假设需要打开ABD三个开关,我们可以构造一个掩码变量。

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

上面代码对ABD三个变量进行二进制或运算,得到掩码值为二进制的1011

有了掩码,二进制或运算可以确保打开指定的开关。

flags = flags | mask;

上面代码中,计算后得到的flags变量,代表三个开关的二进制位都打开了。

二进制与运算可以将当前设置中凡是与开关设置不一样的项,全部关闭。

flags = flags & mask;

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

flags = flags ^ mask;

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

flags = ~flags;

void运算符

  • void运算符的作用是执行一个表达式,然后不返回任何值。
var x = 3;
void (x = 5) //undefined
x // 5
  • 注意的是,我们建议使用void运算符时用圆括号括起来,因为void运算符优先级很高,避免错误结果。比如,void 4 + 7实际上等同于(void 4) + 7
  • 这个运算符主要用于做浏览器的书签工具,以及在超级链接中插入代码防止页面跳转
<a href="javascript: void(document.form.submit())">
  提交
</a>

如上边,用户点击链接提交表单,但是不产生页面跳转

,逗号运算符

  • 逗号运算符用于对两个表达式求值,并返回后一个表达式的值
var x = 0;
var y = (x++, 10);
x // 1
y // 10
  • 用途:在返回一个值前,进行一些辅助操作
var value = (console.log('Hi!'), true);
// Hi!

value // true

运算顺序

  • 注意括号的使用,来避免运算顺序与预期不同
  • 左结合和右结合。大部分都是左结合,但是赋值运算符、三元运算符和指数运算符是右结合
w = x = y = z;
q = a ? b : c ? d : e ? f : g;
w = (x = (y = z));
q = a ? b : (c ? d : (e ? f : g));
2 ** 3 ** 2
// 相当于 2 ** (3 ** 2)
// 512

无欲则刚
76 声望15 粉丝