Skip to content

JS 运算符

算数运算符

概述

  • 加减乘除: + - * /
  • 自增自减 ++ --
  • 指数运算符: x ** y
  • 取余运算符:%

特殊地

加法重载

JS的相加运算在遇到字符串时会发生重载,变为字符串拼接。其他的- * / 都是将字符串转数值,然后再运算

js
// 非数字转数字后,再计算
console.log(true + true === 1 + true, 1 + true === 2, false + 2 === 2) // true, true, true

// 遇到字符串时,发生加法重载
console.log('3' + 4 + 5, 3 + '4' + 5, 3 + 4 + '5') // 345 345 75
console.log(true + '1', false + '1') // 'true1' 'false1'

// 其他的减乘除运算都不会发生重载,都是转为数值,再计算
console.log(true - true === 0, true - false === 1, true * 2 - 3 === -1) // true, true, true
console.log('3' - 4 - 5, 3 - '4' - 5, 3 - 4 - 5) // -6 -6 -6
console.log('123' - 2 === 121, '62' % 4 === 2) // true, true

对象类型的运算

{a: 1} + 1 = ?
对象会先把自己转为原始类型的值,通过先valueOf()拿到返回值(通常是自身),如果不是原始值,就执行toString(),然后如果是加法的话,自然会发生加法重载。

valueOf()toString() 是 Object.prototype 上的方法, 可通过重写对象的valueOf方法,实现自定义类型转换逻辑。

js
console.log({ a: 1 } - 1) // NaN
console.log({ a: 1 } + 1) // [object Object]1

const obj = { a: 1 }
// 同理,重写toString也能实现同样的效果,
// 如果都重写了的话,先看valueOf的结果是否是原始类型,如果是那就拿去进行后序计算,如果不是,再调用toString
obj.valueOf = function(){
    return 100
}
console.log(obj - 1) // 99
console.log(obj + 1) // 101

取余的结果正负

当取余运算涉及负数时,结果取决于第一个数字的正负性。(吐槽一下,真有人?会对负数取余吗??)

js
console.log(-1 % 2) // -1
console.log(1 % -2) // 1


// 避免正负数的影响,余数应该总是一个整数
function isOdd(n) {
  return Math.abs(n % 2) === 1;
}

指数运算符是右结合

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

比较运算符

概述

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

特殊地

字符串逐字符按照字典序(Unicode码点)比较

js
console.log('cat' > 'dog') // false
console.log('cat' > 'catalog') // false
console.log('cat' > 'Cat') // true'
console.log('大' > '小') // false

原始类型转为数值再比较

js
console.log(5 > '4') // true
console.log(true > false )// true
console.log(2 > true) // true

对象先 valueOftoString

与前一节同理, 如有特殊需要,可重写方法

NaN与任何值比较都返回false

js

console.log(1 > NaN    )  // false
console.log(1 <= NaN   )  // false
console.log('1' > NaN  )  // false
console.log('1' <= NaN )  // false
console.log(NaN > NaN  )  // false
console.log(NaN <= NaN )  // false

严格相等与相等

==比较两个值经过转换后是否相等,===比较两个是否为"同一个"值

严格相等

  • 不同类型的值,直接false

  • 同一类型的原始值,比较其值是否相等,NaN与任何值(包括自身)都不相等

  • 对象类型,比较两个是否指向同一地址

  • undefined 和 null 与自身严格相等

普通相等

原始类型,转换为数值再比较

注意:字符串转数字时,只有纯数字,或纯数字+转义字符,才能被转为正确数字

js

console.log(Number('123'))   //  123
console.log(Number('  \t 123  \n'))   //  123
console.log(Number('  \t 1 23  \n'))   //  NaN
console.log(Number('$123'))   //  NaN
console.log(Number('123$'))   //  NaN
console.log(Number('hello'))   //  NaN

对象类型,转为原始类型再比较

  • 通常都是转为数值再比较
  • 如果遇到字符串,比较字符串

注意:单个元素的数组可转为单个数字

js

console.log(Number([1]) == 1)   //  true

null与undefined

  • 与其他类型比较时,总是返回false
  • 他俩比较时,返回true
  • 如果强行转数字,那么null会是0, undefine会是NaN
js
// 与其他类型比较时,总是返回false
console.log(null == 0, undefined == 0)   //  false, false
console.log(null == false, undefined == false) //  false, false

// 他俩比较时,返回true
console.log(undefined == null, undefined === null) // true, false

// 如果强行转数字,那么null会是0, undefine会是NaN
console.log(Number(null), Number(undefined)) // 0, NaN

false, 0 与空字符串

js
// 不同的两个原始类型转为数字比较
console.log(0 == false, 1 == true)    // true, true
console.log(0 == '', 0 == '0')    // true, true
console.log(false == '', false == '0')    // true, true

console.log(false == 'false', Number(false), Number('false')) // false 0 NaN

// 同类型的直接比较,字符串的比较规则是逐字符比较
console.log('' == '0') // 所以false


console.log(' \t\r\n ' == 0, ' \t\r\n ' == false, ' \t\r\n ' == '')    // true, true, false

布尔运算符

概述

  • 取反:!
  • 且:&&
  • 或:||
  • 三元运算符:? :

特殊的

六个取反为true的

以下几个取反为true,其他都是false

  • undefined
  • null
  • false
  • 0
  • NaN
  • ''
js
console.log(!'') // true
console.log(!' /t /d  ', !' \t\r\n ') // false, false
console.log(!{}, ![])  // false, false

&&运算,当第一个为false时直接短路

||运算,当第一个为true时直接短路

位运算符

关于二进制位

正数不用说,二进制1对应十进制1, 二进制11对应十进制3,二进制101对应十进制5

补码表示法

补码表示法在计算机底层的标准实现,补码保证了+0-0具备同一的表示,且使得加法能算减法 正数的补码 = 本身负数的补码 = 对应正数的补码取反 + 1

下面以八位有符号整数进行举例:

| 十进制数字 | 二进制补码 | | ......... | ......... | | 1 | 00000001 | | -1 | 11111111 | | 2 | 00000010 | | -2 | 11111110 | | 3 | 00000011 | | -3 | 11111101 |

如果是八位无符整数,那11111111就是 255 了,但是现在却变成了 -1 ?

总结分析:

  • 因为补码表示法的存在,负数在正数的基础上取反加一,这个加一导致了进位,所以:以 0 作为区分,最终正数总是表现为首位是 0负数的首位总是 1 ,这就使得最大能表示正整数是01111111 => 127, 同样的,最大能表示的负整数就是 -127。所以在有符二进制数里,首位通常被成为符号位

  • 同时, 这也提醒我们相同的 bit 位,用不同的解释方式得到的结果也不同,如果是 UInt8 ,那最大就是255,如果是 Int8 ,那最大就是 127 ,这其实也就是 ES6+ 中 TypedArray 的机理,即:同一个 arrayBuffer ,用不同的 arrayView,往往可以解析得到不同的数值。

概述

位运算直接处理每一个位(bit),速度快,但不太直观。位运算符只对整数起作用,如果一个运算子不是整数,会自动转为整数后再执行。

  • 二进制或(or):符号为|,表示若两个二进制位都为0,则结果为0,否则为1, 有一个1就是1
  • 二进制与(and):符号为&,表示若两个二进制位都为1,则结果为1,否则为0, 有一个0就是0
  • 二进制否(not):符号为~,表示对一个二进制位取反
  • 异或(xor):符号为^,表示若两个二进制位不相同则为1,相同为0
  • 左移(left shift):符号为<<,整个二进制数位向右移,右移一位相当于十进制数/ 2
  • 右移符(right shift):符号为>>,整个二进制数位向左移,左移一位相当于十进制数* 2
  • 头部补零的右移(zero filled right shift):符号为>>>

应用

快速取整

在 JS 内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数, 基于此可以实现一个快捷的靠0取整方法。相当于直接抛弃小数部分,但是有限制,只能处理 2^32 - 1 量级的数

js
console.log(2.9 | 0, Math.floor(2.9)) // 2, 2
console.log(-2.9 | 0, Math.floor(-2.9)) // -2 , -3

快速开关

其实就是跳过了比较时的数据类型转换,利用底层的位运算来进行直接比较,并且以位为单位,通过一些位运算来获取信息,以此实现极致的优化。

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

// runtime
var flags = 5; // 二进制的0101, 表示 C 开了, A开了

只出现一次的数

leetcode 136. 只出现一次的数字

  • 描述:给一个nums,其中元素要么出现一次,要么两次,找出只出现一次的数字
  • 实现:不用哈希表,从0开始,遍历字符,并异或运算,最后剩下的就是只出现一次的数字
    • 0 ^ 123 = 123
    • 123 ^ 123 = 0

其他运算符

void 运算符

void 运算符的作用是执行一个表达式,忽视返回值(返回 undefined),因为优先级比较高,建议带上括号。

js
var x = 3;
console.log(void (x = 5))  //undefined
console.log(x) // 5

感觉应用场景并不多,早期的时候,用来简化组织页面跳转

html
<a href="javascript: void(submit())">文字</a>

逗号运算符

逗号运算符总是返回最后一个表达式的值

js
var name = 'bob'
var foo = (console.log('Hi!'), name = 'jk',  'byebye !'); // 'Hi!'
console.log(name) // jk
console.log(foo) // byebye !

优先级

MDN - 运算符优先级表