ES6入门---一文了解ES6的新特性

190 阅读1小时+

前言

这里分享一下在开发的过程里面关于ES6使用的比较多的模块的使用经验。

一、变量的声明

说明: ECMAScript变量是松散类型的,也就是说变量是可以保存任何的数据类型,其中有三个关键字可以用来声明变量,分别是let(用于变量的声明)const(用于常量的声明)var,而前两个关键字是ES6新增的,所以具体分享前面两个。

(1)块级作用域

说明: 它是属于ES6新增的一种作用域,主要以{}为界限,定义在{}内的内容外界无法访问,跟其他作用域一样,可以嵌套使用,值得注意的是,并不是所有关键字声明的变量都存在块级作用域,只有let和const声明的变量才存在,所声明的变量只属于当前的作用域

// 常见的块级作用域出现的几种情况(看let/const和{}):

// {}单独使用
{
    let a = 10
    const b = 10
}

// if判断中使用
if (true) {
    let c = 10;
    const d = 10;
}


// 函数中使用
function f1() {
  let e = 5;
}

// 常见声明的这几种情况在块级作用域内打印都可以打印出来,但是如果在作用域
// 外打印的话就会报错
// 块级作用域的嵌套:
{
    let a = 10
    const b = 10
    
    // 对于块级作用域的嵌套,变量名是可以重复的,因为作用域外不可访问作用
    // 域内的内容,重复使用不会报错
    {
        let a = 10
        const b = 10
    }
}

对于函数的声明而言,如果运行环境是ES6浏览器,那么声明在块级作用域内的函数相当于使用let声明变量一样,这个函数在作用域外是无法被访问的

注意: 在相同作用域中使用let、const、var关键字定义变量的时候,变量名不可以重复,否则会报错`

(2)暂时性死区

说明: 在使用letconst关键字声明变量的时候,只允许在声明后使用,如果在声明前就使用的话在执行的瞬间被称为暂时性死区,使用的话会报错的。

// 只允许先声明后使用:

{
    console.log(a) // 报错
    let a = 1
    console.log(a) // 1
}

由于暂时性死区的出现,就导致不存在变量的提升(在声明前使用会报错),那么使用typeof操作符就不再进行变量检测的时候就会出现问题(检测未定义的变量返回undefined,在使用关键字let/const定义的变量之前进行检测则会报错)

(3)顶层对象window

说明: 对于浏览器而言,它的顶层对象是window,在ES5的时候,时候var关键字声明的变量会成为window的属性,也就是说可以直接使用window.变量名来访问,就会出现顶层对象的属性是到处可以读写的,非常不利于模块化编程,于是在ES6中,使用let和const关键字声明的变量不再变成window的属性了

var a = 0
console.log(a) // 0
console.log(window.a) // 0

let b = 0
console.log(b) // 0
console.log(window.b) // undefined,因为没有这个属性b

(4)const

说明: 对于const关键字而言,它与let的行为差不多,它也有块级作用域、暂时性死区、变量不可重复声明这些特点,区别在于它用于声明常量,在声明的时候必须赋值,同时,也不可以修改const声明的变量的值

const a // 报错,必须赋值
const a = 0 // 正解
a = 1 // 报错,不可以修改a的值

注意: 值得注意的是,const声明限制只适用于它指向的变量的引用,也就是说如果使用const声明一个对象,那么是可以修改对象里面的属性的。

const a = { }
a.b = 'const定义对象的内部的属性可以修改'
console.log(a.b) // const定义对象的内部的属性可以修改

// 使用Object.freeze方法可以冻结对象,被冻结的对象是无法对对象进行操作的
const c = Object.freeze({ })

(5)变量声明与for循环

// var与for循环:

for(var i = 0; i < 5; i++) {
    setInterval( () => {
        console.log(i)
    }, 2000)
}
// 结果:5、5、5、5、5
// 说明:使用var声明的变量在for循环中,变量会迭代并渗透到循环体的内部
//       在退出循环的时候,迭代的变量保存的是导致退出循环的值,所以在
//       定时器执行的时候,所打印的变量都是同一个变量,因此输出的值都
//       是同一个值。
// let与for循环:

for(let i = 0; i < 5; i++) {
    setInterval( () => {
        console.log(i)
    }, 2000)
}
// 结果:0、1、2、3、4、5
// 说明:在使用let声明的变量进行迭代的时候,js的引擎在后台会为每个迭
//       代循环声明一个新的迭代变量,导致每个定时器引用的是不同的变量
//       实例,所以每次打印出来的值就都是每次迭代生成的值。

注意: 对于在for循环中使用const声明变量的话,变量的值是不会改变的,会是一个固定的值,不会参与迭代的过程

二、解构赋值

说明: 解构赋值可以理解为按需匹配,将等号左边的结构和等号右边的结构对应起来,然后按照对应位置,对变量赋值,直接使用变量就可以了,如果左边的变量的数量与右边值的数量相等,这种称为完全解构,其他的均称为不完全解构,一般情况下,左边的变量的数量少于右边的值的数量,变量与值从左到右一一对应,如果左边数量多余右边,那左边的变量在右边没有值对应的话会转换为undefined(解构不成功,变量的值就等于undefined)

(1)数组的解构

// 完全解构:一一对应
let [ a, b, c ] = [1, 2, 3]

// 说明:此时左右两边都是数组结构,并且数组左边变量和右边数组中的值
//       一一对应起来了,那取右边数组中的值就可以使用变量a、b、c代
//       替了
// 不完全解构:没有对应的变量值是undefined
let [ a, b ] = [1, 2, 3]
let [ a, b, c, d ] = [1, 2, 3]

// 说明:第一种情况下变量a、b的值分别是1、2
//       第二种情况下变量a、b、c、d的值分别是1、2、3、undefined
// 嵌套解构:可以进行多层的嵌套
let [ a, b, [ c ] ] = [ 1, 2, [ 3 ] ]

// 说明:多层嵌套的时候注意结构需要一致,不然会取不到值

注意: 只要数据结构具有可遍历的接口,都可以进行解构赋值

(2)数组解构的默认值

说明: 对于默认值,可以理解为如果默认给一个变量一个值(这个值一般是基本数据类型),如果在解构的过程中这个变量没有得到相应的解构出来的值,那么这个变量就会使用开头给定的值,也就是默认值,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

// 基础用法:

let [ foo = true ] = [  ] <=> let [ foo = true ] = [ undefined ]

// 上面这两种表示的方法是等价的,就是变量对应的位置在左边找不到值与它对应
// 或者对应的值是undefined,那么如果解构的时候变量存在默认值,此时就会使
// 用默认值来给变量进行赋值
// 注意:null与undefined不一样

let [ foo = true ] = [ undefined ]

// 这里变量foo在右边找到null值与它对应,此时不会使用默认值,因为
// null !== undefined
// 惰性使用:如果默认值是一个表达式,那么默认值只有在使用的时候,
//          才会去计算表达式的结果并赋值

function f() {
  console.log('aaa');
}

let [ x = f() ] = [1]; // x = 1 

// 这里的默认值f函数的执行只有才右边没有值或者值是undefined对应的
// 时候才会执行进行结果的计算并赋值,所以这里的x只能表示为1

注意: 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

(3)对象的解构

说明: 跟数组一样,使用与对象匹配的结构来实现对象属性赋值

// 基本使用:

let person = {
    name: '张三',
    age: 20
}

// 如果不使用解构赋值的话进行取值和赋值
let personName = person.name
    personAge = person.age

console.log(personName) // 张三
console.log(personAge) // 20

// 使用解构赋值的话进行取值和赋值
let { name: personName, age: personAge  } = person

console.log(personName) // 张三
console.log(personAge) // 20

上面这两段代码是等价的,其中需要注意的点在于,对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值,其次,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者,前者可以理解为一个匹配的机制,后者才是被赋值的变量,所以改变的值在于后者,不在于前者。

// 简写形式:

let person = {
    name: '张三',
    age: 20
}

let { name: name, age: age  } = person
let { name, age  } = person

console.log(name) // 张三
console.log(age) // 20

上面的这两种写法也是等级的,这就体现了前者理解成匹配机制,后者才是赋值的对象,这种写法一般用于想让变量名直接使用对象的属性名的情况就可以采用这种简写的方式

// 当然引用不存在的属性同样会得到undefined

let person = {
    name: '张三',
    age: 20
}


let { name, Age  } = person

console.log(name) // 张三
console.log(Age) // undefined

// 说明:因为在对象中不存在Age这个匹配的规则,当然也就不存在赋值的操作了,
//       没有会得到默认值undefined

注意: 解构在函数内部使用ToObject()将原数据转换为对象,也就是说原始值会被当做对象,同时根据ToObject方法的定义,unll和undefined不能被解构,解构会报错

当然,对象也是存在解构的默认值的,原理跟数组解构的原理一样,只不过数组用等号表示,而对象用间值对的形式表示

// 嵌套解构来匹配属性进行赋值:

let person = {
    name: '张三',
    age: {
        number: 20
    }
}

let { name, age: { number } } = person

console.log(number) // 20
// 嵌套解构赋值:

let person = {
    name: '张三',
    age: 20
}
let personCopy = {  }

{
    {
        name: personCopy.name,
        age: personCopy.age
    } = person
}

// 说明:这里将person这个对象的引用给了personCopy,那么现在这两个对象是
//       相互影响的

这里值得注意的是,嵌套解构条件需要从外到内依次满足条件才可以,也就是说如果外层属性在解构的时候不存在,是无法使用嵌套解构进行赋值操作的,同时,如果一次解构多个属性值的话,会从左到右依次解构并对其赋值,在这期间,如果有一个解构的属性不存在,那么解构就会在这里停止,不再往下进行

(4)字符串的解构

说明: 在解构的时候字符串被转换成一个类数组的结构,字符串的每个元素都是一个值,当然,跟数组挂钩的话肯定也会有length属性,这个属性表示字符串的长度。

// 基本用法:
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
// 解构时length的使用:
let {length : len} = 'hello';
len // 5

// 这里注意使用length属性的时候需要解构为对象的形式才可以

注意: 对于布尔值和数值而言,根据解构赋值的规定,它们会被转换成对象,然后分别调用自己构造函数的原型上面的toString方法来取值(不常用)

当然,函数的参数也可以来解构赋值,后面我去分享函数的时候我再来说,同时这里的解构赋值我只是分享那种很常见的使用方式,这些的话一般场景是够用的,如果需要使用解构赋值来进行操作的简化的话,需要大家去发掘了。

三、Symbol

说明: 由于ES5中对象的属性名都是字符串,这样的话就很容易造成命名的冲突,所以ES6中引入了一个值类型Symbol来表示一个独一无二的值,这样就可以从根本上解决命名的冲突问题,

(1)基本用法

// Symbol的初始化:通过Symbol函数来生成

let s = Symbol();
let S = Symbol();

console.log( s === S ) // false

// 说明:这表示创建的Symbol实例是独一无二的

上面说过Symbol属于原始类型,那么使用typeof操作符的时候其返回值是Symbol

// Symbol实例的描述:通过Symbol函数进行初始化Symbol的时候,
//                  可以传入一个字符串来描述这个Symbol的实
//                  例,便于区分,同时,为了方便获取Symbol
//                  的描述,可以调用description的方法获取

let l = Symbol('l');

console.log(l) // Symbol(l)
console.log(l.description) // 获取描述Symbol的描述l

// 说明:这里加上描述是为了方便区分,如果没有这个描述的话那
//       么打印出来都是Symbol(),多起来的话你就懵了

注意: Symbol()函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象,所以不能使用new命令来调用。另外,由于 Symbol 值不是对象,所以也不能添加属性,而且如果创建时传递的参数仅仅作为描述的作用,所以就算描述相同创建的Symbol实例也不可能相等

此外,如果 Symbol 的参数是一个对象,就会调用该对象的toString()方法,将其转为字符串,然后才生成一个 Symbol 值,另外,Symbol值不能进行计算,在类型转换的过程里面,仅仅只能转换为布尔值和字符串而已

(2)作为属性名

说明: Symbol作为对象的属性的时候,可以确保这个属性是不会被覆盖掉的,也就是独一无二的,一般有对象字面量属性、Object.defineProperty()两种方法来使用。

// 举例:

let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });


// 以上写法都得到同样结果
a[mySymbol] // "Hello!"

注意: 在使用Symbol值作为属性名的时候,最好不要使用.操作符来进行命名,因为其操作符后跟的是字符串,导致命名实际上使用字符串的方式命名对象的属性,而不是使用Symbol值作为属性名,所以一般属性命名的话使用[ ]最合适,当然,在对象中也最好使用[ ]命名属性

Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。

const mySymbol = Symbol();
const a = {};

// 使用.操作符的话相当于这个属性命名变成了字符串'mySymbol'
// 而不是一个Symbol值作为属性,所以下面取值用到了'mySymbol'
a.mySymbol = 'Hello!';

a[mySymbol] // undefined
a['mySymbol'] // "Hello!"

(3)属性名的遍历

说明: 当Symbol值作为对象的属性名的时候,它不能够被常规的遍历方法所遍历出来(for...in,for...of等等),可以通过Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。另外一个方法就是Reflect.ownKeys()方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

// Object.getOwnPropertySymbols的使用:

const obj = {};
let a = Symbol('a');
let b = Symbol('b');

obj[a] = 'Hello';
obj[b] = 'World';

const objectSymbols = Object.getOwnPropertySymbols(obj);

objectSymbols // [Symbol(a), Symbol(b)]
// Reflect.ownKeys的使用:
let obj = {
  [Symbol('my_key')]: 1,
  enum: 2,
  nonEnum: 3
};

Reflect.ownKeys(obj) //  ["enum", "nonEnum", Symbol(my_key)]

(4)全局的Symbol

说明: 有时候需要用到相同的Symbol实例,那么就可以通过Symbol.for()的方法来创建一个Symbol,它接受一个字符串的参数(如果不是字符串参数,会被转换为字符串),这个参数也会作为Symbol的描述,这个方法在执行的时候,会进行全局检查,看看是否当前的Symbol被注册,如果已经注册,那么就会返回注册的Symbol,如果没有注册过,那么就会重新注册一个新的Symbol实例,当然,它有一个配套的方法Symbol.keyFor(),这个方法用来查询使用Symbol.for()注册的Symbol的描述,它接受一个Symbol作为参数,返回它的描述(如果传递的参数不是Symbol的话,会报错)

// Symbol.for():

let aA = Symbol('a')
let a = Symbol.for('a')
let A = Symbol.for('a')
let Aa = Symbol.for()

console.log( a === aA ) // false
console.log( a === A ) // true
console.log( Aa ) // undefined

注意: 即使所注册的Symbol的描述相同,但是使用Symbol函数创建和Symbol.for方法创建的不相等,**Symbol.for方法的参数是一个字符串

let s1 = Symbol.for("foo");
let s2 = Symbol("foo");

Symbol.keyFor(s1) // "foo"
Symbol.keyFor(s2) // undefined
Symbol.keyFor('参数不是Symbol') // 报错

Symbol.keyFor对于全局的Symbol会返回全局Symbol的描述,Symbol函数创建的Symbol会返回undefined,参数不是Symbol的会报错

后面还有一个内置的Symbol值,这部分可以去看看文档(因为自己对这部分不太了解,后面自己理解完后再补一下),一般情况前面所写应该覆盖大部分使用场景,后面的有兴趣可以看看

四、Map

说明: JS的对象本质上是键值对的集合,但是传统上只能用字符串当作键。这给它的使用带来了很大的限制,所以在ES6中新增了Map结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键,就导致任何具有迭代器接口(可遍历)并且每个成员都是双元素的数据结构都可以作为Map构造函数的参数

(1)基本使用

// 每个成员都是双元素的数据结构:(举例)

const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);
// 多次的赋值:

const map = new Map();

map
.set(1, 'aaa')
.set(1, 'bbb');

map.get(1) // "bbb"
// 内存地址的问题:

const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined

// 表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的
// 同名属性:

const map = new Map();

const k1 = ['a'];
const k2 = ['a'];

map
.set(k1, 111)
.set(k2, 222);

map.get(k1) // 111
map.get(k2) // 222

// k1和k2的值是一样的,但是它们在 Map 结构中被视为两个键,
// 引用类型的内存地址不一致,只要内存地址不一样,就视为两个键。
// 这就解决了同名属性碰撞的问题,我们扩展别人的库的时候,如果
// 使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

注意:

  • 对于同一个键的多次赋值,后者会覆盖前者
  • Map结构的键跟内存地址绑定,只要内存地址不同,键就不同
  • 两个值严格相等,Map才会视为一个键
  • undefined !== null
  • NaN === NaN

(2)基本的API

// size:用于返回Map结构中的成员总数
// set:用于设置键名、键值,然后返回整个 Map 结构,所以有链式调用,
//      如果键名已经有值,则键值会被更新,否则就新生成该键。
// get:用于读取键名对应的键值,取不到就返回undefined
// has:用于判断某一个键名是否在它的Map结构中,返回布尔值
// delete:用于删除Map结构中的某个键,返回值为布尔值
// clear:用于清除Map结构中的所有键,

const map = new Map();

// 以键值对的形式添加成员到Map结构中
map.set('foo', true);
map.set('bar', false);

// 与上面写法等价:
map.set('foo', true).set('bar', false);;

// 获取Map结构成员数量为2
map.size 

// 读取键名对应的键值,取不到就返回undefined
m.get(foo) // true
m.get(haha) // undefined

// 用于判断某一个键名是否在它的Map结构中,返回布尔值
m.has(foo) // true
m.has(haha) // false

// 用于删除Map结构中的某个键,返回值为布尔值
m.delete(foo) // true,表示删除成功
m.delete(haha) // false,表示删除失败,因为Map结构中不存在这个键名

// 清除所有的Map结构中的成员,它没有返回值
m.clear()

(3)Map结构的遍历

说明: Map结构会维护间值对的插入顺序,那么就可以根据插入的顺序进行遍历的操作

// keys():会将Map结构所有的键名保存在数组中返回。
// values():会将Map结构所有的键值保存在数组中返回。
// entries():会将Map结构的每一元素以 [ 键名, 键值 ]的形式保存在数组中返回。
// forEach(函数,改变this指向):函数会遍历Map结构的所有成员根据需求自定义操作

const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 遍历获取键和值
map.forEach((value, key) => {
  console.log(key, value)
});

注意: entries() === Symbol.iterators属性,然后Map结构提供默认的迭代器可以通过entries()方法获取,也就是说可以直接对Map结构进行扩展的操作(...操作符)来将其转换为数组

// 理解:

const m1 = new Map([
    ['key1', 'val']
])

for(let key of m1.keys()){
    key = 'new'
    console.log(key) // new
    console.log(m1.get(key)) // val
}
console.log(m1) // Map(1) {'key1' => 'val'}

// 说明:这里需要注意一下,表面上我改变了Map结构中某个键名的值,但实际上
//       作为键的字符串的原始值是改变不了的,这也就说明了后面我去打印Map
//       结构跟改变前原来的结构是一样的
// 理解:

const key = { id: 1 }

const m1 = new Map([
    ['key', 'val']
])

for(let key of m1.keys()){
    key.id = 'new'
    console.log(key) // { id: new }
    console.log(m1.get(key)) // 1
}
console.log(m1) // Map(1) { { id: 1 } => 'val'}

// 说明:这里虽然我改变了对象里面的属性,但是这个对象的内存地址在Map结构中
//       是没有改变的,所以后面打印来的Map结构跟原来的是一样的

注意: 键和值在遍历的时候是可以修改的,但是它们的内存地址在Map结构中是无法修改的,也就是说,修改了键或者值得对象的内部属性,但是不影响它们在Map结构中的真实身份(可以理解为人虽然整容了,但是它还是它)

Map与Object的区别:

  • 内存占用:Map大约可以比Object多储存50%的间值对
  • 插入性能:Map在所有浏览器会稍微好一点(大量的插入体现的更加明显)
  • 查找速度:两者间的差异比较小,但是推荐使用Object
  • 删除性能:Object的删除功能有点稀烂的味道,这里推荐使用Map结构

五、WeakMap

说明: WeakMap是Map的兄弟类型,其API是Map结构的子集,WeakMap中的weak是弱的意思,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用,这也就解决了当使用对象进行存储的时候,如果不使用这个对象并且不手动删除这个对象的引用的话,会造成内存泄露的问题

(1)基本API

说明: 跟Map相比,它的基本API要少一点,只有get()set()has()delete(),当然初始化使用new关键字,这些方法的使用跟Map的使用是一致的,不过WeakMap中的键只能是Object或者继承Object的类型,尝试使用非对象的键会报错,但是值没有限制

注意: 如果初始化的时候需要给WeakMap填充内容,那么new构造函数可以接受一个可迭代(遍历)的对象,其中需要包含键值对的数组,然后会按照可迭代对象的每个键值对的顺序依次插入到新的实例中去。

// 举例:嵌套数组初始化WeakMap
const key1 = { id: 1 },
      key2 = { id: 2 },
      key3 = { id: 3 };

const vm = new WeakMap([
  [key1, 'val'],
  [key2, 'val'],
  [key3, 'val']
])

console.log(vm.get(key1)) // val
console.log(vm.get(key2)) // val
console.log(vm.get(key3)) // val

注意:

  • 对于WeakMap的初始化而言,它是一个全有或者全无的操作,只要有一个键无效就会报错,导致整个的初始化会失败
  • 原始值可以先调用自身的方法将其包装成对象再来用作键

(2)弱键

说明: 前面说到过WeakMap的键是弱键的问题,但是键所对应的值并没有弱键的意思,只要键存在,值就会存在于WeakMap里面

const wm = new WeakMap();

wm.set( {}, 'val')

注意: 上面初始化了一个对象并将它用作WeakMap的键,但这个对象没有指明其引用,那么当代码执行完毕后,对象的键会被垃圾回收处理掉,导致这个键值对的消失,是WeakMap变成一个空的映射。

const wm = new WeakMap();

const container = { 
  key: {}
};

wm.set( container.key, 'val')

这个例子与上面的区别在于container对象维护者一个对WeakMap键的引用,因此键不会被处理掉,当这个键的值被赋为null的时候,结果与上面就一样了

(3)键的不可迭代

说明: 用于WeakMap中的键值对在任何情况下都可能会被销毁,那么也就没必要提供可迭代的能力,那么Map中的Keys、Values、entries、size、clear这些方法都没有,它之所以限制键只能为对象,是为了保证通过键对象的引用才能取值,如果允许原始值,那就没有办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了

六、Set以及WeakSet

说明: Set在很多方面有点像强化版的Map,毕竟它们大多数的API和行为都是共有的,它类似于数组,但是成员的值都是唯一的,没有重复的值。如果Map是与Object相比,那set就是与Array相比了。

注意:

  • 跟Map一样,可以用...来获取Set结构中的内容,也就是将Set结构转换为数组,同时,Array.from方法也可以完成这样的需求,可以用来进行数组去重的操作
  • NaN === NaN
  • { } !== { }
  • Set结构采用===的方式来判断是否相等

基本API: Set结构一般可以使用new关键字sizehascleardelete,这些API的话使用起来跟Map结构使用的方式一模一样,只是添加的内容由键值对的形式转换成值得形式就可以,然后添加的话改变成了add方法

Set的遍历: 关于遍历跟Map一模一样,都是那些方法和那些操作,只是要注意的是,由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

理解: 其实和Map是一样的,Map99%有的Set一定有,剩下的就是一些改变了,改变不是很大,一个类似对象,一个类似数组,由于可以使用...将Set结构转换为数组,那么数组的一些方法也可以间接的去给Set来使用了,具体怎么使用看自己怎么操作吧,因为前面将Map、WeakMap的使用写的比较详细了,然后Set是Map的兄弟的话,这里就不写它们相同点了,重复的东西只会让你感到心烦,最后关于WeakSet的话,它跟WeakMap基本上一模一样,不同的点在于Set结构没有键名,只有键值...

七、代理(Proxy)与反射(Reflect)

说明: ES6新增的代理以及反射向开发者提供了拦截并向基本操作嵌入额外行为的能力,简单来说,可以给目标对象定义一个关联的代理对象,这个代理对象可以作为抽象的目标对象来使用,在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制

注意: 代理和反射只有在百分百支持的平台上才有用,可以检测代理是否存在从而准备备用的代码,但是这会导致代码的重复,不推荐

(1)空代理的创建

说明: 最简单的代理就是空代理,这个代理就只有作为目标对象的抽象,其它什么作用也没有,默认情况下,代理对象上面执行的所有操作都会无障碍的转播到目标对象上面,代理是使用Proxy函数构建的,这个函数存在两个参数,目标对象以及处理程序的对象,缺少任意一个都会报错,对于空代理而言,可以传递一个简单的字面量作为处理程序对象,从而让所有的操作都畅通无阻的抵达目标对象。

// 创建目标对象
const target = {
  id: 'target'
}

// 创建处理程序对象
const handle = {}

// 使用proxy函数创建代理
const proxy = new proxy(target, handle)

// id属性会访问同一个值
console.log(target.id)
console.log(proxy.id)

// 给目标属性赋值会反应到两个对象上面,原因是两个对象访问的是同一个值
target.id = 'foo'
console.log(target.id)
console.log(proxy.id)

// 给代理属性赋值会反应到两个对象上面,原因是赋值会转移到目标对象上面
target.id = 'foo'
console.log(target.id)
console.log(proxy.id)

注意: 由于proxy没有原型,也就是proxy.prototype的值是undefined,所以不能够使用instanceof操作符,同时严格相等可以用来区分代理对象与目标对象

(2)捕获器的设置

说明: 设置代理的主要目的是可以设置捕获器,每个程序对象可以设置0个或者是多个捕获器,每个捕获器对应一种基本操作,可以直接或间接在代理对象上面调用,每次在代理对象上面调用这些基本操作的时候,代理可以在这些操作传递到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为

// 举例:定义一个get()的捕获器

// 创建目标对象
const target = {
  id: 'bar'
}

// 创建处理程序对象
const handle = {
  // 捕获器的定义是以函数的方式呈现
  get(){
    return 'handle override'
  }
}

// 使用proxy函数创建代理
const proxy = new proxy(target, handle)

console.log(target.id) // bar
console.log(proxy.id) // handle override

console.log(target['id']) // bar
console.log(target['id']) // handle override

console.log(Object.create(target)['id']) // bar
console.log(Object.create(proxy)['id']) // handle override

注意: 代理对象执行get()操作的时候,就会触发定义的get()捕获器,当然,这个方法对象target是无法触发的,只有通过proxy.prototype、proxy[prototype]、Object.create(proxy)[prototype]这几个操作触发,触发时get捕获器就可以获取属性,也就是当你想获取目标对象上面的属性的时候,其实返回的结果是经过捕获器处理之后的结果,因此这些操作只要发生在代理对象上面,就会触发get捕获器,但目标对象上操作属性只会产生正常的行为

(3)捕获器参数以及反射API

说明: 在代理中,每个捕获器都有可以访问的相应的参数,可以通过参数来重写捕获器所做的行为(原始行为),

// 以get捕获器为例:

const target = {
  id: 'bar'
}

const handle = {
  // trapTarget: 目标对象
  // property: 查询的属性
  // receiver: 代理对象
  get(trapTarget, property, receiver){
    console.log(trapTarget === target ) // true
    console.log(property)
    console.log(receiver === proxy ) // true
    
    return trapTarget[property] // 这样就可以通过捕获的方法来重建原始的行为了
  }
}

const proxy = new proxy(target, handle)

console.log(target.id) // bar
console.log(proxy.id) // bar

注意: 代理中存在多个捕获器,每个捕获器的原始行为都不一样,如果自己根据参数去手动封装,会非常麻烦,幸运的是,反射对象帮我们将这些捕获器的方法进行了一定的封装,它们的方法名是一致的,同时,实现的效果也是一致的,当你需要捕获器实现什么功能的时候,直接调用反射对象上对应的方法就可以

// 这段代码与上面的代码的效果是一致的,可以看出操作要简单的多
const target = {
  id: 'bar'
}

const handle = {
  get(){
    return Reflect.get(...arguments)
  }
  
  // 这个get捕获器更简单的写法(与上面等价)
  // get: Reflect.get
}

const proxy = new proxy(target, handle)

console.log(target.id) // bar
console.log(proxy.id) // bar

如果想创建一个可以捕获所有方法,并将每个方法发送给反射对应的API来处理,在设置代理的时候,直接将处理程序对象转换成Refect对象就可以

(4)捕获器不变式

说明: 对于它的理解就是说捕获器可以改变所有基本方法的行为,但是它也会受一定的限制,这个限制来源于捕获器本身,不同的捕获器的限制条件是不一致的,简单理解就死限制条件

// 举例:

const target = { }

Object.defineProperty(target, 'foo', { 
  configurable: false, // 不可配置
  writable: false, // 不可写
  value: 'bar' // 属性值为bar
})

const handle = {
  get(){
    return '我想要更改foo属性的值'
  }
}

const proxy = new proxy(target, handle)

console.log(target.foo) // 会报错

(5)可撤销代理

说明: 做任何事都可以反悔,那代理也一样,我既然可以创建代理,那么我也可以撤销代理,但是,传统的使用new关键字来创建代理的方式已经不行了,这种创建是没有办法撤销代理的,需要使用Proxy中的revocable的方法才可以,它有一个revoke函数用来撤销代理,注意,这个撤销的动作是不可逆的,并且,撤销函数执行的结果都一样,在撤销完毕之后再次使用代理获取属性会报错

// 代理的撤销:

const target = {
  foo: 'bar'
}

const handle = {
  get(){
    return '我想要更改foo属性的值'
  }
}

// 这里创建的代理对象和撤销函数的生成是同步的
const { proxy, revoke } = Proxy.revocable(target, handle)

console.log(target.foo) // bar
console.log(proxy.foo) // bar

revoke() // 撤销代理

console.log(proxy.foo) // 报错

(6)多层代理

说明: 代理可以拦截反射API的操作,也就是说代理也可以代理另一个代理,那么就可以实现一个目标对象存在多个代理来进行相应的处理操作。

const target = {
  foo: 'bar'
}

const firstProxy = new Proxy(target, {
  get(){
    console.log('这是第一个代理')
    return Reflect.get(...arguments)
  }
})

const secondProxy = new Proxy(firstProxy, {
  get(){
    console.log('这是第二个代理')
    return Reflect.get(...arguments)
  }
})

console.log(secondProxy.foo) // 这是第二个代理、这是第一个代理、bar

注意: 代理中的this它是一直指向代理实例的(这个会引起很多使用的问题,在用的时候需要注意一下)

(7)代理中的捕获器

说明: 这里将13个捕获器与其相关的内容列举出来,关于参数的对照即可

// get捕获器:会在获取属性值的操作被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  get(target, property, reciver) {
    console.log('get()')
    return Reflect.get(...arguments)
  }
})

proxy.foo // get()

// 返回值:
// 没有限制

// 执行捕获器的操作:
// proxy.property
// proxy[property]
// Object.create(proxy)[property]
// Reflect.get(proxy, property, reciver)

// 捕获器参数:
// target: 目标对象
// property: 引用目标对象上面的属性
// reciver: 代理对象

// 捕获器不定式:
// 如果target.property不可写不可配置 --> 处理程序的返回值必须与target.property匹配
// 如果target.property不可配置且get特性为undefined --> 处理程序的返回值必须是undefined

// 对应反射API的方法:
Reflect.get()
// set捕获器:会在设置属性值的操作被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  set(target, property, value, reciver) {
    console.log('set()')
    return Reflect.set(...arguments)
  }
})

proxy.foo = 'bar' // set()

// 返回值:
// true: 表示成功
// false: 表示失败,在严格模式下面会报错

// 执行捕获器的操作:
// proxy.property = value
// proxy[property] = value
// Object.create(proxy)[property] = value
// Reflect.get(proxy, property, value, reciver)

// 捕获器参数:
// target: 目标对象
// property: 引用目标对象上面的属性
// value: 要给属性赋的值
// reciver: 接收最初赋值的对象

// 捕获器不定式:
// 如果target.property不可写不可配置 --> 则不可修改目标的属性值
// 如果target.property不可配置且get特性为undefined --> 则不可修改目标的属性值
// 如果在严格模式下面,处理程序中范湖false会报错

// 对应反射API的方法:
Reflect.set()
// has捕获器:会在in操作符中被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  has(target, property) {
    console.log('has()')
    return Reflect.has(...arguments)
  }
})

'foo' in proxy // has()

// 返回值:
// has一定会返回布尔值,表示属性是否存在
// 如果返回非布尔值的话会被转换成布尔值类型

// 执行捕获器的操作:
// property in proxy
// property in Object.create(proxy)
// with(proxy) { (property); }
// Reflect.has(proxy, property)

// 捕获器参数:
// target: 目标对象
// property: 引用目标对象上面的属性

// 捕获器不定式:
// 如果target.property存在且不可配置 --> 则处理程序必须返回true
// 如果target.property存在且目标对象不可扩展 --> 则处理程序必须返回true

// 对应反射API的方法:
Reflect.has()
// defineProperty捕获器:会在Object.defieneProperty()中被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  defineProperty(target, property, descriptor) {
    console.log('defineProperty()')
    return Reflect.defineProperty(...arguments)
  }
})

Object.defineProperty(proxy, 'foo', {
  value: 'bar',
}) // defineProperty()

// 返回值:
// defineProperty一定会返回布尔值,表示属性是否成功定义
// 如果返回非布尔值的话会被转换成布尔值类型

// 执行捕获器的操作:
// Object.defineProperty(proxy, property, descriptor)
// Reflect.defineProperty(proxy, property, descriptor)

// 捕获器参数:
// target: 目标对象
// property: 引用目标对象上面的属性
// descriptor: 一个配置对象(enumerable, configurable, writable, value, get, set,这五个可选的参数)

// 捕获器不定式:
// 如果目标对象不可扩展 --> 无法定义属性
// 如果目标对象有一个可配置属性 --> 则不可以添加同名的不可配置属性
// 如果目标对象有一个不可配置属性 --> 则不可以添加同名的可配置属性

// 对应反射API的方法:
Reflect.defineProperty()
// getOwnPropertyDescriptor捕获器:会在Object.getOwnPropertyDescriptor()中被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  getOwnPropertyDescriptor(target, property) {
    console.log('getOwnPropertyDescriptor()')
    return Reflect.getOwnPropertyDescriptor(...arguments)
  }
})

Object.getOwnPropertyDescriptor(proxy, 'foo') // getOwnPropertyDescriptor()

// 返回值:
// getOwnPropertyDescriptor必须返回对象
// 属性不存在的时候会返回undefined

// 执行捕获器的操作:
// Object.getOwnPropertyDescriptor(proxy, property)
// Reflect.getOwnPropertyDescriptor(proxy, property)

// 捕获器参数:
// target: 目标对象
// property: 引用目标对象上面的属性

// 捕获器不定式:
// 如果自身的target.property存在且不可以配置 --> 处理程序需要返回一个表示该属性存在的对象
// 如果自身的target.property存在且可以配置 --> 处理程序需要返回一个表示该属性可配置的对象
// 如果自身的target.property存在且target不可以扩展 --> 处理程序需要返回一个表示该属性存在的对象
// 如果target.property不存在且target不可以扩展 --> 处理程序需要返回一个undefined表示该属性不存在
// 如果target.property不存在 --> 处理程序不可以返回该属性可配置的对象

// 对应反射API的方法:
Reflect.getOwnPropertyDescriptor()
// ownKeys捕获器:会在Object.keys以及类似的方法中被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  ownKeys(target) {
    console.log('ownKeys()')
    return Reflect.ownKeys(...arguments)
  }
})

Object.keys(proxy) // ownKeys()

// 返回值:
// ownKeys必须返回包含字符串或symbol的可枚举对象

// 执行捕获器的操作:
// Object.getOwnPropertyNames(proxy)
// Object.getOwnPropertySymbols(proxy)
// Object.keys(proxy)
// Reflect.ownKeys(proxy)


// 捕获器参数:
// target: 目标对象

// 捕获器不定式:
// 返回的可枚举对象必须包含target的所有不可配置的自有属性
// 如果target不可以扩展,则返回可枚举对象必须准确的包含自有属性键

// 对应反射API的方法:
Reflect.ownKeys()
// getPrototypeOf捕获器:会在Object.keys以及类似的方法中被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  getPrototypeOf(target) {
    console.log('getPrototypeOf()')
    return Reflect.getPrototypeOf(...arguments)
  }
})

Object.getPrototypeOf(proxy) // getPrototypeOf()

// 返回值:
// getPrototypeOf必须返回对象或者是null

// 执行捕获器的操作:
// Object.getPrototypeOf(proxy)
// Object.prototype.isPrototypeOf(proxy)
// proxy.__proto__
// proxy instanceof Object
// Reflect.getPrototypeOf(proxy)


// 捕获器参数:
// target: 目标对象

// 捕获器不定式:
// 如果target不可以扩展,则Object.getPrototypeOf(proxy)唯一有效的返回值就是Object.getPrototypeOf(target)的返回值

// 对应反射API的方法:
Reflect.getPrototypeOf()
// setPrototypeOf捕获器:会在Object.getPrototypeOf()中被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  setPrototypeOf(target, prototype) {
    console.log('setPrototypeOf()')
    return Reflect.setPrototypeOf(...arguments)
  }
})

Object.setPrototypeOf(proxy) // setPrototypeOf()

// 返回值:
// setPrototypeOf必须返回布尔值,表示是否赋值成功
// 不是布尔值的时候会被转换为布尔值

// 执行捕获器的操作:
// Object.setPrototypeOf(proxy)
// Reflect.setPrototypeOf(proxy)


// 捕获器参数:
// target: 目标对象
// prototype: target的替代原型,如果是顶级原型就是null

// 捕获器不定式:
// 如果target不可以扩展,则prototype唯一有效的返回值就是Object.setPrototypeOf(target)的返回值

// 对应反射API的方法:
Reflect.setPrototypeOf()
// isExtensible捕获器:会在Object.isExtensible()中被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  isExtensible(target) {
    console.log('isExtensible()')
    return Reflect.isExtensible(...arguments)
  }
})

Object.isExtensible(proxy) // isExtensible()

// 返回值:
// isExtensible必须返回布尔值,表示target是否可以扩展
// 不是布尔值的时候会被转换为布尔值

// 执行捕获器的操作:
// Object.isExtensible(proxy)
// Reflect.isExtensible(proxy)


// 捕获器参数:
// target: 目标对象

// 捕获器不定式:
// 如果target不可以扩展,则处理程序必须返回false
// 如果target可以扩展,则处理程序必须返回true

// 对应反射API的方法:
Reflect.isExtensible()
// preventExtensions捕获器:会在Object.preventExtensions()中被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  preventExtensions(target) {
    console.log('preventExtensions()')
    return Reflect.preventExtensions(...arguments)
  }
})

Object.preventExtensions(proxy) // preventExtensions()

// 返回值:
// preventExtensions必须返回布尔值,表示target是否不可以扩展
// 不是布尔值的时候会被转换为布尔值

// 执行捕获器的操作:
// Object.preventExtensions(proxy)
// Reflect.preventExtensions(proxy)


// 捕获器参数:
// target: 目标对象

// 捕获器不定式:
// 如果Object.preventExtensions(proxy)的返回值是false --> 处理程序必须返回true

// 对应反射API的方法:
Reflect.preventExtensions()
// apply捕获器:会在调用函数的时候被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  apply(target, thisArg, ...argumentsList) {
    console.log('apply()')
    return Reflect.apply(...arguments)
  }
})

proxy() // apply()

// 返回值:
// 返回值没有限制

// 执行捕获器的操作:
// proxy(...argumentsList)
// Function.prototype.apply(thisArg, argumentsList)
// Function.prototype.call(thisArg, argumentsList)
// Reflect.apply(target, thisArg, argumentsList)


// 捕获器参数:
// target: 目标对象
// thisArg: 调用函数时this参数
// argumentsList: 调用函数时的参数列表

// 捕获器不定式:
// target必须是一个函数对象

// 对应反射API的方法:
Reflect.apply()
// construct捕获器:会在new操作符的时候被调用

const myTarget = {  };

const proxy = new Proxy(myTarget, {
  construct(target, argumentsList, newTarget) {
    console.log('construct()')
    return Reflect.construct(...arguments)
  }
})

new proxy // construct()

// 返回值:
// construct()必须返回一个对象

// 执行捕获器的操作:
// new proxy(...argumentsList)
// Reflect.construct(target, argumentsList, newTarget)


// 捕获器参数:
// target: 目标构造函数
// argumentsList: 传给目标构造函数的参数列表
// newTarget: 最初被调用的构造函数

// 捕获器不定式:
// target必须可以用作构造函数

// 对应反射API的方法:
Reflect.construct()

说明: 这些捕获器只有自己慢慢使用,你才能够觉得怎样搭配更适合自己

八、Promise

说明: promise是ES6新增的引用类型,可以通过new关键字来实例化,在实例化的时候需要一个函数作为参数(这个函数叫做执行器函数,这个函数是同步执行的,但是函数里面执行的resolve、reject的方法却是异步的),关于promise,它有三种状态,分别是待定(pedding)、成功(resolved)、失败(rejected),待定是promise最初的状态,在待定状态下,promise可以变成成功状态,也可以变成失败状态,不过,promise也可以永远在待定状态,不去切换,注意,一旦状态发生改变,是不可逆的

promsie的状态是私有的,不能通过js检测和修改,主要是为了避免读取到promise状态的时候将其用同步的方式进行处理,从而将异步操作封装起来隔离外部的同步代码

注意: 根据事件循环的定义,同时存在同步任务和异步任务的时候,先执行完所有的同步任务,再来执行异步任务

(1)用途

说明: promise的用途有两个,一个是将异步操作抽象一下,其状态表示其promise是否完成,待定状态表示尚未开始或者正在进行中,成功状态表示已经完成,失败状态表示没有完成,然后根据状态去提供响应的信息,另一个作用在于promise封装的异步操作会生成某个值,当由待定状态变成成功状态或者失败状态的时候,都会得到一个信息对象,这个对象包含了成功的信息或者是失败的信息,信息对象的默认值是undefined

由于promise的状态是私有的,所以对promise的操作只能在执行器函数中实现,这个函数的作用有两个,一个是初始化promise的异步行为,一个是控制其状态的切换,控制状态的切换由它的两个参数函数实现(通常一个为resolve,另一个为reject,前者将状态由等待状态变为成功状态,后者将等待状态变为失败的状态,当然是不可逆的)

(2)Promise.resolve

说明: 通过调用这个方法可以创建一个成功的promise对象,这个方法的参数只有一个,它保存了promise成功创建保存的值,如果参数过多的话,只会保留一个

// 这两种创建成功状态的promsie的代码是等价的
let p = new Promise((resolve, reject) => resolve());
let p1 = Promise.resolve()
let p2 = Promise.resolve(1)
let p3 = Promise.resolve(1,2)
let p4 = Promise.resolve(p3)

// 参数多余的话会被省略掉
console.log(p1) // Promise {<fulfilled>: undefined}
console.log(p2) // Promise {<fulfilled>: 1}
console.log(p3) // Promise {<fulfilled>: 1}

// 如果这个静态方法接收到一个promise为参数的话,包装后的promise与
// 这个参数promsie是等价的
conosle.log(p4) // Promise {<fulfilled>: 1}
console.log(p4 === p3) // true

注意: 当然,这个静态方法能够包装任何不是promsie的值,包括错误的对象,所以在使用的时候需要注意

(2)Promise.reject

说明: 既然有Promise.resolve的方法,那也会有Promise.reject的方法了,这个方法用于创建一个失败状态的promise,创建和使用跟上面一样,不过需要注意的是,它会抛出一个异步的错误,这个错误只能被它自己接收,错误的信息由Promise.reject方法的第一个参数决定,特别的地方在于这个参数会向后传递,也就是如果Promise.reject接受一个promsie作为参数的话,那么这个promise的返回值就会变成Promise.reject方法的参数,导致创建的前后并不等价

// 这两种创建成功状态的promsie的代码是等价的
let p = new Promise((resolve, reject) => reject());
let p1 = Promise.reject()
let p2 = Promise.reject(1)
let p3 = Promise.reject(1,2)
let p4 = Promise.reject(p3)

// 参数多余的话会被省略掉
console.log(p1) // Promise {<rejected>: undefined}
console.log(p2) // Promise {<rejected>: 1}
console.log(p3) // Promise {<rejected>: 1}

// 如果这个静态方法接收到一个promise为参数的话,失败状态下的Promise
// 所获取的结果就是这个promise参数的返回值,那么就并不等价了
conosle.log(p4) // Promise {<rejected>: Promise}
console.log(p4 === p3) // false

(3)Promise.prototype.then

说明: 这个方法可以为promise添加处理程序,它最多接收两个参数,分别在成功状态下执行(onResolved)以及失败状态下执行(onRejected),这两个参数是可以选择的,如果传递的参数不是函数类型,那么这个参数会被忽略掉由于promise状态的切换是不可逆的,那么这两个操作的话就是互斥的

// then的使用:(举例)

let p1 = new Promise((resolve, reject) => resolve()) // 成功状态的promise
let p2 = new Promise((resolve, reject) => reject()) // 失败状态的promise

// then只会执行一种状态下面的程序
p1.then(() => console.log(111), () => console.log(222)) // 111
p2.then(() => console.log(111), () => console.log(222)) // 222

p1.then('如果不是函数作为参数传递给then那么这个传递的参数就会被忽略掉')

// 只需要第二个参数的时候第一个参数使用undefined或者是null占位
p1.then(undefined / null, () => console.log(222)) // 111

注意: 如果只想提供onRejected参数的话,第一个参数的位置需要给一个undefined或者null去占位,避免在内存中创建多余的对象,注意,then方法执行完毕之后会返回一个新的promise实例,那么执行前后的promise就不会相等了

// 没有参数穿给then方法,信息向后传递
let p1 = Promise.resolve('foo')
let p2 = p1.then()
console.log(p2) // Promise <resolved>: foo

// 参数没有明显的返回值,返回undefined
let p3 = p1.then(() => undefined)
let p4 = p1.then(() => { })
let p5 = p1.then(() => Promise.resolve())
console.log(p3, p4, p5) // Promise <resolved>: undefined

// 有明显的返回值
let p7 = p1.then(() => Promise.resolve('bar'))
let p8 = p1.then(() => 'bar')
console.log(p7, p8) // Promise <resolved>: bar

// 会保留promise的状态
let p9 = p1.then(() => Promise.reject())
console.log(p9) // Promise <rejected>: undefined

注意: 对于then而言,它默认执行的是第一个函数,如果在执行的时候没有传递参数给它, 那么它会将执行这个then方法的promise的信息向后传递,如果这个参数函数,没有明显的返回值,那么会返回undefined,如果有明显的返回值,那么这个值就会被Promise.resolve包装起来,当然,如果这个值是一个promise,那么这个promise的信息也会被保存进来

// 抛出异常的函数会返回失败状态的promise
let p10 = p1.then(() => { throw 'baz' })
console.log(p10) // Promise <rejected>: baz

// 返回错误值的函数会被then包装在一个成功的promise中返回
let p11 = p1.then(() => Error('baz'))
console.log(p11) // Promise <resolved>: Error: baz

对于onRejected处理程序来说,当它捕捉到失败状态的信息的时候,它会将这个失败的信息包装成一个成功状态的promise返回,其它跟onResolved是一致的

(4)Promise.prototype.catch

说明: 这个方法就是then方法只传递第二个参数的简写形式,它们写法是等价的,但结果并不等价,当然,这个方法的调用也会生成一个新的promsie实例

let p1 = Promise.resolve()
let p11 = p1.then(null, () => { throw 'baz' })
let p12 = p1.catch(() => { throw 'baz' })

// 由于它们都是返回的一个新的实例,所以结果并不相等
console.log(p11 === p12) // false

(5)Promise.prototype.finally

说明: 这个方法无论在成功状态下还是在失败状态下都会执行,并且它只有一个函数参数,当然,这个方法和之前的方法一样,执行的时候会返回一个新的promise实例

// finally方法的使用:
let p1 = Promise.resolve()
let p2 = Promise.reject()
let p3 = p1.finally(() => console.log(111)) // 111
p2.finally(() => console.log(222)) // 222

// 由于finally方法执行的时候会返回一个新的promsie,也就会导致执行前后并不等价
console.log(p3 === p1) // false

由于finally这个方法与状态无关,它大多数的时候是将父promise的信息进行传递,但如果返回的是一个待定的promise或者finally方法在执行的时候抛出错误或执行的时候返回一个失败状态的promise,那么就会返回这个待定或失败的promise的信息

// 这里创建一个成功状态的promise
let p1 = Promise.resolve() 

// 下面这个情况调用finally方法都会返回p1的信息 --> Promise <resolved>: undefined
let p2 = p1.finally() 
let p3 = p1.finally(() => undefined) 
let p4 = p1.finally(() => 'hahah') 
let p5 = p1.finally(() => Error('bar')) 
let p6 = p1.finally(() => Promise.resolve()) 
let p7 = p1.finally(() => Promise.resolve('bar')) 

// 保留promise信息的情况:
let p8 = p1.finally(() => new Promise(() => {})) // --> Promise <pedding>
let p9 = p1.finally(() => Promise.reject('bar'))  // --> Promise <rejected>: bar

(6)promise的非重入性

说明: 听起来很高大上,这个简单来说就是promsie的thencatchfinally方法中的函数参数的执行是异步的必须要等到所有的同步代码执行完毕之后才执行这里面定义的程序

非重入特性:当promsie进入落定状态时,与该状态相关的处理程序仅仅会被排期,而并非立即执行,跟在这个处理程序之后的同步代码一定会在处理程序之前执行,这个特性由js运行时保证。

(7)邻近处理程序的执行顺序

说明: 如果一个promise添加了catch、then、finally方法的话,那么它们会按照从左到右的顺序依次执行

这里注意一下: catch、then、finally方法的参数不都是一个函数嘛,其实这些函数是有参数的,如果是成功状态下执行,那么这个参数一般用data来表示成功状态下所获取到的数据,如果是失败状态下执行,那这个参数一般用error来表示错误的信息

let p1 = Promise.resolve('111')
let p2 = Promise.reject('222')

// 成功和失败的信息都会保留在参数之中
p1.then((data) => console.log(data)) // 111
p2.catch((error) => console.log(error)) // 222

(8)失败状态的promise与拒绝错误处理

说明: 失败状态的promsie类似于throw的表达式,它们都代表一种需要中断或特殊处理的状态,在promise执行的时候抛出错误会生成失败状态的promise,对应的错误对象会成为promise存储的错误信息,所有promise错误的抛出都是异步的,所以不会阻止程序的正常运行

九、异步函数

说明: 异步函数使用async/await关键字声明,是promise在ECMAScript函数中的使用,它在行为和语法上面都增强了js,让以同步方式写的代码能够异步执行

// 下面这样写的话很不方便,因为其他的代码必须添加到promise的参数函数中才可以:
let p = new Promise((resolve, reject) => {
  resolve(3)
})

p.then((data) => {
  console.log(data)
})

(1)async

说明: 这个关键字用于声明异步函数,它可以用在函数声明、函数表达式、箭头函数和方法上面:

// 函数声明:
async function foo() { }

// 函数表达式:
let bar = async function () { }

// 箭头函数:
let baz = async () => { }

// 方法名:
class A {
  async foo() { }
}

使用async关键字可以让函数具有异步的特征,但是求值方面的话是一个同步的行为

// 先打印1,后打印2,表示函数求值的操作是一个同步的行为
async function foo() {
  console.log(1)
}

foo()

console.log(2)

异步函数如果使用了return关键字(没有就会返回undefined),所返回的这个值会被Promise.resolve()方法进行包装,前面说到过,这个方法的执行是一个异步的方法,那么return关键字所返回的值应该在同步方法返回值的后面`,同时,异步函数始终会返回promise对象,在函数外部调用这个函数可以得到返回的promsie对象

async function foo() {
  console.log(1)
  return 3
}
// 这里的3最后打出表示return返回的值是异步读取的,同时这个值是被promsie.resolve方法进行包装后的值
foo().then((data) => console.log(data))

foo()

console.log(2)

// // 这里由返回值可以得到执行异步函数得到的是一个成功状态的promsie对象
let a = foo()
console.log(a) // Promise {<fulfilled>: 3}
  • 如果在异步函数中抛出错误,那么会返回失败状态的promise
  • 但是失败状态的promsie的错误信息是无法被异步函数获取的
async function foo() {
  console.log(1)
  throw 3
}

// 这里的3最后返回,也印证了异步函数中抛出的错误是可以被捕获到的
foo().catch((err) => console.log(err))

// 也同样证实了会返回失败状态的promsie,一般情况都是成功状态的
console.log(foo()) // Promise {<rejected>: 3}

console.log(4)
async function foo() {
  console.log(1)
  Promise.reject(3)
}

// 这里报错表示失败状态的promsie的错误信息是不可以被异步函数所捕获到的
foo().catch((err) => console.log(err)) // Uncaught (in promise) 3

(2)await

说明: 因为异步函数针对是不会马上完成的任务,那么自然就需要一种暂停和执行恢复的执行的能力,此时await关键字就可以完成这样的任务,await关键字会等待一个promise的执行并同时会暂停异步函数函数后面的代码,等到计算出promise的返回值的时候将值传递给表达式,再恢复异步函数的执行(await关键字可以单独使用也可以在表达式中使用)

// 举例:
async function foo() {
  return await Promise.resolve('foo')
}

foo().then((res) => console.log(res))
  • 如果await等待抛出错误的同步操作,那么会返回失败状态的promise
  • 但是失败状态的promsie的错误信息是可以被await关键字所返回的
// 举例:
async function foo() {
  await (() => {
    throw 3
  })()
}

// 这里会捕捉到这个错误的信息3
foo().catch((err) => console.log(err))
// 举例:
async function foo() {
  await Promise.reject(3)
}

// 这里也会捕捉到这个错误的信息3
foo().catch((err) => console.log(err))

注意: 只要出现await关键字,就必须存在async关键字,因为await关键字只存在于异步函数中,如果await关键字出现在同步函数里面,那么就会报错

(3)异步函数的停止与恢复

说明: 对于异步函数的async/await关键字来说,async关键字只是起到一个标识符的作用,重要的其实是await关键字,如果没有这个关键字的话,那么函数的执行跟普通函数的执行没什么区别,其次,当js运行碰到await关键字的时候,会记录从这里暂停,然后向队列中推送一个这个函数恢复执行的任务并退出这个函数的执行,当同步任务都执行完毕之后,再从队列里面按顺序取出需要恢复执行的函数依次执行,直到所有的异步任务全部执行完毕为止(注意,await关键字后面的代码都会变成异步操作)

// 以这个例子来说:

async function foo() {
  console.log(2)
  console.log(await Promise.resolve(8))
  console.log(9)
}

async function bar() {
  console.log(4)
  console.log(await 6)
  console.log(7)
}

console.log(1)
foo()
console.log(3)
bar()
console.log(5)


// 执行结果:1、2、3、4、5、8、9、6、7

// 执行顺序:
// 1.打印数字1
// 2.调用异步函数foo
// 3.在foo函数中打印数字2
// 4.在foo函数中遇到await关键字,暂停函数的执行,同时向任务队列中发送一个foo回复执行的任务,并退出foo函数
// 5.打印数字3
// 6.调用异步函数bar
// 7.在bar函数中打印数字4
// 8.在bar函数中遇到await关键字,暂停函数的执行,同时向任务队列中发送一个bar回复执行的任务,并退出bar函数
// 9.打印数字5,此时所有的同步任务已经全部执行完毕,开始执行队列中的任务,按顺序的话依次是foo、bar
// 10.在异步函数foo中,由于都是异步操作那就按顺序执行,依次打印数字8、9
// 11.在异步函数bar中,由于都是异步操作那就按顺序执行,依次打印数字6、7

十、迭代器与生成器

(1)迭代的理解

说明: 在JS中,计数循环是一种最简单的迭代,循环是迭代的基础,因为它可以指定迭代的次数,以及每次迭代要执行什么操作,每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序是定义好的

// 循环迭代:
for(let i = 0; i < 10; i++) {
    console.log(i)
}

迭代会在一个有序(集合的所有项的顺序是一定的)集合上面进行,数组就是一个有序集合的例子,由于数组长度已知,每一项都可以根据索引来获取,所以数组可以通过递增索引来遍历,但它存在以下的缺点:

  • 迭代之前需要知道如何使用数据结构
  • 遍历的顺序不是数据结构固有的
// 数组的迭代:
let collection = ['bar', 'foo'];

for(let i = 0; i < collection.length; i++) {
    console.log(collection[i])
}

(2)迭代器模式

说明: 有些结构被称为可迭代对象,因为它们实现了可迭代对象接口(Iterable),可以通过迭代器(Iterator) 消费,可迭代对象可以理解成数组或者是集合这样的集合类型的对象或者是类数组的数据结构,它们包含的元素是有限的,而且存在无歧义的遍历程序,迭代器是按需创建的一次性对象,每个迭代器都可以关联一个可迭代的对象,而迭代器会暴露这个对象上面的迭代API,迭代器无需关心其关联的可迭代对象的数据结构,只需要知道如何取连续的值就可以

(3)可迭代协议

说明: 实现可迭代对象需要满足两个条件,支持迭代自我识别能力和创建实现迭代器接口的对象的能力,那么也就意味着需要暴露一个属性来作为默认的迭代器,而这个属性需要用Symbol.iterator作为键,而值的话需要引用一个迭代器工厂函数来返回一个新的迭代器

// 检测内置类型是否是可迭代对象,就看它的Symbol.iterator
// 属性的返回值是不是一个函数

let str = ''
console.log(str[Symbol.iterator]) // f values() { [native code] }
  • 实现迭代对象的内置对象: 字符串、数组、Map结构、Set结构、arguments对象、NodeList等DOM集合类型
  • 没有实现迭代对象的内置对象:数值、对象
  • 触发迭代器接口的操作: for...of、数组解构、扩展运算符、Array.from()、Map的创建、Set的创建、Promise.all()、Promise.race()、yield*操作符

注意: 如果对象原型链上的父类型实现了可迭代对象接口,那么这个对象也实现了这个接口

(4)迭代器协议

说明: 迭代器是一种一次性使用的对象,用于迭代与其关联的对象,迭代器API使用next方法在可迭代对象中遍历数据,每次成功调用next方法的时候,就会返回一个对象,对象中有两个属性,一个是done(这个属性的值是一个布尔值,如果为false,表示可以继续往下面遍历取值,如果变成true的话,后续往下遍历的时候得到的值是相同的,也就是undefined了),一个是value(它表示每次遍历所遍历到的值)

// 迭代器使用举例:
let arr = ['foo', 'baz'];

// arr[Symbol.iterator]得到的是一个函数体,需要执行才可以得到迭代器
let Iarr = arr[Symbol.iterator]();

// 调用next方法进行取值
console.log(Iarr.next()) // {value: 'foo', done: false}
console.log(Iarr.next()) // {value: 'baz', done: false}
console.log(Iarr.next()) // {value: undefined, done: true}
console.log(Iarr.next()) // {value: undefined, done: true}
console.log(Iarr.next()) // {value: undefined, done: true}

注意: 每个迭代器之间的工作是相互独立的

// 这里可以看出每个迭代器是互不影响的
let arr = ['foo', 'baz'];

let Iarr = arr[Symbol.iterator]();
let Iarr1 = arr[Symbol.iterator]();

console.log(Iarr.next()) // {value: 'foo', done: false}
console.log(Iarr1.next()) // {value: 'foo', done: false}
console.log(Iarr.next()) // {value: 'baz', done: false}
console.log(Iarr1.next()) // {value: 'baz', done: false}

console.log(arr === Iarr) // true
  • 同时,如果可迭代对象在迭代的期间发生了修改,那么迭代器在使用的时候其获取的内容也会发生相应的变化

  • 因为每个迭代器实现了可迭代对象的接口,那么Symbol.iterator属性引用的工厂函数会返回相同得到迭代器,也就是它们可以用在任何期待可迭代对象的地方

let arr = ['foo', 'baz'];

let Iarr = arr[Symbol.iterator]();

console.log(Iarr.next()) // {value: 'foo', done: false}

// 在迭代的过程中我添加元素到数组中去,下面我调用next方法的时候,
// 这个元素也会被遍历出来
arr.splice(1, 0, 'bar')

console.log(Iarr.next()) // {value: 'bar', done: false}
console.log(Iarr.next()) // {value: 'baz', done: false}

(5)自定义迭代器

说明: 为了让可迭代对象可以创建多个迭代器,那么就必须每创建一个迭代器就对应一个计数器,就可以把计数器放在闭包中,然后通过闭包来返回迭代器(如果不这样创建就会出现迭代器只能够使用一次的情况

// 这里举个例子:

class Counter {
  constructor(limit) {
    this.limit = limit
  }

  [Symbol.iterator]() {
    let count = 1
    let limit = this.limit

    return {
      next() {
        if (count <= limit) {
          return { done: false, value: count++ }
        } else {
          return { done: true, value: undefined }
        }
      }, 
      
      return() {
        console.log('Exiting early')
        // 这个方法一般只返回{ done: true }就可以
        return { done: true }
      }
    }
  }
}

let counter = new Counter(3)

// 这里能够多次调用迭代器是原因是是用来闭包的好处
for (let i of counter) {
  console.log(i) // 1、2、3
}

for (let i of counter) {
  console.log(i) // 1、2、3
}

// 迭代器的关闭
for (let i of counter) {
  if(i > 2) {
      break;
  }
  console.log(i) // 1、2、Exiting early
}

// 再次执行
for (let i of counter) {
  if(i > 2) {
      break;
  }
  console.log(i) // 3
}

注意: return这个方法是可选的,这个方法用于关闭迭代器,一般在使用for...of循环需要提前退出的时候和解构操作并没有使用所有值得时候,这个return的方法就会自己被调用,如果迭代器没有被关闭,那么下次进行迭代的时候还是从上次离开的地方开始,但并不是所有的迭代器都是可以关闭的(数组的迭代器就不可以关闭),如果想要知道这个迭代器是否可以被关闭,检测一下迭代器实例上面有没有return属性就可以,如果一个迭代器是不可以关闭的,手动添加一个return方法并执行的话迭代器是不会进入关闭状态的,但是return方法还是会被调用

(6)生成器

说明: 生成器的形式是一个函数,函数名称前面加上一个*表示它是一个生成器,只要可以定义函数的地方,都可以定义生成器,关于这个*他的位置不受左右两侧空格的影响

// 例如:

// 生成器函数声明:
function* name() {}

// 生成器函数表达式:
let Name = function* name() {}

// 作为对象字面量方法的生成器函数:
let foo = {
  * name() {}
}

注意: 箭头函数不能用来定义生成器

// 生成器的next方法:
function* name() {}

const g = name()

console.log(g.next) // f next() { native code }
console.log(g.next()) // { done: true, value: undefined }

调用生成器函数会产生一个生成器对象,生成器对象一开始处于暂停的状态,与迭代器类似,生成器对象也实现了迭代器接口(在调用生成器函数的时候,它会自动调用迭代器接口),因此具有next的方法,调用这个方法可以让生成器开始执行,它的返回值跟迭代器类似,也有一个done属性和value属性,如果函数体为空的生成器,调用一个next方法就会将done的状态变成true,当然,value的属性默认值也是undefined,返回值由生成器函数确定

(7)yield关键字

说明: 在生成器函数中,返回值不仅可以使用return关键字,也可以使用yield关键字,它的作用是首先可以返回值,返回值跟调用next方法返回的类型是一样,不过它退出时所返回的状态一直是false(done: false),而return退出时的状态则是true,当然,使用生成器函数创建的不同的生成器中的yiled关键字是不影响的

// yield和return关键字所返回内容的区别
function* name() {
  yield 'foo';
  return 'bar';
};

let Name = name();

console.log(Name.next()) // { value: foo, done: false }
console.log(Name.next()) // { value: bar, done: true }
// 每个生成器函数中的yield关键字是独立的
function* name() {
  yield 'foo';
  return 'bar';
};

let Name = name();
let Name2 = name();

console.log(Name.next()) // { value: foo, done: false }
console.log(Name2.next()) // { value: foo, done: false }

注意: yield关键字只能在生成器函数中才能使用,嵌套在非生成器函数中使用就会出现语法错误

// 这里yield关键字也可以用来做输出使用:
function* name(arg) {
  console.log(arg)
  console.log(yield)
  console.log(yield)
};

let Name = name('foo')

// 注意:第一次并没有输出haha,是因为第一次调用next方法是为了启动生成器函数
Name.next('haha') // foo
Name.next('hehe') // hehe
Name.next('heihie') // heihei
function* name() {
  return yield 'foo'
};

let Name = name()

// 函数需要对整个表达式求值才知道需要返回什么,那么它在遇到yield关键字
// 的时候暂停并计算出响应的值,也就是foo,下一次next调用传入了haha将它
// 作为yield关键字的返回值给return返回
Name.next() // { value: foo, done: false }
Name.next('haha') // { value: haha, done: true }
// yield关键字也可以使用多次(next方法取值使用value来取值):
function* name() {
  for(let i = 0; ; i++) {
      yield i
  }
};

let Name = name()

Name.next().value // 0
Name.next().value // 1
Name.next().value // 2
...

(8)生成器做可迭代对象

说明: 一般在需要定义一个可迭代对象,可以使用生成器来解决

// 举例:
function* name() {
  yield 1;
  yield 2;
  yield 3;
};

for (let i of name()) {
  console.log(i) // 1、2、3
}

(9)使用yield产生可迭代对象

说明: 可以使用*来增强yield关键字的行为,让它可以迭代一个可迭代的对象,从而一次产生一个值

// 这两段生成器函数的声明代码是等价的:
function* gen() {
  for (let x of [1, 2, 3]) {
    yield x
  }
}

function* gen() {
  yield* [1, 2, 3]
}

let Gen = gen()

for(let i of gen()) {
  console.log(i) // 1、2、3
}

使用*可以简化迭代操作,同时*是不受左右两侧空格的影响的,但是有一点需要注意,yield*的值是关联迭代器返回done: true是的属性值,对于普通迭代器来说,这个值是undefined,但对于生成器函数产生的迭代器来说,这个值是生成器函数所返回的值

// 普通迭代器:
function* gen() {
  console.log(yield [1, 2, 3])
}

for (const x of gen()) {
  // [1, 2, 3]:这个是迭代的时候打印出来的
  // undefined:由于迭代的时候执行了生成器函数,那么undefined就作为yield后面的值返回
  console.log(x) // [1, 2, 3]和undefined
}
// 生成器函数产生的迭代器:

// 这是一个生成器函数
function* gen() {
  yield 'foo';
  return 'bar'
}

// 在生成器函数中产生迭代器
function* Gen() {
  console.log(yield* gen())
}

for (const x of Gen()) {
  // foo:当执行到gen生成器函数是,看到yield关键字就停止,然后返回其内容,也就是foo
  // bar:由于迭代的时候执行了生成器函数产生的迭代器,那么bar就作为yield后面的值返回,yield*后面的值需要return关键字返回
  console.log(x) // foo、bar
}

(10)生成器作为默认的迭代器

说明: 因为生成器对象实现了可迭代对象的接口,而且生成器函数和默认迭代器在被调用之后都产生迭代器,所以生成器可以作为默认的迭代器

// 举例:

class Foo {
  constructor() {
    this.values = [1, 2, 3]
  }

  *[Symbol.iterator]() {
    yield* this.values
  }
}

const foo = new Foo()

for (const x of f) {
  console.log(x) // 1、2、3
}

(11)终止生成器

说明: 跟迭代器一样,生成器也可以提前关闭。一个可迭代对象的接口的对象一定有next这个方法和return这个可选的方法,生成器除了这两个方法外,还有一个throw的方法,return和throw方法都可以强制生成器进入关闭的状态

// return: 提供给return方法的值,就死终止迭代器所返回的值

function* gen() {
  yield* [1, 2, 3]
}

const g = gen()

console.log(g) // gen { <suspended> }
console.log(g.return(1)) // {value: 1, done: true}
console.log(g) // gen { <closed> }
// return的注意点:

function* gen() {
  yield* [1, 2, 3]
}

const g = gen()

console.log(g.next()) // {value: 1, done: false}
console.log(g.return(1)) // {value: 1, done: true}
console.log(g.next()) // {value: undefined, done: true}
console.log(g.next()) // {value: undefined, done: true}
console.log(g.next()) // {value: undefined, done: true}
console.log(g.next()) // {value: undefined, done: true}
console.log(g.next()) // {value: undefined, done: true}
console.log(g.next()) // {value: undefined, done: true}

for (let i of gen()) {
  if (i > 1) {
    gen.return(4) // 此时done的状态会被切换成true,那么后面的值就不会被显示出来了
  }

  console.log(i)
}

注意: 与迭代器不同的是,所有生成器对象都有return的方法,只要通过这个方法进入关闭的状态,就无法恢复了,后续调用next方法 就会一直显示done: true的状态,任何提供的返回值都不会被存储以及传播,在for-of等内置语言结构会忽略状态为done:true的返回值

// throw: 会在暂停的时候将一个提供的错误注入到生成器对象中去,如果错误没有被处理,那么生成器就会被关闭

function* gen() {
  yield* [1, 2, 3]
}

const g = gen()

console.log(g) // gen { <suspended> }

try {
  g.throw('foo')
} catch (error) {
  console.log(error) // foo
}

console.log(g) // gen { <closed> }

注意: 如果在生成器函数内部处理了这个错误,那么生成器就不会停止,而且可以恢复执行,错误处理会跳过对应的yield值

function* gen() {
  for (const x of [1, 2, 3]) {
    try {
      yield x
    } catch (error) {
      console.log(error) // foo
    }
  }
}

const g = gen()

console.log(g.next()) // {value: 1, done: false}
g.throw('foo')
console.log(g.next()) // {value: 3, done: false}

十一、class

(1)基础知识

==> 类的定义 <==

说明: 类是ECMAScript中新的基础性语法糖的结构,它与函数类型相似,主要存在两种定义的方法,一种是类声明,一种是类表达式

// 类声明
class Person {}

// 类表达式
const Animal = class {}

注意: 跟函数表达式类似,类表达式在它们被赋值前是不可以被引用的,不过有一点不同,就是函数声明存在提升的情况,但是类不存在

// 函数表达式在求值前是不可以用的
console.log(fn) // undefined
var fn = function () { }
console.log(fn) // function() {}

// 类表达式也是一样
console.log(ce) // undefined
var ce = class { }
console.log(ce) // class {}

// 但是函数声明存在声明提升
console.log(Fn) // undefined
function Fn() { }
console.log(Fn) // Fn() {}

// 但是类声明却不行
console.log(Ce) // 报错
class Ce { }
console.log(Ce) // class Ce {}

注意: 函数存在函数作用域的限制,但是类受块级作用域的影响

{
  function fn() { }
  class ce { }
  console.log(ce) // ce {}
}

console.log(fn) // fn() {}
console.log(ce) // ce is not defined

==> 类的构成 <==

说明: 类可以包含函数方法、实例方法、获取函数、设置函数、静态方法等,但这些都不是必须的,其次,与构造函数一样,类名的首字母建议大写,依次来区别创建的实例

// 空的类
class Foo { }

// 有构造函数的类
class Bar {
  constructor() { }
}

// 有获取函数的类
class Baz {
  get foo() { }
}

// 有静态方法的类
class Baq {
  static my() {}
}

注意: 在把类表达式赋给一个变量之后,可以通过name属性取得类表达式的名称的字符串,不过这个获取的名称的字符串不能在类表达式作用域外部使用

let Person = class PersonName {
  foo() {
    // 表示都可以通过name属性来获取类表达式的名称
    console.log(Person.name, PersonName.name) // PersonName PersonName
    console.log(PersonName) // class PersonName { foo() { console.log(Person.name, PersonName.name) console.log(PersonName) }}
  }
}

let p = new Person()

p.foo()

console.log(Person.name) // PersonName

// 这个名称字符串在类的作用域外是无法使用的
console.log(PersonName) // PersonName is not defined

(2)类构造函数

==> constructor <==

说明: 方法名constructor会告诉解释器在使用new操作符创建类的新实例时,应该调用这个函数,当然,这个构造函数的定义不是必须的,不定义的话相当于将构造函数定义为空函数

new关键字进行实例化所做的事:

  • 在内存中创建一个新的对象

  • 在新对象内部的[[Prototype]]指针被赋值为构造函数的prototype属性

  • 构造函数内部的this被赋值为这个新的对象

  • 执行构造函数内部的代码逻辑

  • 如果构造函数返回非空对象,则返回该对象,否则返回刚创建的新对象

class Animal { }

class Person {
  constructor() {
    console.log(111)
  }
}

class Foo {
  constructor() {
    this.foo = 'baz'
  }
}

// 类实例化时传入的参数会作为构造函数的参数
class Bar {
  constructor(name) {
    // 表示实例化的时候会创建一个name属性,这个属性的值是实例化
    // 的时候传进来的值,这里的name值只是作为一个占位符而已
    this.name = name
  }
}

let a = new Animal()

let P = new Person() // 111

// 如果不需要参数类名后面的括号也是可以省略的
let f = new Foo
let f = new Foo()
console.log(f.foo) // baz

let b = new Bar('zhangsan')
console.log(b.name) // zhangsan

注意: 默认情况下类构造函数在执行完毕之后会返回一个this对象,这个对象会与实例化出来的对象相关联,如果没有什么引用新创建的this对象,那么这个对象就会被销毁;此外,如果返回的不是this对象而是其他的对象的话,那么实例化出来的对象就不能被instanceof操作符所检测出来它与类有关

class Person {
  constructor(boo) {
    if (boo) {
      return {
        foo: 1
      }
    }
  }
}

let p1 = new Person()
let p2 = new Person(true)

console.log(p1 instanceof Person) // true

// 因为返回的不是this对象,那么实例化出来的这个对象与那个类是没有血缘关系的
console.log(p2 instanceof Person) // false 

类构造函数与构造函数的区别: 调用类构造函数必须使用new关键字,忘记使用new关键字就会抛出错误,而构造函数可以不使用,因为那时会以全局的this作为内部对象

function Person() { }

class foo { }

// 构造函数不使用new关键字进行实例化操作的话会把this来构建实例
let p = Person()

// 但是类构造函数必须使用new关键字来实例化,否则会报错
let f = foo() // 报错

let f1 = new foo()
// 使用类的构造函数来创建实例的时候也需要使用new关键字才可以,否则会报错
let f2 = new f1.constructor()

==> 类是特殊函数 <==

// 如果将类看作成一个特殊函数,那么声明一个类之后,可以使用typeof操作符来检测,
// 表示它是一个函数
class Person { }

console.log(Person) // class Person { }
console.log(typeof Person) // function

// 然而类也有prototype原型,这个原型上面存在一个constructor属性指向自己
console.log(Person.prototype) // { constructor: f() }
console.log(Person === Person.prototype.constructor) // true

// 那么,它也可以使用instanceof操作符来检测构造函数与创建的实例的关系
let p = new Person()
console.log(p instanceof Person) // true

// 类本身在使用new的时候就会被当成构造函数,区别在于类中定义的constructor方法
// 并不会被当做构造函数,那么使用instanceof操作符的时候也就会返回false了,如果
// 在创建实例的时候是使用constructor这个构造函数创建的话,那么使用instanceof操
// 作符的结果就会完全相反了
let p1 = new Person()
let p2 = new Person.constructor()

console.log(p1.constructor === Person) // true
console.log(p1 instanceof Person) // true
console.log(p1 instanceof Person.constructor) // false

console.log(p2.constructor === Person) // false
console.log(p2 instanceof Person) // false
console.log(p2 instanceof Person.constructor) // true

// 其次,类可以像函数一样在任何地方进行定义
let classList = [
  class {
    constructor(id) {
      this.id = id
      console.log(this.id)
    }
  }
]

function foo(className, id) {
  return new className(id)
}

let bar = foo(classList[0], 1314) // 1314

// 同时,类也可以像立即执行函数那样立即实例化
let P = new class Baz {
  constructor(x) {
    console.log(x)
  }
}('haha') // haha

(3)类成员与方法

==> 实例成员 <==

说明 :每次通过new关键字创建实例的时候,都会执行构造函数,在这个构造函数的内部,会用this表示创建的这个新的实例对象(当然,每次创建的实例不同,this的指向不一致,也就是它们的属性和方法都不会共享了),那么就可以往这个实例对象上面添加属性和方法了,当然,在实例创建完毕之后依然可以给实例去添加属性或者是方法

class Person {
  constructor() {
    // 往创建的实例上面添加一个name的属性,它的值为zhangsan
    this.name = new String('zhangsan')

    // 同样的道理也可以添加方法
    this.sayName = () => console.log(this.name)

    this.changeName = ['wangwu', 'laoliu']
  }
}

let p1 = new Person()
let p2 = new Person()

// 使用定义的方法
p1.sayName() // zhangsan
p2.sayName() // zhangsan

// 这里就表示虽然创建的模板是一样的,但是内在的行为不同(你是你,我是我)
console.log(p1.name === p2.name) // false
console.log(p1.sayName === p2.sayName) // false
console.log(p1.changeName === p2.changeName) // false

// 在实例创建完毕之后还是可以操作其属性的
p1.name = p1.changeName[1]
console.log(p1.name) // laoliu

注意: 在类中,给this上面绑定的属性和方法是每个实例独自拥有的东西,上面解释过,就算你定义的属性和方法是一模一样的,但是它们使用起来并不是等价的,如果想定义实例间共享的方法的话,直接在类中定义即可(就是不写在constructor构造函数中的方法),还有不能将原始类型或对象作为成员的数据,一般都是通过方法的形式来写的

class Person {
  constructor() {
    // 这种定义在constructor内部的属性或者是方法是不共享的
    this.name = new String('zhangsan')
  }
  
  // 这种方法会被定义在原型上面
  locate() {
      console.log(111)
  }
  
  // 注意,原始值不可以作为成员数据
  name: 'zhangsan'
  
  // 一般都是通过定义方法来写,当然方法名可以是字符串、Symbol值、也可以是计算的值
  [Symbol('haha')]() {
      console.log(222)
  }
  
  ['1' + '2' ]() {
      console.log(333)
  }
}

==> 存值器与取值器 <==

class Person {
  // get表示取值
  get() {
    return this.name
  }

  // set表示存值
  set(value) {
    this.name = value
  }
}

// 首先创建一个p的实例
let p = new Person()

// 这里给实例的name属性进行赋值,此时就会触发实例中的存值器set函数,这个函数有一个参数,
// 这个参数表示的含义就是当前属性修改后的新值,然后通过函数内部的this.name = value操作
// 来改值,将name属性的值标称zhangsan
p.name = 'zhangsan'

// 当需要用到某个值得时候就会触发定义的取值器get,它会根据定义的程序来返回相应的值
console.log(p.name) // zhangsan

==> 静态方法 <==

说明: 静态方法不需要实例的操作就可以使用,定义时在方法名前面加上static关键字将其修饰为一个静态方法

class Person {
  constructor() {
    // 定义在实例上面
    this.locate = () => console.log(this)
  }

  // 定义在类上面
  static foo() {
    console.log(this)
  }

  // 定义在类的原型上面
  baz() {
    console.log(this)
  }
}

let p = new Person()

p.locate() // Person {  }
Person.prototype.baz() // { constructor: ... }
Person.foo() // class Person { }
console.log(p.locate() === Person.foo()) // true

==> 迭代器与生成器 <==

class Person {
  // 原型上面定义生成器
  *foo() {
    yield 1
    yield 2
    yield 3
  }

  // 实例上面定义生成器
  static * baz() {
    yield 4
    yield 5
    yield 6
  }
}

let F = Person.baz()
console.log(F.next().value) // 4
console.log(F.next().value) // 5
console.log(F.next().value) // 6

let p = new Person()
let P = p.foo()
console.log(P.next().value) // 1
console.log(P.next().value) // 2
console.log(P.next().value) // 3
// 默认迭代器的使用:
class Person {
  constructor() {
    this.name = ['zhangsan', 'lisi', 'wangwu']
  }

  *[Symbol.iterator]() {
    yield* this.name.entries()
  }
}

let p = new Person()
for (let [x, y] of p) {
  console.log(y) // 'zhangsan'、'lisi'、'wangwu'
}

(4)继承

说明: ES6类使用extends关键字就可以继承任何拥有Construct和原型的对象,这意味着不仅可以继承一个类,也可以继承普通的构造函数

==> 继承基础 <==

// 类的继承:
class Foo { }

class bar extends Foo { }

let b = new bar()
console.log(b instanceof bar) // true
console.log(b instanceof Foo) // true
// 普通构造函数的继承:
function bas() { }

class baq extends bas { }

let B = new baq()
console.log(B instanceof bas) // true
console.log(B instanceof baq) // true

注意: 实例化的对象都会通过原型链访问到类和原型上面定义的方法,其this的指向调用方法的实例或者是类

class Foo {
  baz(id) {
    console.log(id, this)
  }

  static bar(id) {
    console.log(id, this)
  }
}

class Boo extends Foo { }

let F = new Foo()
let B = new Boo()

console.log(B.baz('B')) // B Boo { }
console.log(F.baz('F')) // F Foo { }

console.log(Foo.bar('Foo')) // Foo class Foo {  }
console.log(Boo.bar('Boo')) // Boo class Boo {  }

注意: extends关键字也可以在类表达式中使用,因此let Bar = class extends Foo {} 是有效的语法

==> super关键字 <==

说明: 继承类的方法可以通过super关键字来引用出它们的原型,这个关键字只能够在继承的类的内部使用,在类构造函数中使用这个super的方法的话可以调用父类的构造函数。

class Vehicle {
  constructor() {
    this.hasEngine = true
  }

  static foo() {
    console.log(111)
  }
}

class Bus extends Vehicle {
  constructor() {
    super()

    // 对于this来说,如果需要在继承的类中使用的话,需要先调用super函数才行,
    // 否则就会报错
    console.log(this)
  }

  static foo() {
    // 可以通过super关键字来调用所继承的静态方法  
    super.foo()
  }
}

new Bus()

Bus.foo() // 111

注意:

  • super关键字只能够在继承的类的构造函数和静态方法中使用

  • 不能够单独的去使用super关键字,比如单纯的去打印super这个关键字,是会报错的

  • 调用super会返回父类的构造函数,并将返回的实例赋值给this

  • super的行为跟调用构造函数一样,如果需要给父类构造函数传参,需要手动传

  • 在继承的类中,不能够在使用super方法之前使用this

  • 在继承的类中如果显式的存在构造函数,那么必须调用super方法或者在其中返回一个对象

==> 抽象类 <==

说明: 这种类可以被其它类继承,但是本身不会被实例化,虽然ECMAScript没有专门的语法,但是可以通过new.target来实现。new.target保存通过new关键字调用的类或函数。通过在实例化时检测new.target是不是抽象基类,可以阻止对抽象基类的实例化

class Vehicle {
  constructor() {
    console.log(new.target);
    if (new.target === Vehicle) {
      throw new Error('Vehicle cannot be directly instantiated');
    }
  }

  foo() {
    console.log('foo');
  }
}
// 派生类 
class Bus extends Vehicle {}
new Bus(); // class Bus {} 
new Vehicle(); // class Vehicle {} 
// Error: Vehicle cannot be directly instantiated 

注意: 通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在 调用类构造函数之前就已经存在了,所以可以通过this关键字来检查相应的方法

// 抽象基类 
class Vehicle {
  constructor() {
    if (new.target === Vehicle) {
      throw new Error('Vehicle cannot be directly instantiated');
    }
    
    if (!this.foo) {
      throw new Error('Inheriting class must define foo()');
    }
    
    console.log('success!');
  }
}

// 派生类 
class Bus extends Vehicle {
  foo() { }
}

// 派生类 
class Van extends Vehicle { }

new Bus(); // success! 
new Van(); // Error: Inheriting class must define foo()

(5)修饰符

说明: 类中的修饰符有三个,分别是publicprotectedprivate,它们都是不想让类暴露更多的细节,只给外界某些属性和方法使用。

class Piece {
    // peotected与private类似,同样会将参数给this,
    // 但是它修饰的属性是对自己以及自己的子类都是可以使用的,
    protected position = '111'
    
    // 构造方法里面的private修饰符回把参数自动给this,并将其设置为自己的属性,
    // 这也意味着实例外面的代码是无法访问的,包括自己的子类
    constructor(private color, files, rank) {
        console.log(color, files, rank)
    }
    
    // 默认情况下类的属性和方法是通过public来修饰,
    // 表示不受任何限制,在任何地方都可以访问
    moveTo(position: Position) {
        this.position = position
    }
}