目录
2.1 get(target, propKey, receiver)
2.2 set(target, propKey, value, receiver)
一. 新增关键字
1. var关键字的缺点
1.1 变量提升机制的问题
我们声明变量的唯一方式是使用 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。
1.2 变量重复声明的问题
我们知道使用 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 的值。
1.3 非块作用域的问题
使用 var 关键字定义的变量只有两种作用域,全局作用域和函数作用域,两者均不是块结构,会造成变量声明的提升。这可能出现下面这种问题:
function func() { for (var i = 0; i < 5; i++) {} document.write(i); // 5 } func();
运行上述代码后,你会发现页面上会显示 5。我们虽然是在 for 循环中定义的 i 变量,但由于变量被提升到 for 语句之上,所以退出循环后,变量 i 并没有被销毁,我们能够在循环外获取它的值。
2. let关键字
2.1 解决变量提升机制问题
ES6 为我们提供了 let 关键字,它解决了变量提升到作用域顶部的问题。因为它的作用域是块,而不是提升机制了。
let 关键字声明变量,其作用域是一个块,如果我们是在花括号 {} 里面声明变量,那么变量会陷入暂时性死区,也就是在声明之前,变量不可以被使用。
2.2 解决变量重复声明的问题
虽然 let 关键字声明的变量可以重新赋值,但是它与 var 关键字有所不同,let 关键字不能在同一作用域内重新声明,而 var 可以。
2.3 解决非块级作用域的问题
前面我们已经说过使用 var 关键字定义变量,只有两种作用域,函数作用域和全局作用域,这两种都是非块级作用域。而 let 关键字定义的变量是块级作用域,就避免了变量提升。我们来看个例子。
3. const关键字
在 ES6 中,为我们提供了另一个关键字 const 用于声明一个只读的常量。且一旦声明,常量的值就不能改变。如果你尝试反复赋值的话,则会引发错误
对于 const 关键字定义的变量值,不可改变在于两个方面:
3.1 值类型
值类型是指变量直接存储的数据,例如:
const num = 20;
这里 num 变量就是值类型,我们使用的是 const 关键字来定义 num,故赋予变量 num 的值 20 是不可改变的。
3.2 引用类型
引用类型是指变量存储数据的引用,而数据是放在数据堆中,比如,用 const 声明一个数组。
const arr = ["一", "二", "三"];
如果你尝试去修改数组,同样会报错。
但是,使用 const 关键字定义的引用类型还是可以通过数组下标去修改值 ⭐️。
因为变量 arr 保存的是数组的引用,并不是数组中的值,只要引用的地址不发生改变就不会保错。这就相当于一个房子,它拥有固定的位置,但住在房子里的人不一定固定。
二. 字符串的扩展
1. 模板字符串
使用反撇号 ` `,可以自动识别换行
2. 字符串占位符
在 ES5 中,如果要把变量和字符串放在一起输出,你可能会想到使用 + 号来拼接。
这样拼接的过程是很容易出错的。
在模板字面量中,你可以把合法的 JavaScript 表达式嵌入到占位符中并将其作为字符串的一部分输出。
在 JavaScript 中,占位符由 ${} 符号组成,在花括号的中间可以包含任意 JavaScript 表达式。
let str = `LanQiao Courses`; let message = `I like ${str}.`; console.log(message);
在上面代码中,占位符 ${str} 会访问变量 str 的字符串,并将其值插入到 message 字符串中,变量 message 会一直保留着这个结果。
3. 标签模板
这里的标签并不是在 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) {}
我们通过这种方式将 literals 和 values 中的值交织起来重新组合成字符串输出。
新建一个 index4.html 文件,在文件中写入以下内容:
function tag(literals, ...values) { let result = ""; // result 变量用来存放重组后的数组 // 根据 values 的数量来确定遍历的次数 for (let i = 0; i < values.length; i++) { result += literals[i]; result += values[i]; } // 合并最后一个 literals result += literals[literals.length - 1]; return result; } let name = `JavaScript`; let str = tag`Welcome to ${name} course.`; console.log(str);
在控制台你会看到如下输出:
在上面代码中定义了一个名为 tag 的标签,在其内部的执行逻辑如下:
- 首先定义了一个名为 result 的空字符串用来存储最终输出字符串的结果。
- 接下来执行了一个 for 循环,遍历了 values 的长度。
- 取出 literals 的首个元素,再取出 values 中的首个元素,然后交替继续取出每一个元素,直到字符串拼接完成。
标签模板实际应用在两个方面:
- 过滤 HTML 字符串,防止用户输入恶意内容。
- 多语言的转换。
4. 字符串新增方法
4.1 判断指定字符串是否存在
在 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"));
在控制台你可以看到如下效果:
注意:传入的字符串需要注意大小写,大小写不同也会造成匹配失败的情况。
4.2 重复字符串
repeat(n) 方法用于返回一个重复 n 次原字符串的新字符串,其参数 n 为整数,如果设置 n 为小数,会自动转换为整数。
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 时,为空。
4.3 替换字符串
在 ES5 中有一个 replace() 方法可以替换指定字符串,不过它只能替换匹配到的第一个字符串,如果想匹配整个字符串中所有的指定字符串是很麻烦的。
在 ES6 中,为我们提供了 replaceAll() 方法来解决这个问题,它可以用来替换所有匹配的字符串。
其语法格式为:
string.replaceAll("待替换的字符", "替换后的新字符");
举例:
let str = "HELLOWHELLOWHELLO"; console.log(str.replaceAll("W", "_"));
在控制台可以看到如下效果:
可以看到原字符串中的所有 W 都用 _ 代替了。
三. 数组的扩展
1. 创建数组的方法
1.1 Array.of( )
Array.of() 的语法格式如下:
Array.of(element 0, element 1, ..., element N)
返回具有 N 个元素的数组。
1.2 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. 数组实例的方法
2.1 find( )方法
find() 方法是用于从数组中寻找一个符合指定条件的值,该方法返回的是第一个符合条件的元素,如果没找到,则返回 undefined.
其语法格式为:
array.find(callback(value, index, arr), thisValue);
参数说明如下:
- callback 是数组中每个元素执行的回调函数。
- value 是当前元素的值,它是一个必须参数。
- index 是数组元素的下标,它是一个可选参数。
- arr 是被 find() 方法操作的数组,它是一个可选参数。
- thisValue 是执行回调时用作 this 的对象,它是一个可选参数。
示例:
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);
在控制台可以看到如下结果:
验证成功~ 😉
2.2 findIndex( )方法
findIndex() 方法返回数组中第一个符合指定条件的元素的索引下标值,如果整个数组没有符合条件的元素,则返回 -1。
其语法格式为:
array.findIndex(callback(value, index, arr), thisArg);
参数说明如下:
- callback 是数组中每个元素都会执行的回调函数。
- value 是当前元素的值,它是一个必须参数。
- index 是数组元素的下标,它是一个必须参数。
- arr 是被 findIndex() 方法操作的数组,它是一个必须参数。
- thisArg 是执行回调时用作 this 的对象,它是一个可选参数。
注意:执行回调函数时,会自动传入 value、index、arr 这三个参数。
2.3 fill( )方法
fill() 方法是用指定的值来填充原始数组的元素。
其使用格式为:
array.fill(value, start, end); start与end,start能取到头,end取不到
其参数说明如下:
- value 是用来填充数组的值,它是一个必须参数。
- start 是被填充数组的索引起始值,它是一个可选参数。
- end 是被填充数组的索引结束值,它是一个可选参数。
注意:如果不指定 start 和 end 参数,该方法会默认填充整个数组的值。
2.4 entries( )方法
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);
再看看控制台可以发现数组中的值都以键值对的形式输出了。
2.5 keys( )方法
keys() 方法只返回数组元素的键值也就是元素对应的索引,不会返回其值。
其语法格式为:
array.keys();
我们继续修改上面的代码:
let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"]; let result = arr.keys(); console.log(result); console.log(...result);
控制台显示如下:
2.6 values( )方法
values() 方法返回的是每个键对应的值。
其语法格式为:
array.values();
我们继续修改上面文件的代码:
let arr = ["🐱", "🐶", "🐰", "🐍", "🐦", "🐟"]; let result = arr.values(); console.log(result); console.log(...result);
在控制台可以看到如下显示:
这三个数组遍历的方法就讲完了,最后画一张图来总结一下三个方法的区别吧!
到这里数组的常用方法就介绍完了,同学们是否掌握了呢?如果没掌握还请回看哦!😂
2.7 for...of循环
在使用 for 语句的时候,会有一些局限性:
- 我们必须要设置一个计数器,比如上面代码中的 i。
- 我们必须有个退出循环的条件,如上面代码那样使用 length 属性获取数组的长度,当计数器大于等于数组长度时退出。
当然数组这样使用没问题,但是很多时候我们还会使用其他结构的数据,使用 for 语句就相当麻烦了。
为了解决 for 中的不足,ES6 提供了 for...of 循环。
for...of 就摆脱了计数器、退出条件等烦恼,它是通过迭代对象的值来循环的。它能迭代的数据结构很多,数组、字符串、列表等。但在本实验中我们重点放在数组的遍历上。
for...of 的语法格式如下所示:
for (variable of iterable) { }
参数说明如下:
- variable:是存放当前迭代对象值的变量,该变量能用 const、let、var 关键字来声明。
- iterable:是可迭代对象。
2.8 扩展运算符
扩展运算符(...)是 ES6 的新语法,它可以将可迭代对象的参数在语法层面上进行展开。
其语法格式为:
// 在数组中的使用 let VariableName = [...value];
使用扩展运算符可以起到将数组展开的作用。
扩展运算符号问世后,成为了程序员的宠儿,但在 ES2018 版本前的它有一个缺点就是只能用在数组和参数上。于是在 ES2018 中又将扩展运算符引入了对象。
在对象上,我们主要有以下三种操作:
- 可以使用扩展运算符将一个对象的全部属性插入到另一个对象中,来创建一个新的对象。
- 可以使用扩展运算符给对象添加属性。
- 可以使用扩展运算符合并两个新对象。
我们来看个例子~
插入另一个对象的全部属性来创建一个新的对象
let student = { name: "小白", age: 17, email: "1[email protected]" }; let NewObj = { ...student }; console.log(NewObj);
控制台会输出:
给对象添加属性
let student = { name: "小白", age: 17, email: "[email protected]" }; let NewObj = { ...student, id: 7 }; console.log(NewObj);
控制台会输出:
合并对象
let studentName = { name: "小白" }; let studentAge = { age: 17 }; let NewObj = { ...studentName, ...studentAge }; console.log(NewObj);
控制台会输出:
四. 函数的扩展
1. 在函数中直接设置默认值
在 ES6 中我们可以直接在函数的形参里设置默认值。
function func(words, name = "🍎") { console.log(words, name); } func("请给我一个"); func("请给我一个", "🍐"); func("请给我一个", "");
2. 使用函数作为默认值
我们还可以使用自定义的函数作为形式参数的默认值。
function parameter() { return "🖤"; } function func(words, name = parameter()) { console.log(words, name); } func("请给我一颗小"); func("请给我一颗小", "💗");
3. 结构参数
解构可以用在函数参数传递的过程中。
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 使用的是默认值。
4. 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 参数收了。
我们刚刚说 rest 参数只能作为函数的最后一个参数,如果把它放在中间,就会报错
5. 箭头函数
5.1 this的指向
this 是指向调用包含自身函数对应的对象。
在箭头函数中的 this 对象,就是定义该函数所在的作用域所指向的对象,而不是使用所在作用域指向的对象。
- 箭头函数的 this 指向是其上下文的 this,没有方法可以改变其指向。
- 普通函数的 this 指向调用它的那个对象。
五. 类的扩展
1. 类表达式
类和函数都有两种存在形式:
- 声明形式(例如:function、class 关键字声明)。
- 表达式形式(例如:const A = class{})。
1.1 匿名类表达式
// 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);
在控制台可以看到如下输出:
1.2 命名类表达式
let DogName = class MyClass { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }; console.log(typeof DogName); console.log(typeof MyClass);
在控制台可以看到如下输出:
2. 类的继承
2.1 使用格式
class child_class_name extends parent_class_name {}
class 定义的类,其内部运行机制还是构造函数与原型
2.2 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() 方法,最终在控制台输出了 “欢迎来到蓝桥云课!”。
3. super
它的使用格式有两种:
- 使用 super.method(...) 来调用父方法。
- 使用 super(...) 调用父构造函数。
3.1 重写构造函数
类中不写 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 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();
这下没问题了~ 👻
4. 静态方法
静态方法的好处是不需要实例化类,就可以直接通过类名去访问,这样不需要消耗资源反复创建对象。
ES6 为我们提供了 static 关键字来定义静态方法。
其使用格式为:
static methodName(){ }
举例
// ES6 语法 class DogType { constructor(name) { this.name = name; } // 对应 DogType.prototype.sayName sayName() { console.log(`大家好!我是一只小${this.name}。`); } // 对应 DogType.create static create(name) { return new DogType(name); } } let dog = DogType.create("柯基"); dog.sayName();
控制台输出:
在上面代码中,使用 static create 创建了静态方法 create,并返回实例化的 DogType,它相当于 ES5 代码中的 DogType.create = function(name){},两者实现的功能相同,区别在于 ES6 使用了 static 关键字来标识这是个静态方法。
有一点需要大家注意一下,如果静态方法中包含 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。
在控制台可以看到如下输出:
观察上面的代码可以发现,我们没有创建实例化对象,直接用「类名.方法名」就可以访问该方法,这就是静态方法的特点了。除了这个特点外,静态方法不能被其实例调用
5. 静态属性
static 关键字除了可以用来定义静态方法外,还可以用于定义静态属性。
静态属性是指类本身的属性(Class.propName),不是定义在实例对象上的属性。
静态属性具有全局唯一性,静态属性只有一个值,任何一次修改都是全局性的影响。
当我们类中需要这么一个具有全局性的属性时,我们可以使用静态属性。
最初定义静态属性写法如下:
class Dog {} Dog.dogName = "闷墩儿"; console.log(Dog.dogName); // 闷墩儿
在上面代码中,我们为 Dog 类定义了一个静态属性 dogName。
现在,我们可以使用 static 关键字 🤩 来定义静态属性。
静态属性的使用格式为:
static propName = propVaule;
6. 静态属性和方法的继承
另外静态方法和静态属性是可以被继承的。我们来举个例子。
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 调用了其父类的静态方法和属性。
7. 私有属性和方法
在某些情况下,如果外部程序可以随意修改类中的属性或调用其方法,将会导致严重的错误。基于这个问题,我们就在类中引入了私有化的概念,私有属性和方法能够降低它们与外界的耦合度,避免很多问题。
在面向对象编程中,关于属性和方法的访问有以下两种情况:
- 类的属性和方法,在其内部和外部均可进行访问,也称为公共属性和方法。
- 类的属性和方法,只能在类中访问,不能在类之外的其他地方访问,也称为私有属性和方法。
到目前为止给大家介绍的方法和属性都是类的外部可以访问的。
接下来我们说一说类的私有属性和方法的定义与使用。先来举个例子,我相信各位同学的家里都有电饭煲吧!电饭煲的操作很简单,插上电源,选择你要烹饪的类型(煮粥、煮饭、煲汤...),然后点击确定,电饭煲开始运行,你只需要等电饭煲的运行时间结束,便可以吃到美味的食物。👻
如果你把电饭煲拆开,你会发现其内部构造并不像我们使用时那么简单,里面一堆电路板和线。
作为一个电饭煲使用者来说,电饭煲的内部结构多么复杂并不需要我们去理解,只要我们可以正常使用就行了。
而内部接口就是这个意思了,电饭煲的内部结构(隐藏的细节)相当于是我们的私有属性和私有方法。
在 ES6 的类中使用 # 可以设置私有方法和私有属性。
其语法格式为:
// 私有属性 #propertiesName; // 私有方法 #methodName;
同学们可能有点疑惑,很多语言中的私有方法是 private 关键字,为什么 ES6 不使用这个关键字呢?🤔
因为 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 私有属性。
8. 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 用来输出对应实例对象的类名。
我们不用 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。
9. new.target的使用场景
当我们想写不能被实例化,必须在继承后才能使用的类时,我们可以用 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。
我们实例化一下 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。
六. 对象的扩展
1. 对象字面量
对象字面量就是使用 {} 去定义对象。
在 ES6 中,对象字面量有了许多增强的写法,这里会给大家介绍以下三种新写法:
- 属性的简洁表示法
- 方法的简洁表示法
- 属性名表达式
2. 属性的简介表示法
在 ES6 之前我们可能会像下面这样来定义:
const name = "闷墩儿"; const age = 2; const dog = { name: name, age: age };
在上面代码中,定义了一个名为 dog 的对象,其属性名与属性值的变量名相同,但在定义的对象还是要重复写两遍。
有了 ES6 之后,我们可以使用属性初始化的简写语法,消除这种属性名称与局部变量之间的重复书写。简洁表示法如下所示:
const dog = { name, age };
新建一个 index.html 文件,我们可以尝试在控制台输出上面代码中的对象,看看其结果是否相同。
在控制台我们可以看到输出都是:
3. 方法的简介表示法
在控制台我们可以看到输出都是:
const name = "闷墩儿"; const dog = { run: function () { return name + "在公园里奔跑!"; }, };
有了 ES6 之后,就可以不用冒号和 function 关键字了。我们可以用以下简洁表示法:
const name = "闷墩儿"; const dog = { run() { return `${name}在公园里奔跑!`; }, };
新建一个 index1.html 文件,我们可以尝试在控制台输出上面代码中的方法,看看其结果是否相同。
在控制台我们可以看到输出都是:
4. 属性名表达式
在 ES6 之前,我们只能使用标识符的方式来作为属性名,例如:
dog.name = "闷墩儿";
而在 ES6 之后,我们还可以使用表达式来作为属性名,例如:
dog["n" + "ame"] = "闷墩儿";
新建一个 index2.html 文件,在控制台输出 dog 对象,其结果为:
我们还可以将定义的模版字面量放入 [] 中,例如:
const key = `name`; const dog = { [key]: "闷墩儿", };
定义在 [] 中的属性说明该属性是可以被计算的动态属性,其内容是可以被计算的,最后会转换成字符串,这提高了代码的灵活性。
5. 对象的扩展运算符
在对象中引入扩展运算符后,我们可以用来遍历参数对象中的所有属性,并将其拷贝到新对象中。
let obj1 = { species: "柯基", name: "闷墩儿", age: 2 }; let obj2 = { ...obj1 }; console.log(obj2);
我们在控制台可以看到 obj2 也拥有 obj1 的属性和属性值。
还可以使用扩展运算符将两个对象合并到一个新对象中。举个例子:
let obj1 = { species: "柯基", name: "闷墩儿", age: 2 }; let obj2 = { food: "狗粮" }; let obj3 = { ...obj1, ...obj2 }; console.log(obj3);
在控制台可以看到 obj1、obj2 的属性和属性值都是拷贝到 obj3 里面了。
6. 对象的新增方法
6.1 Object.is
在 ES6 之前,如果我们要判断两个值是否相等,可以用 == 或者 ===,但是这两种判断方式都存在一些缺点。我们来看看下面的代码:
console.log(-0 == +0); // true console.log(-0 === +0); // true console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(7 == "7"); // true
从输出的结果,我们可以看出其中的问题所在:
- 在 JavaScript 引擎中,-0 和 +0 代表两个完全不同的实体,而使用 == 和 === 的判断结果却是相等的。
- == 和 === 对于 NaN 的判断都是 false。
- 使用 ==,判断整型 7 和字符串 7 的结果是 true。
基于上述这些缺点,在 ES6 中提出了同值相等的算法,就是使用 Object.is 来比较两个值是否相等。
console.log(Object.is(-0, +0)); // false console.log(Object.is(NaN, NaN)); // true console.log(Object.is(7 == "7")); // false
6.2 Object.assign
在 ES6 之前对象组合中我们往往会用到 mixin() 方法,其方法的作用就是一个对象接受另一个对象的属性和方法。
在 ES6 中引入了 Object.assign 来合并对象,该方法一个对象可以接受任意多个对象的属性和方法,我们来看个例子。
const obj1 = { name: "闷墩儿", food: "狗粮" }; const obj2 = { age: 2, hobby: "跑圈圈" }; const obj3 = { food: "鸡胸肉", color: "黑白黄" }; Object.assign(obj1, obj2, obj3); // 将 obj2 和 obj3 合并到 obj1 中 console.log(obj1);
在控制台可以看到以下输出结果:
同学们有没有发现,在 obj1 和 obj3 中有相同名称的属性名 food,从输出结果可以看到 obj1 中的 food 属性被覆盖了,这一点也需要同学们注意哦,被合并的对象中出现同名属性,后面的对象会覆盖前面的对象中的属性值。
还有一点需要大家注意一下哦~
这就是合并方式是一种浅拷贝,也就是说如果被合并对象中的属性发生变化,合并后的对象不会继承修改的属性,我们来看个例子:
let obj1 = { name: "闷墩儿", food: "狗粮" }; let obj2 = { age: 2, hobby: "跑圈圈" }; let obj3 = { color: "黑白黄" }; Object.assign(obj1, obj2, obj3); // 将 obj2 和 obj3 合并到 obj1 中 console.log(obj1); obj2.hobby = "游泳"; console.log(obj2); console.log(obj1);
控制台输出如下:
从上图我们可以看到,修改 obj2 的 hobby 属性后,obj1 没有继承。
七. Set和Map
1. set对象
Set 是 ES6 提供的一种新的数据结构,其结构与数组类似,但与数组不同的是 Set 里面不允许存放相同的元素,也就是说 Set 中的每个值都是独一无二的。
概念清楚了,我们来看看如何创建 Set 对象。
Set 对象有两种创建形式:
- 不带参数的 Set。
let s = new Set();
- 带参数的 Set。
let s = new Set(argument1, argument1,...);
接下来我们一一举例。
1.1 创建不带参数的 Set
新建一个 index.html 文件。
首先,创建一个空的 Set 对象。
let s = new Set();
然后,我们使用 add() 方法往 Set 对象里加入一些元素
s.add(1); s.add(2); s.add(2); s.add(3); s.add(3); console.log(s);
在控制台输出,你会发现,重复的元素只保留了一个。
其执行过程原理如下动图所示:
如果我们想知道 Set 中的元素个数,可以用 size 属性。在上面代码中添加:
console.log(s.size);
1.2 创建带参数的Set
我们再来看看,如何创建带参数的 Set 对象。
有同学做了大胆猜想,这还不简单,把元素写进去用逗号分隔开就可以了,我们来试一试。
新建一个 index1.html 文件,在文件中写入以下内容。
let colors = new Set("Green", "Red", "Orange"); console.log(colors);
控制台的输出如下所示:
哈哈 😄,这一次猜错了吧!Set 可不是这样创建元素的。在()里还需要一对 []。
我们修改一下上面的代码。
let colors = new Set(["Green", "Red", "Orange"]); console.log(colors);
再去控制台看一下,发现是我们想要的结果了。
同学们又会想了,我们是不是也可以像数组那样使用索引去访问元素呢?答案是不可以。
2. Set相关的方法
2.1 add( )
我们已经知道了使用 add() 方法可以往 Set 中添加元素。
2.2 delete( )
在 Set 中使用 delete() 方法来移除指定元素。其使用格式为:
Set.delete(element);
需要注意的是,delete() 里面的参数是要删除的元素,而不是其索引。
2.3 has( )
我们还可以使用 has() 方法来检查某个元素是否存在于 Set 中。
let dogs = new Set(["柯基", "博美", "秋田犬", "藏獒"]); dogs.delete("秋田犬"); console.log(dogs.has("柯基")); console.log(dogs.has("秋田犬"));
在控制台可以看到如下输出:
2.4 clear( )
若我们想删除 Set 中的所有数据,可以使用 clear() 方法。
let dogs = new Set(["柯基", "博美", "秋田犬", "藏獒"]); dogs.clear(); console.log(dogs);
在控制台可以看到如下输出:
可以看到 Set 已经空了。
2.5 Set的遍历
我们使用 forEach() 方法可以遍历 Set 中的元素。
其使用格式为:
Set.prototype.forEach(callback[,thisArg])
参数说明如下:
- callback 是 Set 中每个元素要执行的回调函数。
- thisArg 是回调函数执行过程中的 this 值。
3. WeakSet
Set 实例和变量在存储数据方面的内存分配和垃圾回收机制是一样的。这么说未免有些抽象不太好理解,接下来我们结合例子去看。
如果 Set 实例中的引用一直存在,垃圾回收就不能释放该对象的存储空间,即使你并没有用到它。例如:
let s = new Set(); let obj1 = {}; let obj2 = {}; s.add(obj1); s.add(obj2); console.log(s.size); // 2 obj1 = null; console.log(s.size); // 2
上面代码中,先声明了一个空的 Set 对象 s,然后调用 add 方法向 s 中添加两个空对象元素,控制台打印 s 的元素个数为 2,证明 Set 对象中给空对象也分配了内存;接着把对象 obj1 设为 null,再次打印 s 的元素个数,仍然为 2,证明 Set 实例 s 中元素 1 占用的内存并没有被释放掉。
小 tips:同学们知道什么垃圾回收吗?这里简单的说一下。在 JavaScript 中,当你创建了一个值,分配给你相应的内存空间,值不需要了,就释放掉分配的空间,这就是垃圾回收了。
针对这个缺陷,ES6 又给我们提供了另一种 Set,叫做 WeakSet。
WeakSet 也叫做弱引用 Set,如果将其存储的对象设为了 null,相当于是删除了该对象,当垃圾回收机运行时,会释放掉被删除对象占用的空间。
我们来看看 WeakSet 是怎样使用的。
新建一个 index4.html 文件,在文件中写入以下内容:
let s = new WeakSet(); let obj = { msg: "同学们好!" }; s.add(obj); console.log(s.has(obj)); s.delete(obj); console.log(s.has(obj));
在控制台显示如下:
如果你打印出定义的实例对象,会发现输出为 undefined。
我们针对前面举的 ”Set 中 null 元素占用的内存无法被释放“的小例子做如下修改。
let s = new WeakSet(); let obj1 = {}; let obj2 = {}; s.add(obj1); s.add(obj2); console.log(s.size); obj1 = null; console.log(s.size);
此时的输出都为 undefined 的了。
下面给大家说一说,Set 与 WeakSet 的区别:
- WeakSet 的成员只能是对象且都是弱引用。在 WeakSet 中,add() 方法中不能传入非对象参数,若传入会报错。
- 在 WeakSet 中,给 has() 和 delete() 方法传入非对象参数,虽然不会报错,但是会返回 false。
- WeakSet 对象没有 size 属性,不能被遍历。
由于 WeakSet 里面存储的都是弱引用,内部有多少个成员,取决于垃圾回收机制有没有运行。运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。
4. Map对象
对象,我们不陌生吧!在 ES6 之前,对象是创建键值对数据结构的主要方式,但对象在使用上有一些局限性。
- 在对象中的键名只能是字符串、数字或者 Symbol。
- 对象不可以直接使用 forEach 方法遍历。
而 Map 的出现就解决了上述问题。
Map 是 ES6 中一种存储许多键值对的有序列表,其键值对可以是任意数据类型。Map 是有序的,它会按照键值插入的顺序来排列。
我们先来看看如何来创建 Map 对象。
其语法格式为:
let map = new Map([iterable]);
Map 对象可以接收一个由键值对组成的可叠对象。
我们来举个例子~ 👻
创建一个 Map 类型的变量 bookstore。
let bookstore = new Map();
4.1 set方法添加数据
使用 set() 方法可以向对象中添加数据。其使用格式为:
map.set(key:value);
增加以下代码:
bookstore.set([1, 2, 3], "书籍"); bookstore.set(false, "日用品"); bookstore.set(3, "化妆品"); console.log(bookstore);
- 在 bookstore.set([1,2,3],"书籍") 中创建了数组类型的键值 [1,2,3],其值为普通字符串“书籍”。
- 在 bookstore.set(false,"日用品") 中创建了布尔类型的键值 false,其值为普通字符串“日用品”。
- 在 bookstore.set(3,"test"); 中创建了整数类型的键值 3,其值为普通的字符串“化妆品”。
在控制台可以看到 Map 对布尔类型和数组类型作为键值也是支持的。
4.2 get方法从集合中获取数据
我们要获取集合中的数据,使用 get() 方法即可。
其使用格式为:
map.get(key);
我们来获取一下 bookstore 集合中的数据吧~
console.log(bookstore.get(false));
在控制台可以输出 false 键值映射的数据。
4.3 其它常用方法
除了上方提到的 set() 和 get() 方法,在 Map 中,还有下面三种方法比较常用。
- has() 用来判断指定键名对应的数据是否存在于当前集合中。
- delete() 用来删除指定键名的数据。
- clear() 用来清空集合中的数据。
4.4 Map的遍历
与对象不同,Map 可以使用 forEach() 方法来遍历数据值。
在讲 forEach() 方法之前,先给大家说一说如何在创建 Map 时就为其赋初始值。在前面的例子中,我们都是创建了一个空的 Map,然后使用 set() 方法往里面添加的值。现在我们来创建并初始化一个带数据的 Map。
语法格式如下:
let map = new Map([[key1,value1],[key2,value2]...]);
我们来举个例子~
let userName = new Map([ [1, "小红"], [2, "小蓝"], [3, "小白"], ]); console.log(userName);
在控制台可以看到创建成功了。
接下来我们遍历 userName 中的值。
此外,Map 还有一个 forEach() 方法,与数组的 forEach() 方法类似,可以实现对 Map 实例的遍历。
map.forEach(callback(value, key, ownerMap));
callback 是一个回调函数,其函数包含三个参数:
- value:本次循环所读取的元素的数据。
- key:本次循环所读取的元素的键名。
- ownerMap:Map 集合本身。
我们将上面例子中的 userName 使用 forEach 遍历如下:
let userName = new Map([ [1, "小红"], [2, "小蓝"], [3, "小白"], ]); userName.forEach(function (value, key, ownerMap) { console.log(`userName 中的值有: ${value}`); console.log(`userName 中的键有:${key}`); console.log(ownerMap); });
控制台显示如下:
八. 异步编程
1. 回调地狱
在日常开发中,往往会遇到这样的需求:通过接口 1 的返回值,去获取接口 2 的数据,然后,再通过接口 2 的返回值,获取接口 3 的数据。即每次请求接口数据时,都需要使用上一次的返回值。为了实现这个需求,通常会使用回调函数来完成,即把函数作为参数进行层层嵌套。
虽然可以通过回调函数层层嵌套的形式达到最终数据请求的目的,但代码结构不甚明朗,可读性极差,这就是传说中的回调地狱。
2. Promise对象
为了解决这种地狱式的回调,可以使用 Promise 对象,且代码更优雅,由于 Promise 对象是一个构造函数,因此,必须通过实例化来生成,它的定义格式如下代码:
let p = new Promise(function (resolve, reject) { // 此处做一个异步的事情 });
在定义格式的代码中,需要说明的几个问题:
- 在实例化中,参数为函数,函数中又有两个用于回调的函数。
- 两个回调函数中,resolve 为异步执行成功时的回调,其参数可以传递执行的结果。
- reject 为异步执行失败时的回调,其参数可以传递失败的错误信息。
- 使用 resolve 和 reject 方法传递出去的参数被谁接收到了,是以何种方式接收的?接下来说下 then 方法。
3. Promise对象的then方法
Promise 对象实例化后,可以调用 then 方法获取两个回调函数中的传参值,该方法接收两个回调函数作为参数,第一个参数是必选参数,表示异步成功后执行的 resolve 回调函数,第二个参数是可选参数,表示异步失败后执行的 reject 回调函数,它的调用格式如下:
p.then( function () {}, function () {} );
回调函数带参数的格式如下:
p.then( function (v) {}, function (e) {} );
其中参数 v 值表示 resolve 回调函数中的参数值,e 值表示 reject 回调函数中的参数值,如下列代码所示:
let n = 6; let p2 = new Promise(function (resolve, reject) { setTimeout(function () { if (n > 5) { resolve(n); } else { reject("必须大于5"); } }); }); p2.then( function (v) { console.log(v); }, function (e) { console.log(e); } ); // 执行代码后,由于 n 值大于 5 ,因此,在控制台中输出数字 6 。
此外,一个 then 方法被执行后,如果仍然返回一个 Promise 对象,则可以继续再执行 then 方法,形成链式写法效果,代码如下所示:
p1.then(function (v) { return p1; }).then(function (v) { return p1; });
4. Promise.all方法
日常开发过程中,往往会遇到这种问题,当首次加载某个页面时,由于数据庞大需要分别同时发送多个异步请求向服务器获取数据,最终所有数据返回之后做下一步操作(如“隐藏页面的加载 loading 动画”)。由于很难捕获这些异步请求全部成功的时机,导致这个需求实现起来相当困难。难道就没有解决办法了吗?🤔 这时使用 Promise.all 方法就可以解决这种问题。
4.1 使用格式
Promise.all 方法中的参数是一个数组,数组中的每个元素是实例化后的 Promise 对象,格式如下代码:
Promise.all([p1,p2,p3,...]).then(res=>{ // 所有请求成功后的操作步骤 },error=>{ // 某一个请求失败后的操作步骤 });
上述代码中,p1、p2、p3 都是实例化后的 Promise 对象,并且该方法可以通过链式写法直接调用 Promise.all 中的 then 方法,当全部的实例化对象都执行成功后,进入 then 方法的第一个执行成功的回调函数中,函数参数是每个任务执行成功后的结果,以数组形式保存,如下图所示:
如果在调用 Promise.all 方法时,有一个 Promise 实例对象(比如:p1)的任务执行失败了,则会直接进入 Promise.all 后的 then 方法的失败回调函数中,如下图所示:
通过 Promise.all 方法可以并列完成多个异步的请求,只有当全部请求成功后,才进入 then 方法中的成功回调函数中,否则,进入失败的回调函数中,因此,当首次加载页面时,可以将各种的异步请求放入 Promise.all 方法中,如果全部完成,则在 then 方法中的成功回调函数中执行下步操作,否则,直接进入失败回调函数中。
5. Promise.race方法
与 Promise.all 方法不同,Promise.race 方法是多个 Promise 实例化对象在比赛, 执行最快的那个任务的结果,将返回给 then 方法中的对应回调函数中,通过这种方式,可以检测页面中某个请求是否超时,并输出相关的提示信息。
5.1 使用格式
与 Promise.all 方法一样,Promise.race 中的参数也是一个数组,每个元素也是实例化后的 Promise 对象,格式如下代码:
Promise.race([p1,p2,p3,...]).then( function(v){ //获取最快任务成功时的返回值 }, function(){ //获取最快任务失败时的返回值 } )
6. Promise.then方法的缺点
Promise 对象虽然很优雅地解决了地狱回调的情形,使代码更简洁和易读,但通过 then 方法取值时,代码还是不够时尚和前沿,多层嵌套取值时也不够高效,如下列代码所示:
var p = function (msg) { return new Promise(function (resolve, reject) { setTimeout(function () { resolve(msg); }, 1000); }); }; p("明明") .then(function (v) { return p(v + ",男"); }) .then(function (v) { return p(v + ",今年18岁"); }) .then(function (v) { console.log(v); });
在上述代码中,then 方法在取值和传值时,如果层级多时,它的代码的结构并不易读,下面我们通过 async 和 await 来解决这个问题,一起来看下它们的用法~
7. async关键字
async 英文单词的意思是异步,虽然它是 ES7 中新增加的一个关键字,但它的本质是一种语法糖写法(语法糖是一种简化后的代码写化,它能方便程序员的代码开发),async 通常写在一个函数的前面,表示这是一个异步请求的函数,将返回一个 Promise 对象,并可以通过 then 方法取到函数中的返回值,下面通过一个简单示例来说明它的使用。
async function fn() { return "12345"; } fn().then((val) => { console.log(val); });
在上述代码中,定义一个名称为 fn 的函数,但由于在函数前添加了关键字 async ,使这个函数将返回一个 Promise 对象,因此,函数执行后,可以直接调用 then 方法;同时,fn 函数中的返回值,就是 then 方法中,执行成功回调函数时的参数值,因此,执行上述代码后,将在页面的控制台输出 “12345” 字符,效果如下所示:
通过上述示例,我们明确以下两点:
- 使用 async 关键字定义的函数,将会返回一个 Promise 对象。
- 函数中有返回值,则相当于执行了 Promise.resolve(返回值) 函数,没有返回值,则相当于执行了 Promise.resolve() 函数。
虽然 async 关键字简化了我们之前实现异步请求中返回 Promise 实例对象的那一步,直接返回了一个 Promise 对象,但是仍然需要在 then 方法中处理异步获取到的数据。有没有什么办法可以继续优化呢?比如省去 then 方法的调用,让异步操作写起来更像同步操作那么简洁明了?答案就是—— await ,接下来我们来介绍下它的用法。
8. await关键词
await 可以理解为 async wait 的简写,表示等待异步执行完成,await 必须在 async 定义的函数中,不能单独使用,await 后可以返回任意的表达式,如果是正常内容,则直接执行,如果是异步请求,必须等待请求完成后,才会执行下面的代码,来看下列代码。
// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求 function p(v) { return new Promise(function (resolve) { setTimeout(function () { // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。 resolve(v); }, 2000); }); } // 一个用于正常输出内容的函数 function log() { console.log("2.正在操作"); } async function fn() { console.log("1.开始"); await log(); let p1 = await p("3.异步请求"); console.log(p1); console.log("4.结束"); } fn();
执行上述代码后,页面在控制台输出的效果如下所示:
根据页面效果,源代码解析如下:
- fn 函数执行后,首先,会按照代码执行流程,先输出“1.开始”。
- 其次,对于没有异步请求的内容,在 await 后面都将会正常输出,因此,再输出“2.正在操作”。
- 如果 await 后面是异步请求,那么,必须等待请求完成并获取结果后,才会向下执行。
- 根据上述分析,由于 方法 p 是一个异步请求,因此,必须等待它执行完成后,并将返回值赋给变量 p1,再执行向下代码。
- 所以,最后的执行顺序是,先输出 “3.异步请求”,再输出 "4.结束",在 async 函数中的执行顺序,如下图所示。
9. 多层嵌套传参数的优化
基于 await 的特性,可以将异步请求的代码变成同步请求时的书写格式,代码会更加优雅,特别是处理多层需要嵌套传参时,使用 await 的方式,代码会更少,更易于阅读,如下列需求。
需要发送三次异步请求,第一次请求,成功后获取返回 1,并将该值作为参数并加 2,发送第二次请求,成功后获取返回值,并将该值作为参数并加 3,发送第三次请求,成功后输出全部的返回值,如果三次请求都成功了,则在控制台输出 “登录成功!”的字样。
// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求 function p(v) { return new Promise(function (resolve) { setTimeout(function () { // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。 resolve(v); }, 2000); }); } async function fn() { let p1 = await p("1"); let p2 = await p(p1 + "2"); let p3 = await p(p2 + "3"); console.log(p3); console.log("登录成功!"); } fn();
上述代码执行后的效果如下图所示:
从上述页面效果可以看出,在 fn 函数中,第一次发送请求时,返回值为 “1”,并保存在变量 p1 中,然后,将变量 p1 作为参数,并加 “2” 发送第二次请求,返回值为“12”,并保存在变量 p2 中,然后,将变量 p2 值作为参数,并加 ”3“ 发送第三次请求,返回值为 ”123“,并保存在变量 p3 中,最后,在控制台输出的内容是 p3 的值,即字符 “123”,同时,输出 “登录成功!”的字样。
10. 多个并列异步请求的调优
await 在处理多个异步请求时,如果请求之间有嵌套关系,可以一次一次按顺序发送请求,但是如果各个请求之间无任何关联,则可以将这些请求借助 Promise.all 一次性并列发送,使用 await 关键字获取请求的结果,并根据返回的结果进行下一步操作。如下列需求。
页面首次加载时,将会发送三次无任何关联的异步请求,当这三次请求成功后,在控制台输出“隐藏加载动画!”字样。
// 函数 p 返回的是一个 Promise 对象,在对象中,延时 2 秒,执行成功回调函数,相当于模拟一次异步请求 function p(v) { return new Promise(function (resolve) { setTimeout(function () { // 在 p 函数执行时,将函数的实参值 v ,作为执行成功回调函数的返回值。 resolve(v); }, 2000); }); } async function fn() { await Promise.all([p("a"), p("b"), p("c")]); console.log("隐藏加载动画!"); } fn();
述代码执行后的效果如下图所示:
在上述实现的代码中,方法 Promise.all 中每个实例化的 Promise 对象,都会以并行的方式发送异步请求,当所有请求都成功后,才会去执行输出字符内容的代码,许多初次学习 async 的同学们,可能会将 fn 函数的内容修改成如下代码所示:
async function fn() { await p("a"); await p("b"); await p("c"); console.log("隐藏加载动画!"); } fn2();
需要说明的是,无论是函数修改之前还是修改之后 ,都使用了 async 和 await,并且两次的执行结果都是一样的,但在 fn 函数修改之前,所有的异步请求都是并行发送的,而在函数修改之后,所有的异步请求都是按顺序执行的。
从性能上来看,fn 函数修改之前的异步请求并发执行明显高于修改之后的阻塞式异步请求,因此,虽然,我们学习了 async 和 await ,但也不能完全取代 Promise 对象,需要结合实际需求场景去使用。
九. 模块化
1. export
模块化开发项目是目前的主流趋势,它是将复杂的功能分解成各自独立的子模块,所有子模块按照一种方式进行组合,最终完成复杂功能的过程,它的优势是各模块是独立工作的,更利于复用和维护,同时更有利于节略资源,提高效率。
基于模块化开发的趋势,在前端项目开发时,各个功能都是独立开发的,如登录模块,注册模块和公用模块等,它们都是一个个单独的文件,如果登录模块想要访问公用模块中的某个方法,则需要公用模块开放这个方法,并制定访问的标准,而这时就需要使用 ES6 中新添加的关键字 export 了,功能如下图所示:
关键字 export 可以将一个模块中的方法、变量和其他功能从模块中输出,允许其他需要的模块按指定的标准进行访问,没有使用关键字 export 输出的模块内容,是封闭的,其它模块无法访问到它,下面介绍关键字 export 几种输出的方式 。
- 可以直接输出一个模块文件中的变量,如下代码:
export let name = "小蓝同学"; export let age = 18; let work = "一直在写代码呢!";
在上述代码中,由于变量 name 和 age 之前都使用了输出关键字 export ,因此,它们都可以被其他模块访问,由于变量 work 之前没有添加关键字 export ,所以,其他的模块无法访问到这个变量。
上述代码的这种写法,还可以合并成一个对象,并使用关键字 export 一次性输出,修改后的代码如下:
let name = "小蓝同学"; let age = 18; let work = "一直在写代码呢!"; export { name, age };
修改后的这种方法更加有利于一次性地批量输出多项内容,经常在开发中使用。
- 关键字 export 除能输出模块中的变量外,还可以输出模块中的方法,两者的输出格式都是相同的,如下代码:
function say() { console.log("我的名字叫小蓝"); } function act() { console.log("我的工作是写代码"); } export function log() { console.log("说点什么。。。"); } export { say, act };
关键字 export 在输出方法时,不要去添加执行方法的括号,只要输出方法的名称就可以,否则就会报错;此外,在输出内容的过程中,还可以使用关键字 as 来修改输出后的名称,修改后的代码如下:
export { say(), //报错 act as userWork }
在上述代码中,由于在输出 say 方法时,添加了括号,表示执行该方法,因此,代码报错;同时,在输出 act 方法时,添加了关键字 as ,它的作用是给输出的功能取一个别名,因此,其他模块在使用输出的 act 方法时,只能访问它的别名 userWork 。
关键字 export 是输出模块中的变量和方法,那用什么来接收这些输出的内容呢?这时就需要使用另一个关键字 import 。
2. import
与关键字 export 相对应,import 的功能是输入已经使用关键字 export 输出的内容,它们是对应关系, export 负责输出,而 import 则用于接受输出的内容,即负责输入,功能如下图所示:
关键字 import 在输入模块中加载输出模块的变量时,可以使用大括号包裹全部变量名,各个变量之间使用逗号分割,再通过 from 指定输出模块的路径,这个路径可以是绝对的,也可以是相对的,代码格式如下:
import { 变量1,变量2,变量3,...} from 输出模块位置
在上述格式代码中,大括号中的变量 1,变量 2 也可以通过关键字 as 取一个别名,格式如下:
import { 变量1 as a1,变量2 as fn,变量3,...} from 输出模块位置
取了别名之后,在输入模块中,只能使用这个别名,而不能再使用原先的名称,否则,将会出现变量未定义的错误信息。
3. 数据排序的模块开发
在 ES6 中,引入关键字 export 和 import 的最终目的是为项目的模块提供衔接上的支撑,从而为模块化开发项目提供保障,在模块化开发项目的过程中,开发人员可以将各个功能分解成各个子类模块,各个子类模块通过关键字 export 和 import 进行相互衔接,其效果如下图所示:
十. Proxy
1. 什么是Proxy
Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后通过对象的代理对象进行操作。也可以理解为在外界与对象之间建立了一道门,外界要访问该对象必须先打开这道门,如果想要获得打开该门的钥匙,就要遵守一个访问“条约”,允许对来访人员进行改造(提供一种机制:可以对外界的访问进行过滤和改写)。
用 Proxy 创建代理需要传入两个参数:目标对象(target)和处理程序(handler)。语法格式如下:
var proxy = new Proxy(target, handler);
参数说明如下:
- target:要拦截的目标对象。
- handler:制定拦截行为的对象。
2. Proxy的实例方法
2.1 get(target, propKey, receiver)
在 JavaScript 中,当我们去访问一个对象中不存在的属性时,不会报错,而是返回一个 undefined。如下例所示:
let dog = {}; console.log(dog.name);
这样的模式在大型的代码库中可能会导致严重的问题。ES6 中为我们提供了 get 方法,在访问对象之前检验一下是否存在你要访问的属性,该方法接受三个参数,具体说明如下:
- target:被读取属性的目标对象。
- propKey:要读取的属性键值。
- receiver:操作发生的对象。
示例
let dog = { name: "闷墩儿", }; var proxy = new Proxy(dog, { get(target, propKey) { // 遍历目标对象的属性键值 if (propKey in target) { return target[propKey]; // 返回相应的属性值 } else { throw new ReferenceError(propKey + " 属性不存在"); } }, }); console.log("访问 dog 对象中的 name 属性值为:" + proxy.name); console.log("访问不存在的 age 属性:" + proxy.age);
在控制台可以看见访问 age 属性就报错了。
2.2 set(target, propKey, value, receiver)
如果要创建一个只接受数字作为属性值的对象,那么在创建属性时,必须判断该值是否是数字,若不是数字应该报错。我们使用 set 方法就可以实现这个需求。
set 方法接受四个参数,具体说明如下:
- target:用于接收属性的目标对象。
- propKey:要写入的属性键值。
- value:要写入的属性值。
- receiver:操作发生的对象。
示例
let validator = { set(target, propKey, value) { if (propKey === "age") { // 判断 age 属性值是否时数字 if (!Number.isInteger(value)) { throw new TypeError("狗狗的年龄只能是整型哦!"); } } target[prop] = value; return true; }, }; let dog = new Proxy({}, validator); console.log((dog.age = "22"));
从控制台的输出结果可以看出,当 age 属性值设为字符串时,抛出错误。
2.3 has(target, propKey)
在 ES6 之前如果我们要判断某个属性是否在该对象中,可以使用 in 来判断。例如:
let dog = { name: "闷墩儿", }; console.log("name" in dog); console.log("valueOf" in dog);
在控制台你会看到两个都输出了 true。
这时候同学们可能有疑问了,明明上面的 dog 对象中只有 name 属性,为什么 valueOf 会被判为 true 呢?
这是因为 valueOf 是一个继承自 object 的原型属性。
而在 has 方法中可以拦截这些操作,返回不一样的值。
has 方法接收两个参数,具体说明如下:
- target:读取属性的目标对象。
- propKey:要检查的属性键值。
示例
let dog = { name: "闷墩儿", age: 2, }; let handler = { has(target, propKey) { if (propKey == "age" && target[propKey] < 5) { console.log(`${target.name}的年龄小于 5 岁哦!`); return true; } }, }; let proxy = new Proxy(dog, handler); console.log("age" in proxy);
在控制台可以看到以下输出:
2.4 ownKeys(target)
ownKeys 方法用于拦截对象自身属性的读取操作,具体可以拦截以下四种操作:
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.keys()
- for...in
示例
举一个拦截 for...in 的例子
let dog = { name: "闷墩儿", age: 2, food: "狗罐头", }; const proxy = new Proxy(dog, { ownKeys() { return ["name", "color"]; }, }); for (let key in proxy) { console.log(key); // 输出 name }
在控制台的输出如下:
从上图我们可以看到只输出了 name 属性,这是因为在 dog 对象中不包含 color 属性。