利用ES6,让你的代码更优雅

282 阅读9分钟

1. ES6简介

ES6(ECMAScript 6.0),是由ECMA 国际标准化组织,制定的新一版脚本语言的标准化规范。ES6在语法上的增强,一方面解决了ES5的许多不足之处,比如变量提升、作用域等问题;另一方面,ES6提供的非破坏性更新,其为我们的代码的简化带来了很大的便利,我们可以将其理解为ES5的语法糖。

2. 利用扩展运算符进行变量的解构赋值

// 原数据
const arr = [1, 2, 3];

const obj = {
  name: "Kobe",
  age: 18,
  hobbies: ["basketball", "sing"],
};

2.1 基础语法

MDN-解构

// 1. 数组解构
const arrCopy = [...arr];
// const [...arrCopy] = arr;
console.log("arrCopy", arrCopy); // arrCopy [1, 2, 3]

// 2. 对象解构
const { ...objCopy1 } = obj;
const { name, age: ages, ...objCopy2 } = obj;
console.log("name:", name, "ages:", ages); // name: Kobe ages: 18
console.log("obj", obj); // obj {name: 'Kobe', age: 18, hobbies: ["basketball", "sing"]}
console.log("objCopy1", objCopy1); // objCopy1 {name: 'Kobe', age: 18, hobbies: ["basketball", "sing"]}

console.log("objCopy2", objCopy2); // objCopy2 {hobbies: ['basketball', 'sing']}

// 这里要注意的是,被解构的对象不能为undefined或null,否则会报错,所以在解构时建议提供一个默认值
const {name,age} = obj || {};

2.2 使用场景

场景1. 解决数组与对象进行赋值时出现的浅拷贝问题

①ES6之前的其它方式:

// 数组:toString()和split()
const arrCopy2 = arr.toString().split(",");
arrCopy2[0] = 8;
console.log("arr", arr); // arr [1, 2, 3]
console.log("arrCopy2", arrCopy2); // arrCopy2 [8, '2', '3']

// (2)对象:JSON.stringify() 和 JSON.parse()
const objCopy3 = JSON.parse(JSON.stringify(obj));
objCopy3.name = "James";
console.log("obj", obj); // objCopy1 {name: 'Kobe', age: 18, hobbies: ["basketball", "sing"]}
console.log("objCopy3", objCopy3); // objCopy3 {name: 'James', age: 18, hobbies: ['basketball', 'sing']}

②ES6新特性:扩展运算符和解构

// (1)数组
const arrCopy3 = [ ...arr ];
// const [...arrCopy3] = arr;
arrCopy3[0] = 8;
console.log("arr", arr); // arr [1, 2, 3]
console.log("arrCopy2", arrCopy3); // arrCopy2 [8, '2', '3']

// (2)对象
const objCopy4 = { ...obj };
// const {...objCopy4 } = obj;
objCopy4.name = "Curry";
objCopy4.hobbies[0] = "swimming";
console.log("obj", obj); // obj {name: 'Kobe', age: 18, hobbies: ["swimming", "sing"]}
console.log("objCopy4", objCopy4); // objCopy4 {name: 'Curry', age: 18, hobbies: ["swimming", "sing"]}

注意: 如果数组或对象中的元素是引用类型的元素,那么就是浅拷贝:

const {...objCopy5} = obj;
objCopy5.hobbies[0] = 'Read';
console.log("obj", obj); // obj {name: 'Kobe', age: 18, hobbies: ["Read", "sing"]}

解决方法:我们可以对引用类型的属性进行解构赋值

const objCopy6 = {
  ...obj,
  hobbies: [...obj.hobbies]
}
objCopy6.hobbies[0] = "jump";
console.log('obj', obj); // obj {name: 'Kobe', age: 18, hobbies: ["swimming", "sing"]}
console.log('objCopy6', objCopy6); // objCopy5 {name: 'Curry', age: 18, hobbies: ["jump", "sing"]}

3. 可选链运算符(?.)

MDN-可选链运算符:利用可选链运算符访问一个可能不存在的对象的深度属性时或方法时,可以不用手动去判断该属性或是否存在,如果不存在则会返回undefined,而不是报错进而影响到后面语句的执行。

3.1 语法

const pets = {
  name: "Alice",
  cat: {
    name: "Dinah",
    getPets() {
      return "cat";
    },
  },
};

// 访问对象属性
console.log(pets.cat?.name); // Dinah
console.log(pets.dog?.name); // undefined

console.log(pets.cat.getPets?.()); // cat
// console.log(pets.getPets()); // Uncaught TypeError: pets.getPets is not a function
console.log(pets.getPets?.()); // undefined

3.2 场景:if语句

// 场景1:if语句
if (pets.dog&&pets.dao.name) {
  console.log(pets.dog.name);
}
console.log(pets.dog?.name); // undefined

4. 数组的Array.from()

MDN-Array.from():Array.from()可以将类似数组的对象(array-like object)和可遍历(iterable)对象转为真数组。

场景1:将一个可遍历对象转为真数组转换为真数组

// 可遍历对象
const arrLike = {
  '0': "a",
  '1': "b",
  '2': "c",
  length: 3,
};

①ES6之前的其它方式

// 方式一:将可遍历对象中的值通过索引逐个添加到新数组当中。
let newArr1 = [];
for (let i = 0; i < arrLike.length; i++) {
  newArr1[i] = arrLike[i];
}
console.log("newArr1", newArr1); // ['a', 'b', 'c']

/*
 方法二:
 slice原本是数组的截取子数组的方法,这里给数组的原型对象方法slice的指向强制改成arrLike
*/
const newArr2 = Array.prototype.slice.call(arrLike);
console.log("newArr2", newArr2); // ['a', 'b', 'c']

②Array.from()

// ES6新特性
const newArr3 = Array.from(arrLike);
console.log("newArr3", newArr3); // ['a', 'b', 'c']

场景2:利用扩展运算符将伪数组转换为真数组

伪数组常在接受实参的方法(arguments)和获取DOM元素中出现,伪数组具有以下几个特点:

  1. 拥有length属性,可以获取长度;
  2. 拥有角标索引,可以通过length属性遍历获取所有值。
  3. 但是不可以使用数组的内置方法。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>数组的扩展</title>
  </head>
  <body>
    <div>1</div>
    <div>2</div>
    <div>3</div>
    <div>4</div>
  </body>
  <script>
    // 伪数组
    const divDoms = document.querySelectorAll("div");
    // 方法一:利用forEach
    
    // 方法二:ES6新特性--扩展运算符
    // 方法三:ES6新特性-Array-from()
    const newArr4 = [...divDoms];
    const newArr5 = Array.from(divDoms)
    console.log("newArr4", newArr4); // [div, div, div, div]
    console.log("newArr5", newArr5); // [div, div, div, div]
  </script>
</html>

5. 数组的find()和filter()

MDN-filter()和find():find()方法中找到第一个符合条件的项,就不会继续遍历数组,并将这个项返回;而filter()则会返回由符合条件的全部项所组成的数组,这两者的运用可以分别看作精确过滤和模糊过滤。

场景:查询数组中符合条件的元素并返回,在vue项目中常在computed中使用

const arr = ["a", 2, 88, 66, 6];
// filter()
const result1 = arr.filter((item) => {
  return item === 66;
});

// find()
const result2 = arr.find((item) => {
  return item === "a";
});
console.log("值为66的元素: ", result1); // 值为66的元素:  [66]
console.log("值为'a'的元素: ", result2); // 值为'a'的项: a

6. includes()和some()及every()优化判断条件

const arr = [
  {
    status: 0,
  },
  {
    status: 1,
  },
  {
    status: 1,
  },
];
const a = 1;
const b = "b";

6.1 includes()

MDN-Array.prototype.includes():includes 函数可以用来检测数组或字符串中是否包含某个值,返回布尔值,对NaN同样有效

场景1:使用includes()优化if语句中的逻辑或('||')

// ES6之前的常见方式
if (a === 1 || a === 2 || a === 3 || a === 4) {
  console.log("a:", a);
}
if (b.indexOf("a") != -1 || b.indexOf("b") != -1 || b.indexOf("c") != -1) {
  console.log("b:", b);
}

// ES6的includes
// 对于数值型,可以利用数组的includes()
const arr = [1, 2, 3, 4];
if (arr.includes(a)) {
  console.log(`找到了a元素: ${a}`); // 我找到了a元素: 1
}

// 对于字符串型,可以利用字符串的includes()
const str = "abcdef";
if (str.includes(b)) {
  console.log(`我找到b元素了: ${b}`); // 我找到b元素了: b
}

6.2 some()

MDN-some():some()方法,会根据所提供的函数测试(判断条件)对数组元素进行测试,只要回调函数返回的值出现一个true则会终止对后面的元素测试,并返回ture,否则返回false,需要注意的是,如果是一个空数组进行测试,返回的值一定是false。

场景2:使用some()对数组元素进行函数测试

// 需求:只要有一个对象的status属性为1时才打印

// 方法1:if(arr[0].status === 1 &&arr[1].status === 1...)

// 方法2:使用forEach(),造成资源浪费
let flag = false;
arr.forEach(item => {
  if (item.status === 1) {
    flag = true;
  }
});
if (flag) {
  console.log("1. 我打印啦");  // 1. 我打印啦
}

// 方法3:ES6的filter()
const filterArr = arr.filter((item) => item.status === 1);
if (filterArr.length > 0) {
  console.log("2. 我打印啦"); // 2. 我打印啦
}

// 方法4:ES6的find()
const result = arr.find(item => item.status === 1);
if (result !== undefined) {
  console.log("3. 我打印啦"); // 3. 我打印啦
}

const flag2 = arr.some((item, index) => {
  if (index > 1) {
    console.log("我是第三个元素");
  }
  return item.status === 1;
});
if (flag2) {
  console.log("4. 我打印啦"); // 4. 我打印啦
}

6.3 every()## (ES2016 )

MDN-Array.prototype.every():every():对数组内的所有元素进行测试,需要回调函数返回的所有值都为ture时才会返回true,否则为false,与some方法相反,如果是一个空数组进行测试,返回的值一定是true。

场景3:使用every(),当数组的每个元素的status属性为1时打印

const flag3 = arr2.every((item) => item.status == 1);
if (flag3) {
  console.log("我是every()");
}

7. Set结构

MDN-Set:ES6推出了新的数据结构:Set集合。Set()是一个构造函数,需要配合new创建实例对象,该实例对象是值的集合。Set中的项是唯一的。Set的静态属性和实例方法:

  1. 实例属性:Set.prototype.size:计算Set对象的元素个数

  2. 实例方法:

    (1)Set.prototype.add(value):在Set对象尾部添加一个项,并返回更新后的Set对象;

    (2)Set.prototype.delete(value):删除Set对象指定项,并返回布尔值,表示删除是否成功;

    (3)Set.prototype.has(value):判断指定值是否在集合中;

    (4)Set.prototype.clear():清除集合里的所有项;

    (5)Set.prototype.keys():返回集合所有键组成的数组;

    (6)Set.prototype.values():返回集合所有键值组成的数组;

    (7)Set.prototype.entries():返回所有键值对组成的数组;

    (8)Set.prototype.forEach():遍历每个成员并执行回调函数;

场景1:数组去重

const arr = [1, 2, 5, 6, 9, 1, 2, 5, { a: "hh" }];

// ES6之前的实现方式:forEach()
const newArr1 = [];
arr.forEach((item) => {
  if (newArr1.indexOf(item) === -1) {
    newArr1.push(item);
  }
});
console.log("newArr1:", newArr1); // newArr1: [1, 2, 5, 6, 9, {…}]

// ES6的filter()
const newArr2 = arr.filter((item, index) => arr.indexOf(item) === index); // 因为indexOf 只能查找到第一个
console.log("newArr2:", newArr2); // newArr2: [1, 2, 5, 6, 9, {…}]

// Es6的new Set()
const newArr3 = [...new Set(arr)];
console.log("newArr3:", newArr3); // newArr3: [1, 2, 5, 6, 9, {…}]

场景2:字符串去重

let str = "aawkakkhhll";

// 方法一:ES6之前的实现方法
const StagingArr = [];
str.split("").forEach((item) => {
  if (StagingArr.indexOf(item) === -1) {
    StagingArr.push(item);
  }
});
const newStr1 = StagingArr.join("");
console.log("newStr1:", newStr1); // awkhl

// 方法二:new Set()
// const stagingArr2 = [...new Set(str.split(""))];
// const newStr2 = stagingArr2.join("");
// console.log("newStr2:", newStr2); // newStr2: awkhl

const str = new Set('abad') //  {'a', 'b', 'd'}

8. # Promise的并发和异步

扩展:async和await(ES2017 标准)

MDN-async/await:async 函数是使用async关键字声明的函数,是基于promise实现的,其返回值会将Promise.resolve()封装成 Promise对象。async常与其内部的await结合使用,await用于等待一个异步方法执行完成。 promise的then方法处理异步,相比于回调函数,会显得代码更为简洁,而async和await相当于promise的语法糖。

// 1. promise
function pro1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第一个pormise");
      resolve("promiseFirst");
    }, 1000);
  });
}

function pro2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第二个promise");
      resolve("promiseSecond");
    }, 1500);
  });
}

function pro3() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第三个promise");
      resolve("promiseThird");
    }, 2000);
  });
}

// 2. async和await
function async1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第一个async");
      resolve("asyncFirst");
    }, 1000);
  });
}

function async2() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第二个async");
      resolve("asyncSecond");
    }, 1500);
  });
}

function async3() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("第三个async");
      resolve("asyncThird");
    }, 2000);
  });
}

场景1:promise和async函数的异步队列

// pormise异步队列
function promiseTest() {
  pro1().then((res1) => {
    console.log(res1); // 1000ms
    return pro2()
      .then((res2) => {
        console.log(res2); // 2000ms
        return pro3();
      })
      .then((res3) => {
        console.log(res3); // 4500ms
      });
  });
}
promiseTest();
/* 
  第一个pormise
  promiseFirst
  第二个promise
  promiseSecond
  第三个promise
  promiseThird  用时4500ms
*/

// async和await异步队列
 function asyncTest() {
  
  const res1 = await async1();
  const res2 = await async2();
  const res3 = await async3();
  console.log("res1", res1);
  console.log("res2", res2);
  console.log("res3", res3);
}
asyncTest();
/* 
  第一个async
  第二个async
  第三个async
  res1 asyncFirst
  res2 asyncSecond
  res3 asyncThird 用时4500ms
*/

promise使用all可以实现并发,那async和await该怎么实现呢?

// async和await
async function asyncAll() {
  const arr = [async1, async2, async3];
  const resArr = arr.map(async function (fn) {
    const respone = await fn();
    return respone;
  });
  
  // 阮一峰老师
  // for (const resArrItem of resArr) {
  //   console.log(await resArrItem);
  // }
  
  resArr.forEach(async function (item) {
    console.log(await item);
  });
}
asyncAll();
/*
  第一个async
  asyncFirst
  第二个async
  asyncSecond
  第三个async
  asyncThird
*/

9.番外篇--多个else if优化

如果开发过程中使用到else if,并且出现了较多层判断的话,代码的可读性较差,不便于维护和扩展。

function getValue1(type) {
  if (type === "A") {
    return "A";
  } else if (type === "B") {
    return "B";
  } else if (type === "C") {
    return "C";
  } else if (type === "D") {
    return "D";
  } else {
    return "E";
  }
}

const res1 = getValue1("A");
console.log("res1:", res1); // res1: A

const res1 = getValue1("A");
console.log("res1:", res1); // res1: A

优化方案1:使用switch代替else if

function getValue2(type) {
  switch (type) {
    case "A":
      return "A";
      break;
    case "B":
      return "B";
      break;
    case "C":
      return "C";
      break;
    case "D":
      return "D";
      break;
    default:
      return "E";
      break;
  }
}
const res2 = getValue2("B");
console.log("res2:", res2); // res2: B

优化方案2:对象映射的方式

function getValue3(type) {
  const o = {
    A: () => {
      return "A";
    },
    B: () => {
      return "B";
    },
    C: () => {
      return "C";
    },
    D: () => {
      return "D";
    },
    default: () => {
      return "E";
    },
  };
  return o[type]?.() || o['default']();
}

const res3 = getValue3("E");
console.log("res3:", res3); // res3: E

10. Map结构

MDN-Map:相比于对象,Map集合中存储的键可以为任意类型,并且能够记住键的原始插入顺序。 Set的静态属性和实例方法:

  1. 实例属性:Map.prototype.size:计算Map对象的键值对个数

  2. 实例方法: (1)Map.prototype.set():在Map对象中设置与指定的键key 关联的值,并返回Map对象。

    (2)Map.prototype.get():从Map对象中返回指定的元素;

    (3)Map.prototype.has():方法返回一个布尔值,判断指定键的元素是否存在;

    (4)Map.prototype.clear():清除对象中所有的键值对;

    (5)Set.prototype.keys():返回一个由键所组成的迭代器对象;

    (6)Set.prototype.values():返回一个由值所组成的迭代器对象;

    (7)Map.prototype.entries():返回所有键值对组成的迭代器对象;

    (8)Map.prototype.forEach():按照插入顺序依次对Map中每个键值对执行一次给定的函数;

场景:优化else if嵌套语句

function getValue1(type, status) {
  if (type === "tes1") {
    if (status == 1) {
      return 1;
    } else if (status == 2) {
      return 2;
    } else {
      return "None test1";
    }
  } else if (type === "test2") {
    if (status == 3) {
      return 3;
    } else if (status == 4) {
      return 4;
    } else {
      return "None test2";
    }
  }
}

// 使用Map
function getValue2(type, status) {
  const actions = new Map([
    [
      "test1_1", () => { return 1;},
    ],
    [
      "test1_2", () => { return 2;},
    ], 
    [
      "test2_3", () => { return 3;},
    ], [
      "test2_4", () => { return 4;},
    ],
    [
      "default", () => {return 'None'}
    ]
  ]);
  const key = type + "_" + status;
  return actions.get(key)?.() || actions.get('default')();
}
const res1 = getValue2("test1", 1);
console.log("res1:", res1); // 1