# 新增关键字

它是 2015 年发布的一个 JavaScript 版本,被命名为 ECMAScript 2015,也叫 ECMAScript 6,故简称为 ES6。

ES6 带来了许多新的语法和新的功能,它可以使开发者编写更少的代码,做更多的事情。本节实验我们一起来学习定义变量和常量的关键字。

知识点

  • var 关键字的缺点
  • let
  • const

# 思考 var 关键字的缺点

# 变量提升机制的问题

还记得在 JavaScript 中我们学过定义变量的方式吗?😏

我们声明变量的唯一方式是使用 var 关键字,但其实使用 var 关键字定义变量会遇到一些麻烦,不知道同学们在开发代码时,是否遇到如下情景。

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

我们在控制台可以看到以下报错:

图片描述

很奇怪吧!我们明明在函数中定义了 num 变量,为什么会说没有定义呢?🤔

其实在 JavaScript 中有一个提升机制,就是无论你在哪里使用 var 关键字声明变量,它都会被提升到当前作用域的顶部。在运行 getNumber 函数时,实际上执行结构是下面这个样子。

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

因为在开头定义变量时,没有给 num 变量赋任何值,并且 getNumber 传入的是 false ,导致 if 语句未执行, 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 关键字

基于上述三个问题,下面给大家一一解答~ 👻

# 解决变量提升机制问题

ES6 为我们提供了 let 关键字,它解决了变量提升到作用域顶部的问题。因为它的作用域是 ,而不是提升机制了。

新建一个 index.html 文件,在其中定义一个 getNumber 函数,并用 let 来定义变量,写入以下代码。

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

注意:以后实验中只展示 JS 的代码,关于 HTML 结构不再展示。

报错信息如下:

图片描述

ReferenceError 是一个引用类型的错误,num is not defined 意思是 num 变量并不存在。

这是为什么呢?🤔

我们刚刚说过 let 关键字声明变量,其作用域是一个块,如果我们是在花括号 {} 里面声明变量,那么变量会陷入暂时性死区,也就是在声明之前,变量不可以被使用。

在上面代码中,我们的 num 变量是放在 if(){} 这个块中,并没有在 else{} 块中,所以会形成暂时性死区。

图片描述

# 解决变量重复声明的问题

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

新建一个 index1.html 文件,在文件中使用 let 关键字重复定义变量。

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

可以看到控制台会报参数错误(SyntaxError)。

图片描述

如果将上面代码中的 let 关键字改为 var 关键字便不会报错了。

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

图片描述

# 解决非块级作用域的问题

前面我们已经说过使用 var 关键字定义变量,只有两种作用域,函数作用域和全局作用域,这两种都是非块级作用域。而 let 关键字定义的变量是块级作用域,就避免了变量提升。我们来看个例子。

新建一个 index2.html 文件,在其中写入以下内容。

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

你会发现,控制台报错了。

图片描述

这是因为上面代码中的 i 变量只存在于 for 循环这个块中,当循环结束, i 变量就被销毁了,所以在 for 循环外访问不到。

# 认识 const 关键字

在 ES6 中,为我们提供了另一个关键字 const 用于声明一个只读的常量。且一旦声明,常量的值就不能改变。如果你尝试反复赋值的话,则会引发错误。例如:

const MaxAge = 100;
MaxAge = 10;
console.log(MaxAge);

图片描述

既然是不可改变,那么我们在定义时,必须对它进行初始化,不然也会报错。例如:

const Num;
console.log(Num);

图片描述

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

1. 值类型

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

const num = 20;

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

2. 引用类型

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

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

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

const arr = ["一", "二", "三"];
arr = ["五", "六", "七"];

图片描述

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

例如:

const arr = ["一", "二", "三"];
arr[0] = "四";
arr[1] = "五";
arr[2] = "六";
console.log(arr);

在控制台会显示:

图片描述

这是为什么呢?🤔

因为变量 arr 保存的是数组的引用,并不是数组中的值,只要引用的地址不发生改变就不会保错。这就相当于一个房子,它拥有固定的位置,但住在房子里的人不一定固定。

图片描述

练习要求:

  1. 新建一个 index3.html 文件。
  2. 定义一个数值常量 8 和一个数组常量,数组包含的值如下:
    "I like JavaScript"
    "I just finished HTML"
    "You can try anything"
  3. 循环数组中的值,并对每个值做一次切片操作。
  4. 在控制台会打印出每个数组元素中下标为 2 到下标为 7 的字符。

参考效果如下:

图片描述

<details open=""><summary > 参考答案 </summary>

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      const MAX = 8;
      const arr = [
        "I like JavaScript",
        "I just finished HTML",
        "You can try anything",
      ];
      for (let i = 0; i < arr.length; i++) {
        console.log(arr[i].slice(2, MAX));
      }
    </script>
  </body>
</html>

</details>

[](javascript:😉

# 实验总结

本节实验为大家介绍了 let 和 const 关键字,同学们需要记住使用 const 关键字声明的是常量,必须赋初始值,并且不能在同一作用域内重新声明,也无法重新赋值。

我们来回顾一下 const、let、var 三者之间的区别:

  • var 语句的作用域是函数作用域或者全局作用域;它没有块作用域,故不存在暂时死区;它可分配,也可重复性声明。
  • let 语句属于块作用域,受到暂时死区的约束;它可分配,但不可重新声明。
  • const 语句也属于块作用域,同样受到暂时死区的约束;它既不可重新分配,也不可重新声明。

# 字符串的扩展

知识点

  • 模板字面量
  • 字符串的新增方法

# 模板字面量

模板这个词很常见,指的是可以复用的东西。字面量 在 JavaScript 的英文解释为:“Literals represent values in JavaScript. These are fixed values—not variables—that you literally provide in your script.”,翻译过来就是 “在 JavaScript 中,字面量代表由一些字符组成表达式定义的常量”。

# 模板字面量的基础语法

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

1677996355994

新建一个 index.html 文件,在文件中写入以下内容。

let str = `Hello LanQiao!`;
console.log("输出字符串:" + str);
console.log("字符串类型:" + typeof str);
console.log("字符串长度:" + str.length);

控制台显示如下:

图片描述

在上面代码中,使用模板字符串创建了一个名为 str 的字符串,我们在控制台输出了字符串、字符串的类型、字符串的长度,可以发现,这和我们使用单引号或者双引号,创建字符串得到的结果是一模一样的。说明该语法的使用是没有问题的。

如果我们的字符串中本身就包含反撇号,怎么让其输出呢?🤔

答案很简单,使用斜杠来转义一下。修改上面的代码:

let str = `Hello,\`LanQiao\``;
console.log("输出字符串:" + str);
console.log("字符串类型:" + typeof str);
console.log("字符串长度:" + str.length);

在控制台我们可以看到如下效果:

图片描述

可以看到反撇号也正确显示出来了。

# 多行字符串的处理

在 ES6 之前,我们如何给一个变量赋予多行字符串呢?手动回车?我们来试一试。

1677996442961

你会发现直接报错了,说明无法识别这样的书写格式。

最常用的方法是加入转义字符去处理换行问题,加入转义字符后,可以承接上一行的字符串。但这种方式不是 JavaScript 特有的功能,而是一个长期存在的 bug。

我们来看看这个偏方是如何使用的。

let str =
  "Hello,\
            LanQiao";

1677996480407

可以看到报错提示消失了,加上 console.log ,看看控制台会输出什么。

图片描述

从控制台可以看到并没有换行显示,只是被当作一行的延续了。还记得 \n 这个常见的换行符吗,我们把它加进去试一试。

let str = "Hello,\nLanQiao";

图片描述

可以看到能够换行了,但这种方式并不是长久之计,假如很长的字符串需要多次换行,你需要手动加入多个 \n ,想想就够心累的

识别多行字符串

新建一个 index1.html 文件,在文件中写入以下内容:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      let str = `Hello,
        ECMAScript 6`;
      console.log(str);
    </script>
  </body>
</html>

在控制台可以看到如下显示:

1677996549919

可以看到,它是能够识别多行字符串的,但是在 ECMAScript 6 的前面为何有那么空白?

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

我们写在 index.html 文件中的代码是包含空格的。

1677996605389

我们只要删去第二行字符串中前面的两个空格就对齐了。

1677996626205

# 字符串占位符

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

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

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

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

那什么是 JavaScript 中的占位符呢?

在 JavaScript 中,占位符由 ${} 符号组成,在花括号的中间可以包含任意 JavaScript 表达式。我们来举个例子吧~

新建一个 index2.html 文件,在文件中写入以下内容。

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

在控制台可以看到如下效果:

1677996847874

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

我们刚刚说里面可以包含任意 JavaScript 的表达式,那我们来验证一下,写入一个加法式子,看看能不能正确输出。代码如下所示:

let a = 2;
let b = 1;
let sum = `a+b=${a + b}`;
console.log(sum);

在控制台可以看到如下效果:

1677996872214

练习要求:

  1. 新建一个 index3.html 文件。
  2. 定义一个 dog 对象,其中包含狗狗的名字、品种、年龄。
  3. 使用模板字符串,输出柯基宝宝想对大家说的话。

练习效果如下:

1677996893719

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      const dog = {
        name: "闷墩儿",
        breed: "柯基",
        age: "一岁",
      };
      const introduce = `大家好!我叫${dog.name},我是一只可爱的小${dog.breed}
今天是我${dog.age}的生日,欢迎朋友们给我送礼物哦!`;
      console.log(introduce);
    </script>
  </body>
</html>

</details>

# 字符串的新增方法

在 ES6 中增加了处理字符串的方法,这些新方法弥补了 ES5 语法的一些不足之处。

本文中会给大家介绍以下几种处理字符串的新增方法:

  • includes()
  • startsWith()
  • endsWith()
  • repeat()
  • replaceAll()

# 判断指定字符串是否存在

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

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

新建一个 index5.html 文件,在文件中写入以下内容:

let str = "LanQiao Courses";
console.log("str 字符串中是否存在 Java:" + str.includes("Java"));
console.log("str 字符串的首部是否存在字符 Lan:" + str.startsWith("Lan"));
console.log("str 字符串的尾部是否存在字符 Course:" + str.endsWith("Course"));

在控制台你可以看到如下效果:

1677997058895

  • 在上面代码中,使用 str.includes("Java") 来判断 str 字符串中是否包含指定字符串 Java ,不包含,故结果为 false。
  • 使用 str.startsWith("Lan") 来判断 str 字符串的首部是否包含指定字符串 Lan ,不包含,故结果为 true。
  • 使用 str.endsWith("Course") 来判断 str 字符串的尾部是否包含指定字符串 Coursestr 尾部是 Courses ,故结果为 false。

1677997077315

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

# 重复字符串

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

我们来举几个例子~

新建一个 index6.html 文件。

let str = "HELLO";
console.log(str.repeat(4));

在控制台会看到如下显示:

1677997115086

从上图可以看到输出了由 4 个 HELLO 组成的新字符串。

repeat() 方法中的参数 n 取值只能是整数,如果 n 为负数或者小数,可能会产生如下所示的问题。

let str = "我是一个字符串";
console.log(str.repeat(3.7)); // 我是一个字符串我是一个字符串我是一个字符串
console.log(str.repeat(-1)); // Uncaught RangeError: Invalid count value at String.repeat
console.log(str.repeat(0)); // ""

1677997154836

从上面可以看到,当 n 为小数时,会自动忽略小数部分;当 n 为负数时,会报错;当 n 为 0 时,为空。

# 替换字符串

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

在 ES6 中,为我们提供了 replaceAll() 方法来解决这个问题,它可以用来替换所有匹配的字符串。

其语法格式为:

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

我们来举个例子~

新建一个 index7.html 文件。

let str = "HELLOWHELLOWHELLO";
console.log(str.replaceAll("W", "_"));

在控制台可以看到如下效果:

1677997198578

可以看到原字符串中的所有 W 都用 _ 代替了。

# 实验总结

本节实验给大家介绍了 ES6 中字符串的新变化,分别是模板字面量和处理字符串的新增方法,这里我们一起来总结一下吧~

在模版字面量中,我们主要学了三块内容:

  • 反撇号定义字符串。
  • 字符串中的占位符。
  • 标签模板。

在新增字符串的方法中,我们主要学了:

  • includes()startsWith()endsWith() 来代替 indexOf() 方法判断字符串是否包含指定子串。
  • repeat() 方法自定义次数重复输出指定字符串。
  • replaceAll() 方法来代替了 replace() 替换指定字符串。

# 数组的扩展

知识点

  • 创建数组的方法
  • 数组实例的方法
  • for...of 循环
  • 扩展运算符

# 创建数组的方法

在之前的学习中,我们使用的是 Array 对象来创建数组。在 ES6 中,为我们提供了两个创建数组的新方法简化了数组创建的过程。

  • Array.of()
  • Array.from()

下面我们就一一来学习吧~

# Array.of()

同学们是否发现在之前的学习中,我们用 Array 对象来创建数组,会出现一些奇奇怪怪的问题。比如下面这些情况:

let arr = new Array(5);
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);
console.log("arr[4]:" + arr[4]);

在控制台显示如下:

1678551019975

我们可以看到打印出数组的长度为 5, Array() 中的 5 没有作为数组的元素被输出,而是被当作了数组的长度。

1678551030934

假如 Array() 中不是一个正整数,而是一个字符串,看看会发生什么事呢~

let arr = new Array("5");
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);

在控制台显示如下:

1678551077266

刚刚我们写入一个整数时,被看作是数组的长度,我们再来发挥一下,假如我们在 Array() 中写入两个整数看看会发生什么~

let arr = new Array(3, 4);
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);
console.log("arr[1]:" + arr[1]);

在控制台显示如下:

1678551095105

参数 3 和 4 被看成了数组中的数据。你在 Array() 中多写几个元素,可以看到它们都会被当成数组的元素。

1678551106255

那我们看看加入几个字符串呢?😏

let arr = new Array(4, "7", "8");
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);
console.log("arr[1]:" + arr[1]);
console.log("arr[2]:" + arr[2]);

在控制台显示如下:

1678551116509

可以看到整数和字符类型均被当作了数组的元素。

用上面这样的方式传入值是存在一定风险的。Array.of() 为我们解决了这个问题。我们来看看它是怎么用的。👻

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

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

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

我们来举个例子,新建一个 index.html 文件,在文件中写入以下内容:

let arr = Array.of(7);
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);

在控制台显示如下:

1678551129494

可以看到,当我们在 Array.of() 中只输入一个整数时,该参数不会被识别为数组的长度。

多加几个整数测试一下~

let arr = Array.of(7, 8, 9);
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);
console.log("arr[1]:" + arr[1]);
console.log("arr[2]:" + arr[2]);

在控制台显示如下:

1678551140695

再试着加入字符~

let arr = Array.of(5, "6", "7");
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);
console.log("arr[1]:" + arr[1]);
console.log("arr[2]:" + arr[2]);

在控制台显示如下:

1678551154560

可以看到规则是统一的,写在 Array.of() 里面的内容都会被当作数组的元素。

1678551164623

# Array.from()

在 ES6 之前,如果要把非数组类型的对象转换成一个数组,我们能想到最简单的办法是什么呢?🤔 是不是用 [].slice.call() 把一个非数组类型变为数组类型。举个例子:

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

在控制台显示如下:

1678551290450

在 ES6 中为我们提供了 Array.from() 代替了这种旧办法。

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

  • 类似数组的对象(array-like-object)。
  • 可遍历的对象(iterable-object)。

其基本使用格式为:

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

我们来看看它是怎么使用的~ 😎

新建一个 index1.html 文件,在文件中写入以下内容:

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

在控制台可以看到与上面一样的效果。

注意:Array.from () 方法是基于原来的对象创建的一个新数组。

# 数组实例的方法

本文会给大家介绍以下六种数组实例的方法:

  • find()
  • findIndex()
  • fill()
  • entries()
  • keys()
  • values()

# find () 方法

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

其语法格式为:

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

参数说明如下:

  • callback 是数组中每个元素执行的回调函数。(必选)
  • value 是当前元素的值,它是一个必须参数。
  • index 是数组元素的下标,它是一个可选参数。
  • arr 是被 find () 方法操作的数组,它是一个可选参数。
  • thisValue 是执行回调时用作 this 的对象,它是一个可选参数。

我们来举个例子吧!👻

新建一个 index2.html 文件,在文件中写入以下内容:

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

在控制台会打印出第一个大于 2 的数组元素。

1678551344932

我们把 indexarr 参数也传入看看效果,修改代码如下:

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

在控制台可以看到如下结果:

1678551355211

在代码中,我们返回了每次遍历判断条件的结果、当前元素的下标值、原数组。

从控制台的结果显示可以看出,在遍历数组的过程中,我们遍历第一个数组元素为 1 ,此时 value 的值不满足条件 value>2 ,故返回的是 false ;而遍历其他三个元素都满足条件 value>2 ,故返回的都是 true 。遍历完成后,返回了第一个符合条件的元素 3

1678551366658

对了,我们刚刚说如果数组中没有一个元素满足条件,就会返回 undefined,我们来验证一下,修改代码如下:

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

在控制台可以看到如下结果:

1678551378241

# findIndex () 方法

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

其语法格式为:

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

参数说明如下:

  • callback 是数组中每个元素都会执行的回调函数(必须参数)。
  • value 是当前元素的值,它是一个必须参数。
  • index 是数组元素的下标,它是一个必须参数。
  • arr 是被 findIndex () 方法操作的数组,它是一个必须参数。
  • thisArg 是执行回调时用作 this 的对象,它是一个可选参数。

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

我们来举个例子吧!👻

通过 findIndex () 方法来看看小动物们所对应的下标是多少。

1678551436778

新建一个 index3.html 文件,在文件中写入以下内容:

let arr = ["小猫", "小狗", "兔子"];
arr.findIndex(function (value, index, arr) {
  console.log(value == "兔子");
  console.log(`${value}是数组中的第 ${index + 1} 位。`);
});

在控制台可以看到如下效果:

1678551446703

这样貌似看不出 findIndex () 方法的特性,我们返回一下回调函数的结果:

let arr = ["小猫", "小狗", "兔子"];
let result = arr.findIndex(function (value, index, arr) {
  return value == "兔子";
});
console.log(result);

在控制台可以看到返回的是「兔子」的下标。

1678551458453

刚刚说匹配失败会返回 -1,同样来验证一下~ 😉 修改代码如下:

let arr = ["小猫", "小狗", "兔子"];
let result = arr.findIndex(function (value, index, arr) {
  return value == "老虎";
});
console.log(result);

在控制台可以看到的确打印出 -1。

1678551468652

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

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

# fill () 方法

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

其使用格式为:

array.fill(value, start, end);

其参数说明如下:

  • value 是用来填充数组的值,它是一个必须参数。
  • start 是被填充数组的索引起始值,它是一个可选参数。
  • end 是被填充数组的索引结束值,它是一个可选参数。

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

我们来举个栗子~ 🌰

新建一个 index4.html 文件,在文件中写入以下内容:

let arr = ["🐱", "🐶", "🐰"];
let result = arr.fill("🐷");
console.log(result);

在控制台可以看到数组中的猫猫、狗狗、兔兔,已经被猪头替换了。

1678551523116

我们不想让猪头占满整个数组,把 start 和 end 参数加上,修改代码如下:

let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"];
let result = arr.fill("🐷", 2, 5);
console.log(result);

在控制台可以看到数组中下标为 2 的元素~下标为 4 的元素被替换成了猪头。

1678551535698

同学们可能要疑惑了我们明明指定的是下标 2 ~ 5,怎么只有 2 ~ 4,这是因为我们的下标是从 0 开始的。

# entries()、keys()、values()

entries()keys()values() 是 ES6 中三种数组的遍历方法,三个方法返回的都是 Array Iterator 对象。但三者之间肯定不是完全相同的,接下来我们一一学习。

entries () 方法以键 / 值对的形式返回数组的 [index,value],也就是索引和值。其语法格式为:

array.entries();

我们来举个例子~ 😎

新建一个 index5.html 文件,在文件中写入以下内容。

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

在控制台可以看到:

1678551560351

可以看到结果只输出了 Array Iterator {},并没有以键值对的形式输出值。

我们要输出 Array Iterator 对象里的值,可以用前面提到过的扩展运算符(...)来展开。修改代码如下:

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

再看看控制台可以发现数组中的值都以键值对的形式输出了。

1678551571888

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

其语法格式为:

array.keys();

我们继续修改上面的代码:

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

控制台显示如下:

1678551585758

values () 方法返回的是每个键对应的值。

其语法格式为:

array.values();

我们继续修改上面文件的代码:

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

在控制台可以看到如下显示:

1678551599863

这三个数组遍历的方法就讲完了,最后画一张图来总结一下三个方法的区别吧!

1678551608455

# for...of 循环

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

# 讨论一下 for 的缺点

回顾一下,在学习 JavaScript 的时候,我们是如何遍历数组的呢?🤔

我们是用 for 语句来遍历的,例如:

var arr = [1,2,3,4];
for(var i=0; i<arr.length; i++){
    document.write(arr[i]);// 依次将数组的值输出到页面上
}

同学们在使用 for 语句的时候,有没有感觉到一些局限性,我给大家说两条:

  1. 我们必须要设置一个计数器,比如上面代码中的 i
  2. 我们必须有个退出循环的条件,如上面代码那样使用 length 属性获取数组的长度,当计数器大于等于数组长度时退出。

当然数组这样使用没问题,但是很多时候我们还会使用其他结构的数据,使用 for 语句就相当麻烦了。

大家如果还没弄明白的话,可以思考一个需求:把字符串 ”hello“ 按逐个字符打印。

let str = "hello".split("");
for (i = 0; i < str.length; i++) {
  console.log(str[i]);
}

# for...of 的使用

为了解决 for 中的不足,ES6 提供了 for...of 循环。

for...of 就摆脱了计数器、退出条件等烦恼,它是通过迭代对象的值来循环的。它能迭代的数据结构很多,数组、字符串、列表等。但在本实验中我们重点放在数组的遍历上。

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

for (variable of iterable) {
}

参数说明如下:

  • variable :是存放当前迭代对象值的变量,该变量能用 constletvar 关键字来声明。
  • iterable :是可迭代对象。

学了 for...of 同学们是不是可以优先字符串”hello“按逐个字符打印的例子呢?思考一下~

let str = "hello";
for (let item of str) {
  console.log(item);
}

我们再来举个例子吧

新建一个 index6.html 文件,在文件中写入以下内容。

<!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>

开启 8080 端口,打开 Web 服务,实验效果如下:

1678552174433

  • 在上面代码中,定义了一个名为 arr 的数组,其中有三个名字。
  • 使用 for(let name of arr) 去遍历数组 arr 的值,我们每一次迭代都把值放在临时变量 name 中。
  • 每迭代一次,就会执行 for...of 中的语句,所有页面上打印除了三个名字对应的欢迎语。

练习要求:

  1. 新建一个 index7.html 文件。
  2. 你需要创建一个包含几种水果名的数组。
  3. 使用 for..of 在控制台输出水果的名字。

参考效果:

1678552196328

var fruits = ["苹果", "葡萄", "芒果", "橘子"];
for (let fruit of fruits) {
  console.log(fruit);
}

# 扩展运算符

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

其语法格式为:

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

我们来举个例子~

新建一个 index8.html 文件,在文件中写入以下内容:

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

在控制台可以看到如下输出:

1678552240476

在上面代码中,我们把 animals 数组中的值使用扩展运算符插入到了 zoo 数组中。

除了可以向新数组中插入值,我们还可以用来复制数组。修改代码如下:

let animals = ["老虎🐯", "乌龟🐢", "鱼🐟"];
let newAnimals = [...animals];
console.log(newAnimals);

在控制台可以看到,现在 newAnimals 数组中的值和 animals 数组中的值是一样的。

1678552260453

综合上面的例子我们其实可以这么理解:使用扩展运算符可以起到将数组展开的作用。

let animals = ["老虎🐯", "乌龟🐢", "鱼🐟"];
let newAnimals = [...animals];
animals[0] = "kkk";
newAnimals[0] = 'jjj';
console.log(animals);
console.log(newAnimals);
//animals 和 newAnimals 的输出不一样,说明是两个对象

扩展运算符号问世后,成为了程序员的宠儿,但在 ES2018 版本前的它有一个缺点就是只能用在数组和参数上。于是在 ES2018 中又将扩展运算符引入了对象。

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

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

我们来看个例子~

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

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

控制台会输出:

1678552275509

给对象添加属性

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

控制台会输出:

1678552286712

合并对象

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

控制台会输出:

1678552300116

# 实验总结

在本节实验中,给大家介绍了数组扩展里的四块内容:

  • 两种创建数组的方法:
    • Array.of () 方法用于将一组指定的值转换为数组。
    • Array.from () 方法用于将类数组或者可迭代对象转换为数组。
  • 六种数组实例的方法:
    • find () 方法:返回数组中满足指定条件的第一元素的值,未找到,则返回 undefined。
    • findIndex () 方法:返回数组中满足指定条件的第一元素的索引,未找到,则返回 -1。
    • fill () 方法:用一个固定值去填充数组中指定索引位置的数组值。
    • entries () 方法:返回一个新的 Array Iterator 对象,该对象包含数组中每个索引的键 / 值对。
    • keys () 方法:返回一个包含数组中每个索引键的 Array Iterator 对象。
    • values () 方法:返回一个新的 Array Iterator 对象,该对象包含数组每个索引的值。
  • for...of 循环:可以遍历数组以外的其他数据类型。
  • 扩展运算符。

# 函数的扩展

本节实验我们来探索一下,在 ES6 中函数相关的新变化。

# 知识点

  • 默认参数
  • rest 参数
  • 箭头函数

# 默认参数

默认参数并不是一个陌生的词汇,相信大家都认识它,那在 ES6 中,函数的默认参数有什么新突破呢?🤫

不要胡思乱想了,跟着我一起看看真相吧!

# 回忆一下,ES5 中默认参数的使用

同学们,还记得我们在 ES5 的学习中,是怎样设置默认参数的吗?

忘记了没关系,我们一起来回忆一下。

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

在控制台可以看到如下输出:

1678552401404

在上面代码中:

  • func 函数一共有两个形式参数,在 func('大家好!我是') 我们只指定了第一个形式参数 words 的值,所以 name 使用的是默认值「闷墩儿」。
  • func('大家好!我是','憨憨') 中我们指定了两个形式参数的值,所以 name 没有使用默认值。
  • func('大家好!我是','') 中我们指定第二个形式参数为空字符串,被认为是没有赋值,所以也使用的是默认值。

看到了吧!如果我们想给形式参数设置默认值需要在函数中单独定义,还是有点麻烦的。

# 在函数中直接设置默认值

在 ES6 中我们可以直接在函数的形参里设置默认值。

我们来举个例子~ 👻

新建一个 index.html 文件,在文件中写入以下内容:

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

在控制台可以看到如下输出:

1678552429300

在上面代码中:

  • func('请给我一个') 只传入了第一个参数,第二个参数没传入任何值,故第二个参数使用了默认值红苹果。
  • func('请给我一个','🍐') 传入了二个参数,所以第二个参数没有使用默认值。
  • func('请给我一个','') 第二个参数,虽然传入的是空字符串,空字符串也算是一个参数值,故同样不会使用默认值。

可以看到在函数中设置默认值,如果你传入的第二个参数值为空字符串,会被当作值传入,而不会使用默认值了。

函数参数默认值在实际应用过程中,有哪些注意事项呢?

参数变量是默认声明的,我们不能用 let 或者 const 再次声明。

举个例子~

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

可以看到控制台报错了。

1678552495662

# 注意参数默认值的位置

设置默认值的参数,一般放在其他参数的 后面 ,也就是尾部。这是为什么呢?🤔

很容易能想到,如果你给第一个形参设置了默认值,调用函数传参时,无法省略该参数。

我们来举个例子~

function func(words = "你好", name) {
  console.log(`${words}${name}`);
}
func("小蓝");

在控制台可以看到,我们在函数中传入的值 “小蓝”,被当作第一个参数的值了,而第二个参数没有传入值,所有输出的是 undefined。

1678552508077

除了这样直接在函数中传入一个值,我们还有更高端的玩法,请点击下一步 👇 一探究竟。

# 使用函数作为默认值

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

举个栗子~ 🌰

新建一个 index1.html 文件,在文件中写入以下内容:

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

在控制台可以看到如下输出:

1678552567138

在上面代码中:

  • 我们定义了一个名为 parameter 的函数,在函数中返回了一颗黑色的爱心。
  • func 函数中的第二个形式参数设置默认参数为 parameter 函数中的返回值。
  • 使用 func('请给我一颗小') ,只设置了第一个形参的值,所以第二个参数使用的是默认值「小黑心」。
  • 使用 func('请给我一颗小','💗') ,设置了两个形参的值,所以第二个参数没有使用默认值。

明白了吗?我们来做个练习吧!

练习要求:

  1. 新建一个 index2.html 文件。
  2. 定义一个名为 add 的函数,用来计算 1 ~ n 的连加(其中 n 为任意一个正整数)。
  3. 函数只有一个形式参数,设置默认值为 5。

参考效果:

1678552595588

function add(a = 5) {
  let result = 0;
  for (let i = 1; i <= a; i++) {
    result += i;
  }
  return result;
}
console.log("1~5 相加的和为:", add());
console.log("1~10 相加的和为:", add(10));

还记得我们前面学习的解构赋值吗?偷偷告诉你 ✨,解构赋值还可以用于函数的参数。点击下一步,我们来一探究竟~

# 解构参数

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

我们来举个例子~

新建一个 index3.html 文件,在文件中写入以下内容。

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: "桃子",
});

在控制台可以看到如下输出:

1678552633073

在上面代码中:

  • func 函数包含 4 个参数,其中第 4 个参数是解构参数,解构参数里面包含 4 个参数变量 a、b、c、d。
  • 使用 func('小蓝',5,3,{a:'西瓜',b:'菠萝',c:'桃子'}) 调用该函数,其中传入 name 参数的值为 “小蓝”, value 参数的值为 5, mount 参数的值为 3;解构参数只传入三个值,a 的值为 “西瓜”,b 的值为 “菠萝”,c 的值为 “桃子”,d 使用的是默认值。

从上面代码中我们可以看出,解构参数是以键值对的形式传入的,其参数名与键值名保持一致,即可传入成功。

# rest 参数

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

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

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

  • 改变函数的调用。
  • 数组构造。
  • 数组解构。

rest 参数语法格式为:

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

我们来举个例子~

新建一个 index4.html 文件,在文件中写入以下内容。

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

在控制台可以到如下输出:

1678552661437

在上面代码中,我们给 func 函数传了 10 个参数,形式参数 ab 各取一个值,多余的 8 个参数都由 rest 参数收了。

1678552673760

我们刚刚说 rest 参数只能作为函数的最后一个参数,我们把它放在中间,看看会发生什么事情。😉

修改代码如下:

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

在控制台可以看到报错了:

1678552685217

# 箭头函数

箭头函数,顾名思义,就是用箭头(=>)来表示函数。箭头函数和普通函数都是用来定义函数的,但两者在语法构成上非常不同。我们来举个例子对比一下~

新建一个 index5.html 文件,我们先用 ES5 的方式定义一个函数。

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

上面定义了一个名为 sum 的求和函数。

我们看一看怎么用箭头函数来描述上面函数的功能。修改代码如下:

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

两个代码都会在控制台输出 3。

1678552708325

可以看出使用箭头函数代码变得更加简洁了。

看上面的代码我们可以总结出箭头函数的基本用法。

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

箭头前面 () 里放着函数的参数,箭头后面 {} 里放着函数内部执行的表达式。

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

# this 的指向

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

我们先来看看在箭头函数还未出生的时代里,this 是怎么使用的。

假如,有一个函数能够实现在指定时间周期内一直计数。用 ES5 的语法实现如下所示:

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

在上面例子中,定义了一个名 Number() 的函数,该函数里包含一个 num 参数。在 Number() 函数里有一个 count() 的函数,在其内部让 num 自增。 count 函数是作为 setInterval() 函数的参数而存在的, setInterval() 函数的作用是在某个周期内自动调用函数。

可以看到上面的两个函数中,每个函数会定义自己的 this,this 只存在于你所使用的作用域范围内。

而在箭头函数中的 this 对象,就是定义该函数所在的作用域所指向的对象,而不是使用所在作用域指向的对象。我们使用箭头函数来修改上面的代码。

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

到此处,我们来总结一下箭头函数与普通函数的区别:

  • 箭头函数的 this 指向是其上下文的 this,没有方法可以改变其指向。
  • 普通函数的 this 指向调用它的那个对象。

ok,接下来我们深入学习箭头函数~

# 不带参数的箭头函数

同学们想一想,我们定义函数并不是都需要参数的,那没有参数的箭头函数怎么定义呢?🤔

我们来举个例子~

新建一个 index6.html 文件,在文件中写入以下内容:

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

在控制台可以看到如下输出:

1678552748905

在上面代码中,我们定义了一个名为 DogName 的函数,其函数内部返回狗狗的名字 “闷墩儿”。我把 ES5 语法格式的代码放在下方帮助同学们理解。

let dogName = function () {
  return "闷墩儿";
};
console.log(dogName());

# 带默认参数的箭头函数

箭头函数与普通函数一样也是可以直接给参数设置默认值的。

我们来举个例子~

新建一个 index7.html 文件,在文件中写入以下内容:

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

在控制台可以看到如下输出:

1678552774927

在上面代码中,定义的 func 函数里有两个形参,第二个形式参数里设置默认值为彩虹,函数内部使用模板字面量的方式返回了两个形式参数的值。

✨Tips:这里给大家补充箭头函数的几个简写形式。

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

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

控制台可以看到如下效果:

1678552785432

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

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

可以看到控制台依然能够正常显示。

1678552818169

3. 如果箭头函数直接返回一个对象,需要用小括号包裹,否则就会报错。

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

控制台显示如下:

1678552830422

# 带 rest 参数的箭头函数

我们之前学过的 rest 参数也是可以在箭头函数中使用的。

我们举个例子来看看~

新建一个 index8.html 文件,在文件中写入以下内容:

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);

在控制台可以看到如下输出:

1678552860572

在上面代码中,我们给 func 函数传入了 10 个参数,其中,1 赋给形式参数 a ,2 赋给形式参数 b ,剩余的参数都由 rest 参数收下了。

# 实验总结

在本节实验中,给大家介绍了函数扩展里的三块内容:

  • 函数的默认参数:
    • 可以直接在函数的形参上定义默认值。
    • 可以把函数作为形参的默认值。
  • rest 参数:用 rest 来获取函数的多余参数,这样我们就不会因为需要接收可变的参数列表而使用函数的 arguments 对象了。
  • 箭头函数。

# 类的扩展

本节实验我们来探索一下,在 ES6 中类相关的新变化。

# 知识点

  • 类的声明
  • 类的继承
  • 类的属性和方法
  • new.target 属性

# 类的声明

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

我们来看个 ES5 语法的近类结构,首先创建一个构造函数,然后定义另一个方法并赋值给构造函数的原型。

我们来看个例子~

新建一个 index.html 文件,在文件中写入以下内容:

function MyClass(num) {
  this.num = num;
  this.enginesActive = false;
}
MyClass.prototype.startEngines = function () {
  console.log("starting ...");
  this.enginesActive = true;
};
// 只能在原型中定义类的方法,不能直接写成 MyClass.startEngines
const myclass = new MyClass(1);
myclass.startEngines();
  • 在上面的代码中, MyClass 是一个构造函数,用来创建 MyClass 类型的对象。
  • 使用 this 声明并初始化 MyClass 的属性 numenginesActive
  • 并在原型中存储了一个可供所有实例调用的方法 startEngines
  • 然后使用 “new + 构造函数” 的方式创建实例对象 myclass ,并将 myclassnum 属性初始化为 1。
  • 最后调用执行实例对象 myclassstartEngines 方法。

在 ES6 中,为我们提供了 class 关键字来创建类。类是用来构建对象的蓝图。

什么是蓝图呢?🤔 蓝图就是一个物体的结构,你可以在这个结构的基础上定义不同的属性,比如一件相同款式的衣服,有不同的颜色,衣服相当于蓝图,颜色相当于属性。

我们来举个例子。假如小蓝和小白这两兄弟去买同样款式的衣服,小蓝想要一件黑色,L 号的衣服,小白想要一件蓝色,M 号的衣服。

1678553029089

若以代码的形式构建小蓝和小白的需求就是:

class Clothes {
  //constructor 是构造函数
  constructor(color, size) {
    this.size = size;
    this.color = color;
  }
}
const xiaoLan = new Clothes("黑色", "L");
const xiaoBai = new Clothes("蓝色", "M");

在上面代码中 constructor 是类的构造函数,它是定义类的默认方法,当你使用 new 来创建对象实例化时,会自动调用该方法。如果没有在类中添加 constructor ,也会默认有一个 constructor 方法,所以它在类中是必须有的。

明白了 class 关键字的使用,接下来我们使用 class 关键字来改造一下 MyClass 函数吧!

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();

可以看到代码的结构变得更加清晰、简洁了。👻

在上面代码中, MyClass 的类声明其实与之前构造函数的声明过程是相似的,只是在使用 class 关键字进行类声明中通过特殊的 constructor 方法名来定义了构造函数,且由这种类声明的简洁语法来定义方法。

所以,ES6 的 class 关键字只是一个语法糖,其底层还是函数和原型,只是给它们披上了一件衣服而已。

我们继续深入学习类的语法。

# 类表达式

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

  • 声明形式(例如:function、class 关键字声明)。
  • 表达式形式(例如:const A = class {})。

前面我们已经见过声明形式了,接下来我们看看表达式形式是如何使用的。

新建一个 index1.html 文件。如果我想创建一个狗狗的类型,使用 ES6 的写法如下:

// ES5 语法
function DogType(name) {
  this.name = name;
}
DogType.prototype.sayName = function () {
  console.log("大家好!我是一只小" + this.name + "。");
};
let dog = new DogType("柯基");
dog.sayName();
console.log(dog instanceof DogType);
console.log(dog instanceof Object);

接下来,我们使用 ES6 类表达式来改写上面的代码。

# 匿名类表达式

// 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);

在控制台可以看到如下输出:

1678553076486

在上面代码中 constructor(name){} 等价于 DogType 的构造函数, sayName(){} 等价于 DogType.prototype.sayName = function(){}

# 命名类表达式

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);

在控制台可以看到如下输出:

1678553089532

在上面代码中,类表达式被命名为 MyClass ,该标识符只存在于类定义中,而在类外部是不存在 MyClass 的,所以当我们在类外部查看其类型时,会输出 undefined。

举上面的例子有两个目的,一是告诉大家我们可以在类表达式中命名类名;二是类名不能在类外使用。

匿名类表达式和命名类表达式的区别是是否在关键字 class 后加类名

# 类的继承

继承这个词语并不陌生,现实生活中,我们听过孩子继承父母的财产。而在程序中的继承,差不多也是这个意思,子类可以继承父类的一些属性和方法,这些属性和方法就相当于是财产了。

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

我们来看个例子~新建一个 index2.html 文件,假设有一个 Animal 类,其构造方法中有动物的名字、年龄和奔跑的公里数,并包含两个自定义的方法 run()stop()

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 和 extends 关键字来重写上面的代码。

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();

在控制台看到如下输出,说明 Dog 类是成功继承了 Animal 类的方法(包括构造方法和普通方法)。

1678553119735

  • 在上面代码中,使用 class Dog extends Animal {} 定义了一个 Dog 类继承了 Animal 类的属性和方法。
  • 使用 let dog = new Dog('闷墩儿','一',5) 实例化 Dog 对象。
  • 使用 Dog 类的实例对象 dog 去调用 runstop 方法,程序会先在 Dog 类的原型 Dog.prototype 中查找两个方法是否存在,找不到的情况下会去其父类原型 Animal.prototype 中查找,以此类推,层层向上最终找到并调用执行。

前面曾说过,class 定义的类,其内部运行机制还是构造函数与原型。我们用下图表示一下上面代码的原理。

1678553130585

# 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() 方法,最终在控制台输出了 “欢迎来到蓝桥云课!”。

1678553148488

# 认识 super

在前面 Animal 的案例中, Dog 类从 Animal 类中继承了 runstop 方法。

在现实中大家都知道,狗狗 🐶 的跑和其他动物的跑是有区别的,它尤其独特的表现形式。这种独特的表现形式在程序世界又该如何表现呢?

有同学会想:直接给 Dog 类也定义一个 run 方法是不是就可以解决问题了?

于是有了下面的修改:

class Dog extends Animal {
  run() {
    console.log(`${this.name}开始奔跑了。`);
  }
}

可以看到 Dog 类中的 run 方法完全代替了 Animal 类中的 run 方法,并没有实现扩展。

1678553183730

上面这种做法明显不行。难道就没有解决办法了吗?

当然有,而且上面的思路是正确的,不过在实现上还缺少重要的一步,那就是如何保留从父类中继承过来的 run 方法的步骤,并在此基础上做 Dog 类的差异化扩展。

✨ ES6 为我们提供了超级函数 super 我们的继承变得完整且具备可扩展性。

它的使用格式有两种:

  • 使用 super.method (...) 来调用父方法。
  • 使用 super (...) 调用父构造函数。

我们改写一下 Dog 类中的 run 方法。

class Dog extends Animal {
  run() {
    super.run();
    console.log(`${this.name}开始奔跑了。`);
  }
}

可以看到,使用 super.run() 后,我们的目的达到了, Animal 类中的 run() 方法被成功调用。

1678553196762

# 重写构造函数

在上面的例子中,同学们有没有发现,我们并没有在 Dog 类中创建 constructor

前面说过,类中不写 constructor 的情况下,在被实例化时会自动生成一个 constructor ,如下所示:

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

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

我们给 Dog 类中的 constructor 添加一个新的参数。

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

在控制台可以看到报错了。

1678553209545

报错信息的意思是继承类中的构造函数必须调用 super,并在使用 this 之前执行它。

什么意思呢???🤔

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

  • 当一个普通(即没有父类的类)的构造函数运行时,它会创建一个空对象作为 this,然后继续运行。
  • 但是当子类的构造函数运行时,与上面说的不同,它将调用父构造函数来完成这项工作。

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

我们来修改上面的代码:

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 {
  constructor(name, age, speed, species) {
    super(name);
    this.species = species;
  }
  run() {
    console.log(`${this.name}是一只奔跑的${this.species}`);
  }
}
let dog = new Dog("闷墩儿", "一", 5, "狗");
dog.run();

在上面的代码中,继承类的构造函数在调用父类的构造函数的时候,有部分参数没有传入,所以在实例 dog.age 和 dog.age 为 undefined。应该是,js 中的函数形式参数,当在调用时没有传入实际参数,则形式参数在函数调用中为 undefined。

这下没问题了~ 👻

1678553221460

这里给大家说一说使用 super () 的注意事项:

  • 只能在继承类中使用 super ()。
  • 构造函数中,一定要先调用 super () 再访问 this。

# 类的属性和方法

这里会给大家介绍以下两种类的属性和方法:

  • 静态方法
  • 静态属性
  • 私有方法
  • 私有属性

# 静态方法

在 JavaScript 中,静态方法是给面向对象编程提供的类方法,这种类方法有点类似于 Ruby 语言。

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

静态方法不能被实例调用,实例方法不能被类调用。

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

新建一个 index4.html 文件,添加一个狗狗类的定义如下。

// 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(){
}

将前面使用 ES5 方式定义静态方法的代码使用 ES6 实现如下:

// 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();

控制台输出:

1678553289823

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

有一点需要大家注意一下,如果静态方法中包含 this 关键字,这个 this 关键字指的是类,而不是实例。我们举个例子来看看~ 😉

class MyClass {
  static method1() {
    this.method2();
  }
  static method2() {
    console.log(this);
  }
}
MyClass.method1();

在上面代码中, MyClass 类中定义了两个静态方法 method1method2 ,静态方法 method1 调用了 this.method2 ,其中 this 指向的是 MyClass 类而不是 MyClass 的实例,这相当于 MyClass.method2

在控制台可以看到如下输出:

1678553303366

观察上面的代码可以发现,我们没有创建实例化对象,直接用「类名。方法名」就可以访问该方法,这就是静态方法的特点了。除了这个特点外,静态方法不能被其实例调用,我们来试试。

class MyClass {
  static method1() {
    this.method2();
  }
  static method2() {
    console.log("hello,welcome to there!");
  }
}
let myclass = new MyClass();
myclass.method2();

可以看到使用 myclass 实例对象调用 method2 静态方法会报错,则说明静态方法只能通过 “类名。方法名” 调用。

1678553318502

# 静态属性

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

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

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

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

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

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

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

现在,我们可以使用 static 关键字 🤩 来定义静态属性。

静态属性的使用格式为:

static propName = propVaule;

我们来举个例子~

新建一个 index5.html 文件,在文件中改写一下上面的代码。

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

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

比较这两个代码的写法,很明显第二种的代码结构更加清晰易懂。

# 静态属性和方法的继承

另外静态方法和静态属性是可以被继承的。我们来举个例子。

新建一个 index6.html 文件,在文件中写入以下内容。

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}`);

观察控制台的输出可以发现静态方法和属性通过直接使用 Dog 类本身是可以调用的。

1678553366981

上面代码解析如下:

  • Animal 类中声明了静态属性 place 和静态方法 compare 。并在 run 方法中使用 Animal.place 调用了其静态属性。
  • Dog 继承 Animal 类,但没有定义任何自己的属性和方法。
  • 实例化两个 Dog 类对象 “乐乐” 和 “闷墩儿”,并分别调用其 run 方法。
  • 分别使用 Dog.compareDog.place 调用了其父类的静态方法和属性。

关于静态方法和静态属性的定义和使用就先讲到这里,接下来给大家介绍私有属性和私有方法的定义与使用。💪

# 私有方法和私有属性

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

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

  • 类的属性和方法,在其内部和外部均可进行访问,也称为公共属性和方法。
  • 类的属性和方法,只能在类中访问,不能在类之外的其他地方访问,也称为私有属性和方法。

到目前为止给大家介绍的方法和属性都是类的外部可以访问的。

接下来我们说一说类的私有属性和方法的定义与使用。先来举个例子,我相信各位同学的家里都有电饭煲吧!电饭煲的操作很简单,插上电源,选择你要烹饪的类型(煮粥、煮饭、煲汤...),然后点击确定,电饭煲开始运行,你只需要等电饭煲的运行时间结束,便可以吃到美味的食物。👻

如果你把电饭煲拆开,你会发现其内部构造并不像我们使用时那么简单,里面一堆电路板和线。

作为一个电饭煲使用者来说,电饭煲的内部结构多么复杂并不需要我们去理解,只要我们可以正常使用就行了。

而内部接口就是这个意思了,电饭煲的内部结构(隐藏的细节)相当于是我们的私有属性和私有方法。

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

其语法格式为:

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

同学们可能有点疑惑,很多语言中的私有方法是 private 关键字,为什么 ES6 不使用这个关键字呢?🤔

因为 JavaScript 是一门动态语言,没有类型声明,使用独立的符号方便可靠,更容易区分。

清楚了如何使用,我们来举个例子吧!😉

新建一个 index7.html 文件,我们有个星系类(Galaxy),里面包含外星人的名字和所属星系。

class Galaxy {
  #address = `X 星系`;
  constructor(name) {
    this.name = name;
  }
}
let alien = new Galaxy();
console.log(alien.#address);

控制台报错了:

1678553394615

报错意思就是私有属性不能在类的外部被访问。

改写一下上面的代码:

class Galaxy {
  #address = `X 星系`;
  constructor(name) {
    this.name = name;
  }
  message() {
    console.log(`${this.name}住在${this.#address}`);
  }
}
let alien = new Galaxy("小7");
alien.message();

可以看到,在 Galaxy 类中可以访问 #address 私有属性。

1678553405291

我们在上面的代码中的 message 方法改成私有方法。

class Galaxy {
  #address = `X 星系`; // 私有属性 address
  constructor(name) {
    this.name = name;
  }
  #message() {
    // 私有方法 message
    return `${this.name}住在${this.#address}`;
  }
  say() {
    // 访问私有方法
    console.log(this.#message());
  }
}
let alien = new Galaxy("小7");
alien.say();

在上面代码中, Galaxy 类里定义了私有方法 message() ,在方法里返回了一句话(其中包含公有属性 name 和私有属性 address );在公有方法 say() 中我们访问了私有方法 message()

私有方法和属性只能在内部通过 this 来访问,不能类似静态方法那样通过类名来访问。

# new.target 属性

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

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

接下来我们一起看看 new.target 属性在构造函数中是如何使用的。

我们来举个例子~

新建一个 index8.html 文件,在文件中定义一个 Person 类。

class Person {
  constructor(name) {
    this.name = name;
    console.log(new.target.name);
  }
}
class Occupation extends Person {
  constructor(name, occupation) {
    super(name);
    this.occupation = occupation;
  }
}
let person = new Person("小白");
let occupation = new Occupation("小蓝", "前端工程师");

在控制台可以看到输出了对应的类名。

1678553429932

在上面代码中,使用 new.target.name 用来输出对应实例对象的类名。

我们不用 new 去实例化 Person 和 Occupation 看看是否会真输出 undefined。

class Person {
  constructor(name) {
    this.name = name;
  }
  static say() {
    console.log(new.target);
  }
}
class Occupation extends Person {
  constructor(name, occupation) {
    super(name);
    this.occupation = occupation;
  }
}
Person.say();
Occupation.say();

在控制台可以看到输出了两个 undefined。

1678553440359

# new.target 的应用场景

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

我们举个例子看看是如何使用的。

新建一个 index9.html 文件,在文件中写入以下内容:

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

可以看到控制台报错,提示我们不能去实例化 Person。

1678553457974

我们实例化一下 Occupation 类看看是否可以成功访问。

class Person {
  constructor() {
    // 如果实例化对象使用的是 Person 类,则抛出错误
    if (new.target === Person) {
      throw new Error("Person 类不能被实例化。");
    }
  }
}
class Occupation extends Person {
  constructor(name, occupation) {
    super();
    this.name = name;
    this.occupation = occupation;
    console.log(`${name}${occupation}`);
  }
}
let occupation = new Occupation("小蓝", "前端工程师");

从控制台的输出结果可以看到,能成功访问 Occupation

1678553468350

# 实验总结

本节实验给大家介绍了类相关的扩展,这里我们来总结一下:

  • 使用 class 关键字来声明类。
  • 使用 extendssuper 关键字来实现类的继承。
  • 使用 static 关键字来定义静态属性和静态方法,使用 # 来定义私有属性和私有方法。
  • 使用 new.target 来判断构造函数的调用方式。