likes
comments
collection
share

总结js非常实用的技巧

作者站长头像
站长
· 阅读数 11

js的实用技巧

收集整理一些js常用的技巧

不使用for循环和break终止循环遍历

随着es6规范的普及,现在很少使用for循环进行遍历了,我们习惯使用forEach、map来进行遍历操作;

但是当我们想提前结束循环,则使用forEach方法无法满足需求。

这时候如果想在遍历过程中终止循环,则可以通过find,some方法控制回调函数返回值来实现。

let arr = [
  {index: 1, value: 111},
  {index: 2, value: 222},
  {index: 3, value: 333},
  {index: 4, value: 444},
];
// find方式
arr.find(item => {
  if (item.index > 2) return true; // 相当于for循环中的break;
  // 循环内具体业务逻辑
  console.log(item.index);
  return false;
});
// some方式
arr.some(item => {
  if (item.index > 2) return true; // 相当于for循环中的break;
  // 循环内具体业务逻辑
  console.log(item.index);
  return false;
});

当找到符合条件的数据项时,返回true,则会停止遍历

快速创建一个m * n的二维数组

创建一个m行n列的二维数组有多种方式

  • 原始方法:
function makeMatrix(m, n) {
  let result = [];
  for (let i = 0; i < m; i++) {
    result.push(new Array(n).fill(0));
  }
  console.log(result);
}
  • 使用es6的数组方法:
// 方式1:利用new Array和map方法
function makeMatrix1(m, n) {
  return new Array(m).fill(0).map(() => new Array(n).fill(0));
}

// 方式2:Array.from可以在创建数组时传入一个控制函数,对每一项进行处理,其返回值即为每一项的值(相当于一个map方法)
function makeMatrix2(m, n) {
  return Array.from(new Array(m), () => new Array(n).fill(0));
}

String.raw

String.raw`aaa\nbbb` 可以将模板字符串原样效果输出,一般用于编写含有代码的文档,这样就可以输出源代码而不被转义

对象属性链式get

一个obj对象嵌套了很多层, 给定一个字符串key = 'first.second.third' , 根据key得到最终的obj.first.second.third的值

let obj = {
  first: {
    second: {
      third: 'message'
    } 
  }
};
function getResultValue(obj, key) {
  return key.split('.').reduce((o, i) => {
    if (o) return o[i];
  }, obj);
}

let key = 'first.second.third';
let result = getResultValue(obj, key);
console.log(result);

巧用位运算

1、奇偶性判断 —— 按位与(&)

  • 按位与运算是把两个操作数转换成二进制再逐位比较,当前位都为1结果为1,否则为0。
  • 而所有数字转化为二进制的奇偶性就只用看末尾,奇数尾数为1,偶数尾数为0。
if ( a & 1 ) {
  alert('a是奇数!');
} else {  
  alert('a是偶数!');
}

2、两个整数的互换 —— 按位异或(^)

加减互为逆运算,异或和异或互为逆运算.

  • 利用加减:
var a = 1;
var b = 2;
a = a + b;
b = a - b;
a = a - b;
console.log(a, b);
  • 按位异或:
var a = 1;
var b = 2;
a = a ^ b;
b = a ^ b;
a = a ^ b;
console.log(a, b);

两个整数互换可以申明第三个中间变量进行临时存储实现,但是声明变量会占用内存,不友好。而加减又没有位运算效率高。

3、 数字-1的判断 —— 按位取反(~)

判断一个数是否为-1是我们经常遇到的,indexOf()在查找字符串的时候没有找到会返回-1,很多程序,插件,框架错误状态值默认返回-1,在位运算中,~-1===0 的。

// 常规
if ( str.indexOf('a') != -1 ) {
  alert('a在字符串str中');
}
// ~取反
if ( ~str.indexOf('a') ) {
  alert('a在字符串str中');
}

4、数字取整—— ~~

很多时候我们需要得到数字整数部分(非四舍五入),有以下方法:

let num = 13.14;
// parseInt() 
console.log(parseInt(num)); // 参数如果为NaN、null、undefined等等会得到NaN
// Math.floor()
console.log(Math.floor(num)); // 参数如果为字符串、NaN、undefined等等会得到NaN,参数为null结果为0
// ~~
console.log(~~num); // 非数字都会转化为0

5、二进制数去掉最后的1 —— n & (n - 1)

现有二进制数 10101100,最后的1在倒数第三位,想要将最后的1变为0,则可以使用 n & (n - 1),结果为10101000

n & (n - 1)的 运用:

问题:编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量)。比如求 1001 1100 1010 1100 0101 0011 0110 1000 中1的个数

var hammingWeight = function(n) {
  let count = 0;
  while (n != 0) {
    count++;
    n = n & (n - 1);
  }

  return count;
};

6、位移运算 —— >> 和 >>>

>> 是带符号的右移运算符,将运算符左边的对象向右移动指定的位数。如果是正数,高位补0;如果是负数,则在高位补1。 >>>是不带符号的右移运算符,将运算符左边的对象向右移动指定的位数,并且在高位补0 >>>0 对负数执行>>>0操作可以去掉负号(原来负数 + 2的32次方,负数变回无符号整数)

可以得出:

  • 当对正数移位运算时,>>>>>操作结果是一样的;
  • 当对负数移位运算时,>>>>>的操作结果是不一样的,>>将二进制高位用1补上,而>>>将二进制高位用0补上,这就导致了>>>将负数的移位操作结果变成了正数,因为高位用0补上了

运用示例:

问题:编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 '1' 的个数(也被称为汉明重量)。比如求 1001 1100 1010 1100 0101 0011 0110 1000 中1的个数

这个题目使用 n & (n - 1)非常方便,也可以使用 >>>>>实现

// >> 实现
var hammingWeight = function(n) {
  let count = 0;
  
  // 由于‘>>’运算是高位补1操作,所以不能使用while循环判断
  for (let i = 0; i < 32; i++) {
    if ((n >> i) & 1) count++;
  }

  return count;
};

// >>> 实现
var hammingWeight = function(n) {
  let count = 0;
  // `>>>`是高位补零操作,不影响二进制数中1个数的增加,每当右移一次,n == 0时,表示1已经不存在了,计数完毕
  while (n) {
    if (n & 1) count++;
    n = n >>> 1; // n >> 1不适用于负数,会导致while死循环,需要通过 n >>> 1 来进行零补位的移位操作
  }

  return count;
};

给对象动态设置属性/值

1、根据条件判断给哪个属性设置值

var obj = { top: 60 };
// 当元素在盒子左侧,则obj['left'] = 20, 当元素在盒子右侧, 则obj['right'] = 20
obj[['left', 'right'][+(posLeft > containerWidth  / 2)]] = 20;

2、根据条件判断是否添加某个属性和值

可以使用展开运算符来有条件地向对象中添加属性:

const edit = this.$route.query.edit; // 是否编辑状态
const params = {  
  id: editData.id,
  title: editData.title,
  ...(edit == 1 && { type: 'edit' }), // 如果是编辑状态,则设置type属性为‘edit’
};

解析 如果 edit 值为1,则 会给params对象设置属性type值为'edit';如果 edit 值不为1,相当于展开 false ,不会对对象产生影响

有条件地向数组添加元素

这是 CRA 中 Webpack 配置的源码:

module.exports = {
  plugins: [
    new HtmlWebpackPlugin(),
    isEnvProduction && new MiniCssExtractPlugin(),
    useTypeScript && new ForkTsCheckerWebpackPlugin(),
  ].filter(Boolean),
}

解析 只有 isEnvProductiontrue 才会添加 MiniCssExtractPlugin 插件;当 isEnvProductionfalse 则会添加 false 到数组中,最后使用了 filter 过滤值为true的数组项

if/else优化

有这样一个业务处理函数, 根据参数type不同执行不同的方法得到相应的值

function getValue(type) {
  if (type == 'a') {
    return getValue_a();
  } else if (type == 'b') {
    return getValue_b();
  } else if (type == 'c') {
    return getValue_c();
  } else if (type == 'd') {
    return getValue_d();
  } else {
    return getValue_default();
  }
}

这个方法推荐使用switch/case或者对象集合形式替代:

  • switch/case方式:
function getValue(type) {
  switch(type) {
    case 'a':
      return getValue_a();
    case 'b':
      return getValue_b();
    case 'c':
      return getValue_c();
    case 'd':
      return getValue_d();
    default:
      return getValue_default();
}
  • 对象集合形式:
const handlers = {
  'a': () => getValue_a(),
  'b': () => getValue_b(),
  'c': () => getValue_c(),
  'd': () => getValue_d(),
  'default': () => getValue_default(),
}

function getValue(type) {
  return handlers[type || 'default']();
}

判断对象中是否存在某个属性

我们知道使用 in 关键字可以判断对象中是否存在某个属性:

const params = { id: 111, title: '编辑', type: 'edit' };
console.log('type' in params); // true
console.log('content' in person); // false

但是 in 关键字会获取原型上的属性,所以不能区分该属性是当前对象中定义的还是原型上定义的,例如:

"hasOwnProperty" in {}; // true
"toString" in {}; // true

很多情况我们并不关心原型上是否存在某个属性,所以可以通过Object.prototype.hasOwnProperty方式来判断,比如:

Object.prototype.hasOwnProperty.call(params, 'type'); // true
Object.prototype.hasOwnProperty.call(params, 'content'); // false

上面代码每次判断都得写一大长串代码,可以定义一个全局方法,按需使用即可。

const hasOwnProperty = Object.prototype.hasOwnProperty
function hasOwn(target, key) {
  return hasOwnProperty.call(target, key);
}

console.log(hasOwn(params, 'type')); // true

async/await异常处理封装

正常情况async/await使用起来确实方便、简洁,也避免了出现回调地狱的情况

function getUserInfoApi(params) {
  return new Promise((resolve, reject) => {
    axios.get(url, { params }).then(res => {
      if (res.status == 200) {
        resolve(res.data.data);
      } else {
        reject(null);
      }
    }).catch(err => {
      reject(null);
    });
  });
}
async getUserInfo() {
  const userInfo = await getUserInfoApi({ userId: 123 }); // { name: 'zhangsan', age: 30 }
  console.log(userInfo.name);
}

但是,如果getUserInfoApi执行过程中出现异常,则会报错影响后续的代码执行;

所以我们需要一个异常处理解决方案:

方案1

直接在getUserInfoApi方法中进行异常捕获后,resolve一个空对象,不会影响后续 userInfo 的操作

但是实际开发过程中会有非常多的接口请求,如果每个接口都这么处理一遍,那就费时费力还影响效率。

function getUserInfoApi(params) {
  return new Promise((resolve, reject) => {
    axios.get(url, { params }).then(res => {
      if (res.status == 200) {
        resolve(res.data.data);
      } else {
        resolve({});
      }
    }).catch(err => {
      resolve({});
    });
  });
}

方案2

封装一个公共的异常处理函数,对返回值进行包装处理。

function getUserInfoApi(params) {
  return new Promise((resolve, reject) => {
    axios.get(url, { params }).then(res => {
      if (res.status == 200) {
        resolve(res.data.data);
      } else {
        reject(null);
      }
    }).catch(err => {
      reject(null);
    });
  });
}
/**
 * @description: 对awaited异步进行包装,异常处理 (使用async/await时避免try/catch进行处理)
 * @param {*} awaited 需要进行异常处理的请求 (一般情况下是一个Promise)
 * @return {Promise} 返回一个Promise,在调用的地方通过async/await 解构
 * 使用:let [err, result] = await this.wrappedAwait(awaited), 正常请求时result为接口返回的数据,异常时err为错误信息
 */
wrappedAwait(awaited) {
  let p = Promise.resolve(awaited); // 非Promise则转为Promise
  return p.then(
    res => {
      return [null, res];
    },
    err => {
      return [err, null];
    }
  );
}

async function test() {
  const [err, res] = await wrappedAwait(getUserInfoApi({ userId: 123 }));
  console.log('__err__', err);
  console.log('__res__', res);
}

test();

wrappedAwait函数的返回值是一个数组类型,第一项是异常情况时的错误信息,第二项是正常情况返回的接口数据,通过判断返回值进行相应的逻辑处理

JS随机打乱数组

function shuffle(arr) {
  let i = arr.length;
  while (i) {
    let j = Math.floor(Math.random() * i--);
    [arr[j], arr[i]] = [arr[i], arr[j]];
  }
  return arr;
}

利用sort方法实现:

const list = [-34567, 5, 8, 110, -321, 567, 200, 234, -654, -2, 1, 654321];
list.sort(() => Math.random() - 0.5);
console.log(list);

数组扁平化

数组扁平化的几种实现方式:

  • 1、递归实现(常用)

    const arr = [1, [2, [3, 4], [5, [6, 7]]]];
    
    function flatten(arr) {
      let result = [];
    
      arr.forEach(item => {
        if (Array.isArray(item) {
          result = result.concat(flatten(item));
        } else {
          result.push(item);
        }
      });
      return result;
    }
    
    console.log(flatten(arr));
    
  • 2、reduce实现(常用)

    const arr = [1, [2, [3, 4], [5, [6, 7]]]];
    
    function flatten(arr) {
      return arr.reduce((prev, curr) => {
        return prev.concat(Array.isArray(curr) ? flatten(curr) : curr);
      }, []);
    }
    
    console.log(flatten(arr));
    
  • 3、es10-flat()函数 (推荐)

    es10中新增了flat()方法,专门用来对数组扁平化(“拉平”)操作,该方法返回一个新数组。

    flat()方法默认只会“拉平”一层,它接收一个参数,表示要“拉平的”层数,不传参数则默认为1。

    如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

    const arr = [1, [2, [3, 4], [5, [6, 7]]]];
    
    console.log(arr.flat(Infinity));
    

    关于flat()相关的介绍,可以看看阮一峰大佬的 ECMAScript 6 入门

  • 4、replace替换

    let arr = [1, [2, [3, 4], [5, [6, 7]]]];
    let str = JSON.stringify(arr);
    console.log(str);
    let result = str.replace(/(\[|\])/g, '').split(',');
    console.log(result);
    

既然es6已经给我们提供了专用方法,岂有不用之理?

日期格式化

下面是一个常见的日期格式化方法

// 根据自定义format格式进行格式化 1 (只能替换一次)
function formatDateAndTime(value, fmt) {
  let date = value ? new Date(value) : new Date();

  let o = {
    'M+': date.getMonth() + 1,
    'd+': date.getDate(),
    'H+': date.getHours(),
    'h+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds(),
    'S+': date.getMilliseconds()
  };

  if (/(y+)/.test(fmt)) {
    fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
  }
  for (var k in o) {
    if (new RegExp(`(${k})`).test(fmt)) {
      fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (('00' + o[k]).substr(String(o[k]).length)));
    }
  }
  return fmt;
}

上面 formatDateAndTime 方法只能替换一次年月日等信息,一般情况下满足使用要求, 但如果 fmt参数值是类似 'yyyy/MM/dd hh:mm:ss---yyyy年MM月dd日' 这种格式时, 由于yyyy, MM, dd有重复,上面方法中正则只会替换第一次出现的yyyy, 而第二次出现的yyyy则不会转换,

可以对正则替换进行优化,使用全局替换

// 根据自定义format格式进行格式化 2 (全局替换)
function formatDateAndTime(value, fmt) {
  let date = value ? new Date(value) : new Date();

  let o = {
    'M+': date.getMonth() + 1,
    'd+': date.getDate(),
    'H+': date.getHours(),
    'h+': date.getHours(),
    'm+': date.getMinutes(),
    's+': date.getSeconds(),
    'S+': date.getMilliseconds()
  };

  let reg = new RegExp('(y+)', 'g');
  if (reg.test(fmt)) {
    fmt = fmt.replace(reg, (_, curr) => (date.getFullYear() + '').substring(4 - curr.length));
  }
  for (var k in o) {
    let reg_k = new RegExp(`(${k})`, 'g');
    if (reg_k.test(fmt)) {
      fmt = fmt.replace(reg_k, (_, curr) => {
        return curr.length == 1 ? (o[k]) : (`00${o[k]}`).substring(String(o[k]).length)
      });
    }
  }
  return fmt;
}

补零操作

方式1:循环补零直到指定长度

function zerofill(num, n) {
  let str = num.toString();
  let len = str.length;
  while (len < n) {
    str = '0' + str;
    len++;
  }

  return str;
}

方式2:先补齐足够多的0,然后从末尾截取指定长度字符串

// 第一种方法
function zerofill(num, n) {
  return (Array(n).fill(0) + num).slice(-n);
}

方式3:ES2017 引入了字符串补全长度的功能 (padStart, padEnd)

'1'.padStart(4, '0') // '0001'
'1'.padStart(4) // '   1' (如果省略第二个参数,则默认使用空格填充)

生成随机字符串

生成随机字符串是一个很常见的需求,可以根据具体需求定制不同的输出结果。

方式1

利用Math.random方法每次从目标数组中随机抽取一个字符

/**
 * @description: 生成随机字符串
 * @param {Number} len 想要生成的随机字符串长度
 * @param {Boolean} isPlainNumber 随机字符串是否纯数字
 * @return {String} 要输出的字符串
 */
function randomHandler(len = 16, isPlainNumber = false) {
  let chars = isPlainNumber ? '1234567890' : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz1234567890';
  let result = ''; 
  
  for (let i = 0; i < len; i++) {
    let currIndex = ~~(Math.random() * chars.length);
    result += chars[currIndex];
  }

  return result;
}

console.log(randomHandler(20));

方式2

利用Math.random()toString()方法巧妙生成随机字符串

  • Math.random()可以生成一个[0, 1)区间的随机数
  • toString(radix)方法可以将数字转化为 radix 进制的字符串
console.log(Math.random()) // 0.1852958957954327
console.log(Math.random()) // 0.19654440821013286
console.log(Math.random()) // 0.862253082562344
console.log(Math.random().toString()) // 0.8898541967454725
console.log(Math.random().toString()) // 0.21295312937219646
console.log(Math.random().toString(36).substring(2)) // xkji8078cgr
console.log(Math.random().toString(36).substring(2)) // 0e8jhas9zxft
console.log(Math.random().toString(36).substring(2)) // yxnudwel5iq

棒棒哒,一行代码就实现了需求。

哎,等一下,好像发现了一点小瑕疵:Math.random()生成的小数点后面的数字长度是不固定的

那就稍微优化一下:如果一次random达不到所需的目标长度,那就random多次后拼接起来呗

接下来看看具体实现:

/**
 * @description: 生成随机字符串
 * @param {Number} len 想要生成的随机字符串长度
 * @param {Boolean} isPlainNumber 随机字符串是否纯数字
 * @return {String} 要输出的字符串
 */
function randomHandler(len = 16, isPlainNumber = false) {
  let result = '';
  // 如果要求纯数字,则转化为10进制,否则转化为36进制 (26个字母+10个数字)
  let radix = isPlainNumber ? 10 : 36;
  // 使用substring(2) 保留小数位,去掉整数和小数点
  let getOnce = () => Math.random().toString(radix).substring(2);
  while(result.length < len) {
    result += getOnce();
  }
  
  return result.substring(0, len);
}

console.log(randomHandler(20));

最后你发现没,利用Math.random().toString(16)貌似直接就可以实现一个常见的小功能:生成随机颜色

function getRandomColor() { 
  return `#${Math.random().toString(16).substr(2, 6)}`
}

console.log(getRandomColor())

条件判断简化

有这么一个业务判断,用户角色值role有[1,2,3,4,5,6,7,8,9,10],总共十种角色,当role 为 1,2,5,8,10时,则显示 删除 按钮

常规的判断方法可能是这样的:

if (role == 1 || role == 2 || role == 5 || role == 8 || role == 10) {
  this.showDelBtn = true;
}

可以这样优化一下:

if ([1, 2, 5, 8, 10].includes(role)) {
  this.showDelBtn = true;
}