1. 简介
ECMAScript、JavaScript、NodeJs,它们的区别是什么?
- ECMAScript:简称ES,是一个语言标准(循环、判断、变量、数组等数据类型)
- JavaScript:运行在浏览器端的语言,该语言使用ES标准。 ES + web api = JavaScript
- NodeJs:运行在服务器端的语言,该语言使用ES标准。 ES + node api = NodeJs
- 无论JavaScript,还是NodeJs,它们都是ES的超集(super set)
ECMAScript有哪些关键的版本?
- ES3.0: 1999
- ES5.0: 2009
- ES6.0: 2015, 从该版本开始,不再使用数字作为编号,而使用年份
- ES7.0: 2016
为什么ES6如此重要?
- ES6解决JS无法开发大型应用的语言层面的问题
2. 块级绑定
2-1. 使用var
声明变量的问题
- 允许重复的变量声明:导致数据被覆盖
- 变量提升:怪异的数据访问、闭包问题
- 全局变量挂载到全局对象:全局对象成员污染问题
2-2. 使用let
声明变量
ES6不仅引入乐
let
关键字用于解决变量声明的问题,同时引入了块级作用域的概念
块级作用域:代码执行时遇到花括号,会创建一个块级作用域,花括号结束,销毁块级作用域
let
声明的变量不会挂载到全局对象let
声明的变量,不允许当前作用域范围内重复声明- 在块级作用域中用
let
定义的变量,在作用域外不能访问 - 使用
let
不会有变量提升,因此,不能在定义let
变量之前使用它
- 底层实现上,
let
声明的变量实际上也会有提升,但是,提升后会将其放入到“暂时性死区”,如果访问的变量位于暂时性死区,则会报错:“Cannot access ‘a’ before initialization”。当代码运行到该变量的声明语句时,会将其从暂时性死区中移除。- 在循环中,用
let
声明的循环变量,会特殊处理,每次进入循环体,都会开启一个新的作用域,并且将循环变量绑定到该作用域(每次循环,使用的是一个全新的循环变量)- 在循环中使用
let
声明的循环变量,在循环结束后会销毁
2-3. 使用const
声明变量
const
和let
完全相同,仅在于用const
声明的变量,必须在声明时赋值,而且不可以重新赋值。实际上,在开发中,应该尽量使用const来声明变量,以保证变量的值不会随意篡改,原因如下:
- 根据经验,开发中的很多变量,都是不会更改,也不应该更改的
- 后续的很多框架或者是第三方JS库,都要求数据不可变,使用常量可以一定程度上保证这一点
注意的细节:
- 常量不可变,是指声明的常量的内存空间不可变,并不保证内存空间中的地址指向的其他空间不可变。
- 常量的命名
- 特殊的常量:该常量从字面意义上,一定是不可变的,比如圆周率、月地距地或其他一些绝不可能变化的配置。通常,该常量的名称全部使用大写,多个单词之间用下划线分割
- 普通的常量:使用和之前一样的命名即可
- 在for循环中,循环变量不可以使用常量
3. 字符串和正则表达式
3-1. 更好的Unicode支持
- 早期,由于存储空间宝贵,Unicode使用16位二进制来存储文字。我们将一个16位的二进制编码叫做一个码元(Code Unit)
- 后来,由于技术的发展,Unicode对文字编码进行了扩展,将某些文字扩展到了32位(占用两个码元),并且,将某个文字对应的二进制数字叫做码点(Code Point)
- ES6为了解决这个困扰,为字符串提供了方法:codePointAt,根据字符串码元的位置得到其码点
- 同时,ES6为正则表达式添加了一个flag: u,如果添加了该配置,则匹配时,使用码点匹配
3-2. 更多的字符串API
以下均为字符串的实例(原型)方法
includes
判断字符串中是否包含指定的子字符串startsWith
判断字符串中是否以指定的字符串开始endsWith
判断字符串中是否以指定的字符串结尾repeat
将字符串重复指定的次数,然后返回一个新字符串。
3-3. [扩展]正则中的粘连标记
标记名:y
含义:匹配时,完全按照正则对象中的lastIndex位置开始匹配,并且匹配的位置必须在lastIndex位置。
3-4. 模板字符串
- 在ES6中,提供了模板字符串的书写,可以非常方便的换行和拼接,要做的,仅仅是将字符串的开始或结尾改为 ` 符号
- 如果要在字符串中拼接js表达式,只需要在模板字符串中使用
${JS表达式}
3-5. [扩展]模板字符串标记
在模板字符串书写之前,可以加上标记;
作用:可以检测变量,防止变量中存在恶意代码;
1 | 标记名`模板字符串` |
标记是一个函数,函数参数如下:
- 参数1:被插值分割的字符串数组
- 后续参数:所有的插值
4. 函数
4-1. 参数默认值
- 在书写形参时,直接给形参赋值,附的值即为默认值
- 这样一来,当调用函数时,如果没有给对应的参数赋值(给它的值是undefined),则会自动使用默认值,注意传入:null不会使用默认值
1 | fuction sum(a, b = 1, c = 1) { |
4-2. [扩展]对arguments
的影响
- 只要给函数加上参数默认值,该函数会自动变量严格模式下的规则
- 严格模式下
arguments
和形参是脱离的
1 | // "use strict" |
4-3. [扩展]留意暂时性死区
形参和ES6中的
let
或const
声明一样,具有作用域,并且根据参数的声明顺序,存在暂时性死区
1 | function test(a = b, b) { |
4-4. 剩余参数
ES6的剩余参数专门用于收集末尾的所有参数,将其放置到一个形参数组中。
1 | function (...形参名) { |
细节:
- 一个函数,仅能出现一个剩余参数
- 一个函数,如果有剩余参数,剩余参数必须是最后一个参数
arguments
的缺陷:
- 如果和形参配合使用,容易导致混乱。严格模式下
arguments
和形参是脱离的 - 从语义上,使用
arguments
获取参数,由于形参缺失,无法从函数定义上理解函数的真实意图
4-5. 展开运算符
使用方式:...要展开的东西
- 对数组展开 ES6
- 对对象展开 ES7
应用:curry
柯理化,用户固定某个函数的前面的参数,得到一个新的函数,新的函数调用时,接收剩余的参数
1 | function cal(a, b, c, d){ |
4-6. 明确函数的双重用途
ES6提供了一个特殊的API,可以使用该API在函数内部,判断该函数是否使用了new来调用
1 | new.target |
作用:检测方法是否通过 new
来调用
ES6以前:无法检测用过 call
重定向进行的调用
1 | function Person(firstName, lastName) { |
ES6新增API: new.target
1 | function Person(firstName, lastName) { |
4-7. 箭头函数
回顾:this
指向
- 通过对象调用函数,
this
指向对象 - 直接调用函数,
this
指向全局对象 - 如果通过new调用函数,
this
指向新创建的对象 - 如果通过
apply
、call
、bind
调用函数,this
指向指定的数据 - 如果是DOM事件函数,
this
指向事件源
语法说明:
箭头函数是一个函数表达式,理论上,任何使用函数表达式的场景都可以使用箭头函数
完整语法:
1 | (参数1, 参数2, ...)=>{ |
如果参数只有一个,可以省略小括号
1 | 参数 => { |
如果箭头函数只有一条返回语句,可以省略大括号,和return关键字
1 | 参数 => 返回值 |
注意细节:
- 箭头函数的自身没有this,箭头函数的this是在定义时确定的而不是调用者;
- 箭头函数中,不存在this、arguments、new.target,如果使用了,则使用的是函数外层的对应的this、arguments、new.target
1 | const obj = { |
- 箭头函数没有原型
- 箭头函数不能作用构造函数使用
应用场景:
- 临时性使用的函数,并不会刻意调用它,比如:
- 事件处理函数
- 异步处理函数:例如在
setTimeout
中使用避免this
指向window - 其他临时性的函数
- 为了绑定外层this的函数
- 在不影响其他代码的情况下,保持代码的简洁,最常见的,数组方法中的回调函数
1 | const numbers = [3, 7, 78, 3, 5, 345]; |
5. 对象
5-1. 新增的对象字面量语法
成员速写
如果对象字面量初始化时,成员的名称来自于一个变量,并且和变量的名称相同,则可以进行简写
1
2
3
4
5
6
7
8
9
10
11
12function createUser(loginId, loginPwd, nickName) {
const sayHello = function () {
console.log("loginId", this.loginId, "nickname", this.nickName)
}
return {
loginId,
loginPwd,
nickName,
sayHello,
id: Math.random()
}
}方法速写
对象字面初始化时,方法可以省略冒号和function关键字
1
2
3
4
5
6
7const user = {
name: "彼方",
age: 1000,
sayHello(){
console.log(this.name, this.age)
}
}计算属性名
有的时候,初始化对象时,某些属性名可能来自于某个表达式的值,在ES6,可以使用中括号来表示该属性名是通过计算得到的
1
2
3
4
5
6
7
8
9
10
11const prop1 = "name";
const prop2 = "age";
const prop3 = "sayHello";
const user = {
[prop1]: "彼方",
[prop2]: 1000,
[prop3](){
console.log(this[prop1], this[prop2])
}
}
5-2. Object的新增API
注意
Object
是一个构造函数
Object.is
用于判断两个数据是否相等,基本上跟严格相等(===)是一致的,除了以下两点
- NaN和NaN相等
- 2+0和-0不相等
1
2
3
4
5console.log(NaN === NaN); // false
console.log(+0 === -0); // true
console.log(Object.is(NaN, NaN)) // true
console.log(Object.is(+0, -0)) // falseObject.assign
用于混合对象
1
2
3
4//将obj2的数据,覆盖到obj1,并且会对obj1产生改动,然后返回obj1
const obj = Object.assign(obj1, obj2);
const obj = Object.assign({}, obj1, obj2);Object.getOwnPropertyNames
的枚举顺序Object.getOwnPropertyNames方法之前就存在,只不过,官方没有明确要求,对属性的顺序如何排序,如何排序,完全由浏览器厂商决定
ES6规定了该方法返回的数组的排序方式如下:
- 先排数字,并按照升序排序
- 再排其他,按照书写顺序排序
Object.setPrototypeOf
该函数用于设置某个对象的隐式原型
比如:
Object.setPrototypeOf(obj1, obj2)
相当于:obj1.__proto__ = obj2
5-3. 面向对象简介
面向对象:一种编程思想,跟具体的语言无关
对比面向过程:
- 面向过程:思考的切入点是功能的步骤
- 面向对象:思考的切入点是对象的划分
5-4. 类:构造函数的语法糖
传统的构造函数的问题:
- 属性和原型方法定义分离,降低了可读性
- 原型成员可以被枚举
- 默认情况下,构造函数仍然可以被当作普通函数使用
1 | //面向对象中,将 下面对一个对象的所有成员的定义,统称为类 |
类的特点:
- 类声明不会被提升,与 let 和 const 一样,存在暂时性死区
- 类中的所有代码均在严格模式下执行
- 类的所有方法都是不可枚举的
- 类的所有方法都无法被当作构造函数使用
- 类的构造器必须使用 new 来调用
1 | class Animal { |
5-5. 类的其他书写方式
可计算的成员名
使用方式:
[变量]
getter
和setter
ES5:通过
Object.defineProperty
定义某个对象成员属性的读取和设置使用getter和setter控制的属性,不在原型上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38const printName = "print";
class Animal {
constructor(type, name, age, sex) {
this.type = type;
this.name = name;
this.age = age;
this.sex = sex;
}
//创建一个age属性,并给它加上getter,读取该属性时,会运行该函数
get age() {
return this._age + "岁";
}
//创建一个age属性,并给它加上setter,给该属性赋值时,会运行该函数
set age(age) {
if (typeof age !== "number") {
throw new TypeError("age property must be a number");
}
if (age < 0) {
age = 0;
}
else if (age > 1000) {
age = 1000;
}
this._age = age;
}
[printName]() {
console.log(`【种类】:${this.type}`);
console.log(`【名字】:${this.name}`);
console.log(`【年龄】:${this.age}`);
console.log(`【性别】:${this.sex}`);
}
}
var a = new Animal("狗", "旺财", 3, "男");静态成员
- 构造函数本身的成员
- 使用static关键字定义的成员即静态成员
- 静态成员无法通过原型和实例获取;
- 静态成员通过构造函数直接获取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Chess {
constructor(name) {
this.name = name;
}
static width = 50; //ES7字段初始化器写法
static height = 50; //ES7字段初始化器写法
static method() {
}
}
console.log(Chess.width)
console.log(Chess.height)
Chess.method();字段初始化器(ES7)
- 使用static的字段初始化器,添加的是静态成员
- 没有使用static的字段初始化器,添加的成员位于对象上
- 箭头函数在字段初始化器位置上,指向当前对象
1
2
3
4
5
6
7
8
9
10
11
12
13class Test {
//字段初始化器相当于把a、c写到constructor中
static a = 1;
b = 2;
c = 3;
constructor() {
this.d = this.b + this.c;
}
}
const t = new Test();
console.log(t)类表达式
1
2
3
4
5
6
7const A = class { //匿名类,类表达式
a = 1;
b = 2;
}
const a = new A();
console.log(a)[扩展]装饰器(ES7)(Decorator)
- 横切关注点
- 例如类当中有些方法已经过时不建议使用,但又不能删除时进行提示作用
- 装饰器的本质是一个函数
使用案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class Test {
@Obsolete
print() {
console.log("print方法")
}
}
function Obsolete(target, methodName, descriptor) {
// console.log(target, methodName, descriptor);
// function Test
// print
// { value: function print(){}, ... }
const oldFunc = descriptor.value
descriptor.value = function (...args) {
console.warn(`${methodName}方法已过时`);
oldFunc.apply(this, args);
}
}
5-6. 类的继承
如果两个类A和B,如果可以描述为:B 是 A,则,A和B形成继承关系
如果B是A,则:
- B继承自A
- A派生B
- B是A的子类
- A是B的父类
如果A是B的父类,则B会自动拥有A中的所有实例成员
ES5实现:
1 | function Animal(type, name, age, sex) { |
继承的关键字:
- extends:继承,用于类的定义
- super
- 直接当作函数调用,表示父类构造函数
- 如 的原型
注意:
- ES6要求,如果定义了constructor,并且该类是子类,则必须在constructor的第一行手动调用父类的构造函数
- 如果子类不写constructor,则会有默认的构造器,该构造器需要的参数和父类一致,并且自动调用父类构造器
ES6实现:
1 | class Animal { |
【冷知识】:
- 用JS制作抽象类
- 抽象类:一般是父类,不能通过该类创建对象
- 正常情况下,this的指向,this始终指向具体的类的对象
6. 解构
6-1. 对象解构
什么是解构:
使用ES6的一种语法规则,将一个对象或数组的某个属性提取到某个变量中
在解构中使用默认值
{同名变量 = 默认值}
非同名属性解构
{属性名:变量名}
示例:
1 | const user = { |
6-2. 数组解构
普通数组解构:
1 | const numbers = ['a', 'b', 'c', 'd']; |
嵌套数组解构:
1 | const numbers = ["a", "b", "c", "d", [1, 2, 3, 4]]; |
嵌套对象解构:
1 | const numbers = ["a", "b", "c", "d", { |
6-3. 剩余项解构
对象:
1 | const user = { |
数组:
1 | const numbers = [324, 7, 23, 5, 3243]; |
6-4. 参数解构【常用】
参数解构时,如果参数为
undefined
或null
则会报错,通常使用参数默认值进行兼容处理
示例:
1 | function print({ name, age, sex, address: { |
应用场景:
1 | //使用参数默认值进行兼容 |
7. 符号
7-1. 普通符号
符号是ES6新增的一个数据类型,它通过使用函数
Symbol(符号描述)
来创建
符号描述:仅方便开发者理解用途方便调试使用,无任何实际作用
符号设计的初衷,是为了给对象设置私有属性
私有属性:只能在对象内部使用,外面无法使用
符号具有以下特点:
- 没有字面量
- 使用
typeof
得到的类型是symbol
- 每次调用
Symbol
函数得到的符号永远不相等,无论符号名是否相同 - 符号可以作为对象的属性名存在,这种属性称之为符号属性
- 开发者可以通过精心的设计,让这些属性无法通过常规方式被外界访问
- 符号属性是不能枚举的,因此在
for-in
循环中无法读取到符号属性,Object.keys
方法也无法读取到符号属性 Object.getOwnPropertyNames
尽管可以得到所有无法枚举的属性,但是仍然无法读取到符号属性- ES6 新增
Object.getOwnPropertySymbols
方法,可以读取符号,获取结果为symbol
数组
- 符号无法被隐式转换,因此不能被用于数学运算、字符串拼接或其他隐式转换的场景,但符号可以显式的转换为字符串,通过 String 构造函数进行转换即可,console.log 之所以可以输出符号,是它在内部进行了显式转换
使用示例:
1 | //创建一个符号 |
实现私有属性【对象】:
1 | const hero = (function () { |
实现私有属性【类】:
1 |
|
7-2. 共享符号
根据某个符号名称(符号描述)能够得到同一个符号
获取共享符号:Symbol.for("符号名/符号描述")
示例:
1 | //示例一 |
实现原理:
1 | const SymbolFor = (() => { |
7-3. 知名(公共、具名)符号
知名符号是一些具有特殊含义的共享符号,通过
Symbol
的静态属性得到
ES6 延续了 ES5 的思想:减少魔法,暴露内部实现!
因此,ES6 用知名符号暴露了某些场景的内部实现
Symbol.hasInstance
该符号用于定义构造函数的静态成员,它将影响
instanceof
的判定1
2
3
4obj instanceof A
//等效于
A[Symbol.hasInstance](obj) // Function.prototype[Symbol.hasInstance]示例
1
2
3
4
5
6
7
8
9
10
11
12
13function A() {}; //构造函数A
const obj = new A(); //创建实例 obj
console.log(obj instanceof A); //true
console.log(A[Symbol.hasInstance](obj)); //true [等效于上方]
//改写A的 Symbol.hasInstance 方法
Object.defineProperty(A, Symbol.hasInstance, {
value: function (obj) {
return false;
}
});
console.log(obj instanceof A); //false
console.log(A[Symbol.hasInstance](obj)); //false[扩展]
Symbol.isConcatSpreadable
该知名符号会影响数组的
concat
方法拼接数组:
1
2
3
4
5
6
7
8const arr = [3];
const arr2 = [5, 6, 7, 8];
//如果某个参数存在数组索引和length,那么采用分割合并,即:
const result = arr.concat(56,arr2); //[3,56,5,6,7,8]
//修改arr2 数组合并时的合并方式;默认true:拆分合并,false:直接合并
arr2[Symbol.isConcatSpreadable] = false;
const result = arr.concat(56,arr2); //[3, 56, [5,6,7,8]]拼接对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const arr = [1];
const obj1 = {
0: 3,
1: 4,
length: 2,
};
const obj2 = {
0: 3,
1: 4,
length: 2,
[Symbol.isConcatSpreadable]: true
};
const result1 = arr.concat(2, obj1); //[1, 2, {0:3,1:4,length:2}]
const result2 = arr.concat(2, obj2); //[1, 2, 3, 4][扩展]
Symbol.toPrimitive
该知名符号会影响类型转换的结果
示例一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14const obj = {
a:1,
b:2
};
//需要计算,所以obj需要得到一个基本类型
//首先调用obj.valueOf() 得到自身没变:{a:1,b:2}
//在调用obj.toString() 得到基本类型: [object Object]
console.log(obj + 123); //[object Object]123
//改写 Symbol.toPrimitive 方法
obj[Symbol.toPrimitive] = function() {
return 2;
}
console.log(obj * 123); //246示例二:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Temperature {
constructor(degree) {
this.degree = degree;
}
[Symbol.toPrimitive](type) {
if (type === "default") {
return this.degree + "摄氏度";
}
else if (type === "number") {
return this.degree;
}
else if (type === "string") {
return this.degree + "℃";
}
}
}
const t = new Temperature(30);
console.log(t + "!"); //30摄氏度!
console.log(t / 2); //15
console.log(String(t)); //30℃[扩展]
Symbol.toStringTag
该知名符号会影响
Object.prototype.toString
的返回值示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14class Person1 {
};
class Person2 {
[Symbol.toStringTag] = "Person"
};
const p1 = new Person1();
const p2 = new Person2();
const arr = [32424, 45654, 32];
console.log(Object.prototype.toString.apply(p1)); //[object Object]
console.log(Object.prototype.toString.apply(p2)); //[object Person]
console.log(Object.prototype.toString.apply(arr)); //[object Person]其他知名符号
8. 异步处理
8-1. Promise
Promise是一套专门处理异步场景的规范,它能有效的避免回调地狱的产生,使异步代码更加清晰、简洁、统一
8-1-1. Promise规范
这套规范最早诞生于前端社区,规范名称为Promise A+
- 所有的异步场景,都可以看作是一个异步任务,每个异步任务,在JS中应该表现为一个对象,该对象称之为Promise对象,也叫做任务对象
- 每个任务对象,都应该有两个阶段、三个状态
根据常理,它们之间存在以下逻辑:- 任务总是从未决阶段变到已决阶段,无法逆行
- 任务总是从挂起状态变到完成或失败状态,无法逆行
- 时间不能倒流,历史不可改写,任务一旦完成或失败,状态就固定下来,永远无法改变
挂起->完成
,称之为resolve
;挂起->失败
称之为reject
。任务完成时,可能有一个相关数据;任务失败时,可能有一个失败原因。- 可以针对任务进行后续处理,针对完成状态的后续处理称之为onFulfilled,针对失败的后续处理称之为onRejected
8-1-2. PromiseAPI
ES6提供了一套API,实现了Promise A+规范
基本使用如下:
1 | // 创建一个任务对象,该任务立即进入 pending 状态 |
应用示例:
1 | /** |
8-1-3. Promise 的链式调用
atch方法:
.catch(onRejected)
= .then(null, onRejected)
链式调用:
- then方法必定会返回一个新的Promise
可理解为后续处理也是一个任务
- 【重点】新任务的状态取决于后续处理:
- 若没有相关的后续处理,新任务的状态和前任务一致,数据为前任务的数据
- 若有后续处理但还未执行,新任务挂起。
- 若后续处理执行了,则根据后续处理的情况确定新任务的状态
- 后续处理执行无错,新任务的状态为完成,数据为后续处理的返回值
- 后续处理执行有错,新任务的状态为失败,数据为异常对象
- 后续执行后返回的是一个任务对象,新任务的状态和数据与该任务对象一致
注意:.then()
和 .catch()
方法接收的是一个方法函数;如果不是一个方法那么就相当于传入 null
,即没有注册回调函数,所以回调可以直接忽略,代码如下:
1 | Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log); |
8-1-4. Promise 的静态方法
方法名 | 含义 |
---|---|
Promise.resolve(data) | 直接返回一个完成状态的任务 |
Promise.reject(reason) | 直接返回一个拒绝状态的任务 |
Promise.all(任务数组) | 返回一个任务 任务数组全部成功则成功 任何一个失败则失败 |
Promise.any(任务数组) | 返回一个任务 任务数组任一成功则成功 任务全部失败则失败 |
Promise.allSettled(任务数组) | 返回一个任务 任务数组全部已决则成功 该任务不会失败 |
Promise.race(任务数组) | 返回一个任务 任务数组任一已决则已决,状态和其一致 |
8-1-5. async 和 await
消除回调:
有了Promise,异步任务就有了一种统一的处理方式
有了统一的处理方式,ES官方就可以对其进一步优化
ES7推出了两个关键字async
和await
,用于更加优雅的表达Promise
async:
async关键字用于修饰函数,被它修饰的函数,一定返回Promise
1 | async function method1(){ |
await:
await
关键字表示等待某个Promise完成,它必须用于async
函数中
1 | async function method(){ |
await
也可以等待其他数据
1 | async function method(){ |
如果需要针对失败的任务进行处理,可以使用try-catch
语法
1 | async function method(){ |
应用示例:
1 | //需要异步处理的数据 |
9. Fetch Api
9-1. Fetch Api 概述
XMLHttpRequest的问题:
- 所有的功能全部集中在同一个对象上,容易书写出混乱不易维护的代码
- 采用传统的事件驱动模式(回调,注册事件等),无法适配新的 Promise Api
Fetch Api 的特点:
- 并非取代 AJAX,而是对 AJAX 传统 API 的改进
AJAX 是一个概念,不刷新浏览器下,异步请求数据;和实现方法无关。
- 精细的功能分割:头部信息、请求信息、响应信息等均分布到不同的对象,更利于处理各种复杂的 AJAX 场景
- 使用 Promise Api,更利于异步代码的书写
- Fetch Api 并非 ES6 的内容,属于 HTML5 新增的 Web Api
- 需要掌握网络通信的知识
9-2. 基本使用
请求测试地址:
http://101.132.72.36:5100/api/local
使用 fetch
函数即可立即向服务器发送网络请求
9-2-1. 参数
- 必填,字符串,请求地址
- 选填,对象,请求配置
数据请求示例代码:
1 | const url = 'http://101.132.72.36:5100/api/local'; |
请求配置对象:
- method:字符串,请求方法,默认值GET
- headers:对象,请求头信息
- body: 请求体的内容,必须匹配请求头中的 Content-Type
- mode:字符串,请求模式
- cors:默认值,配置为该值,会在请求头中加入 origin 和 referer
- no-cors:配置为该值,不会在请求头中加入 origin 和 referer,跨域的时候可能会出现问题
- same-origin:指示请求必须在同一个域中发生,如果请求其他域,则会报错
- credentials: 如何携带凭据(cookie)
- omit:默认值,不携带cookie
- same-origin:请求同源地址时携带cookie
- include:请求任何地址都携带cookie
- cache:配置缓存模式
- default: 表示fetch请求之前将检查下http的缓存.
- no-store: 表示fetch请求将完全忽略http缓存的存在. 这意味着请求之前将不再检查下http的缓存, 拿到响应后, 它也不会更新http缓存.
- no-cache: 如果存在缓存, 那么fetch将发送一个条件查询request和一个正常的request, 拿到响应后, 它会更新http缓存.
- reload: 表示fetch请求之前将忽略http缓存的存在, 但是请求拿到响应后, 它将主动更新http缓存.
- force-cache: 表示fetch请求不顾一切的依赖缓存, 即使缓存过期了, 它依然从缓存中读取. 除非没有任何缓存, 那么它将发送一个正常的request.
- only-if-cached: 表示fetch请求不顾一切的依赖缓存, 即使缓存过期了, 它依然从缓存中读取. 如果没有缓存, 它将抛出网络错误(该设置只在mode为”same-origin”时有效).
9-2-2. 返回值
fetch
函数返回一个 Promise
对象
- 当收到服务器的返回结果后(无论请求是否成功,无论状态码是多少),
Promise
进入resolved
状态,状态数据为Response
对象 - 当网络发生错误(或其他导致无法完成交互的错误)时,
Promise
进入rejected
状态,状态数据为错误信息。
数据返回使用示例:
1 | async function getProvinces() { |
Response对象:
- ok:boolean,当响应消息码在200~299之间时为true,其他为false
- status:number,响应的状态码
- text():用于处理文本格式的 Ajax 响应。它从响应中获取文本流,将其读完,然后返回一个被解决为 string 对象的 Promise。
- blob():用于处理二进制文件格式(比如图片或者电子表格)的 Ajax 响应。它读取文件的原始数据,一旦读取完整个文件,就返回一个被解决为 blob 对象的 Promise。
- json():用于处理 JSON 格式的 Ajax 的响应。它将 JSON 数据流转换为一个被解决为 JavaScript 对象的promise。
- redirect():可以用于重定向到另一个 URL。它会创建一个新的 Promise,以解决来自重定向的 URL 的响应。
使用示例:
1 | async function getProvinces() { |
9-3. Request 对象
除了使用基本的fetch方法,还可以通过创建一个Request对象来完成请求(实际上,fetch的内部会帮你创建一个Request对象)
1 | new Request(url地址, 配置); |
具体应用:
1 | async function getProvinces() { |
注意点:
尽量保证每次请求都是一个新的Request对象。
如果获得了一个Request对象,但不清楚是否请求过,可以通过
Request
对象上的clone
方法获得一个完全相同的新对象;
1 | //req: Request 对象; |
9-4. Response 对象
fetch() 成功收到服务器的返回的状态数据;详情说明
1 | async function getProvinces() { |
9-5. Headers 对象
在Request和Response对象内部,会将传递的请求头对象,转换为Headers
Headers对象中的方法:
- has(key):检查请求头中是否存在指定的key值
- get(key): 得到请求头中对应的key值
- set(key, value):修改对应的键值对,不存在则新增
- append(key, value):添加对应的键值对,存在不会覆盖,获取值用逗号隔开
- keys(): 得到所有的请求头键的集合
- values(): 得到所有的请求头中的值的集合
- entries(): 得到所有请求头中的键值对的集合
应用场景:
统一封装请求头
1 | function getCommonHeaders() { |
9-6. 文件上传
流程:
- 客户端将文件数据发送给服务器
- 服务器保存上传的文件数据到服务器端
- 服务器响应给客户端一个文件访问地址
测试地址:
http://101.132.72.36:5100/api/upload
键的名称(表单域名称,和后端沟通确定):imagefile
请求方法:POST
请求的表单格式:multipart/form-data
请求体中必须包含一个键值对,键的名称是服务器要求的名称,值是文件数据
HTML5中,JS仍然无法随意的获取文件数据,但是可以获取到input元素中,被用户选中的文件数据
可以利用HTML5提供的FormData构造函数来创建请求体
示例:
1 | /* <input type="file" id="avatar" /> */ |
10. 迭代器和生成器
10-1. 迭代器
背景知识
什么是迭代?
从一个数据集合中按照一定的顺序,不断取出数据的过程
迭代和遍历的区别?
迭代强调的是依次取数据,并不保证取多少,也不保证把所有的数据取完
遍历强调的是要把整个数据依次全部取出
迭代器
对迭代过程的封装,在不同的语言中有不同的表现形式,通常为对象
迭代模式
一种设计模式,用于统一迭代过程,并规范了迭代器规格
- 迭代器应该具有得到下一个数据的能力
- 迭代器应该具有判断是否还有后续数据的能力
JS中的迭代器
JS规定,如果一个对象具有 next
方法,并且该方法返回一个对象,该对象的格式如下:
1 | {value: 值, done: 是否迭代完成} |
则认为该对象是一个迭代器;
示例:
1 | const obj = { |
含义:
- next方法:用于得到下一个数据
- 返回的对象
- value:下一个数据的值
- done:boolean,是否迭代完成
迭代器创建函数示例:
1 | const arr1 = [1, 2, 3, 4, 5]; |
10-2. 可迭代协议 与 for-of 循环
可迭代协议
概念回顾:
- 迭代器(iterator):一个具有next方法的对象,next方法返回下一个数据并且能指示是否迭代完成
- 迭代器创建函数(iterator create):一个返回迭代器的函数
可迭代协议:
ES6规定,如果一个对象具有知名符号属性 Symbol.iterator
,并且属性值是一个迭代器创建函数,则该对象是可迭代的(iterable)
示例:
1 | var obj = { |
for-of 循环
for-of 循环用于遍历可迭代对象,格式如下
1 | //迭代完成后循环结束 |
展开运算符与可迭代对象
展开运算符可以作用于可迭代对象,这样,就可以轻松的将可迭代对象转换为数组。
10-3. 生成器 (Generator)
方便开发者自己书写迭代器
什么是生成器?
生成器是一个通过构造函数Generator创建的对象(无法通过new
关键字创建),生成器既是一个迭代器,同时又是一个可迭代对象如何创建生成器?
生成器的创建,必须使用生成器函数(Generator Function
)如何书写一个生成器函数呢?
再
function
后边或者方法名前边添加*
1
2
3
4
5
6
7
8//这是一个生成器函数,该函数一定返回一个生成器
function *method(){};
function* method(){};
function*() {}
{ *method(){} }生成器函数内部是如何执行的?
- 生成器函数内部是为了给生成器的每次迭代提供的数据(运行生成器函数时不会运行内部函数)
- 每次调用生成器的
next
方法,将导致生成器函数运行到下一个yield
关键字位置 yield
是一个关键字,该关键字只能在生成器函数内部使用,表达“产生”一个迭代数据
1
2
3
4
5
6
7
8
9
10
11
12function* test() {
console.log("第1次运行");
yield 1;
console.log("第2次运行");
yield 2;
console.log("第3次运行");
}
const generator = test(); //这里只作为生成器,不会执行test内部代码
generator.next(); //第1次运行 {value: 1, done: false}
generator.next(); //第2次运行 {value: 2, done: false}
generator.next(); //第3次运行 {value: undefined, done: true}有哪些需要注意的细节?
生成器函数可以有返回值,返回值出现在第一次done为true时的value属性中
调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值
1
2
3
4
5
6
7
8
9
10
11function* test() {
let info = yield 1; //第一次执行next方法代码停在yield这里,info还没有接收到值,而且这里info接收到的值也不是1,如果next传参则yield的返回值为参数,否则为undefined;
console.log(info);
info = yield 2 + info;
console.log(info);
};
const generator1 = test();
generator1.next(); //{value: 1, done: false}
generator1.next(2); //2 {value: 4, done: false}第一次调用next方法时,传参没有任何意义
在生成器函数内部,可以调用其他生成器函数,但是要注意加上
*
号1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function* t1() {
yield "a";
yield "b";
};
function* test() {
yield* t1(); //相当于将上方t1中的代码拷贝到这里,及t1会参与迭代
yield 1;
yield 2;
yield 3;
};
const generator = test();
generator.next(); //{value: 'a', done: false}
generator.next(); //{value: 'b', done: false}
generator.next(); //{value: '1', done: false}
generator.next(); //{value: '2', done: false}
生成器的其他API
- return方法:调用该方法,可以提前结束生成器函数,从而提前让整个迭代过程结束
- throw方法:调用该方法,可以在生成器中产生一个错误
10-4. 生成器应用-异步任务控制
在react中会经常使用
在ES5时,解决没有async和await
使用场景:
在ES5时,实现async和await的效果
1 | //延时函数 |
11. 更多的集合类型
11-1. set 集合
一直以来,JS只能使用数组和对象来保存多个数据,缺乏像其他语言那样拥有丰富的集合类型。因此,ES6新增了两种集合类型(set 和 map),用于在不同的场景中发挥作用。
作用:set用于存放不重复的数据
如何创建set集合
1
2
3new Set(); //创建一个没有任何内容的set集合
new Set(iterable); //创建一个具有初始内容的set集合,内容来自于可迭代对象每一次迭代的结果如何对set集合进行后续操作
- add(数据):添加一个数据到set集合末尾,如果数据已存在,则不进行任何操作;
- set使用Object.is的方式判断两个数据是否相同,但是,针对+0和-0,set认为是相等;
- has(数据):判断set中是是否存在对应的数据;
- delete(数据):删除匹配的数据(同add使用Object.is进行匹配),返回是否删除成功;
- clear():清空整个set集合;
- size: 获取set集合中的元素数量,只读属性,无法重新赋值;
- add(数据):添加一个数据到set集合末尾,如果数据已存在,则不进行任何操作;
如何与数组进行相互装换;
1
2
3
4//数组转set集合
const s = new Set([x,x,x,x,x]);
//set集合转数组
const arr = [...s];如何遍历
- 使用for-of循环
- 使用set中的示例方法forEach
注意:set集合中不存在下标,因此forEach中的回调的第二个参数和第一个参数是一致的,均表示set中的每一项;
11-2. set应用
1 | // 两个数组的并集、交集、差集 (不能出现重复项),得到的结果是一个新数组 |
11-3. [扩展]手写set
模拟实现,与js底层实现不同;js底层可直接操作内存,且数据存储使用的是链表
1 | class MySet { |
11-4. map集合
键值对(key value pair)数据集合的特点:键不可重复
map集合专门用于存储多个键值对数据。
在map出现之前,我们使用的是对象的方式来存储键值对,键是属性名,值是属性值。
使用对象存储有以下问题:
- 键名只能是字符串
- 获取数据的数量不方便
- 键名容易跟原型上的名称冲突
11-4-1. 如何创建map
1 | //创建一个空的map |
11-4-2. 如果进行后续操作
- size: 只读属性,获取当前map中键的数量;
- set(键, 值): 设置一个键值对,键和值可以是任何类型;
- 如果键不存在,则添加一项;
- 如果键已存在,则修改它的值;
- 比较键的方式和set相同;
- get(键):根据一个键得到对应的值;
- has(键):判断某个键是否存在;
- delete(键):删除指定的键;
- clear():清空map;
11-4-3. 和数组互相转换
和set一样
11-4-4. 遍历
- for-of,每次迭代得到的是一个长度为2的数组
- forEach, 通过回调函数遍历
- 参数1:每一项的值
- 参数2:每一项的键
- 参数3:map本身
11-5. [扩展]手写map
模拟实现,与js底层实现不同;js底层可直接操作内存,且数据存储使用的是链表
1 | class MyMap { |
11-6. WeakSet 和 WeakMap
11-6-1. WeakSet
使用该集合,可以实现和set一样的功能,不同的是:
- 它内部存储的对象地址不会影响垃圾回收
通常用来检测变量是否还存在引用;(如果WeakSet中还存在,那么外部还存在引用)
- 他只能添加对象
- 不能遍历(不是可迭代的对象)、没有size属性、没有forEace方法
11-6-2. WeakMap
类似于map的集合,不同的是:
- 它的键存储的地址不会影响垃圾回收
- 它的键只能是对象
- 不能遍历(不是可迭代的对象)、没有size属性、没有forEace方法
12. 代理与反射
12-1. [回顾]属性描述符
Property Descriptor
属性描述符:用于描述一个属性的相关信息
通过 Object.getOwnPropertyDescriptor(对象, 属性名)
可以得到一个对象的某个属性的属性描述符
- value: 属性值
- configurable: 该属性的描述符是否可以修改
- enumerable: 该属性是否可以被枚举
- writable: 该属性是否可以被重新赋值
Object.getOwnPropertyDescriptors(对象)
可以得到某个对象的所有属性描述符
如果需要为某个对象添加属性时 或 修改属性时,配置其属性描述符,可以使用下面的代码:
1 | Object.defineProperty(对象, 属性名, 描述符); |
使用示例:
1 | const obj = { |
12-1-1. 存取器属性
属性描述符中,如果配置了 get 和 set 中的任何一个,则该属性,不在是一个普通属性,而变成了存取器属性。
get 和 set配置均为函数,如果一个属性是存取器属性,则读取该属性时,会运行get方法,将get方法得到的返回值作为属性值;如果给该属性赋值,则会运行set方法
意义:存取器属性最大的意义,在于可以控制属性的读取和赋值
注意:
- 不能再get方法中读取自身【无限递归】
- 不能再set方法中对自身进行赋值【无限递归】
- 如果配置了get和set,那么就不能配置value和writable,否则会报错(因为存取器是没有内存空间的,而value和writable都是配置内存空间的)
使用示例:
1 | const obj = { |
12-2. Reflect 反射
12-2-1. Reflect是什么?
Reflect是一个内置的JS对象,它提供了一系列方法,可以让开发者通过调用这些方法,访问一些JS底层功能;
由于它类似于其他语言的反射,因此取名为Reflect;
12-2-2. 它可以做什么?
使用Reflect可以实现诸如 属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否存在于对象中 等等功能;
12-2-3. 这些功能不是已经存在了吗?为什么还要用Reflect实现一次?
有一个重要的理念,在ES5就被提出:减少魔法、让代码更加纯粹;
这种理念很大程度上是受到函数式编程的影响;
ES6进一步贯彻了这种理念,它认为,对属性内存的控制、原型链的修改、函数的调用等等,这些都属于底层实现,属于一种魔法,因此,需要将它们提取出来,形成一个正常的API,并高度聚合到某个对象中,于是,就造就了Reflect对象;
因此,你可以看到Reflect对象中有很多的API都可以使用过去的某种语法或其他API实现。
12-2-4. 它里面到底提供了哪些API呢?
- Reflect.set(target, propertyKey, value): 设置对象target的属性propertyKey的值为value,等同于给对象的属性赋值
- Reflect.get(target, propertyKey): 读取对象target的属性propertyKey,等同于读取对象的属性值
- Reflect.apply(target, thisArgument, argumentsList):调用一个指定的函数,并绑定this和参数列表。等同于函数调用
- Reflect.deleteProperty(target, propertyKey):删除一个对象的属性
- Reflect.defineProperty(target, propertyKey, attributes):类似于Object.defineProperty,不同的是如果配置出现问题,返回false而不是报错
- Reflect.construct(target, argumentsList):用构造函数的方式创建一个对象
- Reflect.has(target, propertyKey): 判断一个对象是否拥有一个属性
- 其他API:[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect]
12-3. Proxy 代理
代理:提供了修改底层实现的方式
创建:
1 | /** |
使用示例:
1 | const obj = { |
12-4. 【应用】观察者模式
有一个对象,是观察者,它用于观察另外一个对象的属性值变化,当属性值变化后会收到一个通知,可能会做一些事。
示例代码:
1 | //创建一个观察者 |
12-5. 【应用】偷懒的构造函数
示例代码:
1 | class User{ |
12-6. 【应用】可验证的函数参数
示例代码:
1 | function sum(a, b){ |
13. 增强的数组功能
13-1. 新增的数组API
13-1-1. 静态方法
- Array.of(…args): 使用指定的数组项创建一个新数组
消除
new Array()
, 参数为一个且为数值型时的歧义 - Array.from(arg): 通过给定的类数组 或 可迭代对象 创建一个新的数组。
13-1-2. 实例方法
- find(callback): 用于查找满足条件的第一个元素
- findIndex(callback):用于查找满足条件的第一个元素的下标
- fill(data):用指定的数据填充满数组所有的内容
- copyWithin(target, start?, end?): 在数组内部完成复制
- includes(data):判断数组中是否包含某个值,使用Object.is匹配
11-2. 【扩展】类型化数组
11-2-1. 数字存储的前置知识
计算机必须使用固定的位数来存储数字,无论存储的数字是大是小,在内存中占用的空间是固定的。
n位的无符号整数能表示的数字是2^n个,取值范围是:0 ~ 2^n - 1
n位的有符号整数能表示的数字是2^n个,取值范围是:-2^(n-1) ~ 2^(n-1) - 1
浮点数表示法可以用于表示整数和小数,目前分为两种标准:
- 32位浮点数:又称为单精度浮点数,它用1位表示符号,8位表示阶码,23位表示尾数
- 64位浮点数:又称为双精度浮点数,它用1位表示符号,11位表示阶码,52位表示尾数
JS中的所有数字,均使用双精度浮点数保存
11-2-2. 类型化数组
类型化数组:用于优化多个数字的存储
具体分为:
- Int8Array: 8位有符号整数(-128 ~ 127)
- Uint8Array: 8位无符号整数(0 ~ 255)
- Int16Array: …
- Uint16Array: …
- Int32Array: …
- Uint32Array: …
- Float32Array:
- Float64Array
如何创建数组
1
2
3
4
5
6
7
8
9
10
11
12
13//数组构造函数: 上方所有类别(Int8Array...)
//1.创建指定长度
new 数组构造函数(长度)
//2.创建指定内容
数组构造函数.of(元素...)
//3.转换类数组
数组构造函数.from(可迭代对象)
//4.通过已有的类型化数组复制一个新的数组(注意:高位转地位可能出现溢出导致数值异常)
new 数组构造函数(其他类型化数组)得到长度
1
2
3
数组.length //得到元素数量
数组.byteLength //得到占用的字节数其他的用法跟普通数组一致,但是:
- 不能增加和删除数据,类型化数组的长度固定
- 一些返回数组的方法,返回的数组是同类型化的新数组
11-3. 【扩展】ArrayBuffer
ArrayBuffer:一个对象,用于存储一块固定内存大小的数据。
创建:
1 | new ArrayBuffer(字节数) |
- 可以通过属性
byteLength
得到字节数; - 可以通过方法
slice
得到新的ArrayBuffer;
示例:
1 | //创建了一个用于存储10个字节的内存空间 |
读写ArrayBuffer
- 使用DataView
- 通常会在需要混用多种存储格式时使用DataView
- 使用类型化数组
- 实际上,每一个类型化数组都对应一个ArrayBuffer,如果没有手动指定ArrayBuffer,类型化数组创建时,会新建一个ArrayBuffer
11-4. 【扩展】制作黑白图片
示例代码:
1 | /* |
获取图片的二进制信息示例:
1 | //请求获取 |
__END__