ES6是 2015 年发布的一个 JavaScript 版本,被命名为 ECMAScript 2015,也叫 ECMAScript 6,故简称为 ES6。
一、新增关键字
1、var 关键字的缺点
变量提升机制的问题
function getNumber(isNumber) {
if (isNumber) {
var num = "7";
} else {
var notNum = "not number!";
console.log(num);
}
}
getNumber(false);
在控制台可以看到报出“undefined”的错。
在 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。
变量重复声明的问题
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 并没有被销毁,我们能够在循环外获取它的值。
2、let 关键字
解决变量提升机制问题
ES6 为我们提供了 let 关键字,它解决了变量提升到作用域顶部的问题。因为它的作用域是块,而不是提升机制了。
function getNumber(isNumber) {
if (isNumber) {
let num = "7";
} else {
let notNum = "not number!";
console.log(num);
}
}
getNumber(false);
控制台会报出ReferenceError的错,ReferenceError 是一个引用类型的错误,num is not defined 意思是 num 变量并不存在。
因为 let 关键字声明变量,其作用域是一个块,如果我们是在花括号 {} 里面声明变量,那么变量会陷入暂时性死区,也就是在声明之前,变量不可以被使用。
在上面代码中,我们的 num 变量是放在 if(){} 这个块中,并没有在 else{} 块中,所以会形成暂时性死区。
解决变量重复声明的问题
虽然 let 关键字声明的变量可以重新赋值,但是它与 var 关键字有所不同,let 关键字不能在同一作用域内重新声明,而 var 可以。
let i = 5;
let i = 6;
console.log(i);//控制台报参数错误(SyntaxError)
var i = 5;
var i = 6;
console.log(i);//不会报错
解决非块级作用域的问题
let 关键字定义的变量是块级作用域,避免了变量提升。
function func() {
for (let i = 0; i < 5; i++) {}
console.log(i);
}
func();
控制台会报出ReferenceError的错。因为上面代码中的 i 变量只存在于 for 循环这个块中,当循环结束,i 变量就被销毁了,所以在 for 循环外访问不到。
3、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 保存的是数组的引用,并不是数组中的值,只要引用的地址不发生改变就不会保错。这就相当于一个房子,它拥有固定的位置,但住在房子里的人不一定固定。
回顾一下 const、let、var 三者之间的区别:
- var 语句的作用域是函数作用域或者全局作用域;它没有块作用域,故不存在暂时死区;它可分配,也可重复性声明。
- let 语句属于块作用域,受到暂时死区的约束;它可分配,但不可重新声明。
- const 语句也属于块作用域,同样受到暂时死区的约束;它既不可重新分配,也不可重新声明。
二、字符串的扩展
1、模板字面量
模板这个词很常见,指的是可以复用的东西。在 JavaScript 中,字面量代表由一些字符组成表达式定义的常量。
模板字面量的基础语法
在 ES6 中提供了反撇号去代替单引号和双引号。
let str = `Hello LanQiao!`;
console.log("输出字符串:" + str);
console.log("字符串类型:" + typeof str);
console.log("字符串长度:" + str.length);
当使用模板字符串创建了一个名为 str 的字符串,在控制台输出了字符串、字符串的类型、字符串的长度,可以发现,这和以前使用单引号或者双引号,创建字符串得到的结果是一模一样。
多行字符串的处理
let str = `Hello,
ECMAScript 6`;
console.log(str);
模板字面量有个特点,定义在反撇号中的字符串,其中的空格、缩紧、换行都会被保留。
字符串占位符
在 JavaScript 中,占位符由 ${} 符号组成,在花括号的中间可以包含任意 JavaScript 表达式。
let str = `LanQiao Courses`;
let message = `I like ${str}.`;
console.log(message);
let a = 2;
let b = 1;
let sum = `a+b=${a + b}`;
console.log(sum);
标签模板
这里的标签并不是在 HTML 中所说的标签,这里的标签相当于是一个函数。而标签模板就是执行模板字面量上的转换并返回最终的字符串。
let name = `JavaScript`;
let str = tag`Welcome to ${name} course.`;
在上面的代码中,用于模板字面量的模板标签就是 tag 。(注意:上面的 tag 并不是系统自带的,而是需要我们去定义的函数。)
这里的标签,我们可以理解为函数,该函数的参数说明如下:
- 第一个参数是一个数组,数组中存放普通的字符串,例如 str 中的 “Welcome to ”、“course.”。
- 在第一个参数之后的参数,都是每一个占位符的解释值,例如 str 中的 ${name}。
定义 tag 标签函数的格式:
function tag(literals, value1) {
// 返回一个字符串
}
当字符串中的占位符比较多时,也可以使用不定参数的形式来定义后面的参数。
function tag(literals, ...values) {}
例子:
function tag(literals, ...values) {
console.log(literals);
console.log(values);
let result = ""; // result 变量用来存放重组后的数组
// 根据 values 的数量来确定遍历的次数
for (let i = 0; i < values.length; i++) {
result += literals[i];
result += values[i];
console.log(literals[i]);
console.log(values[i]);
console.log(result);
}
// 合并最后一个 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 字符串,防止用户输入恶意内容。
- 多语言的转换。
2、字符串的新增方法
判断指定字符串是否存在
在 ES5 中,我们要判断某个字符串是否包含指定字符串时,可以用 indexOf() 方法来判断,该方法可以返回指定字符串在某个字符串中首次出现的位置。
在 ES6 中,为我们新增了三种方法来判断字符串是否包含在其中。
- includes():判断是否包含指定字符串,如果包含返回 true,反之 false。
- startsWith():判断当前字符串是否以指定的子字符串开头,如果是则返回 true,反之 false。
- endsWith():判断当前字符串是否以指定的子字符串结尾,如果是则返回 true,反之 false。
例子:
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"));
注意:传入的字符串需要注意大小写,大小写不同也会造成匹配失败的情况。
重复字符串
repeat(n) 方法用于返回一个重复 n 次原字符串的新字符串,其参数 n 为整数,如果设置 n 为小数,会自动转换为整数。
例子:
let str = "HELLO";
console.log(str.repeat(4));
替换字符串
在 ES5 中有一个 replace() 方法可以替换指定字符串,不过它只能替换匹配到的第一个字符串,如果想匹配整个字符串中所有的指定字符串是很麻烦的。
在 ES6 中,为我们提供了 replaceAll() 方法来解决这个问题,它可以用来替换所有匹配的字符串。
其语法格式为:
string.replaceAll("待替换的字符", "替换后的新字符");
三、数组的扩展
1、创建数组的方法
Array.of()
let arr = new Array(5);
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);
console.log("arr[4]:" + arr[4]);
从控制台显示可以看到打印出数组的长度为 5,Array() 中的 5 没有作为数组的元素被输出,而是被当作了数组的长度。
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 个元素的数组。
let arr = Array.of(7);
console.log("数组长度:" + arr.length);
console.log("arr[0]:" + arr[0]);
从控制台显示可以看出,当我们在 Array.of() 中只输入一个整数时,该参数不会被识别为数组的长度。
这样规则是统一的,写在 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(待转换的对象);
let arrLike = {
0: "🍎",
1: "🍐",
2: "🍊",
3: "🍇",
length: 4,
};
var arr = Array.from(arrLike);
console.log("arr:" + arr);
注意:Array.from() 方法是基于原来的对象创建的一个新数组。
2、数组实例的方法
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);
在控制台会打印出第一个大于 2 的数组元素。
let arr = [1, 3, 4, 5];
arr.find(function (value, index, arr) {
console.log(value > 2);
console.log(index);
console.log(arr);
});
在代码中,我们返回了每次遍历判断条件的结果、当前元素的下标值、原数组。
findIndex() 方法
findIndex() 方法返回数组中第一个符合指定条件的元素的索引下标值,如果整个数组没有符合条件的元素,则返回 -1。
其语法格式为:
array.findIndex(callback(value, index, arr), thisArg);
参数说明如下:
- callback 是数组中每个元素都会执行的回调函数。
- value 是当前元素的值,它是一个必须参数。
- index 是数组元素的下标,它是一个必须参数。
- arr 是被 findIndex() 方法操作的数组,它是一个必须参数。
- thisArg 是执行回调时用作 this 的对象,它是一个可选参数。
注意:执行回调函数时,会自动传入 value、index、arr 这三个参数。
let arr = ["小猫", "小狗", "兔子"];
let result = arr.findIndex(function (value, index, arr) {
return value == "兔子";
});
console.log(result);
find() 方法和 findIndex() 方法,其实用法很像,最大区别在于两个方面:
- 执行回调函数后的返回值不同,find() 方法返回的是元素本身,而 findIndex() 方法返回的是元素的下标。
- 匹配失败的返回值不同,find() 方法返回的是 undefined,而 findIndex() 方法返回的是 -1。
fill() 方法
fill() 方法是用指定的值来填充原始数组的元素。
其使用格式为:
array.fill(value, start, end);
其参数说明如下:
- value 是用来填充数组的值,它是一个必须参数。
- start 是被填充数组的索引起始值,它是一个可选参数。
- end 是被填充数组的索引结束值,它是一个可选参数。
注意:如果不指定 start 和 end 参数,该方法会默认填充整个数组的值。
let arr = ["🐱", "🐶", "🐰"];
let result = arr.fill("🐷");
console.log(result);
let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"];
let result = arr.fill("🐷", 2, 5);
console.log(result);
entries()、keys()、values()
entries()、keys()、values() 是 ES6 中三种数组的遍历方法,三个方法返回的都是 Array Iterator 对象。但三者之间不是完全相同。
entries() 方法以键/值对的形式返回数组的 [index,value],也就是索引和值。其语法格式为:
array.entries();
我们要输出 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);
3、for…of 循环
在使用 for 语句的时候,有一些局限性,如:
- 必须要设置一个计数器,比如上面代码中的 i。
- 必须有个退出循环的条件,如上面代码那样使用 length 属性获取数组的长度,当计数器大于等于数组长度时退出。
for…of 摆脱了计数器、退出条件等烦恼,它是通过迭代对象的值来循环的。它能迭代的数据结构很多,数组、字符串、列表等。
for…of 的语法格式如下所示:
for (variable of iterable) {
}
参数说明如下:
- variable:是存放当前迭代对象值的变量,该变量能用 const、let、var 关键字来声明。
- iterable:是可迭代对象。
const arr = ["小红", "小蓝", "小绿"];
for (let name of arr) {
document.write("欢迎" + name + "来到河南农业科技大学!" + "<br/>");
}
4、扩展运算符
扩展运算符(…)是 ES6 的新语法,它可以将可迭代对象的参数在语法层面上进行展开。
其语法格式为:
// 在数组中的使用
let VariableName = [...value];
例子:
let animals = ["兔子🐰", "猫咪🐱"];
let zoo = [...animals, "老虎🐯", "乌龟🐢", "鱼🐟"];
console.log(zoo);
let animals = ["老虎🐯", "乌龟🐢", "鱼🐟"];
let newAnimals = [...animals];
console.log(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);
四、函数的扩展
1、默认参数
回忆一下,ES5 中默认参数的使用。
function func(words, name) {
name = name || "🍎";
console.log(words, name);
}
func("大家好!我是");
func("大家好!我是", "🍐");
func("大家好!我是", "");
在上面代码中:
- func 函数一共有两个形式参数,在 func(‘大家好!我是’) 我们只指定了第一个形式参数 words 的值,所以 name 使用的是默认值「🍎」。
- 在 func(‘大家好!我是’,‘🍐’) 中我们指定了两个形式参数的值,所以 name 没有使用默认值。
- 在 func(‘大家好!我是’,‘’) 中我们指定第二个形式参数为空字符串,被认为是没有赋值,所以也使用的是默认值。
在函数中直接设置默认值
在 ES6 中我们可以直接在函数的形参里设置默认值。
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 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: "桃子",
});
在上面代码中:
- func 函数包含 4 个参数,其中第 4 个参数是解构参数,解构参数里面包含 4 个参数变量 a、b、c、d。
- 使用 func(‘小蓝’,5,3,{a:‘西瓜’,b:‘菠萝’,c:‘桃子’}) 调用该函数,其中传入 name 参数的值为“小蓝”,value 参数的值为 5,mount 参数的值为 3;解构参数只传入三个值,a 的值为“西瓜”,b 的值为“菠萝”,c 的值为“桃子”,d 使用的是默认值。
从上面代码中我们可以看出,解构参数是以键值对的形式传入的,其参数名与键值名保持一致,即可传入成功。
2、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);
在上面代码中,我们给 func 函数传了 10 个参数,形式参数 a 和 b 各取一个值,多余的 8 个参数都由 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);
因为 rest 参数只能作为函数的最后一个参数,若把它放在中间,在控制台则可以看到报错。
3、箭头函数
箭头函数,顾名思义,就是用箭头(=>)来表示函数。箭头函数和普通函数都是用来定义函数的,但两者在语法构成上非常不同。
箭头函数的基本用法:
(param1,param2,...,paramN) => {expression}
箭头前面 () 里放着函数的参数,箭头后面 {} 里放着函数内部执行的表达式。
普通函数:
let sum = function (a, b) {
return a + b;
};
console.log(sum(1, 2));
箭头函数:
let sum = (a, b) => a + b;
console.log(sum(1, 2));
箭头函数除了代码简洁以外,它还解决了匿名函数的 this 指向问题。
4、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 指向调用它的那个对象。
不带参数的箭头函数
let dogName = () => "😊";
console.log(dogName());
ES5 语法格式的代码:
let dogName = function () {
return "😊";
};
console.log(dogName());
带默认参数的箭头函数
箭头函数与普通函数一样也是可以直接给参数设置默认值的。
let func = (x, y = "🌈") => {
return `${x}${y}`;
};
console.log(func("请给我一道"));
console.log(func("请给我一朵", "🌺"));
在上面代码中,定义的 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);
在上面代码中,我们给 func 函数传入了 10 个参数,其中,1 赋给形式参数 a,2 赋给形式参数 b,剩余的参数都由 rest 参数收下了。
五、类的扩展
1、类的声明
类的介绍
在 ES6 之前其实是不存在类的,因为 JavaScript 并不是一门基于类的语言,它使用函数来创建对象,并通过原型将它们关联在一起。
先来看个 ES5 语法的近类结构,首先创建一个构造函数,然后定义另一个方法并赋值给构造函数的原型。
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 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 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{})。
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);
(1)匿名类表达式
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(){}。
(2)命名类表达式
在 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。
举上面的例子有两个目的,一是告诉我们可以在类表达式中命名类名;二是类名不能在类外使用。
2、类的继承
类的继承介绍
在 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(…) 调用父构造函数。
class Dog extends Animal {
run() {
super.run();
console.log(`${this.name}开始奔跑了。`);
}
}
重写构造函数
在上面的例子中,我们并没有在 Dog 类中创建 constructor。
前面说过,类中不写 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。
3、类的属性和方法
静态方法
在 JavaScript 中,静态方法是给面向对象编程提供的类方法,这种类方法有点类似于 Ruby 语言。
静态方法的好处是不需要实例化类,就可以直接通过类名去访问,这样不需要消耗资源反复创建对象。
在 ES6 之前,要模仿静态方法的常规操作是将方法直接添加到构造函数中。
ES5语法:
// 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语法:
// 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;
class Dog {
static dogName = "二狗;
}
console.log(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}。`);
- Animal 类中声明了静态属性 place 和静态方法 compare。并在 run 方法中使用 Animal.place
调用了其静态属性。 - Dog 继承 Animal 类,但没有定义任何自己的属性和方法。
- 实例化两个 Dog 类对象“乐乐”和“二狗”,并分别调用其 run 方法。
- 分别使用 Dog.compare 和 Dog.place 调用了其父类的静态方法和属性。
私有方法和私有属性
在某些情况下,如果外部程序可以随意修改类中的属性或调用其方法,将会导致严重的错误。基于这个问题,我们就在类中引入了私有化的概念,私有属性和方法能够降低它们与外界的耦合度,避免很多问题。
在面向对象编程中,关于属性和方法的访问有以下两种情况:
- 类的属性和方法,在其内部和外部均可进行访问,也称为公共属性和方法。
- 类的属性和方法,只能在类中访问,不能在类之外的其他地方访问,也称为私有属性和方法。
在 ES6 的类中使用 # 可以设置私有方法和私有属性。
其语法格式为:
// 私有属性
#propertiesName;
// 私有方法
#methodName;
因为 JavaScript 是一门动态语言,没有类型声明,使用独立的符号方便可靠,更容易区分。
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 私有属性。
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()。
4、new.target 属性
介绍
ES6 为我们提供了 new.target 属性去检查函数或者类的构造函数中是否使用 new 命令。
在构造函数中,若一个构造函数不是使用 new 来调用的,new.target 会返回 undefined。
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 用来输出对应实例对象的类名。
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();
不用 new 去实例化 Person 和 Occupation ,就会输出 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();
运行后可以看到控制台报错,提示不能去实例化 Person。
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 类,从控制台的输出结果可以看到,能成功访问 Occupation。