js运算符妙用指南

481 阅读7分钟

前言

通俗来讲,语言运算符就是对计算策略的一种封装,主要用于计算数值的。在实际开发中,可使用运算符技巧来提供开发效率。不少框架、库的核心代码都有使用运算符来简化代码,如 JavaScript 中的库常有如下情景:!function(){ console.log('!!!!') }(),通过使用运算符!将函数转化为表达式直接调用函数,而我们常接触的类似情景有闭包立即执行函数(function(){ console.log('立即执行函数') }())。可见妙用运算符可得到事半功倍的效果,接下来对常用的运算符作总结~

运算符分类

JavaScript 的常用运算符有以下:

  • 算术运算符:+-*/%-(一元取反) 、 ++--

  • 等同运算符与全同运算符:==(等于) 、 ===(恒等于)、 !=(不等于) 、 !==(不恒等于)

  • 比较运算符:<><=>=

  • 字符串运算符: +

  • 逻辑运算符:&&||!

  • 赋值运算符:=+=*=-=/=

情景妙用

+-~()function

frontender 都清楚表达式可以执行计算结果,而函数是先定义后执行的。

另外大致回顾下函数的函数声明函数表达式的区别,如下。

函数声明一般为具名函数声明,且所定义的函数声明的函数默认会触发声明提前(hoisting),因此允许函数在声明位置前执行,如:

setFn(); // this is setFn
function setFn() {
  console.log('this is setFn');
}

函数表达式一般是匿名函数赋值给变量,因为没有声明提前(hoisting),不像函数声明那样,在定义函数表达式之前不能使用函数表达式,必须履行先定义后调用的规则,如:

setFn(); // VM281:1 Uncaught TypeError: setFn is not a function
var setFn = function() {
  console.log('this is setFn');
};

此时上述函数表达式执行会报错,因为函数表达式严格遵守先定义后调用执行的规律。

报错原因是混淆了函数声明和函数调用,函数声明定义的函数 setFn,就应该以 setFn(); 的方式调用。

此处提出一个疑问:函数能否在声明的时候直接执行使用呢?

答案是不允许的!因为函数声明不是表达式,不能直接执行计算结果的。

那么如果希望执行函数function setFn() { console.log('this is setFn');}()不报错该有啥办法实现?

此时可以尝试使用运算符将函数声明转化为表达式由或者使用闭包立即执行函数,也就是让一个函数声明语句变成了一个表达式即可,如下

<!-- prettier-ignore -->
/*
打印:
this is set Fn
true
 */
!function setFn() {console.log('this is setFn');}();
/*
打印:
this is set Fn
NaN
 */
+function setFn() {console.log('this is setFn');}();

/*
打印:
this is set Fn
NaN
 */
-function setFn() {console.log('this is setFn');}();

/*
打印:
this is set Fn
-1
 */
~function setFn() {console.log('this is setFn');}();

/*
打印:
this is set Fn
undefined
 */
(function setFn() {
  console.log('this is setFn');
})();

此处解析下为啥使用运算符直接执行函数,除了打印字符 this is setFn 外还出现 true、NaN、-1、undefined 呢,因为函数执行后默认会返回undefined,而 undefined 取逻辑!为 true!undefined=>true、undefined 取运算+为 NaN +undefined=>NaN、undefined 取运算-为 NaN-undefined=>NaN、undefined 取逻辑~为 -1+undefined=>-1

从上述案例可见,通过诸如括号、赋值符、逻辑运算符、逗号等各种操作运算符都发挥了一个极其重要的作用,它将一个函数声明转化成了一个表达式,让解析器不再以函数声明的方式处理函数 setFn,而是作为一个函数表达式处理,也因此只有在程序执行到函数 setFn 时它才能被访问。

所以,任何消除函数声明和函数表达式间歧义的方法,都可以被解析器正确识别。比如,对函数一元运算可以算的上是消除歧义最快的方式,感叹号只是其中之一,如果不在乎返回值,使用一元运算都是有效的:

<!-- prettier-ignore -->
var i = function(){return 10}() // undefined
1 && function(){return true}() // true
1, function(){alert('iifksp')}() // undefined

/*
最快的一元
 */
!function setFn() {console.log('this is setFn');}();
+function setFn() {console.log('this is setFn');}();
-function setFn() {console.log('this is setFn');}();
~function setFn() {console.log('this is setFn');}();

甚至下面这些关键字,都能很好的工作:

<!-- prettier-ignore -->
// 甚至下面这些关键字,都能很好的工作:
void function(){ console.log('this is setFn')}() // undefined
new function(){ console.log('this is setFn')}() // Object new方法永远最慢——这也是理所当然的
delete function(){ console.log('this is setFn')}() // true

相信解析至此,聪明的你定能明白我的意思了吧。所有运算符实际上是让一个函数声明语句变成了一个表达式,从而告诉 解析器 此函数非函数声明而是函数表达式,让函数声明转变为解析器认为合法的函数表达式,从而消除函数执行的歧义。问题解决,一等公民函数的生活从此变得更美好了~

对于基础概念————语句,表达式,表达式语句,这些概念如同指针与指针变量一样容易产生混淆。虽然这种混淆对编程无表征影响,但却是一块绊脚石随时可能因为它而头破血流。

-+运算

  • 字符串转数字
// 使用JavaScript方法
var b = '100';
parseInt(b, 10); // 100
Number(b); // 100

// 使用加减法运算的隐形转换
var a = '100';
a = a - '0'; // 100
var c = '100';
c = +'100'; // 100

// 甚至使用位运算转换
'100' | 0; // 100
'100' >> 0; // 100
'100' << 0; // 100

<<>>位运算

  • 生成随机颜色
'#' + ((Math.random() * ffffff) << 2).toString(16);
  • 向下取整
var a = 2.3,
  b = 2.3,
  c = 2.3,
  d = 2.3;

// 普通写法
var b = Math.floor(a);

a = a >> 0;
b = b << 0;
c = c | 0;
d = ~~d;
  • 字符转为数字
'100' | 0; // 100
'100' >> 0; // 100
'100' << 0; // 100
  • 判断数字奇偶性
num & 1; // 奇偶判断
  • 数字开方、去平方根
num >> 1; // 取半,偶数
num << 1; // 2倍

~位运算

  • 对数字按位取反特性:~-1 为 0
<!-- prettier-ignore -->
var string = 'A'
// 普通写法
if (string.indexOf('A') != -1) {console.log('~-1 为 0')}
// 按位取反技巧
if (~string.indexOf('A')) {console.log('~-1 为 0')}

&&|| 逻辑运算

  • 逻辑运算牺牲可读性,简化 if...else、switch 等结构性判断
  • || 具有 短路 特性,当前面存在则不执行后面代码
  • && 具有共存性
/* 1 */
var result;
if status == 1) {
  result = 'Andy'
}
else if(status == 2){
  result = 'Tom';
}
else {
  result = 'John';
}
// if...else 改写
var result = (status == 1 && 'Andy') || (status == 2 && 'Tom') || 'John';
// 或者使用更简洁的方式
var result = {'1': 'Andy', '2': 'Tom'}[status] || 'John';

/* 2 */
var test = test || '1';

/* 3 */
var name = 'JyLie'
if (name === 'JyLie'){
  console.log ('hello ' + name);
}
name === 'JyLie' && console.log ('hello ' + name);

^ 异或运算符

  • 两数交互
var a = 1,
  b = 2,
  temp;
temp = a;
a = b;
b = temp;

// 使用^
var a = 1,
  b = 2;
a = a ^ b;
b = b ^ a;
a = a ^ b;

/*
简单的证明一下上面的代码:
为直观一点方便证明让 aa = a, bb = b
 */
// a = a ^ b
a = aa ^ b
// b = b ^ a
b = aa
  = aa ^ 0
  = aa ^ (bb ^ bb)
  = bb ^ (aa ^ bb)
  = bb ^ a
// a = a ^ b
a = bb
  = bb ^ 0
  = bb ^ (a ^ a)
  = a ^ (bb ^ a)
  = a ^ b

说到这里不得不提到一个经常被问到的面试题:在一个 n - 1 的数组中不重复分布着 1 ~ n 这 n 个数字,找出不在数组中的那个数字是啥?

估计回答最多的是用一个标记数组,然后循环一遍标记一遍。这个题目 O(n)的时间效率无可厚非,那空间上怎么优化呢?

后来有人提出一种办法优化空间,把 1 - n 都加起来减去 n - 1 的数组的和 就得到要找的数了,非常好的办法,也不再需要 O(n)的标记数组了。

除了这种办法,还有别的吗? 异或一样可以告诉我们答案,利用异或的结合律,把 n - 1 数组中的所有数字异或再与 1~n 异或就得到答案了,简单证明下:

为了简单证明我们假设 arr[1] = 1, arr[2] = 2, 以此类推。

a = (arr[1] ^ arr[2] ... ^ arr[n-1]) ^ (1 ^ 2 ... ^ n - 1 ^ n)
  = (arr[1] ^ 1) ^ (arr[2] ^ 2) ... ^ (arr[n-1] ^ n-1) ^ n
  = 0 ^ 0 .. ^ 0 ^ n
  = n

像好多找什么数组中重复的数字啊什么的,一样的道理可以用异或。

相关文档