从简到难,重新学习ES6(下)

2,841 阅读6分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路

开始

快速入门ES6的最后一篇,也是最难的一篇。

附上思维导图:

ES6(下)

Symbol

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,是一种类似于字符串的数据类型。回顾一下,JavaScript有number,string,boolean,null,undefined,object六种数据类型,加上symbol就是七种基本数据类型。

它有以下特点:

  • Symbol 的值是唯一的,用来解决命名冲突的问题
  • Symbol 值不能与其他数据进行运算
  • Symbol 定义 的 对象属 性 不能 使 用 for…in /of循 环遍 历 ,但 是可 以 使 用 Reflect.ownKeysObject.getOwnPropertySymbols 来获取对象的所有键名

Symbol的基本使用

Symbol的创建

let s = Symbol();
console.log(s);
let s2 = Symbol("我是symbol类型");
console.log(s === s2);// false
let s3 = Symbol("我是symbol类型")
console.log(s2===s3)// false

// Symbol.for()
// 也可以创建Symbol类型数据,这样的话,就方便我们通过描述(标识)区分开不同的Symbol了
let s4 = Symbol.for("sym");
let s5 = Symbol.for("sym");
console.log(s4 === s5); // true

不能参与运算

let s = Symbol()
let res = s + 100// 报错
res = s > 200// 报错
res = s + "symbol"// 报错

获得symbol属性

let sy = Symbol();
const user = {
  name: "张三",
  age: 18,
};
user[sy] = "symbol";
console.log(user);

for (let key in user) {
  console.log(key);// name,age
}

console.log(Object.getOwnPropertySymbols(user));// [Symbol()]
console.log(Reflect.ownKeys(user));// ['name', 'age', Symbol()]

Symbol的值是唯一的,我们可以往对象添加属性,解决命名冲突的问题

let game = {
  name: "魂斗罗",
  up: function () {},
  down: function () {},
  //方式一
   [Symbol("手雷")]: function () {
     console.log("扔手雷");
   },
};

let methods = {
  up: Symbol(),
  down: Symbol(),
};

// 方式二
game[methods.up] = function () {
  console.log("我会跳");
};
game[methods.down] = function () {
  console.log("我会蹲");
};
console.log(game);
game[methods.up]();

image-20220210110711971

Symbol内置值

除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方 法。可以称这些方法为魔术方法,因为它们会在特定的场景下自动执行。特别的, Symbol内置值的使用,都是作为某个对象类型的属性去使用。

内置Symbol的值调用时机
Symbol.hasInstance当其他对象使用 instanceof 运算符,判断是否为该对象的实例时,会调用这个方法
Symbol.isConcatSpreadable对象的 Symbol.isConcatSpreadable 属性等于的是一个 布尔值,表示该对象用于 Array.prototype.concat()时, 是否可以展开。
Symbol.species创建衍生对象时,会使用该属性
Symbol.match当执行 str.match(myObject) 时,如果该属性存在,会 调用它,返回该方法的返回值。
Symbol.replace当该对象被 str.replace(myObject)方法调用时,会返回 该方法的返回值。
Symbol.search当该对象被 str.search (myObject)方法调用时,会返回 该方法的返回值。
Symbol.split当该对象被 str.split(myObject)方法调用时,会返回该 方法的返回值。
Symbol.iterator对象进行 for...of 循环时,会调用 Symbol.iterator 方法, 返回该对象的默认遍历器
Symbol.toPrimitive该对象被转为原始类型的值时,会调用这个方法,返 回该对象对应的原始类型值。
Symbol. toStringTag在该对象上面调用 toString 方法时,返回该方法的返 回值
Symbol. unscopables该对象指定了使用 with 关键字时,哪些属性会被 with 环境排除。
// 检测类型
class Person{
    static [Symbol.hasInstance](param){
	console.log(param);
	console.log("我检测类型了");
	return false;
    }
}
let o = {};
console.log(o instanceof Person);

// 合并数组:false数组不可展开,true可展开
const arr = [1,2,3];
const arr2 = [4,5,6];
arr2[Symbol.isConcatSpreadable] = false;
console.log(arr.concat(arr2));

image-20220210112129561

迭代器(Iterator)

迭代器(Iterator)就是一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数 据结构只要部署 Iterator 接口,就可以完成遍历操作。ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。

原生具备 iterator 接口的数据(可用 for of 遍历)

  • Array

  • Arguments

  • Set

  • Map

  • String

  • TypedArray

  • NodeList

工作原理

  1. 创建一个指针对象,指向当前数据结构的起始位置
  2. 第一次调用对象的 next 方法,指针自动指向数据结构的第一个成员
  3. 接下来不断调用 next 方法,指针一直往后移动,直到指向最后一个成员
  4. 每调用 next 方法返回一个包含 value done 属性的对象

什么时候用:需要自定义遍历数据的时候,要想到迭代器

const arr = ["red", "green", "blue","yellow"]

// 使用 for...of 遍历数组
for(let v of arr){
  console.log(v)
}

let iterator = arr[Symbol.iterator]()

// 调用对象的next方法
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())

// 重新初始化对象,指针也会重新回到最前面
let iterator1 = arr[Symbol.iterator]();
console.log(iterator1.next());

image-20220210113908698

迭代器自定义遍历对象

const nb = {
  name: "清北班",
  stu: ["张三", "李四", "王五"],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.stu.length) {
          return { value: this.stu[index++], done: false };
        } else {
          return { value: undefined, done: true };
        }
      },
    };
  },
};

for (let v of nb) {
  console.log(v);// 张三 李四 王五
}

生成器(Generator)

生成器Generator函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

注意点

  • 函数的声明带一个*
  • 函数不能直接执行,用yield分割执行区域,next()依次执行,每次返回一个对象
  • next()方法是可以传入参数的,传入的参数作为上一条yield的返回结果

观察下面的执行:

function* gen() {
  console.log(1);
  yield "代码片1";
  console.log(2);
  yield "代码片2";
  console.log(3);
  yield "代码片3";
  console.log(4);
}

let iterator = gen();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

for (let v of gen()) {
	console.log(v)
}

image-20220210120720114

生成器函数的参数传递:

function* gen(arg){
    console.log(arg);
    let one = yield 1;
    console.log(one);
    let two = yield 2;
    console.log(two);
    let three = yield 3;
    console.log(three);
}
let iterator = gen("A");
console.log(iterator.next()); // 会执行yield 1;

// next()方法是可以传入参数的,传入的参数作为第一条yield的返回结果
console.log(iterator.next("B")); // 会执行yield 2;
console.log(iterator.next("C")); // 会执行yield 3;
console.log(iterator.next("D")); // 继续往后走,未定义;

image-20220214113353527

生成器实例:异步获取用户数据

// 模拟获取: 用户数据 订单数据 商品数据

function getUsers(){
    setTimeout(()=>{
	let data = "用户数据";
	iterator.next(data); // 这里将data传入
    },1000);
}
function getOrders(){
    setTimeout(()=>{
	let data = "订单数据";
	iterator.next(data); // 这里将data传入
    },1000);
}
function getGoods(){
    setTimeout(()=>{
	let data = "商品数据";
	iterator.next(data); // 这里将data传入
    },1000);
}
function* gen(){
    let users = yield getUsers();
    console.log(users);
    let orders = yield getOrders();
    console.log(orders);
    let goods = yield getGoods();
    console.log(goods); 
}


let iterator = gen();
iterator.next()

Promise

Promise 是 ES6 引入的异步编程的新解决方案。语法上 Promise 是一个构造函数,用来封装异步操作 并可以获取其成功或失败的结果。Promise在开发中用非常多,在面试中也是常考点,显得尤为重要。

知识点

  • Promise是一个构造函数,并且一创建就会执行

  • Promise的三种状态:pending(等待态),fulfiled(成功态),rejected(失败态)

  • Promise接收一个参数:函数,并且这个函数需要传入两个参数resolve,reject,也是函数

  • Promise.prototype上的.then():预先指定成功和失败的回调函数

  • Promise.prototype上的.catch():捕获异步操作中的错误

  • Promise.finally()Promise.all()Promise.race()

回调地狱

多层回调函数的相互嵌套,就形成了回调地狱,回调地狱造成代码耦合性太强,难以维护深嵌套,代码可读性差,不美观。如示例代码:

// 回调地狱
setTimeout(() => {
    console.log("开始第一件事")
    setTimeout(() => {
        console.log("开始第二件事")
            setTimeout(() => {
                console.log("开始第三件事")
            }, 1000)
    }, 1000)
}, 1000)

Promise使用

我们再来看看用Promise写的

const p = new Promise((resolve,reject) => {// 初始化,状态置为`pending`(等待态)
    setTimeout(() => {
	let data = "数据";
        resolve(data)// 调用resolve,状态置为`fulfiled`(成功态)
		
	let err = "失败了!";
        //reject(err) // 调用reject,状态置为`rejected`(失败态)

    },1000);
});

// 调用 Promise 对象的then方法,两个参数都为函数,成功的回调必传,失败的回调可传可不传
p.then((res) => { // 成功回调
    console.log(res);// 数据
}, (err) => { // 失败回调
    console.log(err);// 失败了
});

p.catch((err) => console.log(err))// 在catch中也能捕获错误

实践:用Promise封装异步读取多个文件(node.js环境下)

code

还有Promise.finally()和``Promise.all()Promise.race()`,由于内容比较多,不好展开讲,可以去看一下阮一峰 - ES6 入门教程

async和await

ES8引入了asyncawait,更近一步地简化了Promise异步操作,可以让异步代码看起来像同步代码一样,在开发中用的也比较多。

注意点

  • 如果使用了await,则函数必须被async所修饰,async函数的返回值为Promise对象
  • await右侧表达式一般为Promise对象

演示下async/await简单使用:

//
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    let data = "数据"
    resolve(data)
  }, 2000)
})

async function getData() {
  const res1 = await p1
  console.log(res1)
}

getData()

p1.then(res2 => console.log(res2)) // 执行后,可以发现res1的值和res2一样

说简单点,用async修饰后可以得到Promise对象的成功回调的值,不需要再用.then()链式调用,从而简化了Promise的异步操作。

image-20220213215903014

最后

感谢读完本篇文章,希望对你能有所帮助,如有问题欢迎各位指正❤️❤️。

推荐文章

从简到难,重新学习ES6(上)

从简到难,重新学习ES6(中)