# 新增关键字
它是 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
保存的是数组的引用,并不是数组中的值,只要引用的地址不发生改变就不会保错。这就相当于一个房子,它拥有固定的位置,但住在房子里的人不一定固定。
练习要求:
- 新建一个
index3.html
文件。 - 定义一个数值常量
8
和一个数组常量,数组包含的值如下:"I like JavaScript"
"I just finished HTML"
"You can try anything"
- 循环数组中的值,并对每个值做一次切片操作。
- 在控制台会打印出每个数组元素中下标为
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 中提供了反撇号去代替单引号和双引号。
新建一个 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 之前,我们如何给一个变量赋予多行字符串呢?手动回车?我们来试一试。
你会发现直接报错了,说明无法识别这样的书写格式。
最常用的方法是加入转义字符去处理换行问题,加入转义字符后,可以承接上一行的字符串。但这种方式不是 JavaScript 特有的功能,而是一个长期存在的 bug。
我们来看看这个偏方是如何使用的。
let str = | |
"Hello,\ | |
LanQiao"; |
可以看到报错提示消失了,加上 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> |
在控制台可以看到如下显示:
可以看到,它是能够识别多行字符串的,但是在 ECMAScript 6
的前面为何有那么空白?
这是因为模板字面量有个特点,定义在反撇号中的字符串,其中的空格、缩紧、换行都会被保留。
我们写在 index.html
文件中的代码是包含空格的。
我们只要删去第二行字符串中前面的两个空格就对齐了。
# 字符串占位符
在 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); |
在控制台可以看到如下效果:
在上面代码中,占位符 ${str}
会访问变量 str
的字符串,并将其值插入到 message
字符串中,变量 message
会一直保留着这个结果。
我们刚刚说里面可以包含任意 JavaScript 的表达式,那我们来验证一下,写入一个加法式子,看看能不能正确输出。代码如下所示:
let a = 2; | |
let b = 1; | |
let sum = `a+b=${a + b}`; | |
console.log(sum); |
在控制台可以看到如下效果:
练习要求:
- 新建一个
index3.html
文件。 - 定义一个
dog
对象,其中包含狗狗的名字、品种、年龄。 - 使用模板字符串,输出柯基宝宝想对大家说的话。
练习效果如下:
<!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")); |
在控制台你可以看到如下效果:
- 在上面代码中,使用
str.includes("Java")
来判断str
字符串中是否包含指定字符串Java
,不包含,故结果为 false。 - 使用
str.startsWith("Lan")
来判断str
字符串的首部是否包含指定字符串Lan
,不包含,故结果为 true。 - 使用
str.endsWith("Course")
来判断str
字符串的尾部是否包含指定字符串Course
,str
尾部是Courses
,故结果为 false。
注意:传入的字符串需要注意大小写,大小写不同也会造成匹配失败的情况。
# 重复字符串
repeat(n) 方法用于返回一个重复 n
次原字符串的新字符串,其参数 n
为整数,如果设置 n
为小数,会自动转换为整数。
我们来举几个例子~
新建一个 index6.html
文件。
let str = "HELLO"; | |
console.log(str.repeat(4)); |
在控制台会看到如下显示:
从上图可以看到输出了由 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)); // "" |
从上面可以看到,当 n
为小数时,会自动忽略小数部分;当 n
为负数时,会报错;当 n
为 0 时,为空。
# 替换字符串
在 ES5 中有一个 replace()
方法可以替换指定字符串,不过它只能替换匹配到的第一个字符串,如果想匹配整个字符串中所有的指定字符串是很麻烦的。
在 ES6 中,为我们提供了 replaceAll() 方法来解决这个问题,它可以用来替换所有匹配的字符串。
其语法格式为:
string.replaceAll("待替换的字符", "替换后的新字符"); |
我们来举个例子~
新建一个 index7.html
文件。
let str = "HELLOWHELLOWHELLO"; | |
console.log(str.replaceAll("W", "_")); |
在控制台可以看到如下效果:
可以看到原字符串中的所有 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]); |
在控制台显示如下:
我们可以看到打印出数组的长度为 5, Array()
中的 5 没有作为数组的元素被输出,而是被当作了数组的长度。
假如 Array()
中不是一个正整数,而是一个字符串,看看会发生什么事呢~
let arr = new Array("5"); | |
console.log("数组长度:" + arr.length); | |
console.log("arr[0]:" + arr[0]); |
在控制台显示如下:
刚刚我们写入一个整数时,被看作是数组的长度,我们再来发挥一下,假如我们在 Array()
中写入两个整数看看会发生什么~
let arr = new Array(3, 4); | |
console.log("数组长度:" + arr.length); | |
console.log("arr[0]:" + arr[0]); | |
console.log("arr[1]:" + arr[1]); |
在控制台显示如下:
参数 3 和 4 被看成了数组中的数据。你在 Array()
中多写几个元素,可以看到它们都会被当成数组的元素。
那我们看看加入几个字符串呢?😏
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]); |
在控制台显示如下:
可以看到整数和字符类型均被当作了数组的元素。
用上面这样的方式传入值是存在一定风险的。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]); |
在控制台显示如下:
可以看到,当我们在 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]); |
在控制台显示如下:
再试着加入字符~
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]); |
在控制台显示如下:
可以看到规则是统一的,写在 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(待转换的对象); |
我们来看看它是怎么使用的~ 😎
新建一个 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 的数组元素。
我们把 index
、 arr
参数也传入看看效果,修改代码如下:
let arr = [1, 3, 4, 5]; | |
arr.find(function (value, index, arr) { | |
console.log(value > 2); | |
console.log(index); | |
console.log(arr); | |
}); |
在控制台可以看到如下结果:
在代码中,我们返回了每次遍历判断条件的结果、当前元素的下标值、原数组。
从控制台的结果显示可以看出,在遍历数组的过程中,我们遍历第一个数组元素为 1
,此时 value
的值不满足条件 value>2
,故返回的是 false
;而遍历其他三个元素都满足条件 value>2
,故返回的都是 true
。遍历完成后,返回了第一个符合条件的元素 3
。
对了,我们刚刚说如果数组中没有一个元素满足条件,就会返回 undefined,我们来验证一下,修改代码如下:
let arr = [1, 3, 4, 5]; | |
let result = arr.find(function (value) { | |
return value < 1; | |
}); | |
console.log(result); |
在控制台可以看到如下结果:
# findIndex () 方法
findIndex() 方法返回数组中第一个符合指定条件的元素的索引下标值,如果整个数组没有符合条件的元素,则返回 -1。
其语法格式为:
array.findIndex(callback(value, index, arr), thisArg); |
参数说明如下:
callback
是数组中每个元素都会执行的回调函数(必须参数)。value
是当前元素的值,它是一个必须参数。index
是数组元素的下标,它是一个必须参数。arr
是被 findIndex () 方法操作的数组,它是一个必须参数。thisArg
是执行回调时用作 this 的对象,它是一个可选参数。
注意:执行回调函数时,会自动传入 value、index、arr 这三个参数。
我们来举个例子吧!👻
通过 findIndex () 方法来看看小动物们所对应的下标是多少。
新建一个 index3.html
文件,在文件中写入以下内容:
let arr = ["小猫", "小狗", "兔子"]; | |
arr.findIndex(function (value, index, arr) { | |
console.log(value == "兔子"); | |
console.log(`${value}是数组中的第 ${index + 1} 位。`); | |
}); |
在控制台可以看到如下效果:
这样貌似看不出 findIndex () 方法的特性,我们返回一下回调函数的结果:
let arr = ["小猫", "小狗", "兔子"]; | |
let result = arr.findIndex(function (value, index, arr) { | |
return value == "兔子"; | |
}); | |
console.log(result); |
在控制台可以看到返回的是「兔子」的下标。
刚刚说匹配失败会返回 -1,同样来验证一下~ 😉 修改代码如下:
let arr = ["小猫", "小狗", "兔子"]; | |
let result = arr.findIndex(function (value, index, arr) { | |
return value == "老虎"; | |
}); | |
console.log(result); |
在控制台可以看到的确打印出 -1。
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); |
在控制台可以看到数组中的猫猫、狗狗、兔兔,已经被猪头替换了。
我们不想让猪头占满整个数组,把 start 和 end 参数加上,修改代码如下:
let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"]; | |
let result = arr.fill("🐷", 2, 5); | |
console.log(result); |
在控制台可以看到数组中下标为 2 的元素~下标为 4 的元素被替换成了猪头。
同学们可能要疑惑了我们明明指定的是下标 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); |
在控制台可以看到:
可以看到结果只输出了 Array Iterator {},并没有以键值对的形式输出值。
我们要输出 Array Iterator 对象里的值,可以用前面提到过的扩展运算符(...)来展开。修改代码如下:
let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"]; | |
let result = arr.entries(); | |
console.log(...result); |
再看看控制台可以发现数组中的值都以键值对的形式输出了。
keys () 方法只返回数组元素的键值也就是元素对应的索引,不会返回其值。
其语法格式为:
array.keys(); |
我们继续修改上面的代码:
let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"]; | |
let result = arr.keys(); | |
console.log(result); | |
console.log(...result); |
控制台显示如下:
values () 方法返回的是每个键对应的值。
其语法格式为:
array.values(); |
我们继续修改上面文件的代码:
let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"]; | |
let result = arr.values(); | |
console.log(result); | |
console.log(...result); |
在控制台可以看到如下显示:
这三个数组遍历的方法就讲完了,最后画一张图来总结一下三个方法的区别吧!
# 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
语句的时候,有没有感觉到一些局限性,我给大家说两条:
- 我们必须要设置一个计数器,比如上面代码中的
i
。 - 我们必须有个退出循环的条件,如上面代码那样使用
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
:是存放当前迭代对象值的变量,该变量能用const
、let
、var
关键字来声明。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 服务,实验效果如下:
- 在上面代码中,定义了一个名为
arr
的数组,其中有三个名字。 - 使用
for(let name of arr)
去遍历数组arr
的值,我们每一次迭代都把值放在临时变量name
中。 - 每迭代一次,就会执行
for...of
中的语句,所有页面上打印除了三个名字对应的欢迎语。
练习要求:
- 新建一个
index7.html
文件。 - 你需要创建一个包含几种水果名的数组。
- 使用
for..of
在控制台输出水果的名字。
参考效果:
var fruits = ["苹果", "葡萄", "芒果", "橘子"]; | |
for (let fruit of fruits) { | |
console.log(fruit); | |
} |
# 扩展运算符
扩展运算符(...)是 ES6 的新语法,它可以将可迭代对象的参数在语法层面上进行展开。
其语法格式为:
// 在数组中的使用 | |
let VariableName = [...value]; |
我们来举个例子~
新建一个 index8.html
文件,在文件中写入以下内容:
let animals = ["兔子🐰", "猫咪🐱"]; | |
let zoo = [...animals, "老虎🐯", "乌龟🐢", "鱼🐟"]; | |
console.log(zoo); |
在控制台可以看到如下输出:
在上面代码中,我们把 animals
数组中的值使用扩展运算符插入到了 zoo
数组中。
除了可以向新数组中插入值,我们还可以用来复制数组。修改代码如下:
let animals = ["老虎🐯", "乌龟🐢", "鱼🐟"]; | |
let newAnimals = [...animals]; | |
console.log(newAnimals); |
在控制台可以看到,现在 newAnimals
数组中的值和 animals
数组中的值是一样的。
综合上面的例子我们其实可以这么理解:使用扩展运算符可以起到将数组展开的作用。
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); |
控制台会输出:
给对象添加属性
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); |
控制台会输出:
# 实验总结
在本节实验中,给大家介绍了数组扩展里的四块内容:
- 两种创建数组的方法:
- 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("大家好!我是", ""); |
在控制台可以看到如下输出:
在上面代码中:
func
函数一共有两个形式参数,在func('大家好!我是')
我们只指定了第一个形式参数words
的值,所以name
使用的是默认值「闷墩儿」。- 在
func('大家好!我是','憨憨')
中我们指定了两个形式参数的值,所以name
没有使用默认值。 - 在
func('大家好!我是','')
中我们指定第二个形式参数为空字符串,被认为是没有赋值,所以也使用的是默认值。
看到了吧!如果我们想给形式参数设置默认值需要在函数中单独定义,还是有点麻烦的。
# 在函数中直接设置默认值
在 ES6 中我们可以直接在函数的形参里设置默认值。
我们来举个例子~ 👻
新建一个 index.html
文件,在文件中写入以下内容:
function func(words, name = "🍎") { | |
console.log(words, name); | |
} | |
func("请给我一个"); | |
func("请给我一个", "🍐"); | |
func("请给我一个", ""); |
在控制台可以看到如下输出:
在上面代码中:
func('请给我一个')
只传入了第一个参数,第二个参数没传入任何值,故第二个参数使用了默认值红苹果。func('请给我一个','🍐')
传入了二个参数,所以第二个参数没有使用默认值。func('请给我一个','')
第二个参数,虽然传入的是空字符串,空字符串也算是一个参数值,故同样不会使用默认值。
可以看到在函数中设置默认值,如果你传入的第二个参数值为空字符串,会被当作值传入,而不会使用默认值了。
函数参数默认值在实际应用过程中,有哪些注意事项呢?
参数变量是默认声明的,我们不能用 let 或者 const 再次声明。
举个例子~
function func(words, name = "🍎") { | |
let words = "我需要一个"; | |
console.log(words, name); | |
} | |
func("请给我一个", "🍐"); |
可以看到控制台报错了。
# 注意参数默认值的位置
设置默认值的参数,一般放在其他参数的 后面 ,也就是尾部。这是为什么呢?🤔
很容易能想到,如果你给第一个形参设置了默认值,调用函数传参时,无法省略该参数。
我们来举个例子~
function func(words = "你好", name) { | |
console.log(`${words}${name}`); | |
} | |
func("小蓝"); |
在控制台可以看到,我们在函数中传入的值 “小蓝”,被当作第一个参数的值了,而第二个参数没有传入值,所有输出的是 undefined。
除了这样直接在函数中传入一个值,我们还有更高端的玩法,请点击下一步 👇 一探究竟。
# 使用函数作为默认值
我们还可以使用自定义的函数作为形式参数的默认值。
举个栗子~ 🌰
新建一个 index1.html
文件,在文件中写入以下内容:
function parameter() { | |
return "🖤"; | |
} | |
function func(words, name = parameter()) { | |
console.log(words, name); | |
} | |
func("请给我一颗小"); | |
func("请给我一颗小", "💗"); |
在控制台可以看到如下输出:
在上面代码中:
- 我们定义了一个名为
parameter
的函数,在函数中返回了一颗黑色的爱心。 - 给
func
函数中的第二个形式参数设置默认参数为parameter
函数中的返回值。 - 使用
func('请给我一颗小')
,只设置了第一个形参的值,所以第二个参数使用的是默认值「小黑心」。 - 使用
func('请给我一颗小','💗')
,设置了两个形参的值,所以第二个参数没有使用默认值。
明白了吗?我们来做个练习吧!
练习要求:
- 新建一个
index2.html
文件。 - 定义一个名为
add
的函数,用来计算 1 ~ n 的连加(其中 n 为任意一个正整数)。 - 函数只有一个形式参数,设置默认值为 5。
参考效果:
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: "桃子", | |
}); |
在控制台可以看到如下输出:
在上面代码中:
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); |
在控制台可以到如下输出:
在上面代码中,我们给 func
函数传了 10 个参数,形式参数 a
和 b
各取一个值,多余的 8 个参数都由 rest
参数收了。
我们刚刚说 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); |
在控制台可以看到报错了:
# 箭头函数
箭头函数,顾名思义,就是用箭头(=>)来表示函数。箭头函数和普通函数都是用来定义函数的,但两者在语法构成上非常不同。我们来举个例子对比一下~
新建一个 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。
可以看出使用箭头函数代码变得更加简洁了。
看上面的代码我们可以总结出箭头函数的基本用法。
(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()); |
在控制台可以看到如下输出:
在上面代码中,我们定义了一个名为 DogName
的函数,其函数内部返回狗狗的名字 “闷墩儿”。我把 ES5 语法格式的代码放在下方帮助同学们理解。
let dogName = function () { | |
return "闷墩儿"; | |
}; | |
console.log(dogName()); |
# 带默认参数的箭头函数
箭头函数与普通函数一样也是可以直接给参数设置默认值的。
我们来举个例子~
新建一个 index7.html
文件,在文件中写入以下内容:
let func = (x, y = "🌈") => { | |
return `${x}${y}`; | |
}; | |
console.log(func("请给我一道")); | |
console.log(func("请给我一朵", "🌺")); |
在控制台可以看到如下输出:
在上面代码中,定义的 func
函数里有两个形参,第二个形式参数里设置默认值为彩虹,函数内部使用模板字面量的方式返回了两个形式参数的值。
✨Tips:这里给大家补充箭头函数的几个简写形式。
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 参数的箭头函数
我们之前学过的 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); |
在控制台可以看到如下输出:
在上面代码中,我们给 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
的属性num
和enginesActive
。 - 并在原型中存储了一个可供所有实例调用的方法
startEngines
。 - 然后使用 “new + 构造函数” 的方式创建实例对象
myclass
,并将myclass
的num
属性初始化为 1。 - 最后调用执行实例对象
myclass
的startEngines
方法。
在 ES6 中,为我们提供了 class
关键字来创建类。类是用来构建对象的蓝图。
什么是蓝图呢?🤔 蓝图就是一个物体的结构,你可以在这个结构的基础上定义不同的属性,比如一件相同款式的衣服,有不同的颜色,衣服相当于蓝图,颜色相当于属性。
我们来举个例子。假如小蓝和小白这两兄弟去买同样款式的衣服,小蓝想要一件黑色,L 号的衣服,小白想要一件蓝色,M 号的衣服。
若以代码的形式构建小蓝和小白的需求就是:
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); |
在控制台可以看到如下输出:
在上面代码中 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); |
在控制台可以看到如下输出:
在上面代码中,类表达式被命名为 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
类的方法(包括构造方法和普通方法)。
- 在上面代码中,使用
class Dog extends Animal {}
定义了一个Dog
类继承了Animal
类的属性和方法。 - 使用
let dog = new Dog('闷墩儿','一',5)
实例化Dog
对象。 - 使用
Dog
类的实例对象dog
去调用run
和stop
方法,程序会先在Dog
类的原型Dog.prototype
中查找两个方法是否存在,找不到的情况下会去其父类原型Animal.prototype
中查找,以此类推,层层向上最终找到并调用执行。
前面曾说过,class 定义的类,其内部运行机制还是构造函数与原型。我们用下图表示一下上面代码的原理。
# 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
在前面 Animal
的案例中, Dog
类从 Animal
类中继承了 run
和 stop
方法。
在现实中大家都知道,狗狗 🐶 的跑和其他动物的跑是有区别的,它尤其独特的表现形式。这种独特的表现形式在程序世界又该如何表现呢?
有同学会想:直接给 Dog
类也定义一个 run
方法是不是就可以解决问题了?
于是有了下面的修改:
class Dog extends Animal { | |
run() { | |
console.log(`${this.name}开始奔跑了。`); | |
} | |
} |
可以看到 Dog
类中的 run
方法完全代替了 Animal
类中的 run
方法,并没有实现扩展。
上面这种做法明显不行。难道就没有解决办法了吗?
当然有,而且上面的思路是正确的,不过在实现上还缺少重要的一步,那就是如何保留从父类中继承过来的 run
方法的步骤,并在此基础上做 Dog
类的差异化扩展。
✨ ES6 为我们提供了超级函数 super 我们的继承变得完整且具备可扩展性。
它的使用格式有两种:
- 使用 super.method (...) 来调用父方法。
- 使用 super (...) 调用父构造函数。
我们改写一下 Dog
类中的 run
方法。
class Dog extends Animal { | |
run() { | |
super.run(); | |
console.log(`${this.name}开始奔跑了。`); | |
} | |
} |
可以看到,使用 super.run()
后,我们的目的达到了, Animal
类中的 run()
方法被成功调用。
# 重写构造函数
在上面的例子中,同学们有没有发现,我们并没有在 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(); |
在控制台可以看到报错了。
报错信息的意思是继承类中的构造函数必须调用 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。
这下没问题了~ 👻
这里给大家说一说使用 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(); |
控制台输出:
在上面代码中,使用 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
类中定义了两个静态方法 method1
和 method2
,静态方法 method1
调用了 this.method2
,其中 this 指向的是 MyClass
类而不是 MyClass
的实例,这相当于 MyClass.method2
。
在控制台可以看到如下输出:
观察上面的代码可以发现,我们没有创建实例化对象,直接用「类名。方法名」就可以访问该方法,这就是静态方法的特点了。除了这个特点外,静态方法不能被其实例调用,我们来试试。
class MyClass { | |
static method1() { | |
this.method2(); | |
} | |
static method2() { | |
console.log("hello,welcome to there!"); | |
} | |
} | |
let myclass = new MyClass(); | |
myclass.method2(); |
可以看到使用 myclass
实例对象调用 method2
静态方法会报错,则说明静态方法只能通过 “类名。方法名” 调用。
# 静态属性
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
类本身是可以调用的。
上面代码解析如下:
Animal
类中声明了静态属性place
和静态方法compare
。并在run
方法中使用Animal.place
调用了其静态属性。Dog
继承Animal
类,但没有定义任何自己的属性和方法。- 实例化两个
Dog
类对象 “乐乐” 和 “闷墩儿”,并分别调用其run
方法。 - 分别使用
Dog.compare
和Dog.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); |
控制台报错了:
报错意思就是私有属性不能在类的外部被访问。
改写一下上面的代码:
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
私有属性。
我们在上面的代码中的 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("小蓝", "前端工程师"); |
在控制台可以看到输出了对应的类名。
在上面代码中,使用 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。
# 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。
我们实例化一下 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
。
# 实验总结
本节实验给大家介绍了类相关的扩展,这里我们来总结一下:
- 使用
class
关键字来声明类。 - 使用
extends
和super
关键字来实现类的继承。 - 使用
static
关键字来定义静态属性和静态方法,使用#
来定义私有属性和私有方法。 - 使用
new.target
来判断构造函数的调用方式。