ES6学习笔记(下)

104 阅读16分钟

@[toc]

1. class类

es6中引入了类(class)的概念。通过class可以定义一个类。这和Java中的类的定义有一定的相似点

基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。

class 类定义的所有方法都是定义在原型上的

使用class定义类的例子:

class Test{
    constructor(id,name,age,sex) {
        this.id=id
        this.name=name
        this.age=age
        this.sex=sex
    }
    print(){
        console.log('学号'+this.id,'姓名'+this.name,'年龄'+this.age,'性别'+this.sex);
    }
}
let p1=new Test('001','张三','10','男');
let p2=new Test('002','哈哈','18','女');
p1.print();
p2.print();

关于类的几点注意事项:

  1. 类名一般首字母大写,用来跟普通函数进行区别
  2. 类不能直接调用,必须进行实例化处理
  3. 类的类型是function
  4. 类本身就指向构造函数
  5. 类不存在变量的位置提升
  6. 类中的this指向是实例化的对象

1.1 构造函数constructor()

constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加。

class Person {
 //handle
}
class Person {
 constructor() {}
 //handle
}
//es5写法
function Person() {}
//Person.prototype.xx 

1.2 类的实例

生成类的实例依然使用new关键字,如果不通过new而直接调用对象的方法会直接报错。

并且这与es5的模式相同,实例的属性除非显式的定义再其本身(即this对象),否则都是定义在原型上

class Test{
    constructor(id,name,age,sex) {
        this.id=id
        this.name=name
        this.age=age
        this.sex=sex
    }
    print(){
        console.log('学号'+this.id,'姓名'+this.name,'年龄'+this.age,'性别'+this.sex);
    }
}
let p1=new Test('001','张三','10','男');

以上代码p1就是Test类的一个实例,并且print就是挂载在Test原型上的方法。id、name等这些属性就是实例的属性

1.3 class表达式

类也可以使用表达式的形式来定义

有以下三种定义:

let Person1 = class { }
let Person2 = class Ha { }
//立即执行的class
let Person3=new class{}()

1.4 getter和setter函数

与 Object 对象一样,在“类”的内部也可以使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截 该属性的存取行为。

class TT{
    constructor() {
        this.name='张三'
    }
    get getname(){
        return this.name;
    }
    set setname(value){
        console.log('我是setter');
        this.name=value;
    }
}
console.log('----------------------');
var t=new TT();
console.log(t.getname);//张三
t.setname='aa';//我是setter
console.log(t.getname);//aa

1.5 generator方法

这个函数后续会单独讲

generator是es6新增的解决异步编程的方法,语法和传统的函数不一样

generator就是一个状态机,封装了多个内部状态。

执行generator函数会返回一个遍历器对象

generator函数有两大特征:

  1. function与函数名之间有一个*
  2. 函数体内部使用yield(产出)表达式定义多个内部状态(用来提取遍历后的数据)

凡是方法名前加*,就表明这是一个generator函数,就可以使用状态机yield。

class Fruits{
    constructor(...args) {
        this.args=args;
    }
    *[Symbol.iterator](){
        for (const arg of this.args) {
            yield arg;
        }
    }
}
for (const v of new Fruits('apple','banana')) {
    console.log(v);
}

1.6 静态属性和静态方法

es6目前已支持通过“static”来声明一个静态方法。但是没有静态属性这个概念。与es5一样,无论是静态属性还是静态方法都需要使用构造函数本身来调用。

class TestSta{
    static printinfo(){
        console.log('我是一个静态方法');
    }
}
TestSta.printinfo();

静态方法的特征:

  1. 它不属于实例化对象,不能通过实例化对象调用,它只属于类自身。
  2. 它只能通过类直接调用
  3. 它可以跟类中的其他方法同名

1.7 类的继承

es6中类的继承使用extends关键字继续,我们回忆一下es5中的继承方式,需要修改子类的this指向或者修改原型链来进行。es6中的继承方式确实就比较简洁明了。

下面是一个es6继承的实例:

class Person {
    
}
class Stuent extends Person {
    constructor() {
        super();
    }
}
var stu = new Stuent();

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

但是当子类没有定义constructor方法时,这个方法会被默认添加,也就是说,不管有没有显式定义,任何子类都有一个constructor方法。除非重写后没有加super()才会出现问题

Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。

Object.getPrototypeOf(ColorPoint) === Point

因此,可以使用这个方法判断,一个类是否继承了另一个类。

注意:

  1. 只有调用super关键字后,才可以使用this关键字,否则会报错(因为子类的构建基于父类实例,而构建父类实例需要super方法)
  2. 父类的静态方法可以被子类继承
  3. 作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。
  4. super作为对象时,在普通方法中,指向父类的原型对象。在静态方法中,指向父类。
  5. 如果属性定义在父类的原型对象上,super就可以取到。
  6. 在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。
  7. 由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。但是不能读取值,直接读值相当于访问在父类的原型上的某个值
  8. 使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错

详情还请看阮老师的《ECMAScript 6 入门

2. Module模块开发

关于模块的概念,就是将一个复杂的程序依据一定的规范封装成几个块,并将其组合在一起。块的内部数据与实现是私有的,只是像外部暴漏一些接口(方法)与外部其他进行模块通信。

目前前端最流行的几个规范分别为,CommonJs,AMD,CMD,ES6。

CommonJs:首先CommonJs是运行在服务端的模块规范。每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。在服务器端,模块的加载是运行时同步加载的;在浏览器端(借助Browserify),模块需要提前编译打包处理。

AMD:CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来着早。

CMD: CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。

ES6: ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。\

本文主要主要还是介绍模块化ES6的规范。关于其他几个方式我们可以参考这篇博文(我认为讲解的非常到位):

juejin.cn/post/684490…

2.1 使用规则

引入使用export,导出使用import

贴一个实例:

//moudules.js
export var a='a';
export let b='b';
export const c='c';
export var obj={
    name:'张三',
    age:'12',
    sex:'女'
}
//app.js
import {a,b,c,obj} from './modules';
console.log(a);
console.log(b);
console.log(c);
console.log(obj.name);
console.log(obj.sex);
console.log(obj.age);

除此之外export和import还有以下不同的几种写法

2.2 export

第一种:直接在需要导出的地方加上export即可

export var a='a';

第二种:批量导出

export {a,b,c,obj}

第三种:重命名导出(导出时需要使用o)

export {a,b,c,obj as o}

第四种: default导出(这样写的好处是导出一个对象, 其他模块加载该模块时,import命令可以为该匿名函数指定任意名字)

var a = 'a';
let b = 'b';
const c = 'c';
var obj = {
    name: '张三',
    age: '12',
    sex: '女'
}
export default{
    a,b,c,obj
}

2.3 import 命令

第一种:(模块的整体加载)(as自定义名称)

import * as tt from './modules';

第二种:选择性导入

import {a,b,c} from './modules';

第三种:通过default导出的导入

import moduleName from './modules';
console.log(moduleName.obj.age);

2.4 ES5模块与CommonJs的差异

  1. CommonJs模块的输出利用的是值得拷贝,也就是说我们不能改变原被导出文件的变量值。而ES6导出的值得引用
  2. CommonJs模块是运行时加载,ES6模块是编译时加载(因为 CommonJS 加载的是一个对象,该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。)

ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块

3. Promise 对象

Promise是JavaScript中进行异步编程的新的解决方式。

从语法上来说:Promise是一个构造函数

从功能上来说:Promise对象用来封装一个异步操作并且可以获取结果。

3.1 Promise的状态改变

pending转为resolved

pending转为rejected

说明:只有这两种变换,并且promise对象只能改变一次。无论变成成功还是失败,都会有一个结果数据。成功的结果数据一般称为value,失败的结果数据一般称为reason。

Promise的基本运行流程:

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

3.2 Promise的基本使用

  const p=new Promise((resolve, reject) => {
        setTimeout(() => {
            let time=Date.now();
            if (time%2==0) {
                resolve('正确的数据'+time);
            } else {
                reject('错误的数据'+time);
            }
        }, 1000);
    });
    p.then(
        value=>{
            console.log('正确:'+value);
        },
        reason=>{
            console.log('错误:'+reason);
        }
    );

其中,Promise中的参数executor是一个执行器函数,它有两个参数resolve和reject。它内部通常有一些异步 操作,如果异步操作成功,则可以调用resolve()来将该实例的状态置为fulfilled,即已完成的,如果一旦失败,可 以调用reject()来将该实例的状态置为rejected,即失败的。

3.3 为什么要使用Promise

1.指定回调函数的方式更加灵活

可以参考以下MDN上的例子。

这是使用普通回调函数:

// 成功的回调函数
function successCallback(result) {
  console.log("音频文件创建成功: " + result);
}

// 失败的回调函数
function failureCallback(error) {
  console.log("音频文件创建失败: " + error);
}

createAudioFileAsync(audioSettings, successCallback, failureCallback)

这是使用Promise的方式:

const promise = createAudioFileAsync(audioSettings);
promise.then(successCallback, failureCallback);

从上面的例子我们看出老式的回调方式必须要在异步操作执行之前进行指定。

而Promise则是使用了同步的方式来进行异步操作,为什么这样说呢?

我们看一下Promise的流程:启动异步任务=>返回promise对象=>给Promise对象绑定回调函数(甚至可以在异步任务完成以后调用都可以)

2.链式调用(解决回调地狱)

什么是回调地狱?

回调函数嵌套调用,外部回调函数异步执行的结果是嵌套的回调函数执行的条件。这样的写法的不利因素主要有不便于阅读和不便于异常处理。

解决的方式就是使用Promise链式调用。终极解决方案为async/await.

3.4 Promise具体用法

1. then()方法

Promise对象含有then方法,then()调用后返回一个Promise对象,意味着实例化后的Promise对象可以进行 链式调用,而且这个then()方法可以接收两个函数,一个是处理成功后的函数,一个是处理错误结果的函数。

2. catch()方法

catch()方法和then()方法一样,都会返回一个新的Promise对象,它主要用于捕获异步操作时出现的异常。因 此,我们通常省略then()方法的第二个参数,把错误处理控制权转交给其后面的catch()函数。

3. all()方法

Promise.all()接收一个参数,它必须是可以迭代的,比如数组。它通常用来处理一些并发的异步操作,即它们 的结果互不干扰,但是又需要异步执行。

它最终只有两种状态:成功或者失败。它的状态受参数内各个值的状态影响,即里面状态全部为fulfilled时, 它才会变成fulfilled,否则变成rejected。成功调用后返回一个数组,数组的值是有序的,即按照传入参数的数组的 值操作后返回的结果。

4.resolve()

Promise.resolve()接受一个参数值,可以是普通的值,也可以是具有then()方法的对象和Promise实例。正常 情况下,它返回一个Promise对象,状态为fulfilled。但是,当解析时发生错误时,返回的Promise对象将会置为 rejected态。

5.reject()

Promise.reject()和Promise.resolve()正好相反,它接收一个参数值reason,即发生异常的原因。此时返回的 Promise对象将会置为rejected状态。

4. async与await

async 函数的返回值为promise对象,promise的结果由async函数执行的返回值来决定。

 async function fn1() {
        return 1;
    }
    const a=fn1();
    console.log(a);//返回promise对象

之所以返回的是一个失败的结果那么我们可以使用then来操作:

   async function fn1() {
       // throw 2
       //return Promise.reject(88)
       return new Promise((resolve, reject) => {
            setTimeout(() => {
                reject(1)
            }, 1000);
        });
    }
    const a=fn1();
    a.then(
        value=>{

        },
        reason=>{
            console.log('失败的结果'+reason);//返回2
        }
    )

是不是发现这里的操作和我们promise的操作是相同的。

await

await右边的表达式一般为promise对象,但是一般也可以为其他的值。此方法替代了then方法。

如果是怕promise对象,await返回的值是promise成功的值。

如果是其他的值,直接将此值作为await的返回值

注意:

  1. await必须要写在async函数内部,但是async内部可以不用await
  2. 如果await中的promise失败了,就会抛出异常,需要使用try...catch来捕获处理

成功值的例子:


    function fn2() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(10)
            }, 1000);
        });
    }
    async function fn3() {
        const a= await fn2();
        console.log(a);//10
    }
    fn3()

失败值的例子(需要使用try-catch):

 function fn2() {
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                reject(10)
            }, 1000);
        });
    }
    async function fn3() {
        try {
            const a = await fn2();
            
        } catch (error) {
            console.log('错误'+error);
        }

    }
    fn3()

5. Symbol类型

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型

前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名 属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

基本用法:

 let a = Symbol('123')
 let b = Symbol('123')
 console.log(a === b);//false

注意:Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是 对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

小tips:

  1. Symbol值不能与其他类型的值进行运算,会报错。
  2. Symbol 值可以显式转为字符串
  3. Symbol 值也可以转为布尔值,但是不能转为数值。

5.1 symbol作为属性名

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不 会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

let person1 = {
        name: Symbol('李四'),
    }
    let person2 = {
        name: Symbol('李四'),
    }
    let score = {
        [person1.name]: {
            js:100,css:100
        },
        [person2.name]: {
            js:50,css:50
        },
    }

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

5.2 Symbol.for(),Symbol.keyFor()

Symbol.for()

有时,我们希望重新使用同一个 Symbol 值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数, 然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字 符串为名称的 Symbol 值。 Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供 搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经 存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值, 但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值。

    const obj = {};
    let a = Symbol('a');
    let b = Symbol('b');
    obj[a] = 'Hello';
    obj[b] = 'World';
    const objectSymbols = Object.getOwnPropertySymbols(obj);
    console.log(objectSymbols);

Symbol.keyFor()

Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key

let s1 = Symbol.for("foo");
console.log(Symbol.keyFor(s1)); // "foo"
let s2 = Symbol("foo");
console.log(Symbol.keyFor(s2)); // undefined