ES6学习笔记

目录

ECMAScript、JavaScript、NodeJs,他们的区别是什么?

ECMAScript:简称ES,是一个语言标准(循环、判断、变量、数组等数据类型)

JavaScript:运行在浏览器端的语言,该语言使用ES标准。ES + webAPI = JavaScript      webAPI也就是指dom元素和dom对象或事件方法

NodeJs:运行在服务器端的语言,该语言使用ES标准。ES + nodeAPI = NodeJS

简单理解:无论JavaScript 还是NodeJS,他们都是ES的超集(也就是指 这两个语言都包含有ES,并且还有一些自己的API)

ECMAScript有哪些关键的版本

ES3.0:1999

ES5.0:2009

ES6.0:2015,从该版本开始,不再使用数字作为编号,而是用年份 也就是ES2015

ES7.0:2016

ES为何如此重要?

ES解决了JS无法开发大型应用的语言层面的问题。

声明变量的问题

使用var声明的变量

允许重复的声明变量:导致数据被覆盖

变量提升:怪异的数据访问、闭包问题

全局变量挂载到全局对象:全局对象成员污染
ES6的解决办法
不再使用var声明变量
使用 let 声明变量

ES6中的变量声明

let---------------------------------

声明的变量,不允许在 当前作用域范围 内重复声明(重复声明了的话,就会报错)

在块级作用域中,用let定义的变量,在作用域外是不能访问的(也就是作用域里面可以访问外面的变量,但是外部的不能访问里面的变量)
块级作用域:代码执行时遇到花括号,会创建一个块级作用域,花括号结束时,块级作用域会被销毁

可以简单认为 不会变量提升(声明变量前 被调用,就会报错)

全局变量不会挂载到全局对象上,解决全局对象成员污染的问题

在循环中(如for循环),用let定义的循环变量,会特殊处理,每次进入循环体,都会开启一个新的作用域(保存当前循环变量的值),并且将循环变量绑定到该作用域中,并且在循环中用let声明的循环变量,在循环结束后会被销毁。
(也就是每次循环,使用的是一个全新的循环变量)
关于let的拓展
底层实现上,let声明的变量实际上也会提升,但是,提升后会将其放进 "暂时性死区" ,如果访问的变量位于"暂时性死区",则会报错:
"Cannot access 'a' before initialization"

当代码运行到该变量的声明语句时,会将其在"暂时性死区"中移除
块级作用域的演示
let a = 10;  //在全局作用域定义的a
{
	let a = 20; //在块级作用域定义的a
	console.log(a);//20
}//块级作用域执行结束后 就会被销毁
console.log(a);//10

const----------------------------

const和let完全相同,仅在于用const声明的变量,在声明时必须赋值,而且不可以重新赋值。

在实际开发中,应该尽量使用const来声明变量,以保证变量的值不被随意篡改(如dom对象的变量,一般来说是不会更改的)原因如下:
1.根据经验,开发中的很多变量,都是不会更改的,也不应该更改。
2.后续的很多框架或者是第三方JS库,都要求数据不可变,使用常量可以一定程度上保证这一点。
需要注意的细节
1.常量不可变,是指声明的变量内存空间不可变,并不保证内存空间中的地址指向的其他空间不可变。(如对象里面的属性值)

2.常量的命名:
	a.特殊的常量:该常量从字面意义上,一定是不可变的,如圆周率、月地距离或其他一些绝不可能变化的配置。
	通常,该常量的名称全部使用大写,多个单词之间用 _ 分割
	b.普通的常量:使用和之前一样的命名即可

3.在for循环中,循环变量不可使用 常量!!

注意

let和const声明的变量,不能跟形参重名,否则会报错(其实因为形参也跟let或则const声明的一样,后面默认值拓展有提到)

开发中的思路

养成良好习惯:
在文档需要声明变量的时候 最好先用 const声明
到后来,发现这个变量是需要改变的,再将这个变量改成用 let声明

字符串码元和码点的概念

早期,由于存储空间宝贵,Unicode使用16位二进制来存储文字。我们将一个16位的二进制编码叫做一个码元(Code Unit)。

后来,由于技术不断发展,Unicode对文字编码进行了扩展,将某些文字扩展到了32位(占用两个码元),并且,将某个文字对应的二进制数字叫做码点(Code Point)。码点 有可能是16位 也有可能是32位

字符串中length的计算问题

实际上是按照Unicode编码所占用的码元进行计算的,下面的文字 占用了2个码元(32位)

const str = '𠮷';
console.log(str.length);//2
console.log(/^.$/.test(str));//false  因为正则表达式也是按照码元来匹配的,而这里的正则只能匹配到16位的文字
//也就是说,如果要匹配到这个文字,需要按照匹配两个文字的格式来编写正则
字符串中想获取第一个码元,有一个方法: str.charCodeAt(0);str.charCodeAt(1);//返回第一个码元,和第二个码元,而且值不同
//也就证明该文字由两个码元组成

所以导致无法准确的计算出字符串的长度

ES6中解决了length计算问题

ES6中,给出了一个获取字符串码点的方法 codePointAt( )

根据字符串码元的位置得到码点

const str = '𠮷';
console.log(str.codePointAt(0));//方法会根据文字的第一个码元的数值判断是16位还是32位,然后返回相对应的码点

封装一个方法判断字符是否是32位的

		function is32bit(char) {
            return char.codePointAt(0) > "0xffff";//如果码点大于16进制的最大值
        }
ES6中解决正则匹配字符串问题
ES6位正则表达式添加了一个flag :u ,如果添加了该设置,则匹配时,使用码点匹配,如:  /^.$/u

ES6中新增的字符串API

includes

判断字符串中是否包含指定的子字符串

语法: str.includes('a',0);//表示在第0位开始往后查找是否含有 'a'  不填索引  默认全局查找
const text = '成哥是个狠人';
const result = text.includes('狠');//返回 true / false
console.log(result);//true
startWith()
str.startWith('a',0);//从第0位字符开始,判断字符串是否以 'a'开头
endWith()
str.endWith('a');//判断字符串是否以 'a' 结尾
repeat()
str.repeat(4);//将str字符串重复4次,并返回结果
正则中的粘连标记
标记名: y
用法:/^.$/y

含义:匹配时,完全按照正则对象的lastIndex位置开始匹配,并且匹配的位置必须在lastIndex位置
模板字符串

ES6之前处理字符串繁琐的两个方面

1.多行字符串
2.字符串拼接

ES6提供了模板字符串的书写,可以方便的换行和拼接,要做的,仅仅是将字符串的开始或结尾改为 反引号

通过${}进行传参和计算
里面填的值是表达式,所以可以是计算公式
表达式是可以嵌套的  ${'123'${'456'}}
如果需要显示${},可以使用转义字符  \${},这样就能在文档上显示'${}'

参数默认值

在书写形参时,直接给形参赋值,赋的值即为默认值

当调用函数时,如果没有给对应的参数赋值 (给它的值是undefined),则会自动使用默认值

注意:null不行,只有传undefined才会使用默认值

function foo(a,b = 1,c = 2){//设置形参b的默认值为1,形参c的默认值为2
	return a+b+c;
}
console.log(foo(1));//4

拓展

对arguments的影响

只要给函数加上参数默认值,该函数就会自动变成严格模式下的规则:
形参与实参(arguments)相分离

留意暂时性死区

形参和ES6中的let或const声明一样,具有作用域,并且根据参数的声明顺序,存在暂时性死区(也就是不能在参数未定义之前使用)

也就意味着,let或则const声明的变量,不能跟形参重名,否则报错

剩余参数

arguments的缺陷
1.如果和形参配合使用,容易导致会混乱
2.从语义上,使用arguments获取参数,由于形参缺失,无法从函数定义上理解函数的真是意义(也就是不知道要不要传参,传什么参数)
剩余参数

剩余参数解决了上述问题

作用:

专门用于收集末尾的所有参数,将其放到一个形参数组中

语法

在括号的最后参数位,添加一个形参作为收集参数的数组,形参前面加上 … 就表示剩余参数

字面意思理解:就是省略号,表示不知道会有多少个参数

function joo(...形参名){};
实例:
function foo(...a){
    //...a收集了所有参数,形成的一个数组
	console.log(a);//[1,2,3,4,5]
    console.log(a[0]);//1
}
foo(1,2,3,4,5);
注意
1.一个函数,仅能出现一个剩余参数
2.一个函数,如果有剩余参数,剩余参数必须是最后一个参数

有了剩余参数后,后面就基本可以不使用arguments了

展开运算符

需求
有时候,有些函数可能需要传递多个参数
但是,你获取到的数据是一个数组
那么这时候就需要将每个数组拆开成多个的参数传递给函数
此时就需要使用 展开运算符了
展开运算符

语法

格式与剩余参数类似,也是以 …实参名 进行传值

但是,这是用于传递数据的,而剩余参数是用于接收数据的

const arr = [1,2,3,4];
function sum(...list){// ...list是剩余参数
	let sum = null;
	for(let i = 0; i < list.length; i++){
		sum += list[i];
	}
	return sum;
}
sum(...arr);// ...arr是将arr数组展开成多个元素进行传值,相当于: sum(1,2,3,4)

展开运算符没有规定只能在最后传值,哪个实参位置都能使用

作用

可以用于克隆数组
const arr1 = [1,2,3,4];
const arr2 = [...arr1];//arr2克隆了arr1
ES7的展开运算符能在对象上使用
const obj1 = {
	name:'小白',
	age:18,
    list:[1,2,3,4,5],
    son:{
        name:'小黄',
        age:1
    }
}
const obj2 = {
	...obj1,//这样就实现了克隆对象obj1了
    list:[...obj1.list],
    son:{
        ...obj1.son//遇到应用类型的属性,需要再次展开赋值才可以实现深拷贝,不然只会赋值地址指向
    }
}

注意:展开运算符的克隆,只能克隆对象的属性地址指向,也就是说是浅克隆

也就是指两者的属性所指向的地址是相同的,如果克隆后改变地址里面的数据(应用类型),则两者都会跟着改变

函数柯里化

柯里化:用户固定某个函数的前面的参数,得到一个新的函数(newCal),新的函数调用时(newCal(3,4)),接收剩余的参数

function cal(a, b, c, d) {
            return a + b * c - d;
        }
        //柯里化:用户固定某个函数的前面的参数,得到一个新的函数(newCal),新的函数调用时(newCal(3,4)),接收剩余的参数
        function curry(func, ...args) { //...args --->1,2
            return function(...subArgs) { //...subArgs --->3,4
                const allArgs = [...args, ...subArgs];
                if (allArgs.length === func.length) {
                    return func(...allArgs);
                } else {
                    return curry(func, ...allArgs);//再返回一次函数接收参数,以补足参数调用函数
                }
            }
        }
        const newCal = curry(cal, 1, 2); //a,b 固定一个值进行传递
        console.log(newCal(3, 4)); //1+2*3-4
        console.log(newCal(5)(6)); //1+2*5-6

明确函数的双重用途

也就是判断,该函数是当作构造函数使用还是普通函数调用

ES6一个特殊的API —new.target

可以使用这个API在函数内部,判断该函数是否使用了new来调用

该表达式,得到的是:

如果没有使用new来调用函数,则返回undefined

如果使用new调用函数,则得到的是new关键字后面的函数本身

function Father() {
            if (!new.target) {//如果返回的值是undefined
                throw new Error("没有使用new调用方法")
            }
            this.name = '小白';
            this.age = 18;
        }
        const son1 = new Father();
        const son2 = Father();

箭头函数

回顾this指向
1.通过对象调用函数:this指向对象
2.直接调用函数:this指向全局对象
3.通过new调用函数:this指向新创建的对象
4.通过call、apply、bind调用函数:this指向指定的数据
5.如果是DOM事件函数:this指向事件源(也就是给谁注册的事件,this就指向谁)
箭头函数

箭头函数是一个表达式

意味着他不会被预编译过程提升上去

理论上,任何使用函数表达式的场景都可以使用箭头函数

使用语法

去掉function

在() 后加上箭头 =>

const func = (参数1,参数2,...) => {
    	//函数体
	}
注意的细节
1.箭头函数中,不存在 this、arguments、new.target,如果使用了,则使用的是:定义函数外层作用域对应的 this、arguments、new.target

2.如果参数只有一个,可以省略小括号
	const func = 参数 => {}
    
3.如果箭头函数只有一条返回语句,可以省略大括号,和return关键字
	const func = 参数 => 返回值

4.如果箭头函数只有一条返回语句,并且返回的是对象,则可以在对象的大括号外再套一层小括号,表示表达式,这样程序就能理解了
	const func = 参数 => ({
        name:'小白',
        age:18
    })
    
5.箭头函数没有原型,undefined

6.由于没有原型,所以箭头函数不能作为构造函数使用
应用场景
1.临时性使用的函数,并不会直接调用它,比如:
    1.事件处理函数(dom事件)
    2.异步处理函数(setInterval、setTimeout)
    3.其他临时性的函数
    
2.为了绑定外层this的函数

3.在不影响其他代码的情况下,保持代码的简洁,最常见的:数组方法中的回调函数(fillter、map、reduce等)
注意:对象的属性是不需要用箭头函数的,他会导致箭头函数的this指向对象外层的this,并不是指向对象
注意
image-20201122215202843

定时器使用注意点

当定时器函数是function时
const obj = {
	say(){
		this.timer = setTimeout(function(){
			console.log(this);
		},100)
	}
}
obj.say();
//  输出的this 是 全局对象 window
// 因为定时器里面的函数 是由js引擎调用的(独立调用),所以会指向window
当定时器函数是箭头函数时
const obj = {
    say() {
        this.timer = setTimeout(() => {
            console.log(this);
        }, 100)
    }
}
obj.say();
//输出的this 是say函数里面的this,指向obj
//因为箭头函数是没有this的,他只会保留定义箭头函数时,外层的this(也就是say的this)

新增的对象字面量语法

对象字面量就是指,初始化对象时,如

const obj = {
	name:'小白',
	age:18
}

这种就是字面量初始化对象

成员速写

原本的语法

function createPerson(username,age,sex){
	return {
		username:username,
		age:age,
		sex:sex
	}
}

成员速写语法

如果对象字面量初始化时,成员的名称来自于一个变量,而这个变量的名字和成员名称相同,则可以进行简写,如下:

function createPerson(username,age,sex){
	return {
		username,
		age,
		sex
	}
}
方法速写

对象字面量初始化时,方法可以省略冒号和function关键字

//原本写法
const obj = {
	name:'小白',
	say:function(){
		console.log(this.name);
	}
}
//速写
const obj = {
	name:'小白',
	say(){
		console.log(this.name);
	}
}
计算属性名

有的时候,初始化对象时,某些属性名可能来自于某个表达式的值,在ES6中,可以使用中括号来表示该属性名是通过计算得到的

const prop1 = 'name';
const prop2 = 'age';
const prop3 = 'sex';
const prop4 = 'say';//say方法
const obj = {
	[prop1]:'小白',
	[prop2]:18,
	[prop3]:'男',
    [prop4](){
        console.log(this[prop1],this[prop2]);
    }
}

中括号里面返回的是该表达式的值

构造函数Object新增的API

Object.is

用于判断两个数据是否相等,基本上跟严格相等(===)是一致的,除了一下两点:

//===判断
NaN === NaN ;//false
+0 === -0 ;//true

//Object.is判断
1. NaNNaN相等
Object.is(NaN,NaN);//返回true
2. +0-0相等
Object.is(+0,-0);//返回false
Object.assign

对象覆盖

可以传任意个参数,方法使用的是剩余参数

方法只会对第一个参数产生改动,并返回第一个参数

const obj1 = {
	name:'小白',
	age:18
}
const obj2 = {
	name:'小黑',
	age:19
}
//将obj2的数据,覆盖到obj1,并且会对obj1产生改动,然后返回obj1
const obj = Object,assign(obj1,obj2);

//可以把第一个参数换成是{},这样最后改动的只是{},其他对象不会被改动
const obj = Object.assign({},obj1,obj2);

建议不用这个,使用展开运算符比较好,不然改动对象有可能会影响后面使用的框架

Object.getOwnPropertyNames

枚举对象属性名,返回一个数组

枚举顺序

Object.getOwnPropertyNames方法之前就存在,只不过,官方没有明确要求,堆属性的顺序如何排序,如何排序,完全由浏览器厂商决定

ES6规定了该方法返回的数组的排序方式如下:
先排数字,并按照升序排序
再排其他,按照书写顺序排序
Object.setPrototypeOf(obj1,obj2)

设置obj1的原型指向obj2

设置的是 proto

比较少用到

const obj1 = {
	a:1
}
const obj2 = {
	b:2
}
Object.setPrototypeOf(obj1,obj2);
console.log(obj1.b);

什么是实例

实例就是由构造函数创建出来的对象。

function Person() {}//  此函数为构造函数   

var p=new Person();  // p为构造函数创建出来的对象
实例成员和静态成员

实例成员:由构造函数创建出来的对象能直接访问的属性和方法,包括:对象本身 以及原型中的所有的属性和方法。

例如:

function Person(name, age) {
        this.name = name;  
        this.age = age;  
}

Person.prototype.sayHi = function() {
        console.log("hello, rose");   
};  
          var p = new Person("jack", 19);    

        // ( p.name  p.age  p.sayHi)  括号内三个就是实例成员了。

静态成员:由构造函数直接访问到的属性和方法。大家注意是直接访问的属性和方法,间接获取就不是了。

function Person(name, age) {  
         this.name = name;
         this.age = age;  
}
var p = new Person("jack", 19);                
Person.say = function() {alert("这是静态方法");};        

    //     Person.say  就是静态成员        

   //Person.length  也是静态成员,因为length是函数中的 方法,是来直接获取函数中形参的个数。

面向对象简介

面向对象:一种编程思想,更具体的语言

对比面向过程:

以把大象转进冰箱为例

面向过程:思考的切入点是功能的步骤-------------------------------------

第一步:把冰箱门打开
第二步:把大象装进去
第三步:关闭冰箱门

面向对象:思考的切入点是对象的划分-------------------------------------

有两个对象:大象、冰箱
分别对这两个对象进行功能描述,如:大象会走路、会吃东西,冰箱会制冷、能自动开门关门等功能
function Elethons(){};//大象
Elethons.prototype.move = function(){};//赋予功能
function Frige(){};//冰箱
Frige.prototype.cook = function(){};//赋予功能

传统的构造函数的问题

1.属性和原型方法定义分离,降低了可读性
2.原型成员可以被枚举
3.默认情况下,构造函数仍然可以被当作普通函数使用

类class

//面向对象中,将 下面对一个对象的所有成员的定义,统称为类
        //构造函数  构造器
        function Animal(type, name, age, sex) {
            this.type = type;
            this.name = name;
            this.age = age;
            this.sex = sex;
        }
        //定义实例方法(原型方法)
        Animal.prototype.print = function() {
            console.log(`种类:${this.type}`);
            console.log(`名字:${this.name}`);
            console.log(`年龄:${this.age}`);
            console.log(`性别:${this.sex}`);
        }
        const dog = new Animal('狗', '二哈', '2', '公');
        dog.print();
类的特点
1.类声明不会被提升,与let、const一样,存在暂时性死区
2.类中的所有代码均在严格模式下执行
3.类的所有方法都是不可枚举的
4.类的所有方法都无法被当作构造函数使用
5.类的构造器必须使用 new 来调用
语法
class newAnimal {
            constructor(type, name, age, sex) {//相当于构造器   构造函数   这里面定义的属性最后会挂载到实例对象上
                this.type = type;
                this.name = name;
                this.age = age;
                this.sex = sex;
            }
            print() {//class里面定义的方法最后都会挂载到原型上,并且函数写法类似速写
                console.log(`种类:${this.type}`);
                console.log(`名字:${this.name}`);
                console.log(`年龄:${this.age}`);
                console.log(`性别:${this.sex}`);
            }
        }
        const dog2 = new newAnimal('狗', '二哈', '2', '公');
        dog2.print();

如果constructor没有参数,是空的,可以不写

类的其他书写方式
1.可计算的成员名

就如成员速写一样

const prop1 = 'say';
class Son{
	constructor(name){
		this.name = name;
	}
	get name(){
		return this._name;
	}
	set name(name){
		name = '吴'+name;
		this._name = name;
	}
    //直接用表达式代替方法名
	[prop1](){
		console.log(this.name)
	}
}
const son = new Son('讲得');
son[prop1]();//吴讲得
2.getter和setter

在回顾属性描述符中有详解

Object.defineProperty 可定义某个对象成员属性的读取和设置
使用getter 和 setter控制的属性,不在原型上(是普通对象的方法)
ES6的写法:           
get 属性名(){}                   
set 属性名(value){}

例子

class newAnimal {
            constructor(type, name, age, sex) {
                this.age = age;
                this.name = name;
                this.type = type;
                //原本ES5的属性访问器写法
                    // Object.property(this,'type',{
                    //     set(value){},
                    //     get(){}
                    // })
                this.sex = sex;
            }
            //访问器:  get() 和  set()
            get type() { //一旦需要获取属性,就会调用get()
                return this._type;
            }
            set type(type) { //一旦需要设置属性时,就会调用set()
                if (type === '狗') {
                    type = '单身狗'
                } else {
                    type = this.type;
                }
                this._type = type;
            }
            print() {
                console.log(`种类:${this.type}`);
                console.log(`名字:${this.name}`);
                console.log(`年龄:${this.age}`);
                console.log(`性别:${this.sex}`);
            }
        }
        const dog2 = new newAnimal('狗', '二哈', '2', '公');
        dog2.print();
3.class的静态成员和实例成员
image-20201119174245434
静态成员:只能通过构造函数本身来访问(就像obj.age,只有obj才能访问到age属性)
//静态成员  语法: 用关键字  static定义
//静态成员一般是固定不变的值,便于后续使用
	class Father {
		constructor(name){
			this.name = name;//实例成员
		}
		//声明静态成员
		static sex = '男';
		static age = '18';
		//静态方法
		static say(){
            console.log('性别:'+this.sex);//用Father代替this也可以
        }
		//实例方法
		print(){
            console.log('姓名:'+this.name);
        }
	}
    const son = new Father('小白');
	son.print();//输出 姓名:小白
	Father.say();//输出 性别:男

实例成员:由构造函数创建出来的对象能直接访问的属性和方法,包括:对象本身 以及原型中的所有的属性和方法
4.字段初始化器(ES7)

当遇到下面这种情况,可以使用字段初始化器书写

如果一开始就已经赋值了的,可以使用字段初始化器书写 也就是直接在class里面写 : 变量名 = 值

可以理解为:他自动会把变量放到构造函数(constructor)中

class Test{
	d = 4;
	e = 5
	constructor(){
	//如果有这种直接就给他赋了值的字段,就可以直接像上面的这样书写(作用是相同的)
		this.a = 1;
		this.b = 2;
		this.c = 3;
        //下面字段初始化器的print写法等同于这个写法
        //this.print = () => {
            //console.log(this.a);
        //}
	}
	//书写方法   这样书写,是为了防止this的指向被改变  导致程序错误
	print = () => {//this保存的是函数定义时外层作用域的this,如果是function则是undefined(class的代码均在严格模式下执行)
        console.log(this.a);
    }
}
const t = new Test();
const p = t.print;
p();//输出1      如果不使用上面的方法,这个调用会报错,因为在严格模式下预编译过程中函数的this是undefined

注意

把方法挂载在实例对象上,会相对消耗内存,但可能并不大

1.使用static的字段初始化器,添加的是静态成员
2.没有使用static的字段初始化器,添加的成员位于实例对象上
3.当使用字段初始化器定义方法后,该方法就不是原型上的了,而是实例对象上的方法了

5.匿名表达式写法

匿名类,类表达式

作用一样

const Son = class{
	a = 1;
	b = 2;
}

类的继承

判断两个类是否有继承关系
如果两个类A和B,如果可以描述为:B 是 A,则A 和 B形成继承关系。简单举例: 男人 是 人,所以男人和人形成继承关系

如果B 是 A,则有如下描述方式:
1.B继承自A
2.A派生B
3.B是A的子类
4.A是B的父类

如果A是B的父类,则B会自动拥有A中的所有实例成员( 实例方法、实例属性 )

extends关键字实现继承

extends:继承,用于类的定义

super关键字调用父类的构造函数

class Animal {
            constructor(type, name, age, sex) {
                this.type = type;
                this.name = name;
                this.age = age;
                this.sex = sex;
            }
            print() {
                console.log(this.type + '都会觅食');
            }
        }
        class Dog extends Animal { //使用 extends 继承Animal类
            constructor(name, age, sex) {
                super('犬科动物', name, age, sex); // 使用 super关键字调用 父类构造函数
            }
        }
        const dog = new Dog('二哈', 2, '雄性');
        dog.print();//犬科动物都会觅食
super关键字调用父类构造函数

原理:

通过call(this,参数); 借用父类构造函数,以下代码和上面使用extends关键字的实现继承 相同效果

 	   function Animal(type, name, age, sex) {
            this.type = type;
            this.name = name;
            this.age = age;
            this.sex = sex;
        }
        Animal.prototype.print = function() {
            console.log(this.type + '都会觅食');
        }

        function Dog(name, age, sex) {
            Animal.call(this, '犬科动物', name, age, sex); //借用Animal构造函数
            //通过 setPrototypeOf 设置Dog.prototype.__proto__指向Animal.prototype 实现继承
            Object.setPrototypeOf(Dog.prototype, Animal.prototype);
        }
        const dog = new Dog('二哈', 2, '雄性');
        dog.print();

**作用一:**直接当作函数调用,当作父类构造函数

	class Dog extends Animal { //使用 extends 继承Animal类
            constructor(name, age, sex) {
                //super必须写在第一行
                super('犬科动物', name, age, sex); // 使用 super关键字调用 父类构造函数
            }
        }

**作用二:**如果当作对象使用,则表示父类的原型(prototype)

		class Animal {
            constructor(type, name, age, sex) {
                this.type = type;
                this.name = name;
                this.age = age;
                this.sex = sex;
            }
            print() {
                console.log(this.type + '都会觅食');
            }
        }
		class Dog extends Animal { //使用 extends 继承Animal类
            constructor(name, age, sex) {
                //super()必须写在第一行
                super('犬科动物', name, age, sex); // 使用 super关键字调用 父类构造函数
            }
        //子类重写print方法
        print(){
            super.print();
            console.log(this.type + '除了会觅食,还很忠心');
        }
       }
	    const dog = new Dog('二哈', 2, '雄性');
        dog.print();// 输出 犬科动物都会觅食     犬科动物除了会觅食,还很忠心
注意

ES6要求:

如果定义了constuctor,并且该类是子类,则必须在constructor的第一行手动调用父类的构造函数

否则,会导致程序报错

如果没有定义constructor,并且该类是子类,则会有默认的构造函数,该构造器需要的参数和父类一致,并且自动调用父类构造函数

相当于在子类自动添加了以下代码:

            constructor(name, age, sex) {
                super('犬科动物', name, age, sex); 
            }

冷知识

用js模拟抽象类

抽象类:一般是父类,不能通过该类创建对象

简单理解: 世界上没有人, 因为根本找不到一个叫人的实物,只有叫什么名字的人,什么性别的人,但是没有人这个东西。也就相当于人其实是指代一个范围,他是一个统称,而实际并不存在

下面,Animal也是一个抽象类,所以不能直接创建Animal的实例对象(new Animal)

		class Animal {
            constructor(type, name, age, sex) {
                if(new.target === Animal){
                    //判断是否是new调用方法,是的话看看调用者new后面的方法是不是Animal,是的话,报错
                    throw new TypeError('不能直接调用Animal进行实例对象');
                }         
                this.type = type;
                this.name = name;
                this.age = age;
                this.sex = sex;
            }
            print() {
                console.log(this.type + '都会觅食');
            }
        }
		class Dog extends Animal { //使用 extends 继承Animal类
            constructor(name, age, sex) {
                //super()必须写在第一行
                super('犬科动物', name, age, sex); // 使用 super关键字调用 父类构造函数
            }
        //子类重写print方法
        print(){
            super.print();
            console.log(this.type + '除了会觅食,还很忠心');
        }
       }
	    const dog = new Dog('二哈', 2, '雄性');
        dog.print();// 输出 犬科动物都会觅食     犬科动物除了会觅食,还很忠心
this指向

正常情况下,this的指向,this始终指向具体的类的实例对象

对象解构

解构语法和原理

不用解构使用对象里面的属性

const obj = {
    name:'小白';
    age:18,
    sex:'男'
}
let name,age,sex;
name = obj.name;
age = obj.age;
sex = obj.sex;

初步使用对象解构使用对象的属性

const obj = {
    name:'小白';
    age:18,
    sex:'男'
}
//{name,age,sex} = obj;             //这样书写 跟上面代码一样效果   但是还不完善
//这样写程序会报错 会认为{}是一条语句,obj的 = 左边缺失东西
//所以需要加上一个小括号括起来  让程序知道这是一个整体
let name,age,sex;
({里面放的是跟属性名同名的变量} = 对象名) 
({name,age,sex} = obj);
console.log(name,age,sex);//输出 小白 18 男

最终的简写语法----语法糖

**原理:**先定义需要用的变量,然后从对象中读取同名属性,放到变量中

let {name,age,sex} = obj;

注意:如果定义的变量在对象中找不到对应的属性,那么该变量的值是 undefined

解构的时候不会对被解构的目标造成影响

给解构变量设置默认值

当对象里面没有找到与变量同名的属性时,使用默认值替代

如果能找到,那就会用同名属性的值

语法

let {变量名 = 默认值} = 对象
//比如
let {name,age,sex,abc = 1} = obj;//obj里面没有abc
console.log(abc);//1
非同名写法

当你不想使用跟属性名相同的变量时,可以使用非同名写法

let {属性名:变量名} = obj;
//比如

let {name:username,age,sex,abc = 1} = obj;
//先定义四个变量:username、age、sex、abc
//再从对象obj中读取同名属性赋值(其中,username读取的是name属性)

console.log(username);//'小白'
对于复杂对象的解构
const obj = {
	name : '小白',
	age : 18,
	son : {
		sonName : '微白'
	}
}
//先定义三个变量 name、age、son
//再解构son对象的sonName
let {name,age,son:{sonName}} = obj;
console.log(name,age,son,sonName); // 小白 18 son对象 微白
数组解构

其实原理跟解构对象一样

可以理解为,数组的属性名是以数字命名的

所以解构 需要用到非同名写法进行解构

const arr = ['a','b','c','d'];
const {
	0:n1,
	1:n2,
	2:n3,
	3:n4
} = arr;
console.log(n1,n2,n3,n4);// abcd

语法糖写法

const arr = ['a','b','c','d'];
//它会按照顺序一次赋值给变量
const [n1,n2,n3,n4] = arr;
console.log(n1,n2,n3,n4);//abcd

//如果指向要第三项元素的值
//只需要用 逗号 空开不需要的元素就行了
const [,,n3] = arr;
console.log(n3);//c

数组解构实际上和对象解构一样,所以其他用法都相同,只是获取方式用的是[ ],对象用的是{ }

注意

对象的属性不需要逗号隔开 属性是直接对照名字查找的

练习

const arr = [1,2,3,{a:10,b:20,c:30}];
//解构数组中对象里面的b
const [,,,{b:n}] = arr;
console.log(n);// 20
结合展开运算符解构

注意展开运算符在解构中使用时,只能放到最后面

不然程序就不知道你要展开多少个了

----------------------------对象
const user = {
	name:'小白',
	age:18,
	sex:'男',
	address:{
		city:'清远'
	}
}
//想获取name属性,剩下的属性放到 obj对象中
const {name,...obj} = user;
console.log(name,obj);
//小白   {age: 18, sex: "男", address: {…}}
------------------------------数组
const arr = [1, 2, 3, 4, 5, 6, 7];
//想将前两项 分别放到a,b中,剩余的元素放到数组nums中
const [a, b, ...nums] = arr;
console.log(a, b, nums);
//1 2 (5) [3, 4, 5, 6, 7]

通过解构实现变量值互换

let a = 1,b = 2;
//这样就能实现值互换了
[b,a] = [a,b];
console.log(a,b);// 2 1

函数参数解构

//通过解构的方式进行参数解构,方便函数使用
        function foo({
            name,
            age,
            sex,
            son: {
                sonName
            }
        }) {
            console.log(`姓名:${name}`);
            console.log(`年龄:${age}`);
            console.log(`性别:${sex}`);
            console.log(`儿子姓名:${sonName}`);
        }
        const user = {
            name: '大白',
            age: 18,
            sex: '男',
            son: {
                sonName: '小白'
            }
        }
        foo(user);

模拟ajax请求时,设置参数默认值场景

//比如 默认请求方式使用 get
function ajax({
	method = 'get',
	url
} = {}){//给参数设置默认值  当函数没有传递参数时,就会默认传了 {}
	console.log(method,url);
}
ajax({
	url:'/abc'
})
//get /abc
注意

undefined、null是不能解构的,会报错

所以如果函数设置了参数解构,那么该函数不传参数就会报错,除非函数参数设置了默认值

普通符号

符号是ES6新增的一个原始值数据类型,它通过使用函数 Symbol(描述信息) 来创建的

描述信息只是方便调试,方便开发者辨别的而已

创建符号

const smb1 = Symbol();
const smb2 = Symbol('abc');
console.log(smb1,smb2);//Symbol()    Symbol(abc);

设计的初衷

是为了给对象设置私有属性的

**私有属性:**只能在对象内部使用,外部无法使用

(可以理解为:只想显示有用的功能给别人使用,其他那些辅助性功能(如:获取随机数函数),别人用不到的就设置成私有属性)

符号的特点
1.没有字面量(比如number的字面量:1,2,3等等;对象的字面量:{...};undefined的字面量:undefined

2.使用typeof 返回的值就是 'symnol'

3.每次调用Symbol()函数得到的符号永远不相等,无论符号名是否相同(Symbol()每次返回的值都不相同)

4.符号可以作为对象的属性名存在,这种属性称之为  符号属性  (采用计算属性方式使用)
	const syb1 = Symbol('这是一个符号属性');
	const obj = {
		a : 1,
		[syb1]:2
	}
	console.log(obj);
	//{a: 1, Symbol(这是一个符号属性): 2}
                                     
5.每个符号都是唯一的(独一无二)

6.符号是不能被隐式转换的,因此不能被用于数学运算、字符串拼接或其他隐式转换的场景。
但是,字符可以显式的转换为字符串,通过String构造函数进行转换,console.log之所以可以输出,就是因为内部使用了String构造函数显式转换

所以从ES6开始,(对象、数组)属性名除了只能用字符串表示外,现在还可以用符号表示

符号属性

用符号作为属性,可以是外部无法使用该属性

const obj = (function() {
            //把符号属性 写道return上面
            const getRandom = Symbol('这是获取随机数函数');
            // const getRandom = 'getRandom';//这样写的话   是可以被外部调用的(因为产生了闭包)
            return {
                name: '小白',
                //我不想让 外面的人可以访问到getRandom(),只想给他们使用luckyNum()
                [getRandom]() {
                    return parseInt(Math.random() * (10 - 1) + 1); //获取1到9的随机数
                },
                luckyNum() {
                    return this[getRandom]();
                }
            }
        })()
        console.log(obj.luckyNum()); //3
        console.log(obj.getRandom()); //报错
        console.log(obj[getRandom]()); //报错

符号属性是不能枚举的

因此在 for … in 循环中无法读取到符号属性,Object.keys方法也无法读到符号属性

(Object.keys 是返回一个存储可枚举属性名的数组)

虽然**Object.getOwnPropertyNames()**可以得到所有 可枚举或不可枚举 的属性名,但是 依然得不到符号属性

	    const syb1 = Symbol();
        const obj = {
            a: 1,
            b: 2,
            [syb1]: 3
        }
        for (var prop in obj) {
            console.log(obj[prop]); // 1   2
        }

Object.getOwnPropertySymbols(对象)

返回一个存储着对象中符号属性的数组

ES6新增方法,就是为了能够获取符号

所以可以通过这个方法来实现在外部使用符号属性

const obj = (function() {
            //把符号属性 写道return上面
            const getRandom = Symbol('这是获取随机数函数');
            // const getRandom = 'getRandom';//这样写的话   是可以被外部调用的(因为产生了闭包)
            return {
                name: '小白',
                //我不想让 外面的人可以访问到getRandom(),只想给他们使用luckyNum()
                [getRandom]() {
                    return parseInt(Math.random() * (10 - 1) + 1); //获取1到9的随机数
                },
                luckyNum() {
                    return this[getRandom]();
                }
            }
        })()
//通过Object.getOwnPropertySymbols(obj)  获取到对象的符号属性数组
const syb1 = Object.getOwnPropertySymbols(obj);
        console.log(syb1);
        console.log(obj[syb1[0]]());//1

符号是不能被隐式转换的,因此不能被用于数学运算、字符串拼接或其他隐式转换的场景

共享符号

共享符号就是 如果创建的 Symbol(符号描述) 相同

那么就认为是同一个符号

语法

Symbol.for('符号名/符号描述');//获取共享符号
//这里面的for属于Symbol的静态成员
const obj = {
    a:1,
    [Symbol.for('c')]:2
}
console.log(obj[Symbol.for('c')]);//2

模拟共享符号Symbol.for()

const SymbolFor = (() => {
            const global = {};
            return function(prop) {
                if (!global[prop]) { //如果属性未定义
                    global[prop] = Symbol(prop);
                }
                return global[prop];
            }
        })()
        const a = SymbolFor('a');
        const b = SymbolFor('a');
        console.log(a, b);//Symbol(a) Symbol(a)

知名符号

概念

知名符号是一些具有特殊含义的共享符号,通过Symbol的静态属性得到

ES6中延续了ES5的思想:减少魔法,暴露内部实现

Symbol.hasInstance

参与instanceof()的内部实现

实际上内部是 执行了如下代码

function A(){};
const obj = new A();
obj instanceof A ;//true

//instanceof里面的内部执行代码是下面的语句,效果相同
A[Symbol.hasInstance](obj);//true

这个 Symbol.hasInstance是可以修改的,如果修改了该函数,那么也会影响instanceof改变

修改该函数

Object.defineProperty(A,Symbol.hasInstance,{//Object.defineProperty(修改的对象,属性名,{修改的内容})
	value:function(obj){
		return false;
	}
})

事件循环

执行栈

JS运行的环境称之为宿主环境

执行栈:call stack,一个数据结构,用于存放各种函数的执行环境,每一个函数执行之前,它的相关信息会加入到执行栈。函数调用之前,创建执行环境,然后加入到执行栈;函数调用之后,销毁执行环境。

JS引擎永远执行的是执行栈的最顶部

image-20201123143308422
浏览器宿主环境中包含5个线程
  1. JS引擎:负责执行执行栈的最顶部代码
  2. GUI线程:负责渲染页面
  3. 事件监听线程:负责监听各种事件
  4. 计时线程:负责计时
  5. 网络线程:负责网络通信
异步函数

异步函数:某些函数不会立即执行,需要等到某个时机到达后才会执行,这样的函数称之为异步函数。比如事件处理函数。异步函数的执行时机,会被宿主环境控制。

事件队列

当上面的线程发生了某些事请,如果该线程发现,这件事情有处理程序,它会将该处理程序加入一个叫做事件队列的内存。当JS引擎发现,执行栈中已经没有了任何内容后,会将事件队列中的第一个函数加入到执行栈中执行

JS引擎对事件队列的取出执行方式,以及与宿主环境的配合,称之为事件循环。

事件队列在不同的宿主环境中有所差异,大部分宿主环境会将事件队列进行细分。在浏览器中,事件队列分为两种:

  • 宏任务(队列):macroTask,计时器结束的回调、事件回调、http回调等等绝大部分异步函数进入宏队列

  • 微任务(队列)MutationObserverPromise产生的回调进入微队列

    注意

  • 当执行栈清空时,JS引擎首先会将微任务中的所有任务依次执行结束,如果没有微任务,则执行宏任务

image-20201123144837273

事件和回调函数的缺陷

我们习惯于使用传统的回调或事件处理来解决异步问题

事件:某个对象的属性是一个函数,当发生某一件事时,运行该函数

dom.onclick = function(){

}

回调:运行某个函数以实现某个功能的时候,传入一个函数作为参数,当发生某件事的时候,会运行该函数。

dom.addEventListener("click", function(){

})

本质上,事件和回调并没有本质的区别,只是把函数放置的位置不同而已。

一直以来,该模式都运作良好。

直到前端工程越来越复杂…

目前,该模式主要面临以下两个问题

  1. 回调地狱:某个异步操作需要等待之前的异步操作完成,无论用回调还是事件,都会陷入不断的嵌套
  2. 异步之间的联系:某个异步操作要等待多个异步操作的结果,对这种联系的处理,会让代码的复杂度剧增
异步处理的通用模型

ES官方参考了大量的异步场景,总结出了一套异步的通用模型,该模型可以覆盖几乎所有的异步场景,甚至是同步场景。

值得注意的是,为了兼容旧系统,ES6 并不打算抛弃掉过去的做法,只是基于该模型推出一个全新的 API,使用该API,会让异步处理更加的简洁优雅。

理解该 API,最重要的,是理解它的异步模型

  1. ES6 将某一件可能发生异步操作的事情,分为两个阶段:unsettledsettled
  • unsettled: 未决阶段,表示事情还在进行前期的处理,并没有发生通向结果的那件事
  • settled:已决阶段,事情已经有了一个结果,不管这个结果是好是坏,整件事情无法逆转

事情总是从 未决阶段 逐步发展到 已决阶段的。并且,未决阶段拥有控制何时通向已决阶段的能力。

  1. ES6将事情划分为三种状态: pending、resolved、rejected
  • pending: 挂起,处于未决阶段,则表示这件事情还在挂起(最终的结果还没出来)
  • resolved:已处理,已决阶段的一种状态,表示整件事情已经出现结果,并是一个可以按照正常逻辑进行下去的结果
  • rejected:已拒绝,已决阶段的一种状态,表示整件事情已经出现结果,并是一个无法按照正常逻辑进行下去的结果,通常用于表示有一个错误

既然未决阶段有权力决定事情的走向,因此,未决阶段可以决定事情最终的状态!

我们将 把事情变为resolved状态的过程叫做:resolve,推向该状态时,可能会传递一些数据

我们将 把事情变为rejected状态的过程叫做:reject,推向该状态时,同样可能会传递一些数据,通常为错误信息

始终记住,无论是阶段,还是状态,是不可逆的!

  1. 当事情达到已决阶段后,通常需要进行后续处理,不同的已决状态,决定了不同的后续处理。
  • resolved状态:这是一个正常的已决状态,后续处理表示为 thenable
  • rejected状态:这是一个非正常的已决状态,后续处理表示为 catchable

后续处理可能有多个,因此会形成作业队列,这些后续处理会按照顺序,当状态到达后依次执行

  1. 整件事称之为Promise

理解上面的概念,对学习Promise至关重要!

Promise

promise的理解

promise并没有消除回调,而是让回调变得可控

promise的基本使用
const pro = new Promise((resolve, reject)=>{
    //这里的代码会立即执行       这里主要是为了处理什么时候表示成功,然后调用resolve,什么时候表示失败,调用reject
    // 未决阶段的处理
    // 通过调用resolve函数将Promise推向已决阶段的fulfilled状态
    // 通过调用reject函数将Promise推向已决阶段的rejected状态
    // resolve和reject均可以传递最多一个参数,表示推向状态的数据
    //resolve(data);
    //reject(err);
})

//pro:resolved / rejected
pro.then(data=>{//thenable函数
    //这是thenable函数,如果当前的Promise已经是resolved状态,该函数会立即执行
    //如果当前是未决阶段,则会加入到作业队列,等待到达resolved状态后执行
    //data为状态数据
}, err=>{//catchable函数
    //这是catchable函数,如果当前的Promise已经是rejected状态,该函数会立即执行
    //如果当前是未决阶段,则会加入到作业队列,等待到达rejected状态后执行
    //err为状态数据
})

then()里面放的函数都是异步执行的函数,而且当promise变成resolved或rejected后,会被放到微队列,等待同步代码执行完后再执行微队列的代码

另外一种写法

const pro = new Promise((resolve,reject)=>{
	
});

pro.then((data)=>{
	
});
pro.catch((err)=>{

})

细节

  1. 未决阶段的处理函数是同步的,会立即执行
  2. thenable和catchable函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,并且,加入的队列是微队列
  3. pro.then可以只添加thenable函数,pro.catch可以单独添加catchable函数
  4. 在未决阶段的处理函数中,如果发生未捕获的错误(throw new Error())会将状态推向rejected并会被catchable捕获
  5. 一旦状态推向了已决阶段,无法再对状态做任何更改,所以后面如果还继续调用resolve或reject,也就是无效代码(可以忽略)
  6. Promise并没有消除回调,只是让回调变得可控
promise串联

当后续的Promise需要用到之前的Promise的处理结果时,需要Promise的串联

Promise对象中,无论是then方法还是catch方法,它们都具有返回值返回的是一个全新的Promise对象,它的状态满足下面的规则:

  1. 如果当前的Promise是未决的,得到的新的Promise是挂起状态(pendding)
  2. 如果当前的Promise是已决的,会运行相应的后续处理函数,并将后续处理函数的结果(返回值)作为resolved状态数据,应用到新的Promise中;
    如果后续处理函数发生错误,则把返回值作为rejected状态数据,应用到新的Promise中。

后续的Promise一定会等到前面的Promise有了后续处理结果后,才会变成已决状态

如果前面的Promise的后续处理,返回的是一个Promise,则返回的新的Promise状态数据后续处理返回的Promise状态数据保持一致。

	   const pro1 = new Promise((resolve, reject) => {
            resolve(1);
        })
        const pro2 = pro1.then(data => {
            return data * 2; //会返回一个新的promis对象,return 会把数据返回给新的promise对象,当作传输的数据
        })
        console.log(pro2); // 输出的promis对象是 pendding状态
        //因为then(异步函数);里面的代码会放到微队列中等待同步代码执行完,所以因为pro1的thenable函数还没有执行,以至于pro2还是pendding
        pro2.then(data => { //这里的data是pro1对象return的数据   这时then可以执行,是因为pro2是第二个进入微队列,比pro1的thenable慢,所以此时pro1和pro2都已经处于fulfilled状态了
            console.log(data); //2
            console.log(pro2); //fulfilled状态:完成状态
        })

细节练习

只要pro1的作业队列函数没有出错(报错),pro2就是thenable状态

const pro1 = new Promise((resolve, reject) => {
            throw 2;//抛出错误信息 2
        })
        const pro2 = pro1.then(data => {
            return data * 2;
        }, err => {
            return err * 3; //6    如果改为 throw err * 3,那么pro2就是调用catchable函数了
        })
        
        
        pro2.then(data => { //当pro1的catchable函数没有发生抛出错误,pro2就会执行thenable函数
            console.log(data * 2);
        }, err => { //如果抛出错误了就会执行catchable函数
            console.log(err * 3);
        })

pro2只会对应一个then/catch函数,也就是 = 后面的then/catch

尽管后续pro1有多少个作业队列,都是不影响的

        const pro1 = new Promise((resolve, reject) => {
            throw 2;
        })
        const pro2 = pro1.then(data => {
            return data * 2;
        }, err => {
            return err * 3; //6
        })
        pro1.catch(err => {
            return err * 2; //4
        })


        pro2.then(data => { //当pro1的catchable函数没有发生错误,pro2就会执行thenable函数
            console.log(data * 2);//依然是 6 * 2 = 12
        }, err => { //如果错误了就会执行catchable函数
            console.log(err * 3); 
            //这里选择err是6,而不是4;因为pro2只会对应一个then/catch函数,也就是 = 后面的then/catch
            //所以后面的catch并不会影响pro2的数据
        })

因为返回的是promise对象,所以会将promise对象的数据return给pro3,而不是return对象

 const pro1 = new Promise((resolve, reject) => {
            resolve(1);
        })
        const pro2 = new Promise((resolve, reject) => {
            resolve(2);
        })
        const pro3 = pro1.then(data => {
            return pro2; //此时 因为返回的是promise对象,所以会将promise对象的数据return给pro3,而不是return pro2对象
        })
        pro3.then(data => {
            console.log(data); //2
        })

链式写法,下面代码与上面的一样效果:不用创建变量,直接用返回的promise对象调用即可

 const pro1 = new Promise((resolve, reject) => {
            resolve(1);
        })
        const pro2 = new Promise((resolve, reject) => {
            resolve(2);
        })
        pro1.then(data => {
            return pro2; //此时 因为返回的是promise对象,所以会将promise对象的数据return给pro3,而不是return对象
        }).then(data => {
            console.log(data); //2
        })

始终记住:上一个promise的返回的结果,就是下一个promise的数据

邓哥表白女神案例(练习串联)
      function biaobai(god) {
            return new Promise((resolve, reject) => {
                console.log(`邓哥向${god}表白`);
                setTimeout(() => {
                    if (Math.random() < 0.1) {
                        resolve(true);
                    } else {
                        resolve(false);
                    }
                }, 2000)
            })
        }
        const pro1 = biaobai('女神1').then(resp => {
            if (resp) {
                console.log('邓哥表白女神1成功');
            } else {
                return biaobai('女神2')
            }
        })
        const pro2 = pro1.then(resp => {
        if (resp === undefined) {//如果是undefined说明 邓哥前面已经表白成功
            return;
        } else if (resp) {
            console.log('邓哥表白女神2成功');
        } else {
            return biaobai('女神3')
        }
        })
        const pro3 = pro2.then(resp => {
            if (resp === undefined) {
                return;
            } else if (resp) {
                console.log('邓哥表白女神3成功');
            } else {
                console.log('邓哥表白全部失败了');
                return;
            }
        })

通过for循环缩减代码

        function biaobai(god) {
            return new Promise((resolve, reject) => {
                console.log(`邓哥向${god}表白`);
                setTimeout(() => {
                    if (Math.random() < 0.1) {
                        resolve(true);
                    } else {
                        resolve(false);
                    }
                }, 2000)
            })
        }
        const arr = ['女神1', '女神2', '女神3', '女神4'];
        let pro = null;
        for (let i = 0; i < arr.length; i++) {
            if (i == 0) {
                pro = biaobai(arr[i]);
            }
            pro = pro.then(resp => {
                if (resp === undefined) {
                    return;
                } else if (resp) {
                    console.log(`邓哥表白${arr[i]}成功`);
                } else {
                    if (i < arr.length - 1) { //当i为数组长度-1时,已经没有下一个女神了
                        return biaobai(arr[i + 1])
                    } else {
                        console.log('表白全部失败了');
                        return;
                    }

                }
            })
        }

promise其他的API

原型成员 (实例成员)
  • then:注册一个后续处理函数,当Promise为resolved状态时运行该函数(其实then也可以注册rejected函数)

  • catch:注册一个后续处理函数,当Promise为rejected状态时运行该函数

  • finally:[ES2018]注册一个后续处理函数(无参),当Promise为已决时运行该函数

    (无论是resolved还是rejected,他都会运行,并且它的运行顺序跟注册顺序有关,先注册就先执行)

构造函数成员 (静态成员)
  • resolve(数据):该方法返回一个fulfilled状态的Promise,传递的数据作为状态数据

    const pro = new Promise((resolve,reject)=>{
    	resolve(1);//直接就推向resolved状态
    })
    //下面代码等同上面的代码
    const pro2 = Promise.resolve(1);
    
    //如果resolve/reject的括号中传了个promise对象进去,则
    const pro3 = Promise.resolve(pro2);
    //等效于下面代码
    const pro4 = pro3;
    console.log(pro4 === pro3);//true
    
    • 特殊情况:如果传递的数据是Promise,则直接返回传递的Promise对象
  • reject(数据):该方法返回一个rejected状态的Promise,传递的数据作为状态数据

  • all(iterable):这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功一旦有任何一个iterable里面的promise对象失败则立即触发该promise对象的失败

    这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值的数组作为成功回调的返回值,顺序跟iterable的顺序保持一致

    如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。

    Promise.all方法常被用于处理多个promise对象的状态集合。

    const pros = [];
    	for(let i = 0;i < 10; i++){
    		setTimeout(()=>{
    			pros.push(new Promise((resolve,reject)=>{
    				resolve(i);
    			}))
    		},1000)
    	}
    const pro = Promise.all(pros);
    pro.then(resp=>{//里面的promise全部resolved,pro才能触发thenable/catchable函数
    	console.log('全部完成',resp);//全部完成[0,1,2,3,4,5,6,7,8,9]
    })
    
    • race(iterable):当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象

手写Promise

const myPromise = (function () {
    //状态值
    const RESOLVED = "RESOLVED",
        REJECTED = "REJECTED",
        PENDDING = "PENDDING",
        //属性
        PromiseState = Symbol("PromiseState"),
        PromiseValue = Symbol("PromiseValue"),
        thenables = Symbol("thenables"),
        catchables = Symbol("catchables");
    //内部函数
    const changeState = Symbol("changeState"),//改变状态
        runHandle = Symbol("runHandle"),//执行异步函数
        thenValue = Symbol("thenValue");//处理异步函数的返回值
    return class myPromise{
        constructor(run) {
            //状态初始化
            this[PromiseState] = PENDDING;
            this[PromiseValue] = undefined;
            //初始化作用队列
            this[thenables] = [];
            this[catchables] = [];
            //创建resolve、reject
            const resolve = data => {
                //改变状态
                this[changeState](data,RESOLVED,this[thenables])
            };
            const reject = err => {
                //改变状态
                this[changeState](err,REJECTED,this[catchables])
            }
            //调用run,为了实现抛出异常也会改变状态,所以使用try
            try {
                run(resolve, reject);
            } catch (err) {
                reject(err);
            }
        }
        /**
         * 改变状态
         * @param {data} 接收的状态数据
         * @param {state} 需要转变的状态
         * @param {list} 状态对应的作用队列
         */
        [changeState](data,state,list) {
            if (this[PromiseState] !== PENDDING) {
                return;
            }
            if (this[PromiseState] === PENDDING) {
                this[PromiseState] = state;
                this[PromiseValue] = data;
                list.forEach(item => item(this[PromiseValue]));
            }
        }
        /**
         * 执行异步函数
         * @param {state} 函数执行需要的状态
         * @param {handle} then/catch传进来的函数
         * @param {list} 作用队列
         */
        [runHandle](state,handle,list) {
            if (this[PromiseState] === state) {
                setTimeout(() => {
                    handle(this[PromiseValue]);
                },0)
            }else {
                list.push(handle);
            }
        }
        /**
         * 处理then函数的返回值
         * @param {thenable} then的第一个参数
         * @param {catchable} then的第二个参数
         */
        [thenValue](thenable, catchable) {
            //检验result值
            function testResult(result, resolve) {
                if (result instanceof myPromise) {
                    //递归调用then,得到myPromise对象的数据
                    result.then(d => {//将result的状态数据 赋值给 返回的promise状态数据
                        resolve(d);
                    }, e => {
                        resolve(e);
                    })
                } else {//否则将promise的状态数据等于thenable的返回数据
                    resolve(result);
                }
            }
            return new myPromise((resolve, reject) => {
                //thenable
                this[runHandle](RESOLVED, data => {
                    try {
                        if (typeof thenable !== "function") {
                            return;
                        }
                        const result = thenable(data); //有可能抛出异常,抛出异常执行catch代码
                        testResult(result, resolve);
                    } catch (err) {
                        reject(err);
                    }
                }, this[thenables])
                //catchable
                this[runHandle](REJECTED, data => {
                    try {
                        if (typeof catchable !== "function") {
                            return;
                        }
                        const result = catchable(data);
                        testResult(result, resolve);
                    } catch (err) {
                        reject(err);
                    }
                }, this[catchables])
            })
        }
        then(thenable,catchable) {
            return this[thenValue](thenable, catchable);
        }
        catch(catchable) {
            return this[thenValue](undefined, catchable);
        }

    }
})()

async 和 await

async 和 await 是 ES2016 新增两个关键字,它们借鉴了 ES2015 中生成器在实际开发中的应用,目的是简化 Promise api 的使用,并非是替代 Promise。

async

目的是简化在函数的返回值中对Promise的创建

async 用于修饰函数(无论是函数字面量还是函数表达式),放置在函数最开始的位置,被修饰函数的返回结果一定是 Promise 对象

async function test(){
    console.log(1);
    return 2;//如果没有return  那么最后Promise对象得到的值就是undefined
    // throw  2    得到的promise对象就是rejected状态
}

//等效于

function test(){
    return new Promise((resolve, reject)=>{
        console.log(1);
        resolve(2);
    })
}

注意:在async函数中不能在定时器中return数据,因为这样return的不是async函数,而是定时器中的函数return

async function test1(){
	setTimeout(()=>{
        return 2;//这样并不能实现resolve效果  return的是定时器里面的箭头函数
    })	
}

await

await关键字必须出现在async函数中!!!!

await用在某个表达式之前,如果表达式是一个Promise,则得到的是thenable中的状态数据。

async function test1(){
    console.log(1);
    return 2;
}

async function test2(){//返回一个promise对象  值是undefined   状态fulfilled
    const result = await test1();
    console.log(result);
    //如果这里 return 2;  等待await处于已决阶段之后,返回的promise对象  值是2  状态fulfilled
}

test2();

等效于

function test1(){
    return new Promise((resolve, reject)=>{
        console.log(1);
        resolve(2);
    })
}

function test2(){
    return new Promise((resolve, reject)=>{
        test1().then(data => {//当test1()成功后把data传给result
            const result = data;
            console.log(result);
            resolve();
        })
    })
}

test2();

面试题可能会考:如果await的表达式不是Promise,则会将其使用Promise.resolve包装后按照规则运行

async function test(){
	const a = await 1;
    console.log(a);
}
test();
console.log(2);
//先输出 2 
//再输出 1

等同于下面代码
function test(){
    return new Promise((resolve,reject)=>{
        Promise.resolve(1).then(data=>{
            const a = data;
            console.log(a);
        })
    })
}
test();
console.log(2);

async和await的细节注意

await等待一个promise运行完后,才能执行下面的代码,不然会一直等待

        function time() {//由于async中使用定时器无法实现resolve所以才不使用async模拟
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve(1);
                }, 5000);
            })
        }
        async function test() {
            console.log(1);
            const a = await time();
            console.log(2);
        }
        test();

// 先输出1
//等待5秒后  输出2

输出顺序,await后面的代码相当于是then函数里面的thenable函数,是异步函数,在微队列中等待

        async function time() {
            console.log(1);//同步代码
        }
        async function test() {
            const a = await time();//相当于在then中执行的代码  也就是下面的代码被放到了微队列中等待同步代码执行完毕 
            console.log(2);
            console.log(time());
        }
        // test();
        console.log(test());
// 输出顺序
1
Promise {<pending>}
2
1
Promise {<fulfilled>: undefined}

async可以修饰匿名函数,如下面立即执行函数

async function test1(){//也可以使用箭头函数
    console.log(1);
    return 2;
}
(async function(){
	const a = await test1();
    console.log(a);//2
}())

成功状态和错误状态的数据传输(需要使用try,不然抛出错误后,后面的代码就无法执行了)

async function test(){
	if(Math.random() < 0.5){
        return 1;//resolve
    }else{
        throw 2;//reject
    }
}
(async()=>{
    try{
        const a = await test();
        console.log('成功状态:'+a);//如果这里是抛出错误,则执行catch里的代码
    }catch(err){
        console.log('错误状态:'+err);
    }
}());
// 输出 成功状态:1
//或 错误状态:2

promise改造定时器

实现指定时间后才会推向已决状态

直接用setTimeout或setInterval是无法实现的

//promise改造定时器
        function delay(duration) {//等待duration时间后才会推向已决状态
            return new Promise((resolve, reject) => {
                setTimeout(() => {
                    resolve();
                }, duration);
            })
        }
        async function test() {
            console.log(1);
            await delay(2000); //等待2秒钟
            console.log(2); //2秒后输出2
        }
        test();

FetchAPI的概述

XMLHttpRequest的问题
  1. 所有的功能全部集中在同一个对象上,容易书写出混乱不易维护的代码
  2. 采用传统的事件驱动模式,无法适配新的 Promise Api
Fetch Api 的特点
  1. 并非取代 AJAX,而是对 AJAX 传统 API 的改进

    ajax是一个浏览器向服务器请求的一个概念,可以理解为一个标准,并非取代ajax
    只不过我们之前使用的是传统方式的ajax,而fetch是改进过后的ajax使用方式
    
  2. 精细的功能分割:头部信息、请求信息、响应信息等均分布到不同的对象,更利于处理各种复杂的 AJAX 场景

  3. 使用 Promise Api,更利于异步代码的书写

  4. Fetch Api 并非 ES6 的内容属于 HTML5 新增的 Web Api

  5. 需要掌握网络通信的知识

Fetch基本使用

语法

函数有两个参数:

1.必填,字符串,请求地址

2.选填,对象,请求配置

const url = 'http://101.132.72.36:5100/api/local';
const config = {
    method:'POST',
    headers:{
        Content-Type:'application/json',//请求的内容是 json格式数据
    },
    body:`{'a':'1'}`
}
fetch(url,config);//这样就能发送请求了
请求配置对象

请求配置对象

  • method:字符串,请求方法,默认值GET
  • headers:对象,请求头信息
  • body: 请求体的内容,必须匹配请求头中的 Content-Type(比如需要post请求时,请求参数是放在请求体中的,就需要配置在body上)
  • 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”时有效).
返回值

fetch 函数返回一个 Promise 对象

  • 当收到服务器的返回结果后,Promise 进入resolved状态,状态数据为 Response 对象

  • 当网络发生错误(或其他导致无法完成交互的错误)时,Promise 进入 rejected 状态,状态数据为错误信息

    const url = 'http://101.132.72.36:5100/api/local';
    console.log(fetch(url));
    
    image-20201126145552215

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 的响应。

    test( ):解析成文本

        (async() => {
            const url = 'http://101.132.72.36:5100/api/local';
            const pro = await fetch(url); //返回Response对象
            const text = await pro.text();
            console.log(text); //所需要的数据
        })();

json():解析成json格式的对象

        (async() => {
            const url = 'http://101.132.72.36:5100/api/local';
            const pro = await fetch(url); //返回Response对象
            const data = await pro.json();
            console.log(data); //所需要的json格式对象
        })();
Request 对象

除了使用基本的fetch方法,还可以通过创建一个Request对象来完成请求(实际上,fetch的内部会帮你创建一个Request对象

new Request(url地址, 配置)

注意点:

尽量保证每次请求都是一个新的Request对象

用法

        (async() => {
            const url = 'http://101.132.72.36:5100/api/local';
            const req = new Request(url);
            const pro = await fetch(req); //返回Response对象
            const data = await pro.json();
            console.log(data); //所需要的json格式对象
        })();
Response对象

有时候可能不想用url调试代码,

就可以创建一个response对象进行测试

就可以使用**new Response(响应体,属性配置)**创建

       (async() => {
            const resp = new Response(`[
                {"id":1,"name":"清远"},
                {"id":1,"name":"清远"}
            ]`, {
                ok: true,
                status: 200
            })
            const data = await resp.json();
            console.log(data); //所需要的json格式数据
        })();
Headers对象

在fetch方法中,配置对象里面其实也可以配置headers

其实,它运行时,实际上是创建了一个Headers对象,然后再放进配置对象中进行请求的

所以代码可以写成这样

        (async() => {
            const url = 'http://101.132.72.36:5100/api/local';
            const head = new Headers({
                a: 1,
                b: 2
            });
            const pro = await fetch(url, {
                headers: head //这样请求头里 就会有 {a:1,b:2}
            }); //返回Response对象
            const data = await pro.json();
            console.log(data); //所需要的json格式对象
        })();

Headers对象中的方法:

  • has(key):检查请求头中是否存在指定的key值
  • get(key): 得到请求头中对应的key值(如果key不存在,则返回null)
  • set(key, value):修改对应的键值对(如果修改的key不存在,则会新创建一个key)
  • append(key, value):添加对应的键值对
  • keys(): 得到所有的请求头键的集合
  • values(): 得到所有的请求头中的值的集合
  • entries(): 得到所有请求头中的键值对的集合
文件上传

流程:

  1. 客户端将文件数据发送给服务器
  2. 服务器保存上传的文件数据到服务器端
  3. 服务器响应给客户端一个文件访问地址

测试地址:http://101.132.72.36:5100/api/upload 键的名称(表单域名称):imagefile

请求方法:POST

请求的表单格式:multipart/form-data 请求体中必须包含一个键值对,键的名称是服务器要求的名称(要和服务器的名称相对应),值是文件数据(input对象.files[0])

HTML5中,JS仍然无法随意的获取文件数据,但是可以获取到input元素中,被用户选中的文件数据 可以利用HTML5提供的FormData构造函数来创建请求体

可以获取到input元素中被选中的文件数据,如下:

<body>
    <input type="file" id="avatar">
    <!--上传文件-->
    <script>
        function upload() {
            const inp = document.getElementById('avatar');
            console.log(inp.files);
        }
    </script>
</body>
image-20201126162659149

上传图片数据

<body>
    <input type="file" id="avatar">
    <!--上传文件-->
    <script>
        async function upload() {
            const inp = document.getElementById('avatar');
            if (!inp.files) {
                alert('请选择文件上传');
                return;
            }
            const formData = new FormData(); //创建请求体
            //添加键值
            formData.append("imagefile", inp.files[0]);
            //然后就是发送请求
            const url = "http://101.132.72.36:5100/api/upload";
            const pro = await fetch(url, {
                method: 'POST',
                body: formData, //请求体放创建好的就行
            });
            //转为json格式对象
            const result = await pro.json();
            console.log(result);//输出图片地址
        //{path: "http://images.yuanjin.tech/Fgqr9auiMtyS0sUc57GgYJ8TUdva"}
        }
    </script>
</body>

迭代器

背景知识
  1. 什么是迭代?

从一个数据集合中按照一定的顺序,不断取出数据的过程

  1. 迭代和遍历的区别?

迭代强调的是依次取数据,并不保证取多少也不保证把所有的数据取完

遍历强调的是要把整个数据依次全部取出

  1. 迭代器

对迭代过程的封装,在不同的语言中有不同的表现形式,通常为对象

  1. 迭代模式

一种设计模式,用于统一迭代过程,并规范了迭代器规格:

  • 迭代器应该具有得到下一个数据的能力
  • 迭代器应该具有判断是否还有后续数据的能力
JS中的迭代器

JS规定,如果一个对象具有next方法,并且该方法返回一个对象,该对象的格式如下:

{value: 值, done: 是否迭代完成}

则认为该对象是一个迭代器

如:

       const obj = {//obj就是迭代器  符合条件的都叫迭代器
            next() {
                return {
                    value: xxx,
                    done: xx
                }
            }
        }

含义:

  • next方法:用于得到下一个数据
  • 返回的对象
    • value:下一个数据的值
    • done:boolean,是否迭代完成(true/false)
代码实现迭代器原理
       const arr = [1,2,3,4,5];
       const iterator = {
            i: 0, //表示元素当前数组下标
            next() {
                return {
                    value: arr[this.i++], //value = this[i],然后i++
                    done: this.i >= arr.length // 如果i++之后超出了最大下标,则表示下标完成
                }
            }
        }
       //编写完迭代器之后,如果需要数据,只需要调用一次函数即可拿到下一个数据
        let data = iterator.next();//得到第一个数据
        while (!data.done) {//如果done不为true  ,就一直执行下面代码
            console.log(data.value);
            data = iterator.next();//调用next()得到下一个数据
        }
        console.log('迭代完成');//跳出循环后输出 迭代完成

通过迭代器可以实现将数组和获取数据者分离开,获取数据者完全不需要见到数组

**简单理解:**迭代器就是一个厂库(数组)的管家,而获取数据者只需要向管家拿数据即可,至于厂库里面到底怎么运作的不需要管

迭代器创建函数

一个返回迭代器的函数

//迭代器创建函数   iterator creator
        function createIterator(arr) {
            let i = 0; //表示元素当前数组下标
            return { //返回迭代器
                next() {
                    return {
                        value: arr[i++], //value = this[i],然后i++
                        done: i >= arr.length // 如果i++之后超出了最大下标,则表示下标完成
                    }
                }
            }
        }

迭代器的优势之一

当需要对一个无限长的数组进行遍历或者获取数据时,使用迭代器是最好的方式

它不需要知道数组有多长,不需要把整个数组装进去遍历

而如果使用常规遍历,则就需要把整个数组都得装进去进行遍历,可能会耗费很大的性能

迭代器的核心思想

就相当于一个机器

只会无脑地一直取下一个,并判断下一个是否还有值

迭代器协议

可迭代协议

ES6规定,如果一个对象具有知名符号属性Symbol.iterator,并且属性值是一个迭代器创建函数,则该对象是可迭代的(iterable)

比如

const obj = {
	[Symbol.iterator](){
		return {
			next(){
				return{
					value:1,
					done:false
				}
			}
		}
	}
}

思考

如何知道这个对象是不是可迭代的?
1.查看这个对象是否含有Symbol(Symbol.iterator)属性,并且是一个方法
2.该方法返回一个迭代器对象
3.迭代器对象要求含有属性 value:当前值,done:是否迭代完成  
如何遍历一个可迭代对象?

        const arr = [1, 2, 3, 4, 5, 6, 7];
        const iterator = arr[Symbol.iterator](); //调用数组的迭代器创建函数,以获取数组的迭代器
        let iterable = iterator.next(); //获取迭代器返回的第一个对象
        while (!iterable.done) {
            const item = iterable.value;
            console.log(item);
            //下一次迭代
            iterable = iterator.next();
        }
        console.log('迭代完成');

在ES6中就把所有数组都变成了可迭代对象,里面也有Symbol(Symbol.iterator)属性

for-of 循环

for-of 循环用于遍历可迭代对象,格式如下

所用到的原理就是上面所写的代码

//迭代完成后循环结束
for(const item in iterable){
    //iterable:可迭代对象
    //item:每次迭代得到的数据(相当于iterable.value)
}

通过自定义迭代器,可以改变for-of的item返回的值

//自定义编写Symbol(Symbol.iterator),实现for   of遍历输出对象的属性:值
        const obj = {
            a: 1,
            b: 2,
            [Symbol.iterator]() {
                let keys = Object.keys(this);//获取对象的所有属性名,返回一个数组
                let i = 0;
                return {
                    next: () => {
                        const key = keys[i];//属性名
                        const keyValue = this[keys[i]];//属性值
                        i++; //判定前i已经是1了,所以是>keys.length   也就是3的时候变成true
                        return {
                            value: {
                                [key]: keyValue
                            },
                            done: i > keys.length ? true : false
                        }
                    }
                }
            }
        }
        for (const item of obj) {
            console.log(item);
        }

展开运算符与可迭代对象

展开运算符可以作用于可迭代对象,这样,就可以轻松的将可迭代对象转换为数组。

实际上展开运算符展开的是可迭代对象

只不过展开运算之后,返回的是一个数组(即使对可迭代对象进行展开)

迭代器和可迭代对象的关系

  • **可迭代对象:**如果对象中含有Symbol(Symbol.iterator)属性方法,则表示这是一个可迭代对象
  • **迭代器:**是Symbol(Symbol.iterator)属性方法执行返回的对象,对象中含有next()方法,next()可以返回一个对象,里面有value、done属性;

生成器

实际上,生成器可以理解为是操作迭代器的一个语法糖

什么是生成器?
  • 生成器是一个通过构造函数Generator创建的对象(但是,我们是不能直接new构造函数创建生成器,因为Generator只给内部使用
  • 生成器既是一个迭代器,也是一个可迭代对象(说明:生成器同时含有**next()方法以及Symbol(Symbol.iterator)**方法)

注意:生成器实际上并没有next()Symbol(Symbol.iterator)这两个属性,但是它的原型上有这两个属性,所以可以正常调用;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kCOrgZpe-1627093369331)(C:\Users\26704\AppData\Roaming\Typora\typora-user-images\image-20201127151550030.png)]

如何创建生成器?
  • 生成器的创建,必须使用生成器函数(Generator Function)
  • 生成器函数的使用方式:只需要在函数名前面function关键字后面加上一个 ***** ,就是表示生成器函数,比如:
    该函数一定会返回一个生成器(即使函数里面没有任何东西)
    所以:函数不能同时添加 async、* 两个关键字,async返回promise对象,* 返回生成器对象
//写法一:
function *method(){};
//写法二:
function* method(){};

注意:

调用生成器函数,作用只有一个:就是创建生成器

所以调用生成器函数时,即使生成器函数里面有多少代码,都不会执行

生成器函数如何执行?

实际上,生成器就是为了简便迭代器的代码编写

  • 生成器函数的内部:是为了给生成器的每一次迭代提供数据的

所以:生成器函数的内部的代码,只有进行迭代才会执行

每次调用生成器的next方法,将导致生成器函数运行到下一个yield关键字位置,直到找不到yield关键字后

返回{value : undefined , done : true}

  • yield:(读音:一偶的)是一个关键字,该关键字只能在生成器函数内部使用,表达:“产生”一个迭代数据的意思。

  • 调用生成器迭代的方式:生成器对象.next()

    运行过程如下:

        function* method() {
            console.log('第一次运行');
            yield 1; //产生一个{value : 1,done:false/true取决于后面是否还存在yield关键字}的对象,并返回
            console.log('第二次运行');
            yield 2;
            console.log('第三次运行');
            //运行到这的时候,因为后面没有yield了,所以产生的数据是{value:undefined,done:true},并返回
        };
        const generator = method();
        //调用生成器迭代的方式:生成器对象.next()
image-20201127141917833

生成器的小练习

迭代数组

通过生成器,就能把生成器函数的内部变得可控,便捷于迭代数据

        const arr = [1, 2, 3, 4, 5];

        function* getArr(arr) {
            for (const item of arr) { //此处相当于生成arr.length个yield关键字,并且产生的value = item
                yield item;
            }
        }
        const props = getArr(arr); //返回一个生成器对象
        console.log(props.next()); //{value: 1, done: false}
        console.log(props.next()); //{value: 2, done: false}
        console.log(props.next()); //{value: 3, done: false}
        console.log(props.next()); //{value: 4, done: false}
        console.log(props.next()); //{value: 5, done: false}
        console.log(props.next()); //{value: undefined, done: true}
斐波拉契数列迭代
      function* getFeiboGenerator() {
            let prev1 = 1,//前一位
                prev2 = 1,//前二位
                n = 1;
            while (true) { //斐波拉契数列是无限的,所以使用死循环
                if (n <= 2) {
                    yield 1;
                } else {
                    const result = prev1 + prev2;
                    yield result;
                    prev2 = prev1;
                    prev1 = result;
                }
                n++;
            }
        }
        const feibo = getFeiboGenerator();
        console.log(feibo.next()); //{value: 1, done: false}
        console.log(feibo.next()); //{value: 1, done: false}
        console.log(feibo.next()); //{value: 2, done: false}
        console.log(feibo.next()); //{value: 3, done: false}
        console.log(feibo.next()); //{value: 5, done: false}
生成器需要注意的细节

有哪些需要注意的细节?

  1. 生成器函数可以有返回值,返回值出现在第一次done为true时的value属性中

    **可以理解为:**当程序读到return时,就会把返回对象中的done变成true,并把值赋给value(也就是把迭代结束了)

    导致 return 后面如果还遇到yield关键字,则返回的数据都是 {value : undefined , done : true}

            function* method() {
                console.log('第一次运行');
                yield 1;
                console.log('第二次运行');
                yield 2;
                console.log('第三次运行');
                return 10;
            }
            const generator = method();
            console.log(generator.next());
            console.log(generator.next());
            console.log(generator.next());
    
    image-20201127144903554
  2. 调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值

    function* method() {
       let info = yield 1; //第一次next没传参数时,返回{value: 1, done: false},并且程序停在这
        
       console.log(info); //第二次next(5)传了参数后,就会把(yield 1)的返回值变为参数值5,所以此时的info = 5,并输出5
        
       info = yield 2 + info; // info = yield 2 + 5;返回{value: 7, done: false},并且程序停在这
        
       console.log(info); //第三次next(1)传了参数后,把(yield 2 + 5)变成参数值1,所以此时的info = 1,并输出1
        
                          //由于查找不到yield了,所以返回{value: undefined, done: true}
       }
       const generator = method();
    
    image-20201127151155853
  3. 所以第一次调用next方法时,传参没有任何意义

  4. 在生成器函数内部,可以调用其他生成器函数,但是要注意加上*号

            function* t1() {
                yield 'a';
                yield 'b';
            }
    
            function* test() {
                yield* t1();
                //这样调用可以理解为在这里加入了以下代码:
                //yield 'a';
                //yield 'b';
                yield 1;
                yield 2;
            }
    	   const generator = test();
         let iterable = generator.next();
            while (!iterable.done) {
                console.log(iterable.value);
                iterable = generator.next();
            }
    
生成器的其他API
  • return方法:调用该方法,可以提前结束生成器函数,从而提前让整个迭代过程结束
  • throw方法:调用该方法,可以在生成器中产生一个错误
使用生成器模拟异步函数处理
        function* task() {
            const d = yield 1;
            console.log(d); //1
            const resp = yield fetch('http://101.132.72.36:5100/api/local');//返回promise对象
            //然后通过next()传值,把 yield fetch得到的数据转为data
            result = yield resp.json();//当执行该行代码时,resp已经是请求得到的data了
            console.log(result);
        }
        run(task);

        function run(generatorFunc) {
            const generator = generatorFunc();
            let result = generator.next(); //启动任务 并获得第一个生成器对象数据{value:1,done:false}
            //判断数据
            handleResult();

            function handleResult() {
                if (result.done) { //直到result.done为true时,表示任务完成,结束操作
                    return;
                }
                if (typeof(result.value.then) === 'function') { //返回的value是一个promise对象
                    //promise对象数据
                    result.value.then(data => { //当fetch请求成功后,返回promise对象的data数据
                        result = generator.next(data); //此处的data赋值给了resp,并继续迭代下一个数据
                        handleResult(); //继续进行判断
                    }, err => {
                        throw err;
                    })
                } else {
                    //普通数据
                    result = generator.next(result.value); //把value赋值给result,也就是1,并继续迭代下一个数据
                    handleResult(); //继续进行判断
                }
            }
        }

更多的集合类型

set集合

一直以来,JS只能使用数组和对象来保存多个数据,缺乏像其他语言那样拥有丰富的集合类型。因此,ES6新增了两种集合类型(set 和 map),用于在不同的场景中发挥作用。

set用于存放不重复的数据

  1. 如何创建set集合
new Set(); //创建一个没有任何内容的set集合

new Set(iterable); //创建一个具有初始内容的set集合,内容来自于可迭代对象每一次迭代的结果

const a = new Set();
console.log(a);//{}

const b = new Set([1,2,3,4]);
console.log(b);//{1, 2, 3, 4}

//如果是一些重复的数据,则Set会自动去重
const c = new Set([1,2,3,1,2,4,4,5]);
console.log(c);//{1, 2, 3, 4, 5}
const d = new Set('aabbcssddss');
console.log(d);//{"a", "b", "c", "s", "d"}
  1. 如何对set集合进行后续操作
  • add(数据): 添加一个数据到set集合末尾,如果数据已存在,则不进行任何操作
    • set使用Object.is的方式判断两个数据是否相同,但是,针对+0和-0,set认为是相等
  • has(数据): 判断set中是否存在对应的数据
  • delete(数据):删除匹配的数据,返回是否删除成功(注意:括号里面不是放下标,是数据值)
  • clear():清空整个set集合
  • size: 获取set集合中的元素数量,只读属性,无法重新赋值
  1. 如何与数组进行相互转换
const s = new Set([x,x,x,x,x]);
// set本身也是一个可迭代对象,每次迭代的结果就是每一项的值
const arr = [...s];
  1. 如何遍历

1). 使用for-of循环

2). 使用set中的实例方法forEach

注意:set集合中不存在下标,因此forEach中的回调的第二个参数和第一个参数是一致的,均表示set中的每一项

set应用

求两个数组的并集、交集、差集

        const arr1 = [11, 22, 33, 44, 55, 55, 22, 77];
        const arr2 = [33, 88, 12, 22, 99, 99, 66, 55];
        console.log('arr1', arr1);
        console.log('arr2', arr2);
        //并集
        console.log('并集', [...new Set([...new Set(arr1), ...new Set(arr2)])]);
        console.log('并集', [...new Set(arr1.concat(arr2))]);

        //交集
        const cross = [...new Set(arr1)].filter(item => arr2.indexOf(item) >= 0);
        console.log('交集', cross);

        //差集(在并集中找出arr1独有的和arr2独有的元素或用 并集-交集)
        console.log('差集', [...new Set([...new Set(arr1), ...new Set(arr2)])].filter(item => arr1.indexOf(item) >= 0 && arr2.indexOf(item) < 0 || arr1.indexOf(item) < 0 && arr2.indexOf(item) >= 0));
        console.log('差集', [...new Set([...new Set(arr1), ...new Set(arr2)])].filter(item => cross.indexOf(item) < 0));
手写Set()
          class MySet {
            constructor(iterable = []) { //如果不传值,默认[]
                //先判断是否为可迭代对象
                if (typeof iterable[Symbol.iterator] !== 'function') {
                    throw new TypeError(iterable + '不是可迭代对象');
                }
                this._datas = []; //只能借助数组来模仿Set()的返回值
                for (const item of iterable) {
                    this.add(item);
                }
                // for (const item of this._datas) {

                // }
            }
            add(data) {
                if (!this.has(data)) {
                    this._datas.push(data);
                }
            }
            has(data) {
                for (const item of this._datas) {
                    if (this.isEqual(data, item)) { //如果数值相同,则返回true
                        return true;
                    }
                }
                return false;
            }
            isEqual(data, target) {
                if (data == 0 && target == 0) {
                    return true;
                }
                return Object.is(data, target);
            };
            [Symbol.iterator] = function*() { //生成器函数
                for (const item of this._datas) {
                    yield item;
                }
            }
            forEach(func) {
                if (typeof func !== 'function') {
                    return;
                }
                for (const item of this._datas) {
                    func(item, item, this);
                }
            }
        }
        const a = new MySet([1, 2, 3, 3, 4]);
        console.log(a);

map集合

键值对(key value pair)数据集合的特点:键不可重复

map集合专门用于存储多个键值对数据。

在map出现之前,我们使用的是对象的方式来存储键值对,键是属性名,值是属性值。

使用对象存储有以下问题:

  1. 键名只能是字符串
  2. 获取数据的数量不方便
  3. 键名容易跟原型上的名称冲突

何时使用对象,何时使用集合

  • 对象:当需要存储的信息可以构成一个完整的整体,并且基本不会改变数据的时候,使用对象。(如:用户信息)
  • map集合:当存储的数据需要较多次的修改,即使少了一个或多个数据,也并不影响后续操作时,使用集合(如:请求头信息)
如何创建map
new Map();//创建了一个空的map集合

new Map(iterable); //创建一个具有初始内容的map,初始内容来自于可迭代对象每一次迭代的结果,但是,它要求每一次迭代的结果必须是一个长度为2的数组,数组第一项表示键,数组的第二项表示值

        const a = new Map([
            ['a', 1],
            ['b', 2]
        ]);
        console.log(a);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yVEZlVDF-1627093369333)(C:\Users\26704\AppData\Roaming\Typora\typora-user-images\image-20201128104927300.png)]

如何进行后续操作
  • size:只读属性,获取当前map中键的数量
  • set(键, 值):设置一个键值对,键和值可以是任何类型
    • 如果键不存在,则添加一项
    • 如果键已存在,则修改它的值
    • 比较键的方式和set相同
  • get(键): 根据一个键得到对应的值
  • has(键):判断某个键是否存在
  • delete(键):删除指定的键
  • clear(): 清空map
和数组互相转换

和set一样(拓展运算符)

  1. 遍历
  • for-of,每次迭代得到的是一个长度为2的数组
  • forEach,通过回调函数遍历
    • 参数1:每一项的值
    • 参数2:每一项的键
    • 参数3:map本身
手写map集合
class MyMap {
            constructor(iterable = []) {
                if (typeof iterable[Symbol.iterator] !== 'function') { //传进来的数据不是可迭代对象
                    throw new TypeError(`${iterable}不是可迭代对象`);
                }
                for (const item of iterable) {
                    if (typeof item[Symbol.iterator] !== 'function') {
                        throw new TypeError(`${item}不是可迭代对象`);
                    }
                }
                this._datas = [];
                for (const item of iterable) {
                    this.set(item[0], item[1]); //{key:item[0],value:[item1]}
                }
            }
            get size() {
                return this._datas.length;
            }
            _getObj(key) { //根据key返回对应的对象
                for (const item of this._datas) {
                    if (this._isEqual(item.key, key)) {//判断是否已经存在
                        return item; //返回对象
                    }
                }
            }
            set(key, value) {
                const obj = this._getObj(key);
                if (obj) { //如果里面已经有key
                    //修改
                    obj.value = value;

                } else {
                    this._datas.push({
                        key,
                        value
                    });
                }
            }
            get(key) {
                return this._getObj(key).value;
            }
            has(key) {
                for (const item of this._datas) {
                    if (this._isEqual(item.key, key)) { //如果true,证明里面有相同的key
                        return true;
                    }
                }
                return false;
            }
            delete(key) {
                for (let i = 0; i < this._datas.length; i++) {
                    const element = this._datas[i];
                    if (element.key === key) {
                        this._datas.splice(i, 1);
                        return true;
                    }
                }
                return false;
            }
            clear() {
                this._datas.length = 0;
            }
            _isEqual(data1, data2) {
                if (data1 == 0 && data2 == 0) {
                    return true;
                }
                return Object.is(data1, data2);
            }

            [Symbol.iterator] = function*() { //生成器函数,
                for (const {
                        key,
                        value
                    }
                    of this._datas) { //{key,value}是将{key:xxx,value:xxx}解构
                    yield [key, value]; //返回一个{value:[key,value],done:false};数据
                }
            }
            forEach(func) {
                for (let i = 0; i < this._datas.length; i++) {
                    const element = this._datas[i];
                    func(element, i, this._datas);

                }
            }
        }
        const a = new MyMap([
            ['a', 1],
            ['b', 2],
            ['c', 3],
            ['b', 10],
            ['a', 20]
        ]);
        console.log(a);

WeakSet和WeakMap

WeakSet

使用该集合,可以实现和set一样的功能,不同的是:

  1. 它内部存储的对象地址不会影响垃圾回收(set集合是会阻碍系统垃圾回收的,即使该地址没有变量使用了,也不会被回收)
  2. 只能添加对象(因为他就是用来监测对象引用的)
  3. 不能遍历(不是可迭代的对象)、没有size属性、没有forEach方法

垃圾回收是指:

如果该地址已经没有变量使用它了,那么系统就会把他当作垃圾回收起来。

WeakMap

类似于map的集合,不同的是:

  1. 它的键存储的地址不会影响垃圾回收
  2. 它的键只能是对象
  3. 不能遍历(不是可迭代的对象)、没有size属性、没有forEach方法

回顾属性描述符(非ES6内容)

Property Descriptor

Property Descriptor 属性描述符 是一个普通对象,用于描述一个属性的相关信息

通过**Object.getOwnPropertyDescriptor(对象, 属性名)**可以得到一个对象的某个属性的属性描述符

  • configurable(默认false):该属性的描述符是否可以修改(属性描述符还有一些其他的相关信息,也是用于配置属性)

  • enumerable(默认false):该属性是否可以被枚举(也就是:是否可以使用for-in枚举)

    -------------------------------------------------数据描述符具有以下可选键值

  • value(可选,默认undefined):属性值

  • writable(可选,默认false):该属性是否可以被重新赋值(也就是指value是否可以修改)

    -------------------------------------------------存取描述符具有以下可选键值

  • set(可选,默认undefined):属性的 setter 函数。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值)。(如:对象.属性名=值)

  • get(可选,默认undefined):属性的 getter 函数。当访问该属性时,会调用此函数。执行时不传入任何参数,该函数的返回值会被用作属性的值。

**Object.getOwnPropertyDescriptors(对象)**可以得到某个对象的所有属性描述符(这里的跟上面的方法多了个s)

Object.defineProperty

如果需要为某个对象添加属性时修改属性时, 配置其属性描述符,可以使用下面的代码:

Object.defineProperty(对象, 属性名, 描述符);
Object.defineProperties(对象, 多个属性的描述符)

如:

Object.defineProperty()

const obj = {};
Object.defineProperty(obj,'a',{
	value:1,
	configurable:true,//描述符可修改
	enumerable:true,//可枚举
	writable:true//value可修改
})

Object.defineProperties()

const obj = {};
Object.defineProperty(obj,{
	a:{//属性a
        value:1,
	   configurable:true,
	   enumerable:true,
	   writable:true
    },
    b:{//属性b
        value:1,
	   configurable:true,
	   enumerable:true,
	   writable:true
    },
})
存取器属性

属性描述符中,如果配置了 getset 中的任何一个,则该属性,不再是一个普通属性,而变成了存取器属性

get 和 set配置均为函数

如果一个属性是存取器属性,则读取该属性时,会运行get方法,将get方法得到的返回值作为属性值;

如果给该属性赋值,则会运行set方法。

        const obj = {};
        Object.defineProperty(obj, 'a', {
            get() {
                console.log('属性a被访问了');
            },
            set(val) {
                console.log('属性a被设置了',val);
            }
        })
        console.log(obj.a); //undefined,因为get()没有return数据
	    console.log(obj.a = 1); //1,把等号右边的值赋给a,并且执行set(1)
//控制台输出:
//属性a被访问了
//undefined
//属性a被设置了1
//1

        obj.a = obj.a + 1;// 调用set(obj.a+1);   obj.a调用get()返回undefined,undefined+1 = NaN,此时的obj.a=NaN;
	    console.log(obj.a)//再次调用get() 返回undefined
控制台输出:

//属性a被访问了
//属性a被设置了NaN
//属性a被访问了
//undefined

设置set()和get() 让它变成一个正常可以赋值可以访问的属性

       const obj = {};
        Object.defineProperty(obj, 'a', {
            get() {
                console.log('属性a被访问了');
                return obj._a;
            },
            set(val) {
                console.log('属性a被设置了' + val);
                obj._a = val;
            }
        })
        obj.a = 10;
        console.log(obj.a);//10

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6B5Wj6Nm-1627093369334)(C:\Users\26704\AppData\Roaming\Typora\typora-user-images\image-20201128162032596.png)]

存取器属性最大的意义,在于可以控制属性的读取赋值

注意

当属性配置了valuewritable

那么就不能再配置set()get()

因为value本身就是表示内存空间的数据,writable就是指这个内存空间能不能修改
而设置set()、get()就是变成运行函数了,不占用内存空间的
两者相互矛盾

如果配置了set()、get(),再去配置value、writable,则会报错

Reflect 反射

Reflect是什么?

Reflect是一个内置的JS对象,它提供了一系列方法,可以让开发者通过调用这些方法,访问一些JS底层功能

由于它类似于其他语言的反射,因此取名为Reflect

它可以做什么?

使用Reflect可以实现诸如 属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否存在与对象中 等等功能

这些功能不是已经存在了吗?为什么还需要用Reflect实现一次?

有一个重要的理念,在ES5就被提出:减少魔法让代码更加纯粹

这种理念很大程度上是受到函数式编程的影响

ES6进一步贯彻了这种理念,它认为,对属性内存的控制、原型链的修改、函数的调用等等,这些都属于底层实现,属于一种魔法,因此,需要将它们提取出来,形成一个正常的API,并高度聚合到某个对象中,于是,就造就了Reflect对象

因此,你可以看到Reflect对象中有很多的API都可以使用过去的某种语法或其他API实现。

Reflect对象提供的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): 判断一个对象是否拥有一个属性(类似 in关键字: ‘a’ in obj ,有的话返回true)
  • 其他API:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

Proxy 代理

代理的图像解释:

image-20201129212710116

代理:提供了修改底层实现的方式

//代理一个目标对象
//target:目标对象
//handler:是一个普通对象,其中可以重写底层实现
//返回一个代理对象
new Proxy(target, handler)

使用

       const obj = {
            a: 1,
            b: 2
        }
        const proxy = new Proxy(obj, {
            set(target, propertyKey, value) {//target就是obj
                //target[propertyKey] = value; //代理帮对象的属性赋值
                Reflect.set(target, propertyKey, value); //一样的效果,但是使用Reflect并不会影响到底层代码
            },
            get(target, propertyKey) { //可以自己修改底层js功能,但是是通过重写Reflect反射的方法,实现重写底层
                if (Reflect.has(target, propertyKey)) { //查看是否有该属性
                    return Reflect.get(target, propertyKey);
                } else { //没有则返回-1
                    return -1;
                }
            },
            has(target, propertyKey) {
                return false; //修改底层has(),查找属性时,直接告诉它没有该元素
            }
        })
        obj.a = 10; // 此处就会触发set(obj,{set(obj,a,10)})
        console.log(proxy.a); //10   注意:使用代理后,调用属性也是通过代理调用
        console.log(proxy.c); //-1
        console.log('a' in proxy); //false

代理和反射的关系

简单理解就是:代理提供一种机制,可以重写反射的底层方法,实现重写底层原理

观察者模式

有一个对象,是观察者,它用于观察另外一个对象的属性值变化,当属性值变化后会收到一个通知,可能会做一些事。

其实就是相当于把代理换成观察者,意思差不多

用以前的方式实现观察者模式

function observer(target) {
            const div = document.getElementsByTagName('div')[0];
            const ob = {};
            const props = Object.keys(target); //获取target所有属性名
            for (const prop of props) {
                Object.defineProperty(ob, prop, { //让ob的每一个属性都配置get()  set()
                    get() {
                        return target[prop];
                    },
                    set(val) {
                        target[prop] = val;
                        render();
                    },
                    enumerable: true //注意这里默认是false(不可枚举),需要设置为可枚举,让render()遍历
                })
            }

            function render() {
                let html = '';
                for (const prop of Object.keys(ob)) { //遍历ob属性名
                    html += `<p>
                                <span>${[prop]}:</span>${ob[prop]}<span></span>
                              <p>`
                }
                div.innerHTML = html;
            }
            return ob;
        }
        const target = {
            a: 1,
            b: 2
        }
        const obj = observer(target);
        obj.a = 10; //设置a的值,target也会改变,并且页面渲染也会改变

缺点:搞出了两个对象,并且如果target添加新的属性,观察者obj并没有改变

通过代理Proxy实现

function observer(target) {
            const div = document.getElementsByTagName('div')[0];
            const proxy = new Proxy(target, { //相当于在target外面套一层代理proxy对象,当target属性需要操作时,会通过proxy对target进行处理
                get(target, propertyKey) { //先走proxy代理进行处理(此处就能实现一些其他的操作了),然后proxy再让Reflect对target进行设置
                    return Reflect.get(target, propertyKey);
                    //通过Reflect反射,可以捕获到底层实现,所以使用Reflect之后,即使代理proxy的属性新增、删除、修改、查找等操作,都能监测到
                },
                set(target, propertyKey, value) {
                    Reflect.set(target, propertyKey, value);
                    render();
                }
            })
            render();

            function render() {
                let html = '';
                for (const prop of Object.keys(target)) { //遍历ob属性名
                    html += `<p>
                                <span>${[prop]}:</span>${target[prop]}<span></span>
                              <p>`
                }
                div.innerHTML = html;
            }

            return proxy;
        }
        const target = {
            a: 1,
            b: 2
        }
        const obj = observer(target);
        obj.a = 10; //设置a的值,target也会改变,并且页面渲染也会改变
        obj.c = 30; //新增属性也能被捕获到,页面渲染也会改变
使用proxy实现偷懒构造函数

可以方便实例对象,减少代码量(指的是那些this.属性名 = 属性值,这些操作)

但是不是一定要用这个东西,有时候可能构造函数还有其他操作

  //偷懒构造函数
        class User {};

        function ConstructorProxy(Class, ...propName) { //propName:形参列表
            return new Proxy(Class, { //返回一个User代理对象
                //一旦代理对象被new 实例,底层就会调用construct
                construct(target, argumentsList) { //重写construct
                    //target是Class,argumentsList是传进来的实参列表
                    //通过Reflect对象调用底层construct方法
                    const obj = Reflect.construct(target, argumentsList) //用Class的构造函数创建一个对象
                    propName.forEach((item, index) => {
                        obj[item] = argumentsList[index]; //让obj的属性和传进来的值对应
                    })
                    return obj;
                }
            })
        }
        const user = ConstructorProxy(User, 'firstName', 'lastName'); //user是User的代理对象
        const u = new user('小', '名');
        console.log(u); //{firstName: "小", lastName: "名"}
使用proxy实现函数实参类型验证
      function sum(a, b) { //因为有时候可能传进来的a和b不一定是number类型,所以可以设置代理proxy进行处理
            return a + b;
        }

        function validatorFunction(func, ...type) { //type是设置参数的类型
            return new Proxy(func, {//因为函数也是一个对象,所以返回的proxy对象也可以当作函数使用
                apply(target, thisAgument, argumentList) { //thisArgument是this指向
                    //判断实参是否是number
                    type.forEach((item, index) => {
                            if (typeof argumentList[index] !== item) {
                                throw new TypeError(`${index}个实参的类型不是${item}`);
                            }
                        })
                        //通过Reflect实现底层调用
                    Reflect.apply(target, thisAgument, argumentList);
                }
            })
        }
        const sumV = validatorFunction(sum, 'number', 'number');
        console.log(sumV('1', 2)); //TypeError: 第0个实参的类型不是number!

新增的数组API

静态方法

  • Array.of(…args): 使用指定的数组项创建一个新数组

    与直接 new Array()的区别

    Array.of()
    const arr = Array.of(1,2,3);//[1,2,3]
    
    
    虽然通过new Array()也能创建数组,如: const arr = new Array(1,2,3);//[1,2,3]
    
    但是,如果传的参数只有一位数时,则Array会识别为数组长度,如: const arr = new Array(1);//[empty]  length = 1
    
  • Array.from(arg): 通过给定的类数组可迭代对象 返回一个新的数组。(把类数组 或 可迭代对象 转换成真正的数组)

实例方法

find(callback): 用于查找满足条件的第一个元素(其实跟filter使用基本一样)

**和filter的区别:**filter会全部找出来,而 find 只会找满足要求的第一个

const arr = [
	{
		id:1,
		age:19
	},
	{
		id:2,
		age:20
	}
];
arr.find(item=>{//item数组中的每一项
	if(item.id == 1){
		return true;
	}else{
		return false;
	}
})

findIndex(callback):用于查找满足条件的第一个元素的下标

fill(data):用指定的数据填充满数组所有的内容

const arr = new Array(100);
arr.fill('abc');//将数组的每一项都填充为'abc'

copyWithin(target, start?, end?): 在数组内部完成复制

        const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
        arr1.copyWithin(4); //从下标为4的位置开始复制数组下标0到最后的数据
        console.log(arr1); //[1, 2, 3, 4, 1, 2, 3, 4, 5, 6]

        const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
        arr2.copyWithin(4, 2); //从下标为4的位置开始复制数组下标为4到最后的数据
        console.log(arr2); //[1, 2, 3, 4, 3, 4, 5, 6, 7, 8]

        const arr3 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
        arr3.copyWithin(4, 2, 4); //从下标为4的位置开始复制数组下标为2到4之前的数据
        console.log(arr3); //[1, 2, 3, 4, 3, 4, 7, 8, 9, 0]

includes(data):判断数组中是否包含某个值,使用Object.is匹配(+0 != -0)

类型化数组

数字存储的前置知识
  1. 计算机必须使用固定的位数来存储数字,无论存储的数字是大是小,在内存中占用的空间是固定的。

  2. n位的无符号整数能表示的数字是2^n个,取值范围是:0 ~ 2^n - 1

  3. n位的有符号整数能表示的数字是2n个,取值范围是:-2(n-1) ~ 2^(n-1) - 1

  4. 浮点数表示法可以用于表示整数和小数,目前分为两种标准:

    1. 32位浮点数:又称为单精度浮点数,它用1位表示符号,8位表示阶码,23位表示尾数
    2. 64位浮点数:又称为双精度浮点数,它用1位表示符号,11位表示阶码,52位表示尾数
  5. JS中的所有数字,均使用双精度浮点数保存

    简单理解就是:在JS中,每个数字所占用的内存就是64位

    1 byte(字节) = 8 bit()
    1 mb = 1024 byte
    1 kb = 1024 mb
    1 gb = 1024 kb
    如果要存储1000
    那么内存 =  64 * 100 = 6400位
    也就是 800mb  将近1kb内存类型化数组
    

    类型化数组:用于优化多个数字的存储

    具体分为:

    • Int8Array: 8位有符号整数(-128 ~ 127)
    • Uint8Array: 8位无符号整数(0 ~ 255)
    • Int16Array: …
    • Uint16Array: …
    • Int32Array: …
    • Uint32Array: …
    • Float32Array:
    • Float64Array
    1. 如何创建数组
    new 数组构造函数(长度)
    
    数组构造函数.of(元素...)
    
    数组构造函数.from(可迭代对象)
    
    new 数组构造函数(其他类型化数组)
    
    const arr = Int32Array.of(35111,7,3,11);
    const arr1 = new Int8Array(arr);
    console.log(arr === arr1);//false
    //因为当35111在32位数组中是在可存储范围内的,但是在8位数组的可存储范围外,所以导致出现误差,以至于不相等
    

    得到长度

    数组.length   //得到元素数量
    数组.byteLength //得到占用的字节数
    
    1. 其他的用法跟普通数组一致,但是
    • 不能增加和删除数据,类型化数组的长度固定
    • 一些返回数组的方法,返回的数组是同类型化的新数组

ArrayBuffer

ArrayBuffer:一个对象,用于存储一块固定内存大小的数据。

new ArrayBuffer(字节数)

可以通过属性byteLength得到字节数,可以通过方法slice得到新的ArrayBuffer

读写ArrayBuffer

  1. 使用DataView

通常会在需要混用多种存储格式时使用DataView

  1. 使用类型化数组

实际上,每一个类型化数组都对应一个ArrayBuffer,如果没有手动指定ArrayBuffer,类型化数组创建时,会新建一个ArrayBuffer

        const bf = new ArrayBuffer(10); //10个字节的内存

        const arr1 = new Int8Array(bf);
        const arr2 = new Int16Array(bf);
	    arr2[0] = 2344;//操作了两个字节(16位),如果数值在8位的范围内的话,操作一位字节
        console.log(arr1 === arr2);//false
        console.log(arr1.buffer === arr2.buffer);//true  使用的是同一个内存
                 throw new TypeError(`第${index}个实参的类型不是${item}!`);
                        }
                    })
                    //通过Reflect实现底层调用
                Reflect.apply(target, thisAgument, argumentList);
            }
        })
    }
    const sumV = validatorFunction(sum, 'number', 'number');
    console.log(sumV('1', 2)); //TypeError: 第0个实参的类型不是number!

## 新增的数组API

## 静态方法

- **Array.of(...args)**: 使用指定的数组项创建一个新数组

  **与直接 new Array()的区别**

  ```js
  Array.of()
  const arr = Array.of(1,2,3);//[1,2,3]
  
  
  虽然通过new Array()也能创建数组,如: const arr = new Array(1,2,3);//[1,2,3]
  
  但是,如果传的参数只有一位数时,则Array会识别为数组长度,如: const arr = new Array(1);//[empty]  length = 1
  • Array.from(arg): 通过给定的类数组可迭代对象 返回一个新的数组。(把类数组 或 可迭代对象 转换成真正的数组)

实例方法

find(callback): 用于查找满足条件的第一个元素(其实跟filter使用基本一样)

**和filter的区别:**filter会全部找出来,而 find 只会找满足要求的第一个

const arr = [
	{
		id:1,
		age:19
	},
	{
		id:2,
		age:20
	}
];
arr.find(item=>{//item数组中的每一项
	if(item.id == 1){
		return true;
	}else{
		return false;
	}
})

findIndex(callback):用于查找满足条件的第一个元素的下标

fill(data):用指定的数据填充满数组所有的内容

const arr = new Array(100);
arr.fill('abc');//将数组的每一项都填充为'abc'

copyWithin(target, start?, end?): 在数组内部完成复制

        const arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
        arr1.copyWithin(4); //从下标为4的位置开始复制数组下标0到最后的数据
        console.log(arr1); //[1, 2, 3, 4, 1, 2, 3, 4, 5, 6]

        const arr2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
        arr2.copyWithin(4, 2); //从下标为4的位置开始复制数组下标为4到最后的数据
        console.log(arr2); //[1, 2, 3, 4, 3, 4, 5, 6, 7, 8]

        const arr3 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 0];
        arr3.copyWithin(4, 2, 4); //从下标为4的位置开始复制数组下标为2到4之前的数据
        console.log(arr3); //[1, 2, 3, 4, 3, 4, 7, 8, 9, 0]

includes(data):判断数组中是否包含某个值,使用Object.is匹配(+0 != -0)

类型化数组

数字存储的前置知识
  1. 计算机必须使用固定的位数来存储数字,无论存储的数字是大是小,在内存中占用的空间是固定的。

  2. n位的无符号整数能表示的数字是2^n个,取值范围是:0 ~ 2^n - 1

  3. n位的有符号整数能表示的数字是2n个,取值范围是:-2(n-1) ~ 2^(n-1) - 1

  4. 浮点数表示法可以用于表示整数和小数,目前分为两种标准:

    1. 32位浮点数:又称为单精度浮点数,它用1位表示符号,8位表示阶码,23位表示尾数
    2. 64位浮点数:又称为双精度浮点数,它用1位表示符号,11位表示阶码,52位表示尾数
  5. JS中的所有数字,均使用双精度浮点数保存

    简单理解就是:在JS中,每个数字所占用的内存就是64位

    1 byte(字节) = 8 bit()
    1 mb = 1024 byte
    1 kb = 1024 mb
    1 gb = 1024 kb
    如果要存储1000
    那么内存 =  64 * 100 = 6400位
    也就是 800mb  将近1kb内存类型化数组
    

    类型化数组:用于优化多个数字的存储

    具体分为:

    • Int8Array: 8位有符号整数(-128 ~ 127)
    • Uint8Array: 8位无符号整数(0 ~ 255)
    • Int16Array: …
    • Uint16Array: …
    • Int32Array: …
    • Uint32Array: …
    • Float32Array:
    • Float64Array
    1. 如何创建数组
    new 数组构造函数(长度)
    
    数组构造函数.of(元素...)
    
    数组构造函数.from(可迭代对象)
    
    new 数组构造函数(其他类型化数组)
    
    const arr = Int32Array.of(35111,7,3,11);
    const arr1 = new Int8Array(arr);
    console.log(arr === arr1);//false
    //因为当35111在32位数组中是在可存储范围内的,但是在8位数组的可存储范围外,所以导致出现误差,以至于不相等
    

    得到长度

    数组.length   //得到元素数量
    数组.byteLength //得到占用的字节数
    
    1. 其他的用法跟普通数组一致,但是
    • 不能增加和删除数据,类型化数组的长度固定
    • 一些返回数组的方法,返回的数组是同类型化的新数组

ArrayBuffer

ArrayBuffer:一个对象,用于存储一块固定内存大小的数据。

new ArrayBuffer(字节数)

可以通过属性byteLength得到字节数,可以通过方法slice得到新的ArrayBuffer

读写ArrayBuffer

  1. 使用DataView

通常会在需要混用多种存储格式时使用DataView

  1. 使用类型化数组

实际上,每一个类型化数组都对应一个ArrayBuffer,如果没有手动指定ArrayBuffer,类型化数组创建时,会新建一个ArrayBuffer

        const bf = new ArrayBuffer(10); //10个字节的内存

        const arr1 = new Int8Array(bf);
        const arr2 = new Int16Array(bf);
	    arr2[0] = 2344;//操作了两个字节(16位),如果数值在8位的范围内的话,操作一位字节
        console.log(arr1 === arr2);//false
        console.log(arr1.buffer === arr2.buffer);//true  使用的是同一个内存
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值