蓝桥云课ES6

新增关键字

var关键字的缺点

JavaScript的变量提升机制

在JavaScript中有一个变量提升机制,无论在哪里使用var声明变量,都会被提升到当前作用域的顶部。

例如以下代码:

function getNumber(isNumber) {
  if (isNumber) {
    var num = "7";
  } else {
    var notNum = "not number!";
    console.log(num);
  }
}
getNumber(false);

控制台报错num变量未定义。

变量提升后代码:

function getNumber(isNumber) {
  var num;
  var notNum;
  if (isNumber) {
    num = "7";
  } else {
    notNum = "not number!";
    console.log(num);
  }
}
getNumber(false);

所以可以看出当传入变量为false时,num没有被赋值,因此控制台输出undefined

变量重复声明问题

我们知道使用 var 关键字声明的变量可以重复赋值,但在某些情况下会造成一些问题。例如下面的情景:

function Sum(arrList) {
  var sum = 0;
  for (var i = 0; i < arrList.length; i++) {
    var arr = arrList[i];
    for (var i = 0; i < arr.length; i++) {
      sum += arr[i];
    }
  }
  return sum;
}
var arr = [1, 2, 3, 4, 5];
document.write(Sum(arr));

如果在我们的环境中运行这段代码,环境会卡死。🥶

这是因为在两层 for 循环中我们使用同一变量 i 进行赋值时,代码在执行过程中,第二层的 for 循环会覆盖外层变量 i 的值。

非块作用域的问题

使用 var 关键字定义的变量只有两种作用域,全局作用域和函数作用域,两者均不是块结构,会造成变量声明的提升。这可能出现下面这种问题:

function func() {
  for (var i = 0; i < 5; i++) {}
  document.write(i); // 5
}
func();

运行上述代码后,你会发现页面上会显示 5。我们虽然是在 for 循环中定义的 i 变量,但由于变量被提升到 for 语句之上,所以退出循环后,变量 i 并没有被销毁,我们能够在循环外获取它的值。

认识let关键字

解决变量提升机制问题

let的作用域是块,解决了变量提升到作用域顶部的问题。

例如:

function getNumber(isNumber) {
  if (isNumber) {
    let num = "7";
  } else {
    let notNum = "not number!";
    console.log(num);
  }
}
getNumber(false);

控制台报错:num变量不存在。

原因:let关键字声明的变量作用域是一个块,而上述代码是在if{}里声明的变量,因此该变量的作用域为该花括号快,因此变量形成暂时性死区。

解决变量重复声明问题

虽然 let 关键字声明的变量可以重新赋值,但是它与 var 关键字有所不同,let 关键字不能在同一作用域内重新声明,而 var 可以。

例如:

let i = 5;
let i = 6;
console.log(i);

控制台报错:i变量已经被定义。

关键字改为var则不会报错

解决非块级作用域的问题

var关键字定义变量,只有两种作用域:函数作用域和全局作用域,而这两种作用域都是非块级作用域。let关键字定义的变量是块级作用域,避免了变量提升。

例如:

function func() {
  for (let i = 0; i < 5; i++) {}
  console.log(i);
}
func();

上段代码控制台报错:i is not defined

原因是因为let声明的变量i作用域仅在for循环这个块中,当循环结束时i即被销毁。因此在for循环之外不可以访问到i变量

认识const关键字

在ES6中,提供了关键字const用于声明一个只读的变量,变量一旦声明,值就不会被改变,如果反复赋值则会报错。并且需要在定义时就对变量进行初始化。

对于 const 关键字定义的变量值,不可改变在于两个方面:

1. 值类型

值类型是指变量直接存储的数据,例如:

const num = 20;

这里 num 变量就是值类型,我们使用的是 const 关键字来定义 num,故赋予变量 num 的值 20 是不可改变的。

2. 引用类型

引用类型是指变量存储数据的引用,而数据是放在数据堆中,比如,用 const 声明一个数组。

const arr = ["一", "二", "三"];

如果你尝试去修改数组,同样会报错。

但是,使用 const 关键字定义的引用类型还是可以通过数组下标去修改值 ⭐。

例如:

const arr = ["一", "二", "三"];
arr[0] = "四";
arr[1] = "五";
arr[2] = "六";

console.log(arr);

因为变量 arr 保存的是数组的引用,并不是数组中的值,只要引用的地址不发生改变就不会报错。

字符串的扩展

模板字面量的基本用法

模板字面量:在JavaScript中,字面量代表由一些字符组成表达式定义的常量。

在之前的学习中,我们要定义一个字符串,会用单引号'' 或者双引号"" 去包裹字符串。在 ES6 中提供了反撇号去代替单引号和双引号。

多行字符串的处理

当多行文字需要换行处理时,ES6之前的操作使用\n转义符进行换行:

console.log('java/nscript')

但这种方式并不是长久之计,假如很长的字符串需要多次换行,你需要手动加入多个 \n

在ES6中使用反引号,模板字面量有个特点,定义在反撇号中的字符串,其中的空格、缩紧、换行都会被保留。

字符串占位符

在 ES5 中,如果要把变量和字符串放在一起输出,你可能会想到使用 + 号来拼接。

let a = 2;
let b = 4;
console.log("a = " + a + ", b = " + b + ", sum = " + (a + b));

这样拼接的过程是很容易出错的。

在模板字面量中,你可以把合法的 JavaScript 表达式嵌入到占位符中并将其作为字符串的一部分输出

在JavaScript中,占位符由${}符号组成,在花括号中间可以包含任意JavaScript表达式

例如:

let str = `LanQiao Courses`;
let message = `I like ${str}.`;
console.log(message);

在上面代码中,占位符 ${str} 会访问变量 str 的字符串,并将其值插入到 message 字符串中,变量 message 会一直保留着这个结果。

标签模板

这里的标签并不是在 HTML 中所说的标签,这里的标签相当于是一个函数。而标签模板就是执行模板字面量上的转换并返回最终的字符串。

例如:

let name = `JavaScript`;
let str = tag`Welcome to ${name} course.`;

在上面的代码中,用于模板字面量的模板标签就是 tag 了。

这里的标签可以理解为一个函数,该函数的参数说明如下:

function tag(literals, value1) {
  // 返回一个字符串
}
  • 第一个参数是一个数组,数组中存放普通的字符串,例如 str 中的 Welcome、to、course、.。

  • 第一个参数之后的参数,都是每一个占位符的解释值,例如 str 中的 ${name}。

当字符串中的占位符比较多时,也可以使用不定参数的形式来定义后面的参数。

function tag(literals, ...values) {}

我们通过这种方式将 literals 和 values 中的值交织起来重新组合成字符串输出。

使用如下:

function tag(literals, ...values) {
  let result = ""; // result 变量用来存放重组后的数组
  // 根据 values 的数量来确定遍历的次数
  for (let i = 0; i < values.length; i++) {
    result += literals[i];
    result += values[i];
  }
  // 合并最后一个 literals
  result += literals[literals.length - 1];
  return result;
}
let name = `JavaScript`;
let str = tag`Welcome to ${name} course.`;
console.log(str);

在上面代码中定义了一个名为 tag 的标签,在其内部的执行逻辑如下:

  • 首先定义了一个名为 result 的空字符串用来存储最终输出字符串的结果。

  • 接下来执行了一个 for 循环,遍历了 values 的长度。

  • 取出 literals 的首个元素,再取出 values 中的首个元素,然后交替继续取出每一个元素,直到字符串拼接完成。

标签模板的实际应用在:

  • 过滤HTML字符串,防止用户输入恶意内容

  • 多语言的转换

字符串新增的方法

判断指定字符串是否存在

在 ES5 中,我们要判断某个字符串是否包含指定字符串时,可以用 indexOf() 方法来判断,该方法可以返回指定字符串在某个字符串中首次出现的位置,其实这样还是比较麻烦的。在 ES6 中,为我们新增了三种方法来判断字符串是否包含在其中。

  • includes():判断是否包含指定字符串,如果包含返回 true,反之 false。

  • startsWith():判断当前字符串是否以指定的子字符串开头,如果是则返回 true,反之 false。

  • endsWith():判断当前字符串是否以指定的子字符串结尾,如果是则返回 true,反之 false。

注意:传入的字符串需要注意大小写,大小写不同也会造成匹配失败的情况。

重复字符串

repeat(n)方法用于返回一个重复n次原字符串的新字符串,其参数n为整数,如果设置n为小数,会自动转换为整数

repeat() 方法中的参数 n 取值只能是整数,如果 n 为负数时,会报错;如果n为小数时,会自动忽略小数部分

替换字符串

在 ES5 中有一个 replace() 方法可以替换指定字符串,不过它只能替换匹配到的第一个字符串,如果想匹配整个字符串中所有的指定字符串是很麻烦的。

在ES6中提供了replaceAll()方法来解决这个问题,可以用来替换所有匹配的字符串,语法格式如下:

string.replaceAll("待替换的字符", "替换后的新字符");

数组的扩展

创建数组的方法

Array.of()

Array.of() 的语法格式如下:

Array.of(element 0, element 1, ..., element N)

返回具有 N 个元素的数组。

当Array.of方法中只输入一个整数时,该参数不会被识别为数组的长度,而是作为数组的元素

Array.from()

在ES6之前,把非数组类型对象转换成一个数组,需要使用[].slice.call()把一个非数组类型变为数组类型。例如:

let arrLike = {
  0: "🍎",
  1: "🍐",
  2: "🍊",
  3: "🍇",
  length: 4,
};
var arr = [].slice.call(arrLike);
console.log("arr:" + arr);

在ES6中,提供了Array.from()代替了这种旧办法

Array.from() 方法可以将以下两类对象转为数组。

  • 类似数组的对象(array-like-object)。

  • 可遍历的对象(iterable-object)。

其基本使用格式为:

Array.from(待转换的对象);

数组实例的方法

find()方法

find()方法用于从数组中寻找一个符合指定条件的值,该方法返回的是第一个符合条件的元素,若没找到则返回undefined

其语法格式为:

array.find(callback(value, index, arr), thisValue);

参数说明如下:

  • callback 是数组中每个元素执行的回调函数。

  • value 是当前元素的值,它是一个必须参数。

  • index 是数组元素的下标,它是一个可选参数。

  • arr 是被 find() 方法操作的数组,它是一个可选参数。

  • thisValue 是执行回调时用作 this 的对象,它是一个可选参数。

例如:

let arr = [1, 3, 4, 5];
let result = arr.find(function (value) {
  return value > 2;
});
console.log(result);

findIndex()方法

findIndex()方法返回数组中第一个符合指定条件的元素的索引下标值,若整个数组没有符合条件的元素,则返回-1

语法格式:

array.findIndex(callback(value, index, arr), thisArg);

参数说明如下:

  • callback 是数组中每个元素都会执行的回调函数。

  • value 是当前元素的值,它是一个必须参数。

  • index 是数组元素的下标,它是一个必须参数。

  • arr 是被 findIndex() 方法操作的数组,它是一个必须参数。

  • thisArg 是执行回调时用作 this 的对象,它是一个可选参数。

注意:执行回调函数时,会自动传入 value、index、arr 这三个参数。

find() 方法和 findIndex() 方法,其实用法很像,最大区别在于两个方面:

  • 执行回调函数后的返回值不同,find() 方法返回的是元素本身,而 findIndex() 方法返回的是元素的下标。

  • 匹配失败的返回值不同,find() 方法返回的是 undefined,而 findIndex() 方法返回的是 -1。

fill()方法

fill()方法是用指定的值来填充原始数组的元素

其使用格式为:

array.fill(value, start, end);

其参数说明如下:

  • value 是用来填充数组的值,它是一个必须参数。

  • start 是被填充数组的索引起始值,它是一个可选参数。

  • end 是被填充数组的索引结束值,它是一个可选参数。

注意:如果不指定start和end参数,该方法默认填充整个数组的值

数组遍历的方法

entries()、keys()、values()是ES6中三种数组的遍历方法,三个方法返回的都是Array Iterator对象,但是三者之间不完全相同。

entries()方法以键值对的形式返回数组的[index,value]。语法格式:

array.entries();
let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"];
let result = arr.entries();
console.log(...result);

array.keys()方法只返回数组元素的键值也就是元素对应的索引,不会返回其值。

语法格式为:

array.keys();

for...of循环

for...of 是 ES6 提供的新循环方式。

for...of 就摆脱了计数器、退出条件等烦恼,它是通过迭代对象的值来循环的。它能迭代的数据结构很多,数组、字符串、列表等。

for...of 的语法格式如下所示:

for (variable of iterable) {
}

参数说明如下:

  • variable:是存放当前迭代对象值的变量,该变量能用 const、let、var 关键字来声明。

  • iterable:是可迭代对象。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>for...of</title>
  </head>
  <body>
    <script>
      const arr = ["小红", "小蓝", "小绿"];
      for (let name of arr) {
        document.write("欢迎" + name + "来到蓝桥云课!" + "<br/>");
      }
    </script>
  </body>
</html>
扩展运算符

扩展运算符(...)是ES6新语法,它可以将可迭代对象的参数在语法层面上进行展开。

语法格式:

// 在数组中的使用
let VariableName = [...value];

例如:

let animals = ["兔子🐰", "猫咪🐱"];
let zoo = [...animals, "老虎🐯", "乌龟🐢", "鱼🐟"];
console.log(zoo);

控制台输出:

在 ES2018 中又将扩展运算符引入了对象

在对象上,我们主要有以下三种操作:

  • 可以使用扩展运算符将一个对象的全部属性插入到另一个对象中,来创建一个新的对象。

  • 可以使用扩展运算符给对象添加属性。

  • 可以使用扩展运算符合并两个新对象。

插入另一个对象的全部属性来创建一个新的对象

let student = { name: "小白", age: 17, email: "1234@qq.com" };
let NewObj = { ...student };
console.log(NewObj);

给对象添加属性

let student = { name: "小白", age: 17, email: "1234@qq.com" };
let NewObj = { ...student, id: 7 };
console.log(NewObj);

合并对象

let studentName = { name: "小白" };
let studentAge = { age: 17 };
let NewObj = { ...studentName, ...studentAge };
console.log(NewObj);

函数的扩展

在函数中直接设置默认值

在ES5中设置默认参数的方式:

function func(words, name) {
  name = name || "闷墩儿";
  console.log(words, name);
}
func("大家好!我是");
func("大家好!我是", "憨憨");
func("大家好!我是", "");

在ES6中可以直接在函数的形参里设置默认值,例如:

function func(words, name = "🍎") {
  console.log(words, name);
}
func("请给我一个");
func("请给我一个", "🍐");
func("请给我一个", "");

注意参数默认值的位置

设置默认值的参数,一般放在其他参数的后面,也就是尾部。因为如果给第一个形参设置了默认值,调用函数传参时,无法省略该参数。

使用函数作为默认值

我们还可以使用自定义的函数作为形式参数的默认值。

例如:

function parameter() {
  return "🖤";
}
function func(words, name = parameter()) {
  console.log(words, name);
}
func("请给我一颗小");
func("请给我一颗小", "💗");

解构参数

解构可以用在函数参数传递的过程中

例如:

function func(name, value, mount, { a, b, c, d = "苹果" }) {
  console.log(`${name}用${value}元钱买了${mount}个${d}。`);
  console.log(`${name}用${value}元钱买了${mount}个${c}。`);
}
func("小蓝", 5, 3, {
  a: "西瓜",
  b: "菠萝",
  c: "桃子",
});

rest参数

rest参数又称为剩余参数,用于获取函数的多余参数。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

rest参数和扩展运算符在写法上一样,都是(...),但是两者的使用上截然不同

扩展运算符就像是 rest 参数的逆运算,主要用于以下几个方面:

  • 改变函数的调用。

  • 数组构造。

  • 数组解构。

rest 参数语法格式为:

// 剩余参数必须是函数的最后一个参数
myfunction(parameters, ...rest);

例如:

function func(a, b, ...rest) {
  console.log(rest);
}
func(1, 2, 3, 4, 5, 6, 7, 8, 10);

控制台输出:

箭头函数

箭头函数,就是用=>来表示函数。箭头函数和普通函数都是用来定义函数的,但是在语法构成上不同,

普通函数语法:

let sum = function (a, b) {
  return a + b;
};
console.log(sum(1, 2));

箭头函数语法:

let sum = (a,b)=>a+b;
console.log(sum(1,2));

可以看出箭头函数的基本用法:

(param1,param2,...,paramN) => {expression}

箭头函数除了代码简洁以外,还解决了匿名函数的this指向问题。

this的指向

this是指向调用包含自身函数对应的对象

在普通函数中实现在指定事件周期内一直计数方法:

function Number() {
  var that = this;
  that.num = 1;
  setInterval(function count() {
    // count 函数指向的是 that 变量
    that.num++;
  }, 1000);
}

可以看出以上方法中存在的两个函数都有自己的this,并且作用域都只存在于自己的函数作用域之内。

用箭头函数的方法修改代码:

function Number() {
  this.num = 1;
  setInterval(() => {
    // this 正确地引用了 Number 对象
    this.num++;
  }, 1000);
}

箭头函数中的this对象,就是定义该函数所在的作用域所指向的对象,而不是使用所在作用域指向的对象。

由此总结箭头函数和普通函数的区别:

  • 箭头函数的this指向是其上下文的this,没有方法可以改变其指向

  • 普通函数的this指向调用它的那个对象

不带参数的箭头函数

语法格式如下:

let dogName = () => "闷墩儿";
console.log(dogName());

带默认参数的箭头函数

语法格式如下:

let func = (x, y = "🌈") => {
  return `${x}${y}`;
};
console.log(func("请给我一道"));
console.log(func("请给我一朵", "🌺"));

箭头函数的几个简写形式

1、当箭头函数的参数为单个时,可以省略包裹参数的小括号。

let func = a => {
  return a;
}
console.log(func('嗨!我是单参数'));

2、当return后面为单语句时,可以省略return 和{}花括号

let func = (a) => a;
console.log(func("可以省略我哦~"));

3、如果箭头函数直接返回一个对象,需要用小括号包裹

let student = () => ({ name: "小蓝" });
console.log(student());

带rest参数的箭头函数

let func = (a, b, ...rest) => {
  console.log(a);
  console.log(b);
  console.log(rest);
};
func(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

类的扩展

类的声明

在 ES6 之前其实是不存在类的,因为 JavaScript 并不是一门基于类的语言,它使用函数来创建对象,并通过原型将它们关联在一起。

例如:

function MyClass(num) {
  this.num = num;
  this.enginesActive = false;
}
MyClass.prototype.startEngines = function () {
  console.log("starting ...");
  this.enginesActive = true;
};
const myclass = new MyClass(1);
myclass.startEngines();
  • 在上面的代码中,MyClass 是一个构造函数,用来创建 MyClass 类型的对象。

  • 使用 this 声明并初始化 MyClass 的属性 num 和 enginesActive。

  • 并在原型中存储了一个可供所有实例调用的方法 startEngines。

  • 然后使用 “new+构造函数”的方式创建实例对象 myclass,并将 myclass 的 num 属性初始化为 1。

  • 最后调用执行实例对象 myclass 的 startEngines 方法。

在ES6中可以使用class关键字来创建类。

例如:

class MyClass {
  // constructor 方法是类的默认方法
  constructor(num) {
    this.num = num;
    this.enginesActive = false;
  }
  // 相当于 MyClass.prototype.startEngines
  startEngines() {
    console.log("staring...");
    this.enginesActive = true;
  }
}
const myclass = new MyClass(1);
myclass.startEngines();

类表达式

类和函数都有两种存在形式:

  • 声明形式(例如:function、class关键字声明)

  • 表达式形式(例如:const A = class{})

匿名类表达式

// ES6 语法
let DogType = class {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(`大家好!我是一只小${this.name}。`);
  }
};
let dog = new DogType("柯基");
dog.sayName();
console.log(dog instanceof DogType);
console.log(dog instanceof Object);

命名类表达式

在 let DogType = class 我们定义的是一个匿名类表达式,和函数一样,我们也可以给类表达式命名。我们来举个例子,看看如何给类的表达式命名。

let DogName = class MyClass {
  constructor(name) {
    this.name = name;
  }
  sayName() {
    console.log(this.name);
  }
};
console.log(typeof DogName);
console.log(typeof MyClass);

类的继承

在没有 ES6 的时代,通常是使用构造函数 + 原型对象的方式去模拟实现继承的功能。

function Animal(name, age, speed) {
  this.name = name;
  this.age = age;
  this.speed = speed;
}
Animal.prototype.run = function () {
  console.log(`${this.age}岁的${this.name}酷跑了 ${this.speed} 公里。`);
};
Animal.prototype.stop = function () {
  console.log(`${this.name}停止了奔跑。`);
};
let dog = new Animal("闷墩儿", "一", 5);
dog.run();
dog.stop();

在 ES6 中为我们提供了 extends 关键字来实现继承。

其使用格式如下:

class child_class_name extends parent_class_name {}

上面代码的意思是 child_class_name 类继承了 parent_class_name 类。

class Animal {
  constructor(name, age, speed) {
    this.name = name;
    this.age = age;
    this.speed = speed;
  }
  run() {
    console.log(`${this.age}岁的${this.name}酷跑了 ${this.speed} 公里。`);
  }
  stop() {
    console.log(`${this.name}停止了奔跑。`);
  }
}
class Dog extends Animal {} // Dog 类继承了 Animal 类
// 实例化 Dog 类
let dog = new Dog("闷墩儿", "一", 5);
// 调用 Animal 类中的 run() 方法和 stop() 方法
dog.run();
dog.stop();
  • 在上面代码中,使用 class Dog extends Animal {} 定义了一个 Dog 类继承了 Animal 类的属性和方法。

  • 使用 let dog = new Dog('闷墩儿','一',5) 实例化 Dog 对象。

  • 使用 Dog 类的实例对象 dog 去调用 run 和 stop 方法,程序会先在 Dog 类的原型 Dog.prototype 中查找两个方法是否存在,找不到的情况下会去其父类原型 Animal.prototype 中查找,以此类推,层层向上最终找到并调用执行。

extends后 可接表达式

extends关键字后面不仅可以跟类,还可以接一个表达式。

例如:

function func(message) {
  return class {
    say() {
      console.log(message);
    }
  };
}
class Person extends func("欢迎!") {}
person = new Person();
person.say();

上面代码解析如下:

  • 先创建一个返回匿名类的函数 func,该匿名类中包含一个方法 say()。

  • 然后声明 Person 类继承了 func 函数的返回值(也就是包含方法 say() 的匿名类)。

  • 在上一步继承前,先执行 func 函数,并向其传入参数“欢迎来到蓝桥云课!”。

  • 接着 new 了一个 Person 类的实例对象 person。

  • 然后 person 调用 say() 方法,最终在控制台输出了。

认识super

ES6提供了超级函数super,使继承变得完整且具备可扩展性

使用格式有以下两种:

  • 使用super.method(...)来调用父方法

  • 使用super(...)调用父构造函数

重写构造函数

子类中不写constructor的情况下,在被实例化时会自动生成一个constructor,如:

class Dog extends Animal{
    constructor(...args){
        super(...args);
    }
}

可以看到自动生成的 constructor 中只有一个 super(...args);,执行该 super 函数可以继承并初始化父类 Animal 中构造函数里的属性。

在 JavaScript 中,继承类的构造函数和其他函数是有区别的。继承类的构造函数有一个特殊的内部属性 [[ConstructorKind]]:"derived"。通过该属性会影响 new 的执行:

  • 当一个普通(即没有父类的类)的构造函数运行时,它会创建一个空对象作为 this,然后继续运行。

  • 但是当子类的构造函数运行时,与上面说的不同,它将调用父构造函数来完成这项工作。

所以,继承类的构造函数必须调用 super() 才能执行其父类的构造函数,否则 this 不会创建对象。

class Dog extends Animal {
  constructor(name, age, speed, species) {
    super(name);
    this.species = species;
  }
  run() {
    console.log(`${this.name}是一只奔跑的${this.species}`);
  }
}
let dog = new Dog("闷墩儿", "一", 5, "狗");
dog.run();

使用 super() 的注意事项:

  • 只能在继承类中使用 super()。

  • 构造函数中,一定要先调用 super() 再访问 this。

类的属性和方法

静态方法

在 JavaScript 中,静态方法是给面向对象编程提供的类方法

静态方法的好处是不需要实例化类,就可以直接通过类名去访问,这样不需要消耗资源反复创建对象。

在没有 ES6 的时代,我们要模仿静态方法的常规操作是将方法直接添加到构造函数中。例如:

// ES5 语法
function DogType(name) {
  this.name = name;
}
// 静态方法
DogType.create = function (name) {
  return new DogType(name);
};
// 实例方法
DogType.prototype.sayName = function () {
  console.log(`大家好!我是一只小${this.name}。`);
};
let dog = DogType.create("柯基");
dog.sayName();

上述代码解析如下:

  • 创建一个构造函数 DogType。

  • 以“类名.方法名”的方式为其定义一个静态方法 create,该方法最终创建一个 DogType 实例。

  • 在其原型上添加 sayName 方法。

  • 使用“类名.方法名”的方式调用其静态方法 create 创建一个实例对象 dog。

  • 使用 dog 调用 sayName 方法输出自我介绍的信息。

由于 create 方法可以直接通过类名去访问,不需在被实例化时创建,因而可以被认为是 DogType 类的一个静态方法。

ES6中可以使用static关键字来定义静态方法

使用格式:

static methodName(){
    
}

修改上述代码:

// ES6 语法
class DogType {
  constructor(name) {
    this.name = name;
  }
  // 对应 DogType.prototype.sayName
  sayName() {
    console.log(`大家好!我是一只小${this.name}。`);
  }
  // 对应 DogType.create
  static create(name) {
    return new DogType(name);
  }
}
let dog = DogType.create("柯基");
dog.sayName();

在上面代码中,使用 static create 创建了静态方法 create,并返回实例化的 DogType,它相当于 ES5 代码中的 DogType.create = function(name){},两者实现的功能相同,区别在于 ES6 使用了 static 关键字来标识这是个静态方法。

注意:1、如果静态方法中包括this关键字,则这个关键字指向的是类而不是实例。

2、静态方法只能通过"类名.方法名"调用,而不能被其实例调用。

静态属性

static 关键字除了可以用来定义静态方法外,还可以用于定义静态属性。

静态属性是指类本身的属性(Class.propName),不是定义在实例对象上的属性。

静态属性具有全局唯一性,静态属性只有一个值,任何一次修改都是全局性的影响。

当我们类中需要这么一个具有全局性的属性时,我们可以使用静态属性。

最初定义静态属性写法如下:

class Dog {}
Dog.dogName = "闷墩儿";
console.log(Dog.dogName); // 闷墩儿

在上面代码中,我们为 Dog 类定义了一个静态属性 dogName。

在ES6中可以使用static关键字来定义静态属性,使用格式为:

static propName = propVaule;

例如:

class Dog {
  static dogName = "闷墩儿";
}
console.log(Dog.dogName); // 闷墩儿

在上面的代码中,我们使用 static 关键字给 Dog 类定义了一个名为 dogName 的静态属性。

静态属性和方法的继承

静态方法和静态属性是可以被继承的。

例如:

class Animal {
  static place = "游乐园";
  constructor(name, speed) {
    this.name = name;
    this.speed = speed;
  }
  // place 静态属性是类本身的属性,不是实例对象,所以这里不能用 this.place
  run() {
    console.log(
      `名为${this.name}的狗狗在${Animal.place}里酷跑了 ${this.speed} 公里。`
    );
  }
  static compare(animal1, animal2) {
    return animal1.speed - animal2.speed;
  }
}
class Dog extends Animal {}
// 实例化
let dogs = [new Dog("闷墩儿", 7), new Dog("乐乐", 4)];
dogs[0].run();
dogs[1].run();
// 继承静态方法
console.log(`闷墩儿比乐乐多跑了 ${Dog.compare(dogs[0], dogs[1])} 公里。`);
// 继承静态属性
console.log(`奔跑地点:${Dog.place}。`);

私有方法和私有属性

在某些情况下,如果外部程序可以随意修改类中的属性或调用其方法,将会导致严重的错误。基于这个问题,我们就在类中引入了私有化的概念,私有属性和方法能够降低它们与外界的耦合度,避免很多问题。

在面向对象编程中,关于属性和方法的访问有以下两种情况:

  • 类的属性和方法,在其内部和外部均可进行访问,也称为公共属性和方法。

  • 类的属性和方法,只能在类中访问,不能在类之外的其他地方访问,也称为私有属性和方法。

在 ES6 的类中使用 # 可以设置私有方法和私有属性。

其语法格式为:

// 私有属性
#propertiesName;
// 私有方法
#methodName;

new.target属性

ES6中提供了new.target属性去检查函数或者类的构造函数中是否使用new命令

在构造函数中,若一个构造函数不是使用 new 来调用的,new.target 会返回 undefined。

应用场景:

当我们想写不能被实例化,必须在继承后才能使用的类时,我们可以用 new.target 属性,做为限制其不能被实例化(new)的条件。

例如:

class Person {
  constructor() {
    // 如果实例化对象使用的是 Person 类,则抛出错误
    if (new.target === Person) {
      throw new Error("Person 类不能被实例化。");
    }
  }
}
class Occupation extends Person {
  constructor(name, occupation) {
    super();
  }
}
let person1 = new Person();

对象的扩展

对象字面量

对象字面量就是使用 {} 去定义对象。

在ES6中,对象字面量有许多增强的写法。

属性的简洁表示法

在 ES6 之前我们可能会像下面这样来定义:

const name = "闷墩儿";
const age = 2;
const dog = { name: name, age: age };

在上面代码中,定义了一个名为 dog 的对象,其属性名与属性值的变量名相同,但在定义的对象还是要重复写两遍。

有了 ES6 之后,我们可以使用属性初始化的简写语法,消除这种属性名称与局部变量之间的重复书写。简洁表示法如下所示:

const dog = { name, age };

方法的简洁表示法

在 ES6 之前,如果要为对象添加方法,必须通过指定名称并完整定义函数,我们可能会像下面这样来定义:

const name = "闷墩儿";
const dog = {
  run: function () {
    return name + "在公园里奔跑!";
  },
};

有了 ES6 之后,就可以不用冒号和 function 关键字了。我们可以用以下简洁表示法:

const name = "闷墩儿";
const dog = {
  run() {
    return `${name}在公园里奔跑!`;
  },
};

属性名表达式

在 ES6 之前,我们只能使用标识符的方式来作为属性名,例如:

dog.name = "闷墩儿";

而在 ES6 之后,我们还可以使用表达式来作为属性名,例如:

dog["n" + "ame"] = "闷墩儿";

我们还可以将定义的模版字面量放入 [] 中,例如:

const key = `name`;
const dog = {
  [key]: "闷墩儿",
};

定义在 [] 中的属性说明该属性是可以被计算的动态属性,其内容是可以被计算的,最后会转换成字符串,这提高了代码的灵活性。

对象的扩展运算符

对象中引入扩展运算符后,我们可以用来遍历参数对象中的所有属性,并将其拷贝到新对象中。

let obj1 = { species: "柯基", name: "闷墩儿", age: 2 };
let obj2 = { ...obj1 };
console.log(obj2);

还可以使用扩展运算符将两个对象合并到一个新对象中。举个例子:

let obj1 = { species: "柯基", name: "闷墩儿", age: 2 };
let obj2 = { food: "狗粮" };
let obj3 = { ...obj1, ...obj2 };
console.log(obj3);

对象的新增方法

Object.js

在 ES6 之前,如果我们要判断两个值是否相等,可以用 == 或者 ===,但是这两种判断方式都存在一些缺点。我们来看看下面的代码:

console.log(-0 == +0); // true
console.log(-0 === +0); // true

console.log(NaN == NaN); // false
console.log(NaN === NaN); // false

console.log(7 == "7"); // true

控制台输出结果如下:

从输出的结果,我们可以看出其中的问题所在:

  • 在 JavaScript 引擎中,-0 和 +0 代表两个完全不同的实体,而使用 == 和 === 的判断结果却是相等的。

  • == 和 === 对于 NaN 的判断都是 false。

  • 使用 ==,判断整型 7 和字符串 7 的结果是 true。

基于上述这些缺点,在 ES6 中提出了同值相等的算法,就是使用 Object.is 来比较两个值是否相等。

Object.assign

在 ES6 之前对象组合中我们往往会用到 mixin() 方法,其方法的作用就是一个对象接受另一个对象的属性和方法。

在 ES6 中引入了 Object.assign 来合并对象,该方法一个对象可以接受任意多个对象的属性和方法

const obj1 = { name: "闷墩儿", food: "狗粮" };
const obj2 = { age: 2, hobby: "跑圈圈" };
const obj3 = { food: "鸡胸肉", color: "黑白黄" };
Object.assign(obj1, obj2, obj3); // 将 obj2 和 obj3 合并到 obj1 中
console.log(obj1);

在 obj1 和 obj3 中有相同名称的属性名 food,从输出结果可以看到 obj1 中的 food 属性被覆盖了,说明被合并的对象中出现同名属性,后面的对象会覆盖前面的对象中的属性值。

这就是合并方式是一种浅拷贝,也就是说如果被合并对象中的属性发生变化,合并后的对象不会继承修改的属性,我们来看个例子:

let obj1 = { name: "闷墩儿", food: "狗粮" };
let obj2 = { age: 2, hobby: "跑圈圈" };
let obj3 = { color: "黑白黄" };
Object.assign(obj1, obj2, obj3); // 将 obj2 和 obj3 合并到 obj1 中
console.log(obj1);
obj2.hobby = "游泳";
console.log(obj2);
console.log(obj1);

控制台输出如下:

从上图我们可以看到,修改 obj2 的 hobby 属性后,obj1 没有继承。

Set和Map

Set对象

Set是ES6提供的一种新的数据结构,其结构与数组类似,但与数组不同的是Set里面不允许存放相同的元素,也就是说Set中的每一个值都是唯一的。

Set 对象有两种创建形式:

  • 不带参数的 Set。

let s = new Set();
  • 带参数的 Set。

let s = new Set(argument1, argument1,...);

创建不带参数的Set

首先,创建一个空的 Set 对象。

let s = new Set();

然后,我们使用 add() 方法往 Set 对象里加入一些元素。

s.add(1);
s.add(2);
s.add(2);
s.add(3);
s.add(3);
console.log(s);

输出后可以发现重复的元素只保留了一个

想要知道Set中的元素个数,可以使用size属性。

s.size

创建带参数的 Set

let colors = new Set(["Green", "Red", "Orange"]);
console.log(colors);

Set相关的方法

使用 add() 方法可以往 Set 中添加元素

Set.add(element);

在 Set 中使用 delete() 方法来移除指定元素。其使用格式为:

Set.delete(element);

我们还可以使用 has() 方法来检查某个元素是否存在于 Set 中:

Set.has(element);

若我们想删除 Set 中的所有数据,可以使用 clear() 方法:

Set.clear();

Set的遍历

可以使用forEach()方法遍历Set中的元素,使用格式如下:

Set.prototype.forEach(callback[,thisArg])

参数说明如下:

  • callback 是 Set 中每个元素要执行的回调函数。

  • thisArg 是回调函数执行过程中的 this 值。

let dogs = new Set(["柯基", "博美", "比熊"]);
dogs.forEach(function details(values) {
  console.log(`Hello,我是一只小${values}`);
});
let dogs = new Set(["柯基", "博美", "比熊"]);
// 回调函数
function details(values) {
  console.log(`Hello,我是一只小${values}`);
}
dogs.forEach(details);

WeakSet

Set实例和变量在存储数据方面的内存分配和垃圾回收机制是一样的。

如果Set实例中的引用一直存在,垃圾回收就不能释放该对象的存储空间,即使没有使用它。

let s = new Set();
let obj1 = {};
let obj2 = {};
s.add(obj1);
s.add(obj2);
console.log(s.size); // 2
obj1 = null;
console.log(s.size); // 2

上面代码中,先声明了一个空的 Set 对象 s,然后调用 add 方法向 s 中添加两个空对象元素,控制台打印 s 的元素个数为 2,证明 Set 对象中给空对象也分配了内存;接着把对象 obj1 设为 null,再次打印 s 的元素个数,仍然为 2,证明 Set 实例 s 中元素 1 占用的内存并没有被释放掉。

针对这个缺陷,ES6 又给我们提供了另一种 Set,叫做 WeakSet

WeakSet 也叫做弱引用 Set,如果将其存储的对象设为了 null,相当于是删除了该对象,当垃圾回收机运行时,会释放掉被删除对象占用的空间。

例如:

let s = new WeakSet();
let obj = { msg: "同学们好!" };
s.add(obj);
console.log(s.has(obj));
s.delete(obj);
console.log(s.has(obj));

在控制台显示如下:

如果你打印出定义的实例对象,会发现输出为 undefined。

Set 与 WeakSet 的区别:

  • WeakSet 的成员只能是对象且都是弱引用。

在 WeakSet 中,add() 方法中不能传入非对象参数,若传入会报错。
  • 在 WeakSet 中,给 has() 和 delete() 方法传入非对象参数,虽然不会报错,但是会返回 false。

  • WeakSet 对象没有 size 属性,不能被遍历。

由于 WeakSet 里面存储的都是弱引用,内部有多少个成员,取决于垃圾回收机制有没有运行。运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。

Map对象

在 ES6 之前,对象是创建键值对数据结构的主要方式,但对象在使用上有一些局限性。

  1. 在对象中的键名只能是字符串、数字或者 Symbol。

  1. 对象不可以直接使用 forEach 方法遍历。

Map可以解决以上问题。

Map是ES6中的一种存储许多键值对的有序列表,其键值对可以是任意数据类型。Map是有序的,会按照简直插入的顺序来排列

如何创建Map对象,语法格式:

let map = new Map([iterable]);

Map 对象可以接收一个由键值对组成的可叠对象。

set 方法添加数据

使用 set() 方法可以向对象中添加数据。其使用格式为:

map.set(key:value);
let bookstore = new Map();
bookstore.set([1, 2, 3], "书籍");
bookstore.set(false, "日用品");
bookstore.set(3, "化妆品");
console.log(bookstore);
  • 在 bookstore.set([1,2,3],"书籍") 中创建了数组类型的键值 [1,2,3],其值为普通字符串“书籍”。

  • 在 bookstore.set(false,"日用品") 中创建了布尔类型的键值 false,其值为普通字符串“日用品”。

  • 在 bookstore.set(3,"test"); 中创建了整数类型的键值 3,其值为普通的字符串“化妆品”。

get 方法从集合中获取数据

我们要获取集合中的数据,使用 get() 方法即可。

其使用格式为:

map.get(key);

返回值是key所对应的value值

其他常用方法

除了上方提到的 set() 和 get() 方法,在 Map 中,还有下面三种方法比较常用。

  • has() 用来判断指定键名对应的数据是否存在于当前集合中。

  • delete() 用来删除指定键名的数据。

  • clear() 用来清空集合中的数据。

Map的遍历

与对象不同,Map 可以使用 forEach() 方法来遍历数据值。

创建一个带初值的Map:

let map = new Map([[key1,value1],[key2,value2]...]);

forEach()遍历:

map.forEach(callback(value, key, ownerMap));

callback 是一个回调函数,其函数包含三个参数:

  • value:本次循环所读取的元素的数据。

  • key:本次循环所读取的元素的键名。

  • ownerMap:Map 集合本身。

异步编程

Promise对象基础应用

地狱式回调

在日常开发中,往往会遇到这样的需求:通过接口 1 的返回值,去获取接口 2 的数据,然后,再通过接口 2 的返回值,获取接口 3 的数据。即每次请求接口数据时,都需要使用上一次的返回值。为了实现这个需求,通常会使用回调函数来完成,即把函数作为参数进行层层嵌套。

代码如下:

var outnum = function (n, callback) {
  setTimeout(function () {
    console.log(n);
    callback();
  }, 1000);
};
outnum("1", function () {
  outnum("2", function () {
    outnum("3", function () {
      console.log("0");
    });
  });
});

通过上述代码可以发现,虽然可以通过回调函数层层嵌套的形式实现请求最终数据的目的,但是代码结构不明确,可读性差,这就是回调地狱。

定义Promise对象

为了解决这种地狱式的回调,可以使用 Promise 对象,且代码更优雅,由于 Promise 对象是一个构造函数,因此,必须通过实例化来生成,它的定义格式如下代码:

let p = new Promise(function (resolve, reject) {
  // 此处做一个异步的事情
});

在定义格式的代码中,需要说明的几个问题:

  • 在实例化中,参数为函数,函数中又有两个用于回调的函数。

  • 两个回调函数中,resolve 为异步执行成功时的回调,其参数可以传递执行的结果。

  • reject 为异步执行失败时的回调,其参数可以传递失败的错误信息。

Promise对象的then方法

Promise 对象实例化后,可以调用 then 方法获取两个回调函数中的传参值,该方法接收两个回调函数作为参数,第一个参数是必选参数,表示异步成功后执行的 resolve 回调函数,第二个参数是可选参数,表示异步失败后执行的 reject 回调函数,它的调用格式如下:

p.then(
  function () {},
  function () {}
);

回调函数带参数的格式如下:

p.then(
  function (v) {},
  function (e) {}
);

其中参数 v 值表示 resolve 回调函数中的参数值,e 值表示 reject 回调函数中的参数值

例如:

let n = 6;
let p2 = new Promise(function (resolve, reject) {
  setTimeout(function () {
    if (n > 5) {
      resolve(n);
    } else {
      reject("必须大于5");
    }
  });
});
p2.then(
  function (v) {
    console.log(v);
  },
  function (e) {
    console.log(e);
  }
);
// 执行代码后,由于 n 值大于 5 ,因此,在控制台中输出数字 6 。

此外,一个 then 方法被执行后,如果仍然返回一个 Promise 对象,则可以继续再执行 then 方法,形成链式写法效果,代码如下所示:

p1.then(function (v) {
  return p1;
}).then(function (v) {
  return p1;
});

解决上述地狱回调问题

var outnum = function (order) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      console.log(order);
      resolve();
    }, 1000);
  });
};
outnum("1")
  .then(function () {
    return outnum("2");
  })
  .then(function () {
    return outnum("3");
  })
  .then(function () {
    console.log("0");
  });

执行上述代码之后的页面效果与使用地狱式回调方式是完全一样的,但 Promise 对象实现的代码可读性更强,还可以很方便地取到异步执行后传递的参数值,因此,这种代码的实现方式,更适合在异步编程中使用。

Promise对象中的方法

Promise.all 方法

日常开发过程中,往往会遇到这种问题,当首次加载某个页面时,由于数据庞大需要分别同时发送多个异步请求向服务器获取数据,最终所有数据返回之后做下一步操作(如“隐藏页面的加载 loading 动画”)。由于很难捕获这些异步请求全部成功的时机,导致这个需求实现起来相当困难。这时使用 Promise.all 方法就可以解决这种问题。

使用格式:

Promise.all 方法中的参数是一个数组,数组中的每个元素是实例化后的 Promise 对象,格式如下代码:

Promise.all([p1,p2,p3,...]).then(res=>{
  // 所有请求成功后的操作步骤
},error=>{
  // 某一个请求失败后的操作步骤
});

上述代码中,p1、p2、p3 都是实例化后的 Promise 对象,并且该方法可以通过链式写法直接调用 Promise.all 中的 then 方法,当全部的实例化对象都执行成功后,进入 then 方法的第一个执行成功的回调函数中,函数参数是每个任务执行成功后的结果,以数组形式保存,如下图所示:

如果在调用 Promise.all 方法时,有一个 Promise 实例对象(比如:p1)的任务执行失败了,则会直接进入 Promise.all 后的 then 方法的失败回调函数中,如下图所示:

通过 Promise.all 方法可以并列完成多个异步的请求,只有当全部请求成功后,才进入 then 方法中的成功回调函数中,否则,进入失败的回调函数中,因此,当首次加载页面时,可以将各种的异步请求放入 Promise.all 方法中,如果全部完成,则在 then 方法中的成功回调函数中执行下步操作,否则,直接进入失败回调函数中。

Promise.race方法

与 Promise.all 方法不同,Promise.race 方法是多个 Promise 实例化对象在比赛, 执行最快的那个任务的结果,将返回给 then 方法中的对应回调函数中,通过这种方式,可以检测页面中某个请求是否超时,并输出相关的提示信息。

使用格式:

Promise.race([p1,p2,p3,...]).then(
    function(v){
      //获取最快任务成功时的返回值
  },
  function(){
      //获取最快任务失败时的返回值
  }
)

Promise.then方法的缺点

Promise 对象虽然很优雅地解决了地狱回调的情形,使代码更简洁和易读,但通过 then 方法取值时,代码还是不够时尚和前沿,多层嵌套取值时也不够高效,如下列代码所示:

var p = function (msg) {
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve(msg);
    }, 1000);
  });
};
p("明明")
  .then(function (v) {
    return p(v + ",男");
  })
  .then(function (v) {
    return p(v + ",今年18岁");
  })
  .then(function (v) {
    console.log(v);
  });

使用async和await关键字可以解决上述问题。

async关键字和await关键字

async

async 英文单词的意思是异步,虽然它是 ES7 中新增加的一个关键字,但它的本质是一种语法糖写法(语法糖是一种简化后的代码写化,它能方便程序员的代码开发),async 通常写在一个函数的前面,表示这是一个异步请求的函数,将返回一个 Promise 对象,并可以通过 then 方法取到函数中的返回值,下面通过一个简单示例来说明它的使用。

async function fn() {
  return "12345";
}
fn().then((val) => {
  console.log(val);
});

在上述代码中,定义一个名称为 fn 的函数,但由于在函数前添加了关键字 async ,使这个函数将返回一个 Promise 对象,因此,函数执行后,可以直接调用 then 方法;同时,fn 函数中的返回值,就是 then 方法中,执行成功回调函数时的参数值,因此,执行上述代码后,将在页面的控制台输出 “12345” 字符

明确以下两点:

  • 使用 async 关键字定义的函数,将会返回一个 Promise 对象。

  • 函数中有返回值,则相当于执行了 Promise.resolve(返回值) 函数,没有返回值,则相当于执行了 Promise.resolve() 函数。

虽然 async 关键字简化了我们之前实现异步请求中返回 Promise 实例对象的那一步,直接返回了一个 Promise 对象,但是仍然需要在 then 方法中处理异步获取到的数据,想要省去then方法中处理异步获取到的数据,让异步操作写起来更像同步操作可以使用await。

await

await 可以理解为 async wait 的简写,表示等待异步执行完成,await 必须在 async 定义的函数中,不能单独使用,await 后可以返回任意的表达式,如果是正常内容,则直接执行,如果是异步请求,必须等待请求完成后,才会执行下面的代码

// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}

// 一个用于正常输出内容的函数
function log() {
  console.log("2.正在操作");
}

async function fn() {
  console.log("1.开始");
  await log();
  let p1 = await p("3.异步请求");
  console.log(p1);
  console.log("4.结束");
}
fn();

根据页面效果,源代码解析如下:

  • fn 函数执行后,首先,会按照代码执行流程,先输出“1.开始”。

  • 其次,对于没有异步请求的内容,在 await 后面都将会正常输出,因此,再输出“2.正在操作”。

  • 如果 await 后面是异步请求,那么,必须等待请求完成并获取结果后,才会向下执行。

  • 根据上述分析,由于 方法 p 是一个异步请求,因此,必须等待它执行完成后,并将返回值赋给变量 p1,再执行向下代码。

  • 所以,最后的执行顺序是,先输出 “3.异步请求”,再输出 "4.结束",在 async 函数中的执行顺序,如下图所示。

多层嵌套传参数的优化

基于 await 的特性,可以将异步请求的代码变成同步请求时的书写格式,代码会更加优雅,特别是处理多层需要嵌套传参时,使用 await 的方式,代码会更少,更易于阅读,如下列需求。

需求分析

需要发送三次异步请求,第一次请求,成功后获取返回 1,并将该值作为参数并加 2,发送第二次请求,成功后获取返回值,并将该值作为参数并加 3,发送第三次请求,成功后输出全部的返回值,如果三次请求都成功了,则在控制台输出 “登录成功!”的字样。

实现代码:

// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}
async function fn() {
  let p1 = await p("1");
  let p2 = await p(p1 + "2");
  let p3 = await p(p2 + "3");
  console.log(p3);
  console.log("登录成功!");
}
fn();

从上述页面效果可以看出,在 fn 函数中,第一次发送请求时,返回值为 “1”,并保存在变量 p1 中,然后,将变量 p1 作为参数,并加 “2” 发送第二次请求,返回值为“12”,并保存在变量 p2 中,然后,将变量 p2 值作为参数,并加 ”3“ 发送第三次请求,返回值为 ”123“,并保存在变量 p3 中,最后,在控制台输出的内容是 p3 的值,即字符 “123”,同时,输出 “登录成功!”的字样。

多个并列异步请求的调优

await 在处理多个异步请求时,如果请求之间有嵌套关系,可以一次一次按顺序发送请求,但是如果各个请求之间无任何关联,则可以将这些请求借助 Promise.all 一次性并列发送,使用 await 关键字获取请求的结果,并根据返回的结果进行下一步操作。如下列需求。

需求分析

页面首次加载时,将会发送三次无任何关联的异步请求,当这三次请求成功后,在控制台输出“隐藏加载动画!”字样。

实现代码:

// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求
function p(v) {
  return new Promise(function (resolve) {
    setTimeout(function () {
      // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。
      resolve(v);
    }, 2000);
  });
}
async function fn() {
  await Promise.all([p("a"), p("b"), p("c")]);
  console.log("隐藏加载动画!");
}
fn();

模块化

export

关键字 export 可以将一个模块中的方法、变量和其他功能从模块中输出,允许其他需要的模块按指定的标准进行访问,没有使用关键字 export 输出的模块内容,是封闭的,其它模块无法访问到它,下面介绍关键字 export 几种输出的方式 。

  • 可以直接输出一个模块文件中的变量,如下代码:

export let name = "小蓝同学";
export let age = 18;
let work = "一直在写代码呢!";

在上述代码中,由于变量 name 和 age 之前都使用了输出关键字 export ,因此,它们都可以被其他模块访问,由于变量 work 之前没有添加关键字 export ,所以,其他的模块无法访问到这个变量。

上述代码的这种写法,还可以合并成一个对象,并使用关键字 export 一次性输出,修改后的代码如下:

let name = "小蓝同学";
let age = 18;
let work = "一直在写代码呢!";
export { name, age };

修改后的这种方法更加有利于一次性地批量输出多项内容,经常在开发中使用。

  • 关键字 export 除能输出模块中的变量外,还可以输出模块中的方法,两者的输出格式都是相同的,如下代码:

function say() {
  console.log("我的名字叫小蓝");
}
function act() {
  console.log("我的工作是写代码");
}
export function log() {
  console.log("说点什么。。。");
}
export { say, act };

关键字 export 在输出方法时,不要去添加执行方法的括号,只要输出方法的名称就可以,否则就会报错;此外,在输出内容的过程中,还可以使用关键字 as 来修改输出后的名称,修改后的代码如下:

export {
    say(), //报错
    act as userWork
}

在上述代码中,由于在输出 say 方法时,添加了括号,表示执行该方法,因此,代码报错;同时,在输出 act 方法时,添加了关键字 as ,它的作用是给输出的功能取一个别名,因此,其他模块在使用输出的 act 方法时,只能访问它的别名 userWork 。

import

与关键字 export 相对应,import 的功能是输入已经使用关键字 export 输出的内容,它们是对应关系, export 负责输出,而 import 则用于接受输出的内容,即负责输入

关键字 import 在输入模块中加载输出模块的变量时,可以使用大括号包裹全部变量名,各个变量之间使用逗号分割,再通过 from 指定输出模块的路径,这个路径可以是绝对的,也可以是相对的,代码格式如下:

import { 变量1,变量2,变量3,...} from 输出模块位置

在上述格式代码中,大括号中的变量 1,变量 2 也可以通过关键字 as 取一个别名,格式如下:

import { 变量1 as a1,变量2 as fn,变量3,...} from 输出模块位置

取了别名之后,在输入模块中,只能使用这个别名,而不能再使用原先的名称,否则,将会出现变量未定义的错误信息。

数据排序的模块开发

在 ES6 中,引入关键字 export 和 import 的最终目的是为项目的模块提供衔接上的支撑,从而为模块化开发项目提供保障,在模块化开发项目的过程中,开发人员可以将各个功能分解成各个子类模块,各个子类模块通过关键字 export 和 import 进行相互衔接,其效果如下图所示:

Proxy

Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后通过对象的代理对象进行操作。也可以理解为在外界与对象之间建立了一道门,外界要访问该对象必须先打开这道门,如果想要获得打开该门的钥匙,就要遵守一个访问“条约”,允许对来访人员进行改造(提供一种机制:可以对外界的访问进行过滤和改写)。

用 Proxy 创建代理需要传入两个参数:目标对象(target)和处理程序(handler)。语法格式如下:

var proxy = new Proxy(target, handler);

参数说明如下:

  • target:要拦截的目标对象。

  • handler:制定拦截行为的对象。

例如:

let target = {};
let proxy = new Proxy(target, {});
proxy.name = "闷墩儿";
console.log(proxy.name);
console.log(target.name);

target.name = "憨憨";
console.log(proxy.name);
console.log(target.name);

在上面的例子中 proxy 的所有操作都转发给了 target 对象,当在 proxy 上创建了 name 属性之后,相应的 target 上也会创建 name 属性。由于 target.name 和 proxy.name 都是引用的 target.name,所以当修改了 target.name 的值后,proxy.name 的值也会修改。

Proxy的实例方法

Proxy 的代理拦截方法如下表所示:

拦截方法

方法说明

get(target, propKey, receiver)

拦截对象属性的读取。

set(target, propKey, value, receiver)

拦截对象属性的设置。

has(target, propKey)

拦截 propKey in proxy 的操作。

deleteProperty(target, propKey)

拦截 delete proxy[propKey]的操作。

ownKeys(target)

拦截 Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in 循环,返回一个数组。

getOwnPropertyDescriptor(target, propKey)

拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。

defineProperty(target, propKey, propDesc)

拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。

preventExtensions(target)

拦截 Object.preventExtensions(proxy),返回一个布尔值。

getPrototypeOf(target)

拦截 Object.getPrototypeOf(proxy),返回一个对象。

isExtensible(target)

拦截 Object.isExtensible(proxy),返回一个布尔值。

setPrototypeOf(target, proto)

拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。

apply(target, object, args)

拦截 Proxy 实例作为函数调用的操作。

construct(target, args)

拦截 Proxy 实例作为构造函数调用的操作。

常用方法介绍:

get(target,propKey,receiver)

ES6 中为我们提供了 get 方法,在访问对象之前检验一下是否存在你要访问的属性,该方法接受三个参数,具体说明如下:

  • target:被读取属性的目标对象。

  • propKey:要读取的属性键值。

  • receiver:操作发生的对象。

let dog = {
  name: "闷墩儿",
};
var proxy = new Proxy(dog, {
  get(target, propKey) {
    // 遍历目标对象的属性键值
    if (propKey in target) {
      return target[propKey]; // 返回相应的属性值
    } else {
      throw new ReferenceError(propKey + " 属性不存在");
    }
  },
});
console.log("访问 dog 对象中的 name 属性值为:" + proxy.name);
console.log("访问不存在的 age 属性:" + proxy.age);

set(target, propKey, value, receiver)

如果要创建一个只接受数字作为属性值的对象,那么在创建属性时,必须判断该值是否是数字,若不是数字应该报错。我们使用 set 方法就可以实现这个需求。

set 方法接受四个参数,具体说明如下:

  • target:用于接收属性的目标对象。

  • propKey:要写入的属性键值。

  • value:要写入的属性值。

  • receiver:操作发生的对象。

let validator = {
  set(target, propKey, value) {
    if (propKey === "age") {
      // 判断 age 属性值是否时数字
      if (!Number.isInteger(value)) {
        throw new TypeError("狗狗的年龄只能是整型哦!");
      }
    }
    target[propKey] = value;
    return true;
  },
};

let dog = new Proxy({}, validator);
console.log((dog.age = "22"));

has(target, propKey)

has 方法接收两个参数,具体说明如下:

  • target:读取属性的目标对象。

  • propKey:要检查的属性键值。

let dog = {
  name: "闷墩儿",
  age: 2,
};
let handler = {
  has(target, propKey) {
    if (propKey == "age" && target[propKey] < 5) {
      console.log(`${target.name}的年龄小于 5 岁哦!`);
      return true;
    }
  },
};
let proxy = new Proxy(dog, handler);

console.log("age" in proxy);

ownKeys(target)

ownKeys 方法用于拦截对象自身属性的读取操作,具体可以拦截以下四种操作:

  • Object.getOwnPropertyNames()

  • Object.getOwnPropertySymbols()

  • Object.keys()

  • for...in

下面我们举一个拦截 for...in 的例子吧~

let dog = {
  name: "闷墩儿",
  age: 2,
  food: "狗罐头",
};
const proxy = new Proxy(dog, {
  ownKeys() {
    return ["name", "color"];
  },
});

for (let key in proxy) {
  console.log(key); // 输出 name
}

只输出了 name 属性,这是因为在 dog 对象中不包含 color 属性。

生成数组

挑战介绍

本节我们来挑战一道大厂面试真题 —— 生成数组。

挑战准备

新建一个 createArr.js 文件,在文件里写一个名为 createArr 的函数,并导出这个函数,如下图所示:

这个文件在环境初始化时会自动生成,如果发现没有自动生成就按照上述图片自己创建文件和函数,函数代码如下:

function createArr(n) {
  // 补充代码
}

module.exports = createArr;

挑战内容

请封装一个函数,用来生成一个每项依次为 1 到 n 的数组。

不必考虑 n 过大的情况,本题中 0 <= n <= 10000。

示例:

输入:n = 1
输出:[1]
输入:n = 5
输出:[1,2,3,4,5]
输入:n = 10
输出:[1,2,3,4,5,6,7,8,9,10]
输入:n = 10000
输出:[1,2,3,4,5,6,7,8,9,10,...,9998,9999,10000]

注意事项

  • 文件名、函数名不可随意更改。

  • 文件中编写的函数需要导出,否则将无法提交通过。

题解

function createArr(n) {
  var arr = new Array(n);
  var i = 1;
  for(let j = 0; j < n; j++)
  {
    arr[j]=i++;
  }
  return arr;
}
module.exports = createArr;

不过这是最常规的实现方式,还有一些其他方法可以实现本题,用到的就是 Array.from 这个 API,代码实现如下:

function createArr(n) {
  return Array.from({ length: n }, (item, i) => i + 1);
}

在上面的代码中,Array.from 接收两个参数,第一个参数是一个伪数组,Array.from 会把传入的伪数组转为数组;第二个参数是一个回调函数,新数组中的每个元素会执行该回调函数,就如同执行数组的 map 方法一样,上面的代码等价于下面的代码:

function createArr(n) {
  return Array.from({ length: n }).map((item, i) => i + 1);
}

知识延伸

1.什么是伪数组

伪数组,也叫类数组,英文名称 Array-like,顾名思义,就是像数组但不是数组,有以下特征:

  • 具有 length 属性。

  • 可通过索引访问伪数组里的元素。

  • 不能调用数组上的方法。

举个例子,函数的隐式参数 arguments 就是一个伪数组,示例代码如下:

function add() {
  console.log("arguments :>> ", arguments);
  console.log("arguments.length :>> ", arguments.length);
  console.log("arguments[0] :>> ", arguments[0]);
  arguments.push(1);
}

add(1, 2, 3);

2.JS 常见伪数组有哪些

有 arguments 和 DOM 元素集合,示例代码如下:

function add() {
  console.log("arguments :>> ", arguments);
}

add(1, 2, 3);

const DOMTags = document.getElementsByClassName("test");
console.log("DOMTags :>> ", DOMTags);
console.log("document.childNodes :>> ", document.childNodes);

.伪数组的类型是什么

调用 Object.prototype.toString 方法来看一下伪数组的类型,示例代码如下:

可以看到,伪数组的类型是专门的引用类型

4.伪数组如何转数组

可以使用 Array.from、扩展运算符和 Array.prototype.slice 方法来实现伪数组转数组。

Array.from
function add() {
  const args = Array.from(arguments);
  args.push(1); // 可以使用数组上的方法了
  console.log(Object.prototype.toString.call(args)); // '[object Array]'
}

add(1, 2, 3);
扩展运算符
function add() {
  const args = [...arguments];
  args.push(1); // 可以使用数组上的方法了
  console.log(Object.prototype.toString.call(args)); // '[object Array]'
}

add(1, 2, 3);
数组 slice 方法

这个方法现在不常用了,但 ES6 之前没有扩展运算符和 Array.from,用的就是这个方法。

function add() {
  const args = Array.prototype.slice.call(arguments);
  // 也可以这么写 const args = [].slice.call(arguments)
  args.push(1); // 可以使用数组上的方法了
  console.log(Object.prototype.toString.call(args)); // '[object Array]'
}

add(1, 2, 3);

Array.prototype.slice 方法要调用 call 的原因和 Object.prototype.toString 要调用 call 的原因一样

实现数组去重

挑战介绍

本节我们来挑战一道大厂面试真题 —— 实现数组去重。

挑战准备

新建一个 myUniq.js 文件,在文件里写一个名为 myUniq 的函数,并导出这个函数,如下图所示:

这个文件在环境初始化时会自动生成,如果发现没有自动生成就按照上述图片自己创建文件和函数,函数代码如下:

function myUniq(arr) {
  // 补充代码
}

module.exports = myUniq;

挑战内容

请封装一个函数,用来去掉数组中重复的元素。

该函数返回的是一个新的数组。

不必考虑数组中出现对象、null、undefined、NaN 等特殊情况,数组中的每一项都是数字。

示例:

输入:[1,1,1,2,2,3,3,4,5,1]
输出:[1,2,3,4,5]
输入:[1,1,1]
输出:[1]

注意事项

  • 文件名、函数名不可随意更改。

  • 文件中编写的函数需要导出,否则将无法提交通过。

题解

function myUniq(arr) {
    //利用set对象会自动去重的特点对数组去重
  let s = new Set();
  for(let a of arr)
  {
    s.add(a);
    // console.log(a);
  }
//   console.log(s);
    //将set对象转换为数组对象
  arr = Array.from(s)
  return arr;
}

module.exports = myUniq
myUniq([1,1,1,2,2,3,3,4,5,1])

官方简洁版答案:

利用扩展运算符和set对象实现数组去重

先把数组中的每一项存进 Set 里,如果存储的是重复项,Set 内部就自动去重了,然后再使用拓展运算符把 Set 转换回数组,就得到了去重后的数组。

function myUniq(arr) {
  return [...new Set(arr)];
}

使用 Array.from 和 Set 也能达到同样效果,代码如下:

function myUniq(arr) {
  return Array.from(new Set(arr));
}

知识延伸

1.对象数组根据某个 key 去重

上文中,我们讨论的都是数组中的项都是数字的情况,实际业务场景中,经常会出现去重下面这样的数据结构的需求,比如:

// 去重前
const arr = [
  {
    name: "lin",
  },
  {
    name: "lin",
  },
  {
    name: "xxx",
  },
];

// 去重后
arr = [
  {
    name: "lin",
  },
  {
    name: "xxx",
  },
];

数组中的每一项都是一个对象,需要根据对象中的某个 key 来去重,比如上面的数据,就需要把 name 相同的项去除掉。

这时,使用上文中总结的方法就都没用了,该如何实现这种场景的数组去重呢?

业务开发中,可以使用 lodash 的 uniqBy 方法,测试代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
    <script>
      const arr = [
        {
          name: "lin",
        },
        {
          name: "lin",
        },
        {
          name: "xxx",
        },
      ];

      console.log(_.uniqBy(arr, "name")); // [{name: 'lin'}, {name: 'xxx'}]
    </script>
  </body>
</html>

我们尝试来实现一下 uniqBy 这个方法的部分功能,根据 key 来去重“对象数组”。

其实很简单,只需要把上文中利用对象实现去重的方法稍微改造一下就行,代码如下:

function myUniqBy(arr, key) {
  // 去重函数多接受一个参数,用来传递 key。
  const map = {};
  const res = [];
  for (let item of arr) {
    if (map[item[key]] === undefined) {
      res.push(item);
      map[item[key]] = true; // 对象中存的是 key。
    }
  }
  return res;
}

2.对象数组去重

对象数组去重,如果不只根据对象的某个 key 来去重,而是实打实的判断整个对象是否相等来去重,该怎么办?

// 去重前
const arr = [
  {
    name: "lin",
    age: 18,
    profession: "student",
  },
  {
    name: "lin",
    age: 18,
    profession: "student",
  },
  {
    name: "lin",
    age: 19,
    profession: "programmer",
  },
];

// 去重后
arr = [
  {
    name: "lin",
    age: 18,
    profession: "student",
  },
  {
    name: "lin",
    age: 19,
    profession: "programmer",
  },
];

遇到这样的需求,最好不要去自己实现,因为判断两个对象是否相等非常复杂,已经可以单独讲一篇文章了,最好的办法是使用 lodash 的 uniqWith 方法来实现去重,测试代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Document</title>
  </head>
  <body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
    <script>
      const arr = [
        {
          name: "lin",
          age: 18,
          profession: "student",
        },
        {
          name: "lin",
          age: 18,
          profession: "student",
        },
        {
          name: "lin",
          age: 19,
          profession: "programmer",
        },
      ];

      console.log(_.uniqWith(arr, _.isEqual));
      /**
       * 最终输出的结果为:
       * [
       *    {
       *      name: 'lin',
       *      age: 18,
       *      profession: 'student'
       *    },
       *    {
       *      name: 'lin',
       *      age: 19,
       *      profession: 'programmer'
       *    },
       *  ]
       */
    </script>
  </body>
</html>

实现模板字符串解析

挑战介绍

本节我们来挑战一道大厂面试真题 —— 实现模板字符串解析。

挑战准备

新建一个 strRender.js 文件,在文件里写一个名为 strRender 的函数,并导出这个函数,如下图所示:

这个文件在环境初始化时会自动生成,如果发现没有自动生成就按照上述图片自己创建文件和函数,函数代码如下:

function strRender(str, data) {
  // 补充代码
}

module.exports = strRender;

挑战内容

请封装一个 strRender 函数,用来实现模板字符串解析功能。

strRender 函数接收两个参数,第一个参数是模板字符串 str,第二个参数是需要传入的 data,最终返回一个把模板中的变量替换了的新字符串。

示例:

const str = "My name is ${name}, I am ${age} years old, I come from ${country}";

const data = {
  name: "zhangsan",
  age: "18",
  country: "China",
};

console.log(strRender(str, data)); // 'My name is zhangsan, I am 18 years old, I come from China'

注意事项

  • 文件名、函数名不可随意更改。

  • 文件中编写的函数需要导出,否则将无法提交通过。

题解

function strRender(str, data) {
    var reg = /\$\{(\w+)\}/;
    // 如果字符串中存在 ${xxx},说明还有变量没有被替换,才执行后面的逻辑
    if(reg.test(str))
    {	//使用mathch方法获取key值
        var key = str.match(reg)[1];
        //使用replace方法替换变量为真实的数据
        str = str.replace(reg,data[key])
		//递归调用本身,直到字符串中不存在${xxx}
        return strRender(str,data)
    }
    return str;
}

module.exports = strRender

利用正则表达式,匹配出${}以及里面的内容,利用变量key匹配出需要替换的data对象中的变量,利用replace函数对匹配成功的部分进行替换。

实现 async 函数

挑战介绍

本节我们来挑战一道大厂面试真题 —— 实现 async 函数。

挑战准备

新建一个 myAsync.js 文件,在文件里写一个名为 myAsync 的函数,并导出这个函数,如下图所示:

这个文件在环境初始化时会自动生成,如果发现没有自动生成就按照上述图片自己创建文件和函数,代码如下:

function myAsync(genFn) {}

module.exports = myAsync;

挑战内容

请实现一个 myAsync 函数,这个函数用来模拟 async 函数的功能,最终能通过下面的测试代码即可:

function fn() {
  return myAsync(function* () {
    yield 1;
    yield 2;
    return 3;
  });
}

const p = fn();
p.then((val) => {
  console.log(val); // 打印 3
});

提示:async 函数其实就是 Generator 函数的语法糖,它的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里,就是本题要实现的 myAsync 函数。

这个 myAsync 函数接收一个 Generator 回调函数作为参数,在 myAsync 函数中执行 Generator 函数并自动执行 next 方法,最终返回一个 Promise 实例,把状态为 done 的值 resolve 出来,把错误的信息 reject 出来。

如果对上面提示中提及的概念不明白,可以先去查阅一下 Generator 相关的文档再来做本题。

注意事项

  • 文件名、函数名不可随意更改。

  • 文件中编写的函数需要导出,否则将无法提交通过。

题解

function myAsync(genFn) {
  // 返回一个 Promise 实例
  return new Promise(function (resolve, reject) {
    const gen = genFn();
    // 自动执行器的封装,里面是递归的逻辑
    function step(nextFn) {
      let next;
      //
      try {
        next = nextFn();
      } catch (e) {
        return reject(e);
      }
      // 如果已经到 done 状态了,就 resolve 最终的值
      if (next.done) {
        return resolve(next.value);
      }
      // 不是 done 状态,说明程序还没执行完,就继续递归
      Promise.resolve(next.value).then(
        function (v) {
          step(function () {
            return gen.next(v);
          });
        },
        function (e) {
          // 错误的逻辑 reject 出来
          step(function () {
            return gen.throw(e);
          });
        }
      );
    }
    step(function () {
      return gen.next();
    });
  });
}

async/await准确的来说是Generator的语法糖,Generator 函数是 ES6 提供的一种异步编程解决方案,它是可以用来控制迭代器的函数,

Generator 函数是 ES6 提供的一种异步编程解决方案,它是可以用来控制迭代器的函数,并且语法与传统的函数完全不同,我们来看下面这个示例:

function printNum() {
  console.log(1);
  console.log(2);
  console.log(3);
}

printNum(); // 程序最终依次输出 1,2,3

这是一个正常的函数,如果我们把这个函数改造成 Generator 函数,代码如下:

function* printNum() {
  yield console.log(1);
  yield console.log(2);
  yield console.log(3);
}

printNum(); // 这样执行不会有任何反应

此时执行 printNum,不会有任何反应,加上了 yield 关键字后,程序中的打印逻辑都被中断了。

我们需要调用函数返回值的 next 方法,才会生效,代码如下:

function* printNum() {
  yield console.log(1);
  yield console.log(2);
  yield console.log(3);
}

fn = printNum();
fn.next(); // 打印 1
fn.next(); // 打印 2
fn.next(); // 打印 3

这样,程序的执行就会变得可控,它们可以暂停,然后在需要的时候恢复,小结一下:

  • Generator 函数比普通函数多一个 *。

  • 函数内部用 yield 来控制暂停代码的执行。

  • 函数的返回值通过调用 next 来恢复代码的执行。

async 函数的实现原理

我们知道了 Generator 函数的用法,现在用它来处理异步,代码如下:

const fs = require("fs");

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function (error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const genFn = function* () {
  const a = yield readFile("a.json");
  const b = yield readFile("b.json");
  console.log(JSON.parse(a));
  console.log(JSON.parse(b));
};

上面代码的函数 genFn 可以写成 async 函数,就是下面这样。

const asyncReadFile = async function () {
  const a = await readFile("a.json");
  const b = await readFile("b.json");
  console.log(JSON.parse(a));
  console.log(JSON.parse(b));
};

一比较就会发现,async 函数就是将 Generator 函数的星号(*)替换成 async,将 yield 替换成 await,仅此而已。

但是 Generator 函数的执行,每一步都要执行 next 方法,非常不方便,能不能让它一次性执行完毕呢?

上文中的 genFn 方法,我们让他执行完,代码如下:

let g = genFn();
// next返回值中有一个 value 值,这个 value 是 yield 后面的结果
g.next().value((err, data1) => {
  g.next(data1).value((err, data2) => {
    g.next(data2);
  });
});

注意这里的 value 值,是调用 next 方法生成的,比如:

function* printNum() {
  yield 1;
  yield 2;
  return 3;
}

fn = printNum();
console.log(fn.next()); // {value: 1, done: false}
console.log(fn.next()); // {value: 2, done: false}
console.log(fn.next()); // {value: 3, done: true}

当调用 next 方法时,返回一个对象,它的 value 属性就是当前 yield 表达式的值,done 属性的值表示遍历是否结束。

上文的 genFn 方法中,我们只执行了两个异步操作,万一异步操作多起来,又会陷入回调地狱了,我们把这里的逻辑封装一下:

function step(nextFn) {
  const next = (err, data) => {
    let res = nextFn.next(data);
    // 如果 res.done 为 true,才表示迭代结束,返回
    if (res.done) return;
    // 否则执行递归的逻辑
    res.value(next);
  };
  next();
}
step(genFn());

这里有一个递归的过程,我们把这一步封装称为自动执行 Generator 函数。

而 async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return myAsync(function* () {
    // ...
  });
}

灯的颜色变化

介绍

我们经常会看到各种颜色的灯光,本题我们将实现一个颜色会变化的灯的效果。

准备

开始答题前,需要先打开本题的项目代码文件夹,目录结构如下:

├── effect.gif
├── images
│   ├── greenlight.svg
│   ├── light.svg
│   └── redlight.svg
├── index.html
└── js
    └── trafficlights.js

其中:

  • index.html 是主页面。

  • images 是图片文件夹。

  • js/trafficlights.js 是需要补充的 js 文件。

  • effect.gif 是最终实现的效果图。

注意:打开环境后发现缺少项目代码,请手动键入下述命令进行下载:

cd /home/project
wget https://labfile.oss.aliyuncs.com/courses/9791/04.zip && unzip 04.zip && rm 04.zip

在浏览器中预览 index.html 页面效果如下:

目标

完成 js/trafficlights.js 文件中的 red、green 和 trafficlights 函数,达到以下效果:

  1. 页面加载完成 3 秒后灯的颜色变成红色。

  1. 在灯的颜色变成红色的 3 秒后,灯的颜色变成绿色(即 6 秒后灯光变成绿色)。

  1. 随后颜色不再变化。

  1. 请通过修改 display 属性来显示不同颜色的灯的图片。

完成后的效果见文件夹下面的 gif 图,图片名称为 effect.gif(提示:可以通过 VS Code 或者浏览器预览 gif 图片)。

规定

  • 请通过修改 display 属性来显示不同颜色的灯的图片,以免造成无法判题通过。

  • 请勿修改项目中提供的 id、class、函数名称、已有样式,以免造成无法判题通过。

  • 请严格按照考试步骤操作,切勿修改考试默认提供项目中的文件名称、文件夹路径等。

判分标准

  • 本题完全实现题目目标得满分,否则得 0 分。

题解

利用定时器实现延时调用函数,动态地为元素修改display属性,利用promise对象将函数封装成异步进行函数,实现两个函数的变化是异步进行的,一前一后的执行两个函数。

// TODO:完善此函数 显示红色颜色的灯
function red() {
    return new Promise((resolve,reject)=>{
        setTimeout(function(){
            console.log("red");
            document.getElementById('defaultlight').style.display="none"
            document.getElementById('redlight').style.display="inline-block"
            resolve();
        },3000);
        
    })
}

// TODO:完善此函数  显示绿色颜色的灯
function green() {
    return new Promise((resolve,reject)=>{
        setTimeout(function(){
            console.log("green");
            document.getElementById('redlight').style.display="none"
            document.getElementById('greenlight').style.display="inline-block"
            resolve();
        },3000);
        
    })
}

// TODO:完善此函数
async function trafficlights() {
    await red();
    await green();
}

trafficlights();
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值