课程概述

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

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

JavaScript:运行在浏览器的语言,该语言使用ES标准,ES + web api = JavaScript

NodeJs:运行字服务器端的语言,该语言使用ES标准,ES + node api = NodeJs

2、ECMAScript有哪些关键的版本?

ES3.0:2000年

ES5.0:2009年

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

ES7.0:2016

3、为什么ES6如此重要

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

4、如何应对兼容性问题

5、学习本课程需要的前置只是有哪些?

HTML+CSS、JavaScript

6、这套课程难不难?

难度和JavaScript基础差不多

V8引擎的垃圾回收机制

V8引擎中的垃圾回收可以分为栈内存中的垃圾回收和堆内存中的垃圾回收两个部分

栈内存

在函数执行前,会进行预编译生成一个执行上下文AO并将该AO压入执行栈中,当该函数执行完之后,需要将该函数的执行上下文进行销毁,这里所采用的方法为,执行当前函数时,执行栈中会存在一个ESP指针来指向当前函数的执行上下文,而当前函数执行完之后,会将ESP指针指向其上级作用域所在的执行上下文,在改变ESP指针的过程中,即为执行栈垃圾回收的过程,栈中垃圾回收过后,其函数中指向堆内存中的变量还在堆内存中,此时还需要对堆内存中的内容进行回收,此时就需要用到JavaScript中的垃圾回收器了;

堆内存

在V8引擎中会把堆分为新生代和老生代两个区域,新生代中存放的为生存时间短的对象,而老生代中存放的为生存时间久的对象。

新生代通常只有1~8M,而老生代的容量相对来说就大很多了,V8中,主垃圾回收器主要负责老生代的垃圾回收,而副垃圾回收器主要负责新生代的垃圾回收。

垃圾回收器的大致工作流程:

  • 标记空间中的活动对象和非活动对象,其中活动对象是指还在使用的对象,非活动对象就是可以进行垃圾回收的对象
  • 对所有被标记的非活动对象进行统一垃圾回收
  • 当非活动对象进行垃圾回收完后,堆内存中可能会存在大量不连续的空间,我们将这些不连续的空间称为空间碎片,需要对这些空间碎片进行整理

副垃圾回收器

副垃圾回收器主要负责新生区的垃圾回收,大多数小的对象都会被分配到新生区,所以说这个区域虽然不大,但是垃圾回收还是比较频繁的,新生区采用scavenge算法,即把新生区分为两个区域,一半是对象区域,一半是空闲区域,首先对对象区域的对象进行标注,标注完后,会进入垃圾回收阶段,将非活动对象进行垃圾回收,然后副垃圾回收器会将活动对象拷贝到空闲区域,同时还会把这些对象有序的排列起来,所以这个过程也就相当于完成了内存整理的操作,结果中不会产生内存碎片,完成复制后,对象区域与空闲区域会进行角色翻转,这样就完成了垃圾回收的操作,除此之外,由于新生区的空间不大,因此,经过两次垃圾回收还被判断为存活的对象会被移到老生代

主垃圾回收器

主垃圾回收器主要负责老生区中的垃圾回收。除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。

由于老生区的对象比较大,若要在老生区中使用 Scavenge 算法进行垃圾回收,复制这些大的对象将会花费比较多的时间,从而导致回收执行效率不高,同时还会浪费一半的空间。因而,主垃圾回收器是采用标记 - 清除(Mark-Sweep)的算法进行垃圾回收的。

首先是标记过程阶段。标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。

然后会进入垃圾清除过程,老生区的垃圾回收不同于新生区,由于其对象占用的空间大,采用scavenge算法会耗费大量的时间,因此仅对标记为垃圾的内容进行回收,在回收过后,要进行碎片整理,此时就用到了一个新的算法,即标记 - 整理(Mark-Compact)

全停顿

现在你知道了 V8 是使用副垃圾回收器和主垃圾回收器处理垃圾回收的,不过由于 JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。

为了降低老生代的垃圾回收而造成的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成,我们把这个算法称为增量标记(Incremental Marking)算法。

7、ES6 - 图1

使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,这样当执行上述动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。

块级绑定

声明变量的问题

使用var声明变量会产生的问题:

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

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

如以下代码中,if中的变量声明会被提升到外边的最顶部

  1. if(Math.random() < 0.5){
  2. var a = "abc";
  3. console.log(a);
  4. }else{
  5. console.log(a);
  6. }

3、全局变量挂载到全局对象:全局对象成员污染问题

var console = "abc";
console.log(console)//会报错,因为var console会覆盖默认console对象

let声明变量

ES6不仅引入let关键字用于解决变量声明的问题,同时引入了块级作用域的概念

1、全局变量挂载到全局对象:全局对象成员污染问题

let声明的变量不会挂载到全局对象

let a = "abc";
console.log(window.a);//打印结果为undefined

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

let声明的变量,不允许当前作用域范围内重复声明

块级作用域中用let定义的变量,在外部不可访问, 外部定义的变量,里边可以访问

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

使用let不会有变量提升,因此,不能在定义let变量之前使用它

底层实现上,let声明的变量实际上也会有变量提升,但是,提升后会将其放入到”暂时性死区“,如果访问的变量位于暂时性死区,则会报错:”Cannot access ‘a’ before initialization“,当代码运行到声明变量语句时,会将其从暂时性死区中移除

注:在循环中,用let声明的循环变量,会特殊处理,每次进入循环体,都会开启一个新的作用域,并且将循环变量绑定到该作用域

在循环中,使用let声明的循环变量,在循环结束后会销毁

const声明变量

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

实际上,在开发中,应该尽量使用const来声明变量,以保证变量的值不会随意篡改

原因如下:

1、根据经验,开发中的很多变量,都是不会更改的,也不应该更改的(一般来说div都是不会更改的,因此在dom中可通过const来定义)

2、后续的很多框架或者是第三方JS库,都要求数据不可变,使用常量可以一定程度上保证这一点

注意的细节:

1、常量不可变,是指声明常量的内存空间不可变,并不保证内存空间中的地址指向的其他空间不可变,如声明的对象,可以更改其属性值

2、常量的命名

a、特殊的常量:该常量从字面意义上,一定是不可变的,比如圆周率、月地距离或者其他一些绝不可能变化的配置,通常,**该常量的名称全部使用大写,多个单词之间用下划线分割**

b、普通的常量:使用和之前的命名一样即可

注:在for循环中,循环变量不能使用const进行定义

字符串和正则表达式

Unicode支持

早期,由于存储空间宝贵,Unicode使用16位二进制来存储文字,我们将一个16位的二进制编码叫做一个码元(Code Unit)。后来,由于技术的发展,Unicode对文字编码进行了扩展,将某些文字扩展到了32位(占用2个码元),并且将某个文字对应的二进制数字叫做码点(Code Point)

const text = "𠮷";//占用了两个码元(32位)
console.log(text.length);//

ES6为了解决这个困扰,位字符串提供了方法:codePointAt,根据字符串码元的位置得到码点

//"𠮷":\ud842\udfb7
console.log(text.codePointAt(0));
//小知识:4位的16进制即为16位的2进制

\u开头代表Unicode编码中的16进制,0x开头代表16进制数字

/**
 * 判断字符char,是32位还是16位
 * @param char
 * @param i
 * @returns {boolean}
 */
function is32Bit(char, i){
    return (char.codePointAt(i) > 0xffff);
}

/**
 * 计算字符串的准确长度
 * @param str
 */
function getLengthOfCode(str){
    var len = 0;
    for(let i = 0; i < str.length; i++){
        if (is32Bit(str, i)){
            i++;
        }
        len++;
    }
}

同时,ES6位正则表达式添加了一个标记名flag: u,如果添加了该配置,则用码点匹配

console.log(/^.$/u.test("𠮷"));//打印为true

字符串API

以下均为字符串的实例方法(原型)方法

includes

写法:text.includes(“xx”, n),第二个参数为查找开始位置

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

startsWith

判断字符串是否以指定字符串开始

endsWith

判断字符串是否以指定字符串结尾

repeat

将字符串重复指定的次数,返回一个新字符串,如以下代码将字符串内容重复三次

console.log("abcd".repeat(3));

正则中的黏连标记

了解就行

标记名:y(与以前学过的g / i / m / u一个意思)

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

const text = "Hello World!!!";
const reg = /W\w+/;
console.log(reg.test(text));
const reg2 = /W\w+/y;
console.log(reg2.test(text));

模板字符串

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

1、处理多行字符串

2、字符串拼接

在ES6中,提供了模板字符串的书写,可以非常方便的换行和拼接,要做的仅仅是将字符串的开始或结改为`,

如果要在字符串中拼接js表达式,只需要在模板字符串中使用以下形式

//`{JS表达式}`
let a = "abc";
let b = "def";
let c = `${a}${b}123`
console.log(c)//abcdef123

模板字符串标记(扩展)

标记是一个函数,函数参数如下:

1、参数1:被插值分割的字符串数组

2、后续参数:所有的插值,即插入的表达式

//模板字符串标记
let love1 = "秋葵";
let love2 = "香菜";
let text = myTag`邓哥喜欢${love1}, 邓哥也喜欢${love2}。`
//相当于text = myTag(["邓哥喜欢", "邓哥也喜欢", "。"], "秋葵", "香菜")
function myTag(parts, arg1, arg2){
    console.log(parts, arg1, arg2);//输出[ '邓哥喜欢', ', 邓哥也喜欢', '。' ] 秋葵 香菜
    const values = Array.prototype.slice.apply(arguments).slice(1);//["秋葵", "香菜"]
}

String.raw可将字符串中的所有转移字符全都转为普通字符串

let text = String.raw`abc\t\nbcd`;//打印text结果为abc\t\nbcd
btn.onclick = function(){
    container.innerHTML = safe`<p>
        ${txt.value}
    </p>
    <h1>
        ${txt.value}
    </h1>
    `;
}

function safe(parts){
    const values = Array.prototype.slice.apply(arguments).slice(1);
    let str = "";
    for (let i = 0; i < values.length; i++) {
        const v = values[i].replace(/</g, "&lt;").replace(/>/g, "&gt;");
        str += parts[i] + v;//通过正则表达式替换用户输入的尖括号来防止篡改页面内容
        if (i === values.length - 1) {
            str += parts[i + 1];
        }
    }
    return str;
}

函数

参数默认值

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

这样一来,当调用函数时,如果没有给对应的参数赋值(给它的值是undefined),则会自动使用默认值。如下写法

function sum(a, b = 2, c = 3){
    return a + b + c;//在调用函数时,若b和c未传入实际值,则会利用默认值
}

[扩展]对arguments的影响

只要给函数加上参数默认值,该函数会自动变成严格模式下的规则,arguments和形参脱离

[扩展]留意暂时性死区

形参和ES6中的let或const声明一样,具有作用域,并且根据参数的声明顺序,存在暂时性死区

function test(a = b, b){
    console.log(a, b);
}
test(undefined, 2);//在执行过程中b还未声明,即还在暂时性死区,就被赋值给a

剩余参数

arguments的缺陷:

1、如果跟形参配合使用,容易导致混乱

2、从语义上,使用arguments获取参数,由于形参缺失,无法从函数定义上理解函数的真是意图

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

写法:function(…形参名){}

细节:一个函数只能写一个剩余参数,一个函数如果有剩余参数,必须是最后一个参数

展开运算符

1、对数组展开ES6

写法:在目标参数前加上…

let arr = [1, 2, 3, 4, 5];
sum(1, 2, 3, ...arr, 4, 5, 6);

2、对对象展开ES7

//浅克隆
const obj1 = {
    name: "成哥",
    age: 18,
    love: "邓嫂",
    address: {
        country: "中国",
        province: "黑龙江"
    }
}
const obj2 = {
    ...obj1,
    name: "邓哥"
}
//深克隆
const obj1 = {
    name: "成哥",
    age: 18,
    love: "邓嫂",
    address: {
        country: "中国",
        province: "黑龙江"
    }
}
const obj2 = {
    ...obj1,
    name: "邓哥",
    address: {
        ...obj1.address
    }
}

明确函数的双重用途

函数有两种调用方式,构造函数在调用时要判断其是否时通过new的方式进行调用的,以前需要用instanceof来进行判断,ES6提供了一个特殊的API,可以使用该API在函数内部判断该函数是否使用了new来调用

new.target,该表达式,得到的是,如果没有使用new来调用函数,则返回undefined,如果使用new调用函数,则得到的是new关键字后面的函数本身,可在构造函数中用new.target来判断输出内容,并利用throw来进行抛出错误

function Person(firstName, lastName){
    if(new.target === undefined){
        throw new Error("该函数没有使用new来调用")
    }
    this.firstName = firstName;
    this.lastName = lastName;
}

箭头函数

回顾:this指向

1、通过对象调用函数,this指向对象

2、直接调用函数,this指向全局对象

3、如果通过new调用函数,this指向新创建的对象

4、如果通过apply、call、bind调用函数,this指向指定的数据

5、如果是DOM事件函数,this指向事件源

const obj = {
    count : 0,
    start : function() {
        console.log(this);
        setInterval(function(){
            this.count++;
            console.log(this.count);//定时器中的this为window对象
        }, 1000)
    }
}
obj.start();

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

完整语法:

let 函数名 = (参数1, 参数2, ...) => {函数体}

如果参数只有一个,可以省略小括号

let 函数名 = 参数 => {函数体}

如果箭头函数只有一条返回语句,可以省略大括号,和return关键字

let 函数名 = 参数 => 返回值
const isOdd = num => num % 2 !== 0;

当返回的内容为对象时,需用小括号将要返回的对象括起来

const sum = (a, b) => ({
    a: a,
    b: b,
    sum: a + b
});
console.log(sum(1, 2));

注意细节:

1、箭头函数中不存在this、arguments、new.target,如果使用了,则使用的是函数外层对应的this、arguments、new.target,即箭头函数中的this取决于声明位置的this

const obj = {
    count : 0,
    start : function() {
        console.log(this);
        setInterval(()=>{
            this.count++;
            console.log(this.count);//箭头函数中的this取决于声明位置的this
        }, 1000)
    }
}
obj.start();
const obj = {
    method : function(){
        const func = () => {
            console.log(this);
            console.log(arguments);
        }
        func();
    }
}
obj.method(123);

2、箭头函数没有原型

const a = ()=>{}
console.log(a.prototype)//打印undefined

3、箭头函数不能当构造函数使用

应用场景

1、临时性使用的函数,并不会刻意调用它,比如:

a、事件处理函数

b、异步处理函数(如setTimeout、setInterval)

c、其他临时性函数

2、为了绑定外层this的函数

3、在不影响其他代码的情况下,保持代码的简洁,最常见的,数组方法中的回调函数

对象

新增的对象字面量语法

1、成员速写

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

function createUser(loginId, loginPwd, nickName){
    return {
        loginId: loginId,
        loginPwd: loginPwd,
        nickName: nickName,
        id: Math.random * 100
    }
}
//以上函数可直接写成
function createUser(loginId, loginPwd, nickName){
    const sayHello = function(){}
    return {
        loginId,
        loginPwd,
        nickName,
        sayHello,
        id: Math.random * 100
    }
}

2、方法速写

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

const user = {
    name : '楠子哥',
    age: 24,
    sayHello(){
        console.log(this.name, this.age);
    }
}

3、计算属性名

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

const prop1 = "name";
const prop2 = "age";
const user = {
    [prop1] : '楠子哥',
    [prop2] : 24,
    sayHello(){
        console.log(this.name, this.age);
        //console.log(this[prop1], this[prop2]);//也可写成这样
    }
}

Object的新增API

1、Object.is

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

1)NaN和NaN相等

2)+0和-0不相等

2、Object.assign

用于混合对象,该方法会用后面的覆盖前面的,即前面的obj会被修改

const obj1 = {
    name: "楠子哥",
    age: 24,
    sex: "男"
}
const obj2 = {
    age: 24,
    hobby: "paint"
}
const obj3 = Object.assign(obj1, obj2);
//打印{ name: '楠子哥', age: 24, sex: '男', hobby: 'paint' }

3、Object.getOwnPropertyNames的枚举顺序

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

ES6规定了该方法返回的数组的排序方式如下:

先排数字,并按照升序排序

再排其他,按照书写顺序排序

4、Object.setPrototypeOf

该函数用于设置某个对象的隐式原型

比如:Object.setPrototypeOf(obj1, obj2),相当于:obj1.proto = obj2

面向对象简介

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

对比面向过程:

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

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

构造函数的语法糖

1、传统的构造函数的问题

属性和原型方法定义分离,降低了可读性

原型成员可以被枚举,如for…in循环可以获取原型链上定义的属性

默认情况下,构造函数仍然可以被当作普通的函数使用

ES6类的写法

class Animals{
    constructor(kind, name, age, sex){
        this.kind = kind;
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    print(){
        console.log(`【品种】:${this.kind}`);
        console.log(`【名字】:${this.name}`);
        console.log(`【年龄】:${this.age}`);
        console.log(`【性别】:${this.sex}`);
    }
}
const a = new Animals("dog", "Jhon", 3, "man");
a.print();
console.log(a);

2、类的特点

类的声明不会被提升,与let和const一样,存在暂时性死区

类中的所有代码均在严格模式下执行

类的所有方法都是不可枚举的

类的所有方法都无法被当作构造函数使用

类的构造器必须使用new来调用

类的其他书写方式

1、可计算的成员名

可利用新增的对象字面量语法来书写类的成员名

2、getter和setter

Object.defineProperty可定义某个对象成员属性的读取和设置

Object.defineProperty(obj, propertyName, {description})

使用getter和setter控制的属性,不在原型上

class Animal{
    constructor(kind, name, age, sex){
        this.kind = kind;
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    //创建一个age属性,并给它加上getter,读取属性时,会运行该函数
    get age(){
        console.log("get");
        return `${this._age}岁`;
    }
    //创建一个age属性,并给他加上setter,给该属性赋值时,会运行该函数
    set age(age){
        console.log("set");
        if(typeof age !== "number"){
            throw new TypeError("age property must be a number");
        }
        if(age < 0){
            age = 0;
        }
        else if(age > 1000){
            age = 1000;
        }
        this._age = age;
    }
}

3、静态成员

构造函数本身的成员即为静态成员,实例化的对象无法访问构造函数的静态成员

使用static定义的成员即为静态成员,如下写法

class Chess{
    constructor(name){
        this.name = name;
    }
    static width = 40;
    static method(){

    }
}

回顾:

proto为对象身上的属性,其指向构造函数身上的的prototype属性

prototype为构造函数身上的属性

4、字段初始化器(ES7)

注意:使用static的字段初始化器,添加的是静态成员

没有使用static的字段初始化器,添加的成员位于对象上

箭头函数在字段初始化器位置上,指向当前对象

如下写法

class Test{
    static a = 1;
    b = 2;
    c = 3;
    print = () => {
        console.log(this.b);
    }
    constructor(d){
        this.d = d;
    }
}
let obj = new Test();

5、类表达式

const A = class{//匿名类,类表达式

}

6、[扩展]装饰器(ES7)(Decorator)

装饰器的本质是一个函数(了解一下,后边会详细介绍)

function Obsolete(target, methodName, descriptor){
    const oldFunc = descriptor.value;
    descriptor.value = function(...args){
        console.warn(`${methodName}方法已过时`);
        oldFunc.apply(this, args);
    }
}

类的继承

如果两个类A和B,如果可以描述为:B是A,则,A和B形成继承关系,如狗是动物,即为继承关系

如果B是A,则:

1、B继承自A

2、A派生B

3、B是A的子类

4、A是B的父类

如果A是B的父类,则B会自动拥有A中的所有实例成员

ES5的继承方法

Object.setPrototypeOf()方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null。如:Object.setPrototypeOf(Dog.prototype, Animal.prototype)

ES6实现继承:采用extends进行继承

super:直接当作函数调用,表示父类构造函数

如果当作对象使用,则表示父类的原型,即在子类函数中通过super.方法名来调用父类的方法

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

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

当原型中存在同名的方法,则会覆盖继承来的同名方法

如:

class Dog extends Animals{
    constructor(name, age, sex){
        super("犬类", name, age, sex);
    }
}
class Animals{
    constructor(type, age, sex){
        this.type = type;
        this.sex = sex;
        this.age = age;
    }
    set age(age){
        if(age > 1000){
            age = 1000;
        }else if(typeof age !== "number"){
            throw TypeError("年龄应为数字");
        }else if(age < 0){
            age = 0;
        }
        this._age = age;
    }
    get age(){
        return `${this._age}岁`;
    }
    shout(){
        console.log("汪汪汪汪!")
    }
}

class Dog extends Animals{
    constructor(age, sex, name) {
        super("犬类", age, sex);
        this.name = name;
    }
    shout(){
        super.shout();
        console.log("汪汪!");
    }
}

let dog = new Dog(25, "公", "旺财");
dog.shout();

冷知识:用JS制作抽象类

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

可在目标类中添加if(new.target)来判断该类的执行方法,如果使用该类创建对象,则throw抛出一个错误

像素鸟

知识点:

1、relative和position定位下,top和left定位中的%是相对于父级元素的宽高进行计算的

基于面向对象进行实现

1、所有元素均为会移动的矩形,因此先创建矩形(Rectangle)类,其实例成员包括宽度、高度、left、top、X方向的速度、Y方向的速度、DOM对象

2、为矩形类添加render方法,用来设置dom对象的样式等参数,添加move方法来实现dom元素的移动

3、通过getComputedStyle(DOM对象)可以获取dom对象的自身样式

4、为防止子类实例化对象的背景图片运动出视窗,这里需要在子类中添加一个onMove方法来判断背景图片的left值是否大于或等于其宽度一半的负值,若满足条件,则将其left值设为0,并在其父类设置的move方法中的render渲染前判断实例化对象是否存在onMove方法,若存在则执行onMove进行判断,否则,直接执行render进行渲染

5、小鸟的速度通过重力加速度g实现,在bird子类中添加move方法,在方法中通过super调用父类move方法一次,然后通过g来改变y轴方向上的速度

6、小鸟的运动范围为游戏框顶部到大地表面,其范围高度为游戏框的高度减去大地的高度再减去小鸟的高度,小鸟的jump方法为直接赋予其一个向上的速度

7、小鸟扇动翅膀,通过一个swingStatus参数来改变当前小鸟的运动状态码,配合定时器实现小鸟翅膀的扇动

8、柱子的控制通过三个类,一个类用来生成单个柱子,第二个调用生成柱子类可以实现生成上下柱子对,其生成方式为,柱子间的距离固定为150,柱子的最小高度为80,最大高度通过计算即可得到,生成方式为Math.floor(Math.random() * (max - min) + min),然后另一个柱子的高度通过计算得到

9、柱子生成器,每隔1.5秒调用柱子对类生成一组柱子对,并在定时器中判断柱子对的可用性,将无用柱子对删除

10、定义Game类,通过该类来控制其余所有目标的实例化和运动,游戏结束后,通过window.reload()方法来重新加载页面

11、小鸟碰撞柱子的判断原理:当两个矩形的横向距离和纵向距离均小于两个矩形宽或高的一半时,则判定为碰撞

解构

对象解构

使用ES6的一种语法规则,将一个对象或数组的某个属性提取到某个变量中

先定义n个变量,然后从对象中读取同名属性提取到某个变量中

写法:

const user = {
    name: "kevin",
    age: 11,
    sex: "男",
    address: {
        province: "四川",
        city: "成都"
    }
}
let name, age, sex, address;
({name, age, sex, address}) = user;
//上述代码也可写成let {name, age, sex, address} = user;

上述代码等同于

let name, age, sex, address;
name = user.name;
age = user.age;
sex = user.sex;
address = user.address;

在解构中使用默认值

{同名变量 = 默认值}

let {name, age, sex, address, abc = 123} = user;

非同名属性解构

{属性名: 变量名}

let {name, age, sex: gender, address, abc = 123} = user;

还可通过嵌套进行进一步解构

let {name, age, sex: gender, address: {province}} = user;

上述代码中并没有将address解构出来,只是将address下的province解构出来了

数组解构

数组在实质上也是一个对象,

数组的解构同对象类似,将大括号替换为中括号即可

const numbers = ["a", "b", "c"];
const [n1, , n3] = numbers;

数组也可进行嵌套解构

const numbers = ["a", "b", "c", "d", "e", [1, 2, 3, 4]];
const [, , , , , [n1, , n3, ]] = numbers;

当数组中其中一项为对象时

const numbers = ["a", "b", "c", "d", "e", {a: 1, b: 2}];
const [, , , , , {a}] = numbers;
console.log(a);
const [, , , , , {a: A}] = numbers;
console.log(A);

还可利用展开运算符进行解构

const numbers = ["a", "b", "c", "d", "e"];
const [na, n2, ...num1] = numbers;
console.log(num1);

解构使用较为灵活,可进行搭配使用

参数解构

当参数传入为对象时,可直接在形参位置解构

const user = {
    name: "老王",
    age: 25,
    sex: "男",
    address: {
        province: "山西",
        city: "运城"
    }
}
function print({name, age, sex, address:{
    province, 
    city
}}){
    console.log(name, age, province, city);
}

可用于ajax请求过程中的参数传递,还可直接写入默认值,其解构方法相当于将对象解构或数组解构直接放入形参的位置

符号

普通符号

符号是ES6新增的一个数据类型,它通过使用函数名Symbol(符号名)来创建符号设计的初衷,是为了给对象设置私有属性

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

const syb1 = Symbol();

符号具有以下特点:

1、没有字面量

2、使用typeof得到的类型是symbol

3、每次调用Symbol函数得到的符号永远不相等,无论符号名是否相同

4、符号可以作为对象的属性名存在,这种属性称之为符号属性

a、**开发者可以通过精心的设计,让这些属性无法通过常规方法被外界访问**

b、符号属性是不能枚举的,因此在for-in循环中无法读取到符号属性,Object.keys方法也无法读取带符号属性

c、Object.getOwnPropertyNames尽管可以得到所有无法枚举的属性,但是仍然无法读取到符号属性

d、ES6新增Object.getOwnPropertySymbols方法,可以读取符号

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

如:

const h = new Hero(3, 6, 3);
const sybs = Object.getOwnPropertySymbols(Hero.prototype);
const prop = sybs[0];
console.log(h[prop](3, 5));

例:

const hero = (function(){
    const getRandom = Symbol();

    return {
        attack: 30,
        hp: 300,
        defence: 10,
        gongji(){
            const dmg = this.attack * this[getRandom](0.8, 1.1);
            console.log(dmg);
        },
        [getRandom](min, max){
            return Math.random() * (max - min) + min;
        }
    }
})()

共享符号

根据某个符号名称(符号描述)能够得到同一个符号

Symbol.for("符号名/")

实现原理

const symbolFor = (()=>{
    const global = {};
    return function(name){
        if(!global[name]){
            global[name] = Symbal(name);
        }
        return global[name];
    }
})

知名符号

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

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

因此ES6用知名符号暴露了某些场景的内部实现

1、Symbol.hasInstance

该符号用于定义构造函数的静态成员,它将影像instanceof的判定,该值的修改需要通过Object.defineProperty来实现

obj instanceof A
//等效于
A[Symbol.hasInstance](obj)//Function.prototype[Symbol.hasInstance]

2、[扩展]Symbol.isConcatSpreadable

该知名符号会影响数组的concat方法

const arr = [3];
const arr2 = [5, 6, 7, 8];
const result = arr.concat(56, arr2);//结果为[3, 56, 5, 6, 7, 8]
//若加上arr2[Symbol.isConcatSpreadable] = false,则输出结果为[3, 56, [5, 6, 7, 8]]

3、[扩展]Symbol.toPrimitive

该知名符号会影响类型转换的结果

4、[扩展]Symbol.toStringTag

该知名符号会影响Object.prototype.toString的返回值

5、其他知名符号

异步处理

[回顾]事件循环

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

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

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

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

浏览器宿主环境中包含5个线程:

1、JS引擎:负责执行执行栈的最顶部代码

2、GUI线程:负责渲染页面

3、事件监听线程:负责监听各种事件

4、计时线程:负责计时

5、网络线程:负责网络通信

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

异步函数的执行一定会放到事件队列中,如点击事件、延迟执行

JS引擎对事件队列的去除方式,以及与宿主环境的配合,称之为事件循环

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

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

MutationObserver用于监听某个DOM对象的变化

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

事件和回调函数的缺陷

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

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

dom.onclick = function(){

}

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

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

})

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

一直以来,该模式都运作良好,知道前端工程越来越复杂。。。

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

1、回调地狱:某个异步操作需要等待之前的异步操作完成,无论用回调还是事件,都会陷入不断地嵌套

2、异步之间的联系:某个异步操作要的等待多个异步操作的结束,对这种联系的处理,会让代码的复杂度剧增

异步处理的通用模型

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

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

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

1、ES6将某一件可能发生异步操作的事情,分为两个阶段:unsettledsettled

  • unsettled:未决阶段,表示事情还在进行前期的处理,并没有发生通向结果的那件事
  • settled:已决阶段,事情已经有了一个结果,不管这个结果是好是坏,整件事情无法逆转

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

2、ES6将事情划分为三种状态:pending、resolved、rejected

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

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

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

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

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

3、当事情达到已决阶段后,通常需要进行后续处理,不同的已决状态,决定了不同的猴戏处理

  • resolved状态:这是一个正常的已决状态,后续处理表示为thenable
  • rejected状态:这是一个非正常的已决状态,后续处理表示为catchable

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

4、整件事称之为Promise

Promise的基本使用

const pro = new Promise((resolve, reject) => {
    //未决阶段的处理
    //通过调用resolve函数将Promise推向已决阶段的resolved状态
    //通过调用reject函数将Promise推向已决阶段的rejected状态
    //resolve和reject均可以传递最多一个参数,表示推向状态的数据
})

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

细节

1、未决阶段的处理函数是同步的,会立即执行

2、thenable和catchable函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,而且,加入的队列是微队列(相对于宏队列,微队列具有优先级)

const pro = new Promise((resolve, reject) => {
    console.log("a");
    resolve(1);
    setTimeout(() => {
        console.log("b")
    }, 0)
})
pro.then(result => {
    console.log(result);
})
console.log("d");
//打印为a d 1 b

3、pro.then可以只添加thenable函数,pro.catch可以单独添加catchable函数

4、在未决阶段的处理函数中,如果发生未捕获的错误,会将状态推向rejected,并会被catchable捕获

5、一旦推向了已决阶段,无法再对状态做任何更改

6、Promise并没有消除回调,只是让回调变得可控

Promise的串联

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

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

1、如果当前的Promise是未决的,得到的新Promise是挂起状态

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

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

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

const pro2 = pro1.then(result => result * 2);
console.log(pro2);
//这里的pro2打印为pending状态
//因为在打印pro2时,pro1的异步处理还在等待过程中
pro2.then(result => console.log(result), err => console.log(err));
//这里输出为2,因为pro1在正常运行后将返回结果应用到新的Promise对象中
const pro1 = new Promise((resolve, reject) => {
    throw 1;
})

const pro2 = pro1.then(result => result * 2, err => err * 3);
pro2.then(result => console.log(result * 2), err => console.log(err * 3));
//这里输出6,在pro1中捕捉到错误,故then中执行catchable,在后续处理函数中并没有再产生错误,故返回结果被应用到pro2的thenable中,打印6

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

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

const pro2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(2);
    }, 3000);
})

const pro3 = pro1.then(result => {
    console.log("结果出来了,得到的是一个Promise")
    return pro2;
})
//pro3的状态:pending
pro3.then(result => {
    console.log(result);
})

Promise的其他API

原型成员(实例成员)

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

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

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

构造函数成员(静态成员)

  • resolve(数据):该方法返回一个resolved状态的Promise,传递的数据作为状态数据

    • 特殊情况:如果传递的数据是Promise,则直接返回传递的Promise对象
  • reject(数据):该方法返回一个rejected状态的Promise,传递的数据作为状态数据
  • all(iterable):这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才会触发成功,一旦有任何一个iterable里面的promise对象失败则立刻触发该promise对象的失败。这个新的promise对象在触发成功状态以后,会把一个包含iterable里所有promise返回值得数组作为成功回调得返回值,顺序跟iterable的顺序保持一致;如果这个新的promise对象出发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常被用于处理多个promise对象的状态集合
//以下为全部完成
function getRandom(min, max){
    return Math.floor(Math.random() * (max - min) + min);
}

let proms = [];

for(let i = 0; i < 10; i++){
    proms.push(new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`${i}完成`);
            resolve(i);
        }, getRandom(1000, 5000))
    }))
}
const all_pro = Promise.all(proms);
all_pro.then(data => {
    console.log(data);
})
  • race(iterable):当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象
function getRandom(min, max){
    return Math.floor(Math.random() * (max - min) + min);
}

let proms = [];

for(let i = 0; i < 10; i++){
    proms.push(new Promise((resolve, reject) => {
        setTimeout(() => {
            if(Math.random() < 0.5){
                console.log(`${i}完成`);
                resolve(i);
            }else{
                console.log(`${i}失败`);
            }
        }, getRandom(1000, 5000))
    }))
}
const all_pro = Promise.race(proms);
all_pro.then(() => {
    console.log('有人完成了!');
}, () => {
    console.log('有人失败了!')
})

[扩展]手写Promise-状态控制

async和await

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

async

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

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

async function test(){
    console.log(1);
    return 2;
    //return 2表示执行完后resolve接收的数据为2,throw new Error()表示reject接收错误
}
//等效于
function test(){
    return new Promise((resolve, reject) => {
        console.log(1);
        resolve(2);
    })
}

await

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

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

async function test2(){
    const result = await test1();
    console.log(result);
}
const pro = test2();

例子

function biaobai(i){
    console.log(`向${i}号表白`)
    return new Promise((resolve, reject) => {
        if(Math.random() < 0.1){
            resolve(true);
        }else{
            resolve(false);
        }
    })
}

const gods = ["女神1", "女神2", "女神3", "女神4", "女神5", "女神6"]
async function biaobaiAll(gods){
    for(let i = 0; i < gods.length; i++){
        const g = gods[i];
        console.log(i);
        //当前循环等待的Promise没有resolve,下一次循环不运行
        const result = await biaobai(g);
        if(result){
            console.log(`${g}同意了,不用表白了`);
        }else{
            console.log(`${g}拒绝了`)
        }
    }
}
biaobaiAll(gods);

如果await的表达式不是Promise,则会将其使用Promise.resolve包装后按规则运行

async function test(){
    const result = await 1;
    console.log(result);
}
//相当于
function test(){
    return new Promise((resolve, reject) => {
        Promise.resolve(1).then(data => {
            const result = data;
            console.log(result);
            resolve();
        })
    })
}

处理async返回错误问题

async function getPromise(){
    if(Math.random() < 0.5){
        return 1;
    }else{
        throw 2;
    }
}

async function test(){
    try{
        const result = await getPromise();
        console.log("正常状态", result);
    }catch(err){
        console.log("错误状态", err);
    }
}

Fetch Api

Fetch Api概述

XMLHttpRequest的问题

1、所有的功能全部几种在同一个对象上,容易书写出混乱不易维护的代码

2、采用传统的时间驱动模式,无法适配新的Promise Api

Fetch Api的特点

1、并非取代Ajax,而是对Ajax传统API的改进

2、精细的功能分割:同步信息、请求信息、相应信息等均分不到不同的对象,更利于处理各种复杂的Ajax场景

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

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

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

基本使用

请求测试地址:http://101.132.72.36:5100/api/local

使用fetch函数即可立即向服务器发送网络请求

参数

该函数有两个参数:

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

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

请求配置对象

  • method:字符串,请求方法,默认值GET
  • headers:对象,请求头信息
  • body:请求体的内容,必须匹配请求头中的Content-Type
  • mode:字符串,请求模式

    • cors:默认值,配置为该值,会在请求头中加入origin和referer
    • no-cors:配置为该值,会在请求头中加入origin和referer,跨域的时候可能会出现问题
    • same-origin:只是请求必须在同一个域中发生,如果请求其他域,则会报错
  • credentials:如何携带凭据(cookie)

    • omit:默认值,不携带cookie
    • same-origin:请求同源地址时携带cookie
    • include:请求任何地址都携带cookie
  • cache:配置缓存模式

    • default:表示fetch请求之前将检查下的http缓存
    • no-store:表示fetch请求将完全忽略http缓存的存在,这意味着请求之前将不在检查下http的缓存,拿到响应后,它也不会更新http缓存
    • no-cache:如果存在缓存,那么fetch将发送一个条件查询requert和一个正常的requert,拿到响应后,它会更新http缓存
    • reload:表示fetch请求之前将忽略http缓存的存在,但是请求拿到响应后,它将主动更新http缓存
    • force-cache:表示fetch请求不顾一切的依赖缓存,即使缓存过期了,它依然从缓存中读取,除非没有任何缓存,那么它将发送一个正常的request
    • on-if-cached:表示fetch请求不顾一切的依赖缓存,即使缓存过期了,它依然从缓存中读取,如果没有缓存,他将抛出网络错误(改设置旨在mode为”same-origin“时有效)

返回值

fetch函数返回一个Promise对象

  • 当收到服务器的返回结果后,Promise进入resolved状态,状态数据为Response对象
  • 当网络发生错误(或其他导致无法完成交互的错误)时,Promise进入rejected状态,状态数据为错误信息

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

Request对象

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

new Request(url, 请求配置)

这里新建的Request就相当于将fetch的所有参数封装成了一个对象

注意点:

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

Request对象中有个clone方法,可以克隆出一个配置一样的Request对象

Response对象

也可通过new Response()去自己构建响应对象,构建的response对象也可通过内置API来解析

Headers对象

在Request和Response对象内部,会将传递的请求头对象,转换为Headers

Headers对象中的方法:

  • has(key):检查请求头中是否存在指定的key值
  • get(key):得到请求头中对应的key值
  • set(key, value):修改请求头中对应的键值对
  • append(key, value):添加对应的键值对
  • keys():得到所有的请求头键的集合
  • values():得到所有的请求头值的数组
  • entries():得到请求头中所有的键值对

文件上传

流程:

1、客户端将文件数据发送给服务器

2、服务器保存上传的文件数据到服务器端

3、服务器响应给客户端一个文件访问地址

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

请求方法:POST

请求的表单格式:multipart/form-data(一般情况修改为该格式)

请求体中必须包含一个键值对,键的名称是服务器要求的名称,值是文件数据

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

<input type="file" multiple id="upload">//其中multiple表示可以上传多个文件
<img src="" alt="" id="imgload">
<input type="file" multiple id="upload">
<button>上传</button>
<script>
    const ipt = document.getElementById('upload');
    const btn = document.getElementsByTagName('button')[0];
    async function upload(){
        if(ipt.files.length === 0){
            alert("请选择要上传的文件");
        }
        const formData = new FormData();//构建请求体
        formData.append("imagefile", ipt.files[0]);

        const resp = await fetch("http://101.132.72.36:5100/api/upload", {
            method: "POST",
            body: formData
        });
        const result = await resp.json();
        return result;
    }
    btn.onclick = async function(){
        const result = await upload();
        const img = document.getElementById('imgload');
        img.src = result.path;
    }
</script>

迭代器和生成器

迭代器

背景知识

1、什么是迭代?

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

2、迭代和遍历的区别?

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

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

3、迭代器

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

4、迭代模式

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

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

JS中的迭代器

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

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

则认为该对象是个迭代器

含义:

  • next方法:用于得到下一个数据
  • 返回的对象

    • value:下一个数据的值
    • done:Boolean,是否迭代完成
const arr = [1, 2, 3, 4, 5];
//迭代数组arr
const iterator = {
    i: 0,
    next(){
        var result = {
            value: arr[this.i],
            done: this.i >= arr.length
        }
        this.i++;
        return result;
    }
}
console.log(iterator.next());

通过函数进行封装

function createIterator(arr) {
    let i = 0;
    return {
        next() {
            let result = {
                value: arr[i],
                done: this.i >= arr.length
            }
            i++;
            return result;
        }
    }
}

斐波那契数列迭代器

function createFeiboIterator() {
    let prev1 = 1,
        prev2 = 1,
        n = 1;//当前位置的前一位和前二位

    return {
        next() {
            let value;
            if(n <= 2){
                value = 1;
            }else{
                value = prev1 + prev2;
            }
            const result = {
                value,
                done: false
            }
            prev2 = prev1;
            prev1 = value;
            n++;
            return result;
        }
    }
}

可迭代协议域for-of循环

可迭代协议

概念回顾

  • 迭代器(iterator):一个具有next方法的对象,next方法返回下一个数据并且能指示是否迭代完成
  • 迭代器创建函数(iterator creator):一个返回迭代器的函数

可迭代协议

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

思考:如何知晓一个对象是否是可迭代的?
思考:如何遍历一个可迭代对象?

for-of循环

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

//迭代完成后循环结束
for(const item in iterable){
    //iterable:可迭代对象
    //item:每次迭代得到的数据
}
const obj = {
    a: 1,
    b: 2,
    [Symbol.iterator](){
        let keys = Object.keys(this);
        let n = 0;
        return {
            next: () => {
                const propName = keys[n];
                const propValue = this[propName];
                const result = {
                    value: {propName, propValue},
                    done: n >= keys.length
                }
                n++;
                return result;
            }
        }
    }
}

for(let item of obj){
    console.log(item);
}

展开运算符与可迭代对象

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

const obj = {
    a: 1,
    b: 2,
    [Symbol.iterator](){
        let keys = Object.keys(this);
        let n = 0;
        return {
            next: () => {
                const propName = keys[n];
                const propValue = this[propName];
                const result = {
                    value: {propName, propValue},
                    done: n >= keys.length
                }
                n++;
                return result;
            }
        }
    }
}

for(let item of obj){
    console.log(item);
}
console.log([...obj])

生成器(Generator)

  1. 什么是生成器?

生成器是一个通过构造函数Generator创建的对象,生成器既是一个迭代器,同时又是一个可迭代对象

  1. 如何创建生成器?

生成器的创建,必须使用生成器函数(Generator Function)

  1. 如何书写一个生成器函数呢?
//这是一个生成器函数,该函数一定返回一个生成器
function* method(){

}
  1. 生成器函数内部是如何执行的?

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

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

yield是一个关键字,该关键字只能在生成器函数内部使用,表达“产生”一个迭代数据。

let arr = [1, 2, 3, 4];
function *test(arr){
    for (let item of arr) {
        yield item;
    }
}
const generator = test(arr);
  1. 有哪些需要注意的细节?

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

function *test(){
    console.log("第一次生成");
    yield 1;
    console.log("第二次生成");
    yield 2;
    console.log("第三次生成");
    return 50;
}//在done值第一次为true时,value值为return返回值

2). 调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值

function *test(){
    const info = yield 1;
    yield 5 + info;
}
const generator = test();
console.log(generator.next());//打印1
console.log(generator.next(123));//打印128

3). 第一次调用next方法时,传参没有任何意义

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

生成器的其他API

  • return方法:调用该方法,可以提前结束生成器函数,从而提前让整个迭代过程结束
  • throw方法:调用该方法,可以在生成器中产生一个错误,可配合new Error使用

生成器应用-异步任务控制

通过一个辅助函数来实现一个生成器的迭代,辅助函数:利用目标生成器函数创建一个生成器,在进行第一次执行之后,通过判断生成结果中的值是Promise还是其他数据,当生成结果为Promise时,利用then来处理Promise的状态数据,将状态数据传给result,以备后续可能会使用,如果生成结果是其他数据,则直接将生成结果赋值给result,以备后续使用

function *task(){
    const d = yield 1;
    console.log(d);
    //d: 1
    const resp = yield fetch("http://101.132.72.36:5100/api/local");
    const result = yield resp.json();
    console.log(result);
}

run(task);

function run(generatorFunc){
    const generator = generatorFunc();
    let result = generator.next();//启动任务(开始迭代),得到迭代数据
    handleResult();

    // 对result进行处理
    function handleResult(){
        if(result.done){
            return;//迭代完成,不处理
        }
        //迭代没有完成,分为两种情况
        //1、迭代的数据是一个Promise
        //2、迭代的数据时其他数据
        if(typeof result.value.then === "function"){
            //迭代的数据是一个Promise
            //等待Promise完成后,在进行下一次迭代
            result.value.then(data => {
                result = generator.next(data);
                handleResult();
            }, err => {
                generator.throw(err);
                handleResult();
            })
        }else{
            //迭代的数据是其他数据,直接进行下一次迭代
            result = generator.next(result.value);
            handleResult();
        }
    }
}

更多的集合类型

set集合

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

set用于存放不重复的数据

1、如何创建set集合

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

2、如何对set集合进行后续操作

  • add(数据):添加一个数据到set集合末尾,如果数据已存在,则不进行任何操作

    • set使用Object.is的方式判断两个数据是否相同,但是,针对+0和-0,set认为是相等的
  • has(数据):判断set中是否存在对应的数据,其判断方式同add一致
  • delete(数据):删除匹配的数据,返回是否删除成功(Boolean值)
  • clear():清空整个set集合
  • size:获取set集合中的元素数量,只读属性,无法重新赋值

3、如何与数组进行相互转换

const s = new Set([x, x, x, x, x]);
//set本身也是一个可迭代对象,每次迭代的结果就是每一项的值
const arr = [...s];

还可进行字符串去重

let str = "afrghreahn";
const s = [...new Set(str)].join("");

4、如何遍历

1)for-fo循环

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

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

set应用

//两个数组的交集、并集、差集(不能出现重复项),得到的结果是一个新数组
const arr1 = [11, 23, 14, 31, 213, 11];
const arr2 = [21, 21, 213, 12, 325, 23];
//并集
const result = [...new Set(arr1.concat(arr2))];
//交集
const s1 = new Set(arr1);
const s2 = new Set(arr2);
const cross = [...s1].filter(item => {
    return s2.has(item);
})
//交集
//const cross1 = [...new Set(arr1)].filter(item => arr2.indexOf(item) >= 0)

//差集
const subset = [...new Set([...arr1, ...arr2])].filter(item => arr1.indexOf(item) >= 0 && arr2.indexOf(item) < 0 || arr1.indexOf(item) < 0 && arr2.indexOf(item) >= 0);

手写set

class MySet{
    //iterator设置默认参数,当没有传入该参数时,设为[]
    constructor(iterator = []) {
        //验证是否是可迭代的对象
        if(typeof iterator[Symbol.iterator] !== "function"){
            throw new TypeError(`你提供的${iterator}不是一个可迭代的对象`);
        }
        this._datas = [];
        for (const item of iterator) {
            this.add(item);
        }
    }

    add(data){
        if(!this.has(data)){
            this._datas.push(data);
        }
    }    

    get size(){
        return this._datas.length;
    }

    has(data){
        for (const item of this._datas) {
            if (this.isEqual(data, item)){
                return true;
            }
            return false;
        }
    }

    delete(data){
        for (let i = 0; i < this._datas.length; i++){
            const element = this._datas[i];
            if(this.isEqual(element, data)){
                //删除
                this._datas.splice(i, 1);
                return true;
            }
        }
        return false;
    }

    forEach(callback){
        for (const item of this._datas) {
            callback(item, item, this);
        }
    }

    clear(){
        this._datas.length = 0;
    }

    *[Symbol.iterator](){
        for (const item of this._datas) {
            yield item;
        }
    }

    /**
     * 判断两个数据是否相等
     * @param data1
     * @param data2
     * @returns {boolean}
     */
    isEqual(data1, data2){
        if(data1 === 0 && data2 === 0){
            return true;
        }
        return Object.is(data1, data2);
    }
}

map集合

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

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

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

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

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

1、如何创建map

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

2、如何进行后续操作

  • size:只读属性,获取当前map中键的数量
  • set(键, 值):设置一个键值对,键和值可以是任何类型

    • 如果键不存在,则添加一项
    • 如果键已存在,则修改它的值
    • 比较键的方式和set相同
  • get(键): 根据一个键得到对应的值
  • has(键):判断某个键是否存在
  • delete(键):删除指定的键,返回值为是否删除成功
  • clear(): 清空map

3、和数组互相转换

和set一样

4、遍历

  • for-of,每次迭代得到的是一个长度为2的数组
  • forEach,通过回调函数遍历

    • 参数1:每一项的值
    • 参数2:每一项的键
    • 参数3:map本身

[扩展]WeakSet和WeakMap

let obj = {1: "a"};
const set = new Set();
set.add(obj);
obj = null;
console.log(set);
//打印Set(1) { { '1': 'a' } }

WeakSet

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

1、它内部存储的对象地址不会影响垃圾回收

2、只能添加对象

3、不能遍历(不可迭代的对象),没有size属性、没有forEach方法

let obj = {1: "a"};
const set = new WeakSet();
set.add(obj);
obj = null;
console.log(set);
//打印WeakSet { <items unknown> }

WeakMap

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

1、它的键存储的地址不会影响垃圾回收

2、它的键只能是对象

3、不能遍历

let arr = [{1: "a"}, "b"];
const map = new WeakMap();
map.add(arr);
arr[0] = null;
console.log(map);
//打印WeakMap { <items unknown> }

代理和反射

属性描述符

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

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

  • value:属性值
  • configurable:该属性的描述符是否可以修改
  • enumerable:该属性是否可以被枚举
  • writable:该属性是否可以被重新赋值

Object.getOwnPropertyDsecriptors(对象),可以得到某个对象的所有属性描述符

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

Object.defineProperty(对象, 属性, 描述符)
Object.defineProperty(对象, 多个属性的描述符)
//如
const obj = {
    a: 1,
    b: 2
}
Object.defineProperty(obj, "a", {
    value: 3,
    configurable: false,//属性描述符不可修改,值还可进行修改
    enumerable: false//不可枚举
    writable: false//属性值不能再次修改
})
Object.defineProperty(obj, {
    a: {
        xxx
    }
    b: {
        xxx
    }
})

存取器属性

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

get和set配置均为函数,如果一个属性是存取器属性,则读取该属性时,会运行get方法,将get方法得到的返回值作为属性值;如果给该属性赋值,则会运行set方法

let obj = {a: 1, b: 2};
Object.defineProperty(obj, "a", {
    get(){
        console.log("运行了属性a的get函数");
    },
    set(val){
        console.log("运行了属性a的set函数", val);
    }
})
obj.a = obj.a + 1;
console.log(obj.a);
//打印结果
//运行了属性a的get函数
//运行了属性a的set函数 NaN
//运行了属性a的get函数
//undefined

正常写法

let obj = {a: 1, b: 2};
Object.defineProperty(obj, "a", {
    get(){
        console.log("运行了属性a的get函数");
        return obj._a;
    },
    set(val){
        console.log("运行了属性a的set函数", val);
        obj._a = val;
    }
})

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

Reflect

1、Reflect是什么?

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

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

2、它可以做什么?

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

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

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

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

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

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

4、它里面到底提供了哪些API呢?

  • Reflect.set(target, propertyKey, value): 设置对象target的属性propertyKey的值为value,等同于给对象的属性赋值
  • Reflect.get(target, propertyKey): 读取对象target的属性propertyKey,等同于读取对象的属性值
  • Reflect.apply(target, thisArgument, argumentsList):调用一个指定的函数,并绑定this和参数列表。等同于函数调用
  • Reflect.deleteProperty(target, propertyKey):删除一个对象的属性
  • Reflect.defineProperty(target, propertyKey, attributes):类似于Object.defineProperty,不同的是如果配置出现问题,返回false而不是报错
  • Reflect.construct(target, argumentsList):用构造函数的方式创建一个对象
  • Reflect.has(target, propertyKey): 判断一个对象是否拥有一个属性
  • 其他API:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

Proxy代理

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

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

Proxy可以根据Reflect中的API来修改其底层的方法,如:

const obj = {
    a: 1,
    b: 2
}
const proxy = new Proxy(obj, {
    set(target, propertyKey, value){
        //target[propertyKey] = value;
        Reflect.set(target, propertyKey, value);
    }
})

proxy.a = 10;
console.log(proxy.a);

应用-观察者模式

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

//创建一个观察者
function observer(target) {
    const div = document.getElementById("container");
    const proxy = new Proxy(target, {
        set(target, prop, value) {
            Reflect.set(target, prop, value);
            render();
        },
        get(target, prop){
            return Reflect.get(target, prop);
        }
    })
    render();

    function render() {
        let html = "";
        for (const prop of Object.keys(target)) {
            html += `
                <p><span>${prop}:</span><span>${target[prop]}</span></p>
            `;
        }
        div.innerHTML = html;
    }

    return proxy;
}
const target = {
    a: 1,
    b: 2
}
const obj = observer(target)

应用-偷懒的构造函数

class User {

}

function ConstructorProxy(Class, ...propNames) {
    return new Proxy(Class, {
        construct(target, argumentsList) {
            const obj = Reflect.construct(target, argumentsList)
            propNames.forEach((name, i) => {
                obj[name] = argumentsList[i];
            })
            return obj;
        }
    })
}

const UserProxy = ConstructorProxy(User, "firstName", "lastName", "age")

const obj = new UserProxy("袁", "进", 18);
console.log(obj)

class Monster {

}

const MonsterProxy = ConstructorProxy(Monster, "attack", "defence", "hp", "rate", "name")

const m = new MonsterProxy(10, 20, 100, 30, "怪物")
console.log(m);

应用-可验证的函数参数

function sum(a, b) {
    return a + b;
}

function validatorFunction(func, ...types) {
    const proxy = new Proxy(func, {
        apply(target, thisArgument, argumentsList) {
            types.forEach((t, i) => {
                const arg = argumentsList[i]
                if (typeof arg !== t) {
                    throw new TypeError(`第${i+1}个参数${argumentsList[i]}不满足类型${t}`);
                }
            })
            return Reflect.apply(target, thisArgument, argumentsList);
        }
    })
    return proxy;
}

const sumProxy = validatorFunction(sum, "number", "number")
console.log(sumProxy(1, 2))

增强的数组功能

新增的数组API

静态方法

  • Array.of(…args):使用指定的数组项创建一个新数组
//new Array(args)方法在数组参数中只有一项,且为数字时,会将这个参数当成新建数组的长度,Array.of()可以解决这个问题
let arr = Array.of(1, 2, 3);
console.log(arr);//打印[1, 2, 3]
  • Array.from(ars):通过给定的类数组或可迭代对象,创建一个新的数组

实例方法

  • find(callback):用于查找满足条件的一个元素
const arr = [
    {name: "a", id: 1},
    {name: "b", id: 2},
    {name: "c", id: 3},
    {name: "d", id: 4},
    {name: "e", id: 5},
    {name: "f", id: 6}
]
arr.find(item => return item.id === 5);
  • findIndex(callback):用于查找满足条件的第一个元素的下标,若未找到,返回-1
  • fill(data):用指定的数据填充满数组所有的内容
const arr = new Array(100);
arr.fill("abc");
  • copyWithin(target, start?, end?):在数组内部完成复制
const arr = [1, 2, 3, 4, 5, 6];
arr.copyWithin(2);//[1, 2, 1, 2, 3, 4m
arr.copyWithin(1, 2);//[1, 3, 4, 5, 6, 6]
arr.copyWithin(1, 1, 3);//[1, 2, 2, 3, 5, 6]
  • includes(data)[ES7]:判断数组中是否包含某个值,使用Object.is匹配`

类型化数组

数字存储的前置知识

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

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

3、n位的有符号整数能表示的数字是2(n - 1) ~ 2^(n - 1) - 1

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

  1. 32位浮点数:又称为单精度浮点数,它用1位表示符号,8位表示阶码,23位表示位数
  2. 64位浮点数:又称为双精度浮点数,它用1位表示符号,11位表示阶码,52位表示位数

5、JS中的所有数字,均使用双精度浮点数保存

类型化数组

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

具体分为:

  • Int8Array:8位有符号整数(-128 ~ 127)
  • Uint8Array:8位无符号整数(0 ~ 255)、
  • Int16Array:…
  • Uint16Array:…
  • Int32Array:…
  • Uint32Array:…
  • Int64Array:…
  • Uint64Array:…
const arr1 = new Int8Array(10);
console.log(arr1);
//Int8Array(10) [
  0, 0, 0, 0, 0,
  0, 0, 0, 0, 0
]
const arr2 = new Array(10);
console.log(arr2);
//[ <10 empty items> ]

1、如何创建数组

选择不同类型的类型化数组构造函数,有一定的取值范围,如Int8Array只能存-128~127

new 数组构造函数(长度)
数组构造函数.of(元素)
数组构造函数.from(可迭代对象)
new 数组构造函数(其他类型化数组)//高位数组向低位数组传递时,超过位数会产生误差

2、得到长度

arr.length;//得到元素数量
arr.byteLength//得到占用的字节数

3、其他用法跟普通数组一致,但是:

  • 不能增加和删除数据,类型化数组长度固定,占用内存空间也固定
  • 一些返回数组的方法,返回的数组是同类型化新数组

ArrayBuffer

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

const bf = new ArrayBuffer(n)//创建存储n个字节数据的内存空间

可以通过属性byteLength得到字节数

const bf2 = bf.slice(3, 5);//与数组的slice方法一致

可以通过方法slice得到新的数组

读写ArrayBuffer

1、DataView

const view = new DataView(obj, m, n)

const bf = new ArrayBuffer(10);
const view = new DataView(bf, 2, 3);//第一个参数为目标数组,第二个参数为操作偏移量,第三个参数为操作的长度,如上为从第三个元素开始,操作第3~5的三个元素

修改方法,view.setInt8/16/32/64(m, n)

2、使用类型化数组

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

const bf = new ArrayBuffer(10);
const arr1 = new Int8Array(bf);
console.log(arr1.buffer == bf);//true

在利用Int8Array操作ArrayBuffer时,操作的为一个字节

在利用Int16Array操作ArrayBuffer时,操作的为二个字节

在利用Int32Array操作ArrayBuffer时,操作的为四个字节

在利用Int64Array操作ArrayBuffer时,操作的为八个字节

[扩展]制作黑白图片

const ctx = cvs.getContext(“2d”)获取画笔

ctx.drawImage(img, 0, 0),后两个参数分别为在canvas画布上x和y方向上的定位

context.drawImage(img,sx,sy,swidth,sheight,x,y,width,height);
//img,绘制的图像(必选)
//sx,开始剪切的x坐标位置
//sy,开始剪切的y坐标位置
//swidth,被剪切的图像的宽度
//sheight,被剪切的图像的高度
//x,在画布上放置的x坐标(必选)
//y,在画布上放置的y坐标(必选)
//width,要使用的图像宽度
//height,要使用的图像高度

图像上每个像素点的颜色值通过四个值来进行存储,分别为R、G、B、alpha,将彩色图像转为黑白色图像,只需将每个像素点上RGB三个值均赋值为三者的平均值

通过ctx.getImageData(0, 0, img.width, img.height),四个参数分别为起始点的坐标和终止点的坐标,获得数组的长度为图像总像素点个数*4

利用ctx.putImageData(imageData, 0, 0)可将设置好的图像信息设置到画布

图片数据采用二进制形式进行存储,因此不能用json()方法进行解析,只能用blob,如下:

async function test(){
    const resp = await fetch("./img/liao.jpg")
    const blob = await resp.blob();
    const bf = await blob.arrayBuffer();
    const arr = new Int8Array(bf, 3, 2);
}