复习ES(6-11)语法之ES6下篇

发布时间 2023-07-06 21:32:44作者: 小风车吱呀转

异步操作前置知识

  • JS是单线程的

单线程即一个时间只能处理一个任务。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

  • 同步任务与异步任务

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。

const a = 2
const b = 3
console.log(a + b) // 同步任务
// 异步任务
setTimeout(() => { // 延迟1s执行
    console.log(a + b)
}, 1000)

console.log(1)
setTimeout(() => {
    console.log(2)
}, 1000)
console.log(3)
// 1 3 2

console.log(1)
//不管延迟时间是多少,它都是异步任务,必须等主线程任务执行完成之后再执行
setTimeout(() => {
    console.log(2)
}, 0)
console.log(3)
//1 3 2

  • Ajax原理

Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发送异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。这其中最关键的一步就是从服务器获得请求数据

function ajax(url, callback) {
  // 1、创建XMLHttpRequest对象
  var xmlhttp;
  if (window.XMLHttpRequest) {
    xmlhttp = new XMLHttpRequest();
  } else {
    // 兼容早期浏览器
    xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
  }
  // 2、发送请求
  xmlhttp.open("GET", url, true); // 指定发送请求的操作
  xmlhttp.send(); // 发送
  // 3、服务端响应
  // 监听onreadystatechange 方法
  xmlhttp.onreadystatechange = function () {
    // 事件处理函数
    if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
      var obj = JSON.parse(xmlhttp.responseText);
      // console.log(obj)
      callback(obj); // 将响应得到的数据传给回调,回调函数是自己定义并传入的
    }
  };
}

//获取随机猫咪照片
var url = "https://api.thecatapi.com/v1/images/search?limit=1";
ajax(url, (res) => {
  console.log(res);
});

  • Callback Hell

回调函数可规范调用的顺序,但是当代码层层嵌套越写越深,代码的可维护性、可读性都会降低,就会造成Callback Hell

// 1 -> 2 -> 3
// callback hell
ajax('static/a.json', res => {
    console.log(res)
    ajax('static/b.json', res => {
        console.log(res)
        ajax('static/c.json', res => {
            console.log(res)
        })
    })
})

Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

let p = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log("zzz");
    resolve();
  }, 1000);
}).then(
  (res) => {
    console.log("成功");
  },
  (err) => {
    console.log("失败");
  }
);

如果Promise内部没有写任何异步操作,那么它是会立即执行的。then方法相当于promise的回调函数(它的微任务),待promise内的函数执行完成便执行。

let p = new Promise((resolve, reject) => {
  console.log(1);
  resolve();
});

p.then((res) => {
  console.log(3);
});
console.log(2);

//依次输出:1 2 3

Promise状态一旦确定下来就无法再改变

let p = new Promise((resolve, reject) => {
  resolve(1);
  reject(2);
});
p.then(
  (res) => {
    console.log(res); // 1
  },
  (err) => {
    console.log(err);
  }
);

改造回调深渊Callback Hell

// 传入成功回调与失败回调
function ajax(url, successCallback, failCallback) {
  // 1、创建XMLHttpRequest对象
  var xmlhttp;
  if (window.XMLHttpRequest) {
    xmlhttp = new XMLHttpRequest();
  } else {
    // 兼容早期浏览器
    xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
  }
  // 2、发送请求
  xmlhttp.open("GET", url, true);
  xmlhttp.send();
  // 3、服务端响应
  xmlhttp.onreadystatechange = function () {
    if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
      var obj = JSON.parse(xmlhttp.responseText);
      // console.log(obj)
      successCallback && successCallback(obj);
    } else if (xmlhttp.readyState === 4 && xmlhttp.status === 404) {
      failCallback && failCallback(xmlhttp.statusText);
    }
  };
}
function getPromise(url) {
  return new Promise((resolve, reject) => {
    ajax(
      url,
      (res) => {
        resolve(res);
      },
      (err) => {
        reject(err);
      }
    );
  });
}
getPromise("static/a.json")
  .then((res) => {
    console.log(res);
    return getPromise("static/b.json");
  })
  .then((res) => {
    console.log(res);
    return getPromise("static/c.json");
  })
  .then((res) => {
    console.log(res);
  });

静态方法:

  • Promise.resolve() 表示成功的状态
let p1 = Promise.resolve("success");
p1.then((res) => {
  console.log(res); // success
});
  • Promise.reject() 表示失败的状态
let p2 = Promise.reject("fail");
p2.catch((err) => {
  console.log(err); // fail
});
  • Promise.all() 传入一个数组作为参数,数组内的每一个内容都对应一个Promise对象
let p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log(1);
    resolve("1成功");
  }, 1000);
});

let p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log(2);
    //resolve("2成功");
    reject('2失败')
  }, 2000);
});
let p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log(3);
    resolve("3成功");
  }, 3000);
});
//p1,p2,p3都resolve了才执行后面的then,只要有一个失败了就进入失败状态
Promise.all([p1, p2, p3]).then((res) => {
  console.log(res);
},err=>{
  console.log(err)
});

Promise.all(下文都称p)的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

  • Promise.race() 将多个 Promise 实例,包装成一个新的 Promise 实例
//只要有一个率先完成,那整个状态就是完成的,只要有一个率先失败,那整个状态就是失败的
Promise.race([p1, p2, p3]).then(
  (res) => {
    console.log(res);
  },
  (err) => {
    console.log(err);
  }
);

只要p1p2p3之中有一个实例率先改变状态,Promise.race的状态就跟着改变。

Promise的应用场景举例:

①上传图片

const imgArr = ["1.jpg", "2.jpg", "3.jpg"];
let promiseArr = [];
imgArr.forEach((item) => {
  promiseArr.push(
    new Promise((resolve, reject) => {
      //图片上传的操作
      resolve();
    })
  );
});
Promise.all(promiseArr).then((res) => {
  console.log("图片全部上传成功");
});

②加载图片

function getImg() {
  return new Promise((resolve, reject) => {
    let img = new Image();
    img.onload = function () {
      resolve(img.src);
    };
    // img.src = "http://www/xxx.com/xx.jpg";
    img.src = "https://cdn2.thecatapi.com/images/6fk.jpg";
  });
}
function timeout() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject("图片请求超时");
    }, 2000);
  });
}

Promise.race([getImg(), timeout()])
  .then((res) => {
    console.log("加载图片成功", res);
  })
  .catch((err) => {
    console.log(err);
  });

Generator

原理

  • 从语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
    执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
  • 形式上,Generator 函数是一个普通函数,但是有两个特征。一是, function 关键字与函数名之间有一个星号;二是,函数体内部使用 yield 表达式, 定义不同的内部状态( yield 在英语里的意思就是“产出”)。
  • Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。
  • 必须调用遍历器对象的 next 方法(next方法可以传递参数),使得指针移向下一个状态。也就是说,每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。换言之,Generator 函数是分段执行的, yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。

用法

Generator是可以暂停的,需要调用next方法手动执行, yield指令只能在生成器内部使用。

由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

遍历器对象的next方法的运行逻辑如下。

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined

//普通函数
function foo() {
  for (let i = 0; i < 3; i++) {
    console.log(i);
  }
}
foo(); // 直接输出 0,1,2

// Generator
function* _foo() {
  for (let i = 0; i < 3; i++) {
    yield i;
  }
}
let f = _foo();
// 通过next手动执行
console.log(f.next()); // { value: 0, done: false }
console.log(f.next()); // { value: 1, done: false }
console.log(f.next()); // { value: 2, done: false }
console.log(f.next()); // { value: undefined, done: true }

// yield指令只能在生成器内部使用
// function* gen(args) {
//     args.forEach(item => {
//         yield item + 1
//     })
// }

需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

function* gen(x) {
  let y = 2 * (yield x + 1);
  let z = yield y / 3;
  return x + y + z;
}
let g = gen(5);
console.log(g.next().value); // 6
// 上一次yield表达式没有返回y值,所以变成y = 2* undefined => NaN
console.log(g.next().value); // NaN  
console.log(g.next().value); // NaN  y / 3 => NaN

yield表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

//①
let g = gen(5);
//  x+1 = 6
console.log(g.next().value);// 输出6
// 传递参数6,前面的 (yield x + 1) = 6,所以y = 2* 6 = 12, y /3 = 4
console.log(g.next(6).value); //输出4
 // 传递参数4,前面的 ( yield y / 3 ) = 4,所以z = 4,return x+y+z = 5+12+4 = 21
console.log(g.next(4).value); //输出21
//②
let g = gen(5);
// x+1 = 6
console.log(g.next().value); //输出6
//传递参数8,前面的 (yield x + 1) = 8,所以y = 2* 8 = 16, y /3 = 5.3333
console.log(g.next(8).value); //输出5.3333
// 传递参数12,前面的(yield y /3 ) = 12,所以z = 12, return x+y+z = 5+16+12 = 33
console.log(g.next(12).value); //输出33

可以写一个计数器,每次遇到7的倍数就停止执行

//计数器 遇到7的倍数就停止
function* count(x = 1) {
  while (true) {
    if (x % 7 === 0) {
      yield x;
    }
    x++;
  }
}
let n = count();
console.log(n.next().value); // 7
console.log(n.next().value); // 14
console.log(n.next().value); // 21
console.log(n.next().value); // 28
console.log(n.next().value); // 35

异步状态管理

以ajax为例,请求顺序a→b→c

function ajax(url, callback) {
  // 1、创建XMLHttpRequest对象
  var xmlhttp;
  if (window.XMLHttpRequest) {
    xmlhttp = new XMLHttpRequest();
  } else {
    // 兼容早期浏览器
    xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
  }
  // 2、发送请求
  xmlhttp.open("GET", url, true);
  xmlhttp.send();
  // 3、服务端响应
  xmlhttp.onreadystatechange = function () {
    if (xmlhttp.readyState === 4 && xmlhttp.status === 200) {
      var obj = JSON.parse(xmlhttp.responseText);
      // console.log(obj)
      callback(obj);
    }
  };
}
// 封装请求方法,调用ajax
function request(url) {
  ajax(url, (res) => {
    getData.next(res); // 每一次request请求都会调用next,使Generator对象继续执行
  });
}

function* gen() {
  let res1 = yield request("static/a.json");
  console.log(res1);
  let res2 = yield request("static/b.json");
  console.log(res2);
  let res3 = yield request("static/c.json");
  console.log(res3);
}

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

Iterator

Iterator是一种接口,为各种不同的数据结构提供统一的访问机制,使得不支持遍历的数据结构“可遍历”。Iterator 接口主要供for...of消费。

Iterator 的遍历过程是这样的。

(1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

(2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

function makeIterator(arr) {
  let nextIndex = 0;
  return {
    next() {
      return nextIndex < arr.length
        ? {
            value: arr[nextIndex++],
            done: false,
          }
        : {
            value: undefined,
            done: true,
          };
    },
  };
}
let it = makeIterator(["a", "b", "c"]);
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());

原生具备 Iterator 接口的数据结构

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

下面的例子是数组和Map的Symbol.iterator属性。

let arr = ['a', 'b', 'c'] // 对于数组这样的可迭代结构,它里面自带了Symbol.iterator
console.log(arr) // ["a", "b", "c"]
let it = arr[Symbol.iterator]()
console.log(it.next()) // {value: "a", done: false}
console.log(it.next())
console.log(it.next())
console.log(it.next())// {value: undefined, done: true} ,迭代结束

let map = new Map()
map.set('name', 'es')
map.set('age', 5)
let it = map[Symbol.iterator]()
console.log(it.next())
console.log(it.next())
console.log(it.next())

将不可迭代对象改造成符合上述两种协议的结构,便能够实现遍历:

let courses = {
  allCourse: {
    fronted: ["ES", "Vue", "小程序"],
    backend: ["JAVA", "Python"],
    webapp: ["Android", "IOS"],
  },
};
for (let c of courses) {
  console.log(c); // 报错,不可迭代
}
// 可迭代协议:Symbol.iterator
// 迭代器协议:return { next(){ return { value,done } } }
courses[Symbol.iterator] = function () {
  let allCourse = this.allCourse;
  let keys = Reflect.ownKeys(allCourse);
  let values = [];
  return {
    next() {
      if (!values.length) {
        if (keys.length) {
          values = allCourse[keys[0]];
          keys.shift();
        }
      }
      return {
        done: !values.length,
        value: values.shift(),
      };
    },
  };
};

for (let c of courses) {
  console.log(c); // 可迭代
}

Generator遍历不可迭代对象

Generator自带next方法,所以它也能够帮助不可迭代对象遍历。

courses[Symbol.iterator] = function* () {
  let allCourse = this.allCourse;
  let keys = Reflect.ownKeys(allCourse);
  let values = [];
  while (1) {
    if (!values.length) {
      if (keys.length) {
        values = allCourse[keys[0]];
        keys.shift();
        yield values.shift();
      } else {
        return false;
      }
    } else {
      yield values.shift();
    }
  }
};

for (let c of courses) {
  console.log(c); // 报错,不可迭代
}

模块化规范

CommonJS:Node.js

AMD:require.js

CMD:sea.js

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

import { a } from "./module.js";
console.log(a);

模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。

如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

import { a as aa } from "./module.js";
console.log(aa);

使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

// module.js
const a = 5;
export default a;


//index.js
import aa from "./module.js";
console.log(aa);


export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。