200个案例让你重新学习ES6重要语法。。。三万字长文【建议收藏】上

1,193 阅读24分钟

有部分代码显示不清楚的情况大家可以去我语雀中看:链接

第一部分

包含:

1.var/let/const 变量声明    2.箭头函数

一、var/let/const 变量声明

var的使用:

  • 具有变量提升,在代码执行前会有预解析,将所有的声明提升到全局作用域的最上面(函数中声明的除外),然后在执行代码时再进行赋值,如果一个变量赋值前被调用了那么会输出undefined。

    console.log(a); //输出undefined var a = 18

  • 其属于函数作用域,除了在函数中声明的变量,在其余地方声明的变量都属于全局变量。

  • 使用var声明的变量是可以重复声明同名变量的,后声明的会覆盖前声明的。

  • 关于一些其他:

  • 如果window对象上有一个name属性,那么如果我们通过var声明一个name变量,则会覆盖之前window上的name属性,要解决这个问题其实也非常简单就是使用立即执行函数,让全局作用域成为函数作用域,使变量私有化(这可能也是立即执行函数的一个作用吧)

let的使用:

  • let声明与var声明我认为主要的区别在于前者属于块级作用域,也就是在{}中声明的变量,我们在声明的对象外是无法对其进行访问的。否则会报ReferenceError错误。

  • 使用let初始化的变量是不允许在相同的作用域下有同名变量出现的,否则会报错。

    { var name = "b"; //Identifier 'name' has already been declared}

    { let name = "a"; { let name = "b" // 不报错 } }

    let name = "a"; var name = "b"; // 报错

  • 与var声明不同的是,其声明的变量不会影响window对象上的属性。

  • 变量提升,关于变量提升其实很多人认为使用let声明的变量在未声明前调用会抛出ReferenceError错误是因为没有变量提升的原因,其实不是,其还是具有变量提升的,在我们使用let关键字声明变量后其也是会被提升到作用域的最顶部,但是因为存在暂存性死区的原因,在执行到该声明前我们是无法对其进行访问的(let声明的变量在执行时我们才会对其进行初始化)。

const的使用:

  • 其与let非常相似,区别主要有两部分:
  1. const声明的变量只能是常量,也就是其对应的地址是不可以修改的

  2. 其一旦声明必须进行赋值

使用场景:

  • 常量,不需要改变的值使用const

  • 在其余时候使用let便好

  • 在ES6中应避免使用var

  • let与const的出现是为了解决var声明的变量为未执行还可以调用而产生的bug

二、箭头函数

写法:

简化写法在使用像map,forEach,find,filter等数组方法时非常好用。

// 常规写法
let a = 1;
const add = (num) => {
  return num++;
};
add(a);  // 2

// 当参数为一个变量,返回值为一个语句时可以简写
let a = 1;
const add = num =>  ++num;
add(a);  // 2

特点:

  • 箭头函数其最大的特点是其内部的this指向的是“其外部的this”,举个例子:

    const person = { name: 'mss', hobbies: ['money','sleeping','eating'], output: function () { //console.log(this); //this是person这个对象 this.hobbies.map(function (hobby) { //console.log(this);//this是window console.log(${this.name} loves ${hobby}); }) } }; person.output(); /* 输出结果: loves money loves sleeping loves eating */

在这段代码中我们可以发现如果一个对象以key:fn的方式调用一个函数,那么函数内部的this指向的便是这个对象,但是在使用map这个原生数组的方法时我们可以发现其内部的this指向的是window,但是在这个题中我们为了可以输出对象中的名称,在es6之前我们只能使用创建一个变量指向this的方式来解决:

const person = {
    name: 'mss',
    hobbies: ['money','sleeping','eating'],
    output: function () {
        let that = this;
        this.hobbies.map(function (hobby) {
            console.log(`${that.name} loves ${hobby}`);
        })
    }
};
person.output();
/*  输出结果
    mss loves money
    mss loves sleeping
    mss loves eating
*/

既然箭头函数中的this是指向“其外部的this”,所以我们用ES6的箭头函数便可以轻松的解决。

const person = {
    name: 'mss',
    hobbies: ['money','sleeping','eating'],
    output: function () {
        this.hobbies.map(hobby => {
            console.log(`${this.name} loves ${hobby}`);
        })
    }
};
person.output();
/*  输出结果
    mss loves money
    mss loves sleeping
    mss loves eating
*/

注意点:

  • 箭头函数没有Arguments对象。(arguments的本质是一个装着函数传递过来实参的一个伪数组,我们可以使用Array.from(arguments)来将其转换为真数组)

错误:

let sum = () =>{
    return Array.from(arguments)
        .reduce((prevSum,curValue) => prevSum + curValue);
};
sum(1,2,3); //arguments is not defined

正确写法:

let sum = function(){
    return Array.from(arguments)
        .reduce((prevSum,curValue) => prevSum + curValue);
};
console.log(sum(1, 2, 3)); //6

或者:

let sum = (...args) => {
    return args.reduce((prevSum,curValue) => prevSum + curValue); // args本质是实参组成的数组
};
console.log(sum(1, 2, 3));
  • **箭头函数没有原型对象,**所以我们不能使用箭头函数作为构造函数,从逻辑上判断当我们new一个对象的时候我们知道一共有这几步。

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

  • 让构造函数中的this指向这个新的对象

  • 执行构造函数中的代码

  • 返回这个对象

很显然当我们让this指向这个对象的时候我们知道箭头函数中的this指向的是其外部的this,所以就无法进行实例化,同理我们不可以使用箭头函数给原型对象添加方法,当然给对象添加方法的时候也不可以使用箭头函数。

let Foo = (name,age) => {
    this.name = name;
    this.age = age;
};
let foo = new Foo('ghk',22); // TypeError: Foo is not a constructor
Foo.prototype.say = () => {
    console.log(`hello ${this.name}`); // TypeError: Foo is not a constructor
}
  • 最后在箭头函数中没有yield命令,所以箭头函数不能作为Generate函数

总结一下:

  1. 当参数为一个变量,返回值为一句话时,括号可以省略

  2. 其内部的this指向的是外部的this

  3. 不能作为构造函数,不能给原型对象添加方法,不能给对象添加方法,其内部不能使用arguments,也不能作为Generate函数。

说一些其他的:

  • 首先是Array.from(arrayLike[, mapFn])方法,其作用是将一个伪数组转化为真数组,其总共可以接收三个参数:

  • arrayLike是我们要转化的伪数组

  • mapFn是一个回调函数,新数组里的每一个元素会执行该回调

    console.log(Array.from('foo')); // expected output: Array ["f", "o", "o"]

    console.log(Array.from([1, 2, 3], x => x + x)); // expected output: Array [2, 4, 6]

  • 第二个也是我们数组会常常用到的方法,reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue]),其最大的作用是可以计算某个数组的累加和,当然也可以用来求一个数组的最大值。

  • accumulator回调函数上一次的返回值

  • currentValue数组的当前值

  • index当前索引(可选)

  • array数组(可选)

  • **initialValue初始值,**作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。

    let arr = [1,2,3,4]; let sum = (accumulator,currentValue) => accumulator + currentValue; console.log(arr.reduce(sum)); //10 console.log(arr.reduce(sum, 5)); //15

    let arr = [1,2,3]; let sum = arr.reduce(function(a,b){ return a + b; },4) console.log(sum); // 10 let max = arr.reduce(function(a,b){ return Math.max(a,b); }) console.log(max); // 3

总结一下:

  • Array.from()接收两个参数,第一个伪数组,第二个是一个回调,数组中的每一个值会执行该对调如(a) => (a + 3)

  • reduce是一个迭代器,其接收一个回调函数,第一个参数为上一次的return(第一次为数组的第一个元素的值),第二个参数为数组当前值,还可以设置初始值

第二部分

包含:1.默认参数值     2.模板字符串     3.解构赋值     4.剩余参数     5.扩展运算符     6.对象字面量增强写法

一、默认参数值

当我们创建一个方法的时候我们可以给其指定默认的参数值啦,而在以前我们只能对每一个参数值进行进行判断其是不是undefined。

function [name]([param1[ = defaultValue1 ][, ..., paramN[ = defaultValueN ]]]) { 
    statements 
}

其特点是如果我们传递参数那么形参就是我们传递的值,如果传递的是undefined那么就使用默认参数值。注意:如果传递一个null那么默认参数值便是null也属于一个实参。

function multiply(a = 3, b = 5) {
   return a * b;
}
console.log(multiply(5, 2)); // ab均为传递进来的值,为10
console.log(multiply(5)); // a是5,b是默认值5,结果为25
console.log(multiply(undefined,10)); //a是默认值3,b是10,结果为30

function test(a = 1,b = 2, c = 3, d = 4) {
    return a + b + c + d;
}
console.log(test(undefined, undefined, 4, undefined)); //11

传递null的情况:

function fn (a=3,b=4){
   return a + b;
}
 console.log(fn(undefined,null)); // 输出3

当然在传递默认参数的时候也是允许调用一个函数的:

function getSum(a = "少吃,", b = getDefault()) {
    console.log(a, b);
}
getSum();
// getSum(123, "abc");
function getDefault() {
    return "零食";
}

总结一下:

我们可以给一个函数传递默认参数啦,使用a = value 的形式,如果,传入的undefined则使用默认参数,传入null则使用null。

二、模板字符串

说到模板字符串,其常用的两个点是:

  1. 在其内部可以对我们所需要的字符串进行换行,这在创建标签时极大的方便了我们的创建

    let str = `

    `

    let str = `

    `.trim(); document.write(str);
  2. 我们可以使用${value}的形式在其内部进行传递参数,甚至我们可以在其内部调用函数。

    let name = 'ghk'; let age = 22; let output2 = ${name} is ${age};

    let output3 = ${name} is ${age * 3};

重点说一下标签模板字符串:

我们可以像给函数传递参数那样,将一个模板字符串作为参数传递给一个函数,这个函数接收两个参数,第一个参数为string,其是一个数组,存储在模板字符串中被传递参数所分隔的字符串,第二个参数我们可以设置为...argu这里使用到了扩展运算符,所以我们可以知道argu为模板字符串中变量{}所分隔的字符串,第二个参数我们可以设置为...argu这里使用到了扩展运算符,所以我们可以知道argu为模板字符串中变量{name}代表的字符串所组成的数组。

function myTag(strings, ...values) {
   console.log(strings);
   console.log(values);
}
let name = 'ghk';
let task = 'learning English';
let output = myTag`${name} has planned to ${task}`;

image

当然了我们也可以在函数中对接收的参数进行拼接,最后return等,如果没有返回值那么返回值便是undefined。

三、解构赋值

数组解构:

一般使用中每一个元素可以和数组中的每一个元素相对应。

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

let [one,,three,four] = arr;
console.log(one); //1
console.log(three); //3
console.log(four); //4

也可以使用扩展运算符。但注意如果使用扩展运算符,最后一项一定在最后面。

let [a, ...b] = [1, 3, 5];
console.log( a); //1
console.log(b);  //[3,5]

没有匹配上的输出undefined

let [a, b, c] = [1];
console.log( a); //1
console.log(b); //undefined
console.log(c); //undefined

使用场景:

  • 可以作为变量交换:

    let a = 1; let b = 3; [a, b] = [b, a]; console.log(a); // 3 console.log(b); // 1

对象解构:

  • 常规使用:

    let obj = { name: 'andy', age: 22, gender: 'male' };

    let {name, age,gender} = obj; console.log(name, age, gender); // andy 22 male

  • 解构赋值左边的变量名必须和右边对象的key一样才可以解构,如果右边无对应的key则输出undefined。

    let {a, b} = {name: 'xxx', age: 22,}; console.log(a, b); // undefined undefined

  • 左边的变量数也可以多于右边的key数,可以给变量指定默认值。

    let {name, age, gender} = {name: "xxx",age: 22}; console.log(name, age, gender); // gender是undefined

    let {name, age, gender = 'male'} = {name: "xxx",age: 22}; console.log(name, age, gender); // xxx 22 male

四、剩余参数:

这一部分我们只需要记住两个例子便好:

  • 使用扩展运算符...xxx作为函数的形参在函数中得到的是函数传递过来实参组成的数组。

    const fn =(...xxx)=>{ console.log(xxx); } fn(1,2,3);// [1,2,3]

  • 如果没有指定形参,那么arguments得到的是函数实参组成的一个伪数组,我们可以通过Array.from()将这个伪数组转化为真数组。

    function fn (){ console.log(arguments); console.log(Array.from(arguments)); } fn(1,2,3);

image.png

  • 注意:在箭头函数中不能使用arguments

五、扩展运算符

把一个可遍历对象转为一个用逗号分隔的新的参数序列,相当于剩余参数的逆运算。

什么是可遍历对象?

就是部署了iterator接口,可以用for of循环的数据类型。包括字符串、数组、arguments对象,DOM、NodeList对象,Generator对象等

对于可遍历对象我们后续再说,在这里我们主要看一下扩展运算符的几个用法。

  • 用来做数组合并,当然也可以用来给数组中插入元素,我们之前的快速排序中使用过这个技巧。

    const arr1 = ['1','1','1','1']; const arr2 = ['2','2','2']; //连接数组的作用 let list = [...arr1,...arr2]; console.log(list);// ["1", "1", "1", "1", "2", "2", "2"]

    //连接数组并在相应位置插入元素 let list2 = [...arr1,'separate',...arr2]; console.log(list2); // ["1", "1", "1", "1", "separate", "2", "2", "2"] let list3 = [...arr1,...arr2,'end']; console.log(list3);// ["1", "1", "1", "1", "2", "2", "2", "end"] let list4 = ['start',...arr1,...arr2]; console.log(list4);// ["start", "1", "1", "1", "1", "2", "2", "2"]

  • 用来克隆数组,注意是深拷贝

    let arr1 = [1,2,3]; let arr2 = [4,5,6]; let arr3 = [...arr1,...arr2]; console.log(arr3); // [1,2,3,4,5,6]

    let arr4 = [...arr3]; arr4[0] = 'xxxxx'; console.log(arr3); // [1,2,3,4,5,6] console.log(arr4); // ['xxxxx',2,3,4,5,6]

  • **用来将伪数组转化为真数组,**这点在剩余参数中我们使用过。

    function fn (){ console.log(Array.from(arguments)); console.log([...arguments]); } fn(1,2,3); // [1,2,3] // [1,2,3]

  • 当然我们也可以在给函数传递参数时巧妙利用扩展运算符

    let fruits = ['banana','apple','orange']; let newFruits = ['peach','pear']; fruits.push(...newFruits); console.log(fruits);

    let time = [2019,10,23]; let date = new Date(...time); console.log(date);

六、ES6对象字面量增强写法

过去创建一个对象:

const name = 'xxx';
const age = 18;
const gender = 'man';
  
const obj = {
  name:name,
  age:age,
  gender:gender,
  getName(){
    console.log(this.name);
  }
};

ES6时当一个对象的属性是从外面获取的时候,可以采用下面这种方式:

const name = 'xxx';
const age = 18;
const gender = 'man';
  
const obj = {
  name,
  age,
  gender,
  getName(){
    console.log(this.name);
  }
};

第三部分

包含:[

1.for in 与 for of     2.Iterator接口

](www.yuque.com/andylm/uxcw…)

for in 与 for of

首先说一下我们的常用的**forEach循环,**其特点是不能停止与中断。(不能使用break与countinue.)

arr.forEach(function (element,index,array) {
   console.log(element,index,array);
})
  • for in 循环本质上是遍历对象上可枚举的属性,包括原型对象上的属性。

    let obj = { name:'xxx', age:18 } Object.prototype.xxx = 123; for(let k in obj) { console.log(k); }

image.png

  • **for in 循环的特点是针对key优先****注意:其遍历对象的顺序是无序的。**其不用来遍历数组主要有以下两种原因:

  • 因为其是key优先,所以如果我们给一个数组添加一个属性,很明显当我们遍历数组值时是不希望遍历该数组的,但是使用for in 又可遍历出该属性。

  • 我们无法遍历出一个数组中有空元素的情况如:[1,2,,3,,4];

    Array.prototype.des = function(){ return this[0]; }; let arr = ['god','gosh','jesus','christ']; arr.title = 'words'; for(let key in arr){ console.log(key); //取出的是索引 console.log(arr[key]); //取出的是值 }

image

  • **for of 的本质是用来遍历可迭代对象,也就是部署了Iterator接口的数据结构。包****Array、****Map、****Set、****String、**TypedArray、arguments 对象、NodeList对象,注意其不可以用来遍历对象

    Array.prototype.des = function(){ return this[0]; }; let arr = ['god','gosh','jesus','christ']; arr.title = 'words'; for(let key of arr){ console.log(key); //直接遍历的是值 }

image

遍历出数组值存在空的现象:

let arr = [1,,2,3,4];
 for(let v of arr) {
   console.log('v:'+v);
}

image.png

Iterator接口

这里重点我们需要学习一下Iterator接口。

我们知道 for of 循环可以用来遍历可迭代对象,那么什么是可迭代对象呢?我们在第一部分的时候说过扩展运算符的作用:把一个可遍历对象转为一个用逗号分隔的新的参数序列,相当于剩余参数的逆运算。这里的可遍历对象与可迭代对象是一回事。

可迭代对象就是部署了Iterator接口,或者是定义了[Symbol.iterator]方法的数据结构,在ES6中Iterator接口主要供for of消费。

那么什么又是[Symbol.iterator]呢?主要分为以下几方面:

  • **[Symbol.iterator]**是一个对象的属性,其对应了一个方法。
  • 调用这个方法会返回一个新的对象Array Iterator {}
  • 这个对象的原型对象上又有一个next()方法。
  • 当我们调用next()方法时会返回一个新的对象{value: Array(2), done: false}
  • 这个对象中存储了当前取出的数据和是否取完了的标记,未取完标记是false,取完了标记是true。

image

  • 这样调用一个对象的可迭代对象太过复杂,所以ES6为我们提供了一种可以方便调用可迭代对象的方法:entries()。我们可以通过arr.entries()得到一个新的可迭代对象,我们可以调用这个对象的next()方法,也可以使用for of 遍历这个可迭代对象。(这里其实还可以遍历:arr.keys(),arr.values(),我们在遍历字典时使用的比较多)。也就是说arr.entries();arr[Symbol.iterator]();返回的都是新的Array Iterator对象,二者等价。

    let arr = ['god','gosh','jesus','christ']; for(let key of arr.entries()){ console.log(key); }

image

也可以使用解构赋值:

let arr = ['god','gosh','jesus','christ'];
for(let [index,value] of arr.entries()){
   console.log((`第${index + 1}个单词是${value}`));
}

我们可以自己实现一个迭代器:

Array.prototype.myIterator = function(){
            let value;
            let index = 0;
            let done = false;
            let items = this;
            return {
                next(){
                    if(index < items.length) {
                        value = items[index];
                        index++;
                    }else {
                        value = undefined;
                        done = true;
                    }
                    return {
                        value,
                        done
                    }
                }
            }
        }
        let arr = [1,2,3];
        let objIterator = arr.myIterator();
        console.log(objIterator.next());
        console.log(objIterator.next());
        console.log(objIterator.next());
        console.log(objIterator.next());

总结一下:

  • for in 与 for of 的本质区别在于,一个是用来遍历对象上可枚举的属性的,一个是用来遍历可迭代对象要迭代的数据

  • 可枚举属性包括其自身的属性,也包括其原型上自己添加的属性

  • 可迭代对象就是部署了迭代器的对象

第四部分

包含:[

1.Promise的基本使用与Promsie.all的一个bug修复    2.Symbol数据类型

](www.yuque.com/andylm/uxcw…)

一、Promise

Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值。

promise是ES6非常重要的一个API我们要重点掌握,官方对其的描述:

Promise构造函数执行时立即调用`executor`函数, `resolve` 和 `reject` 两个函数作为参数传递给`executor`(executor 函数在Promise构造函数返回所建promise实例对象前被调用)。`resolve` 和 `reject` 函数被调用时,分别将promise的状态改为fulfilled(完成)或rejected(失败)。executor 内部通常会执行一些异步操作,一旦异步操作执行完毕(可能成功/失败),要么调用resolve函数来将promise状态改成fulfilled,要么调用`reject` 函数将promise的状态改为rejected。如果在executor函数中抛出一个错误,那么该promise 状态为rejected。executor函数的返回值被忽略。

基本概念:

ES6中一种异步编程解决方案,用同步流程解决异步操作,有如下两个作用:

  • 解决异步回调顺序不确定的问题。

  • 解决回调地狱的问题(函数作为参数层层嵌套)

    new Promise((resolve,reject) => { if(xxxx) { resolve('resolve'); }else{ reject('reject'); } })

基本使用:

我们知道Promise接收两个参数,resolve,reject,其有三种状态**Pending,resolved,rejected,**当任何方法都不执行时其是Pending状态(未完成),

  • resolve('resolve')方法在异步操作成功时调用,并将异步操作的结果作为参数返回出去。之后其会变为resolved状态(完成)。

  • rejected('error')方法在异步操作失败时调用,并将异步操作的失败信息作为参数返回出去,之后其会变为rejected状态(失败)。

特点:

  • 一旦状态改变便不会再改变。

Promise的常用方法及作用:

Promise.then()与Promise.catch()

  • 其接收两个回调函数,第一个回调函数中接收resolve传递过来的参数,第二个回调函数用来捕获错误,接收reject传递过来的错误信息。

  • 我们在开发时一般不会使用.then的其第二个回调函数,而是使用catch来代捕获错误,这样.then就专门用来接收请求成功的信息,而.catch专门用来捕获求失败的信息。我们知道其实catch是then的一种语法糖.then(undefined,(error) => {}) === .catch((error) => {});

  • then会返回一个新的Promise对象,新的Promise对象后还可以跟一个新的then方法,我们可以在then的返回值中给后面的then方法传递参数,如果then捕获了这个错误那么后面的Promise状态便是resolved,也就是后面的then方法会在resolve回调中接收到数据。

  • 如果then方法没有捕获错误那么这个错误会一直向后传递,直到被捕获。

  • Promise.then()与Promise.catch()属于异步微任务(这需要用到事件循环与任务队列的知识,在后面再讲)。

    let promise = new Promise(function (resolve, reject) { console.log(1111); // resolve('请求成功'); reject('请求失败'); }); // 返回一个新的promise对象 let promise2 = promise.then(function (data) { console.log(data); }, function (data) {

            console.log(data);
            // 这里也可以返回一个promise对象
            return 'aaa';
    
        })
        // 捕获错误在下一个promise成功的函数中接收数据
        promise2.then(function (data) {
    
            console.log("请求成功" + data);
            // catch也可以用来捕获上一个then的错误
            xxxx; // 这段代码属于创造一个错误
    
        }).catch(function (data) {
            console.log("请求失败" + data);
        })
        console.log(promise2);
        console.log(promise);
    

image.png

通过这个案例我们应该可以明白Promise中的代码在同步队列中执行,而then会被放到异步微任务中。

Promise.all()****:

Promise.all()应该是面试中最常考的Promise问题之一。其用法如下:

  • 其可以以数组的形式接收多个Promise对象,Promise.all([p1,p2,p3]),之后其会返回一个新的Promise对象

  • 新对象的状态由Promise.all接收的多个对象决定,如果这些对象的状态都为resolved那么返回新的Promise对象的状态也为resolved,请求成功的数据可以在.then中同样以数组的形式得到,.then([p1,p2,p3])

  • 如果这些对象中有一个的状态是失败,那么Promise.all返回对象的状态也是失败,注意如果有多个Promise对象的状态都是失败,那么这里只会得到第一个失败的信息。

    let promise = new Promise(function (resolve, reject) { console.log(1111); resolve('请求成功1'); // reject('请求失败1'); }); let promise2 = new Promise(function (resolve, reject) { console.log(1111); // resolve('请求成功2'); reject('请求失败2'); });

        let p1 = promise.then(function (data) {
            console.log(data);
            return 'aaa';
        })
    
    
        let p2 = promise2.then(function (data) {
            console.log(data);
            return 'bbb';
        }).catch((err) => {
            console.log(err);
        })
    
        let p3 = Promise.all([p1, p2]);
        p3.then(function ([p1, p2]) {
            console.log(p1 + 'all');
            console.log(p2 + 'all');
    
        }).catch(function (error) {
            console.log(error + 'all');
        })
        // promise.all也可以返回一个新的promise对象
        console.log(p3);
    

image.png

注意:这里执行的顺序是先执行同步任务,之后执行异步微任务,之后是异步宏任务(任务队列的执行顺序是先执行同步任务,将异步任务放到异步进程中等待触发,异步任务又分为异步微任务与异步宏任务,当同步任务执行完后先去查找所有的异步微任务队列,清空后查找一个宏任务队列将其清空,再查找所有的异步微任务队列,如此循环。)

image

关于Promise.all()的一个bug:

既然我们使用一个新的标题来说明这个Bug那么便说明这个问题非常重要。

从上面Promsie.all的使用中我们可以发现,如果Promise.all接收的任意一个Promise对象的状态为rejected那么Promise.all的状态也变为了rejected,我们无法得知其他的情况,假设当我们通过异步请求多个数据时,如果有多个请求其中一个失败的话我们还是希望请求可以继续下去得到其他的结果。

首先我们来看一下全部resolve的结果:

let p1 = new Promise(function(resolve,reject){
            resolve('p1:resolve')
        })
        let p2 = new Promise(function(resolve,reject){
            // reject('p2:reject');
            resolve('p2:resolve')
        })
        let p3 = new Promise(function(resolve,reject){
            resolve('p3:resolve');
        })
        let p4 = Promise.all([p1,p2,p3]);
        p4.then(([p1,p2,p3]) => {
            console.log(p1);
            console.log(p2);
            console.log(p3);
        }).catch(err => {
            console.log(err);
        })

image.png

有错误的情况:

let p1 = new Promise(function(resolve,reject){
            resolve('p1:resolve')
        })
        let p2 = new Promise(function(resolve,reject){
            reject('p2:reject');
            // resolve('p2:resolve')
        })
        let p3 = new Promise(function(resolve,reject){
            resolve('p3:resolve');
        })
        let p4 = Promise.all([p1,p2,p3]);
        p4.then(([p1,p2,p3]) => {
            console.log(p1);
            console.log(p2);
            console.log(p3);
        }).catch(err => {
            console.log(err);
        })

image.png

为了在存在reject的情况下得到其他Promise的结果我们需要对我们的代码进行改造:

  • 我们在向Promise.all中传入对象的时候对所有的对象使用catch方法进行一次错误捕获,并将错误值返回出去,这样我们在then中得到的永远是全部成功的回调,我们可以根据不同的回调数据来进行相应的处理,甚至我们还可以创建一个变量记录错误请求的次数。

        let p1 = new Promise(function(resolve,reject){
            resolve('p1:resolve')
        })
        let p2 = new Promise(function(resolve,reject){
            reject('p2:reject');
            // resolve('p2:resolve')
        })
        let p3 = new Promise(function(resolve,reject){
            resolve('p3:resolve');
        })
        let p4 = new Promise(function(resolve,reject){
            reject('p4:resolve');
        })
        let count = 0;
        let countRejected = 0;
        let p5 = Promise.all([p1,p2,p3,p4].map((promiseItem) => {
            count++;
            return promiseItem.catch((err) => {
                countRejected++;
                return err;
            })
        }));
        p5.then(([p1,p2,p3,p4]) => {
            console.log(p1);
            console.log(p2);
            console.log(p3);
            console.log(p4);
            console.log('总共有'+count+'个请求,其中失败的有'+countRejected+'个');
        }).catch(err => {
            console.log(err);
        })
    

image.png

  • Promise.race():

其和Promise.all一样可以接收多个Promise对象组成的数组,当哪一个Promise对象最先改变,便在.then的回调中接收对应的Promies对象传递过来参数。

  • Promise.finally():

其使用非常简单,就是不管Promise是啥状态总会执行。

        let p1 = new Promise(function(resolve,reject){
            reject('xxxxxxxx')
        })
        p1.then((data) => {
            console.log(data);
        }).catch((err) => {
            console.log(err);
        }).finally(function(){
            console.log('finally');
        })

image.png

这样关于Promsie的基本使用就说完了,除此之外我们还需了解一下Promise的原理以及Promsie.all的实现原理,这在后续我们再说。

二、Symbol

Symbole属于一种基本数据类型,我们所知的七种数据类型有:数值,字符串,布尔,undefined,null,Symbol(基本数据类型)Object(引用数据类型)。

  • 作用:用来表示一个独一无二的值,用来解决对象属性名重复的问题。

    const peter = Symbol(); console.log(peter); //Symbol() console.log(typeof peter); //symbol

    const student = Symbol(); console.log(student === peter); //false

  • 我们可以在括号中添加描述,其仅仅是作为标记的作用。

    const peter = Symbol('peter'); console.log(peter); //Symbol(peter)

    const student = Symbol('student'); console.log(student); //Symbol(student)

  • 当我们使用Symbol作为对象的key的时候,对象的每一个key都是独一无二的,也就是说对象的每一个value值都不会因为相同的属性名而覆盖。

                let name = Symbol("name");
        let age = Symbol("age");
        let obj = {
            [name]: 'lm',
            [age]: 19,
            gender: 'man',
            hobby: "play"
        }
        obj[Symbol('name')] = "zs";
        console.log(obj);
    

image.png

  • 使用Symbol作为对象的属性名的时候,其对应的值无法被for in (for in 用来遍历对象上可枚举的属性)遍历出,也就是说其是不支持遍历的。但我们还是可以通过属性名来调用属性值的。

                let name = Symbol("name");
        let age = Symbol("age");
        let obj = {
            [name]: 'lm',
            [age]: 19,
            gender: 'man',
            hobby: "play"
        }
        console.log(obj[name]);
    
  • 我们可以通过Object.getOwnPropertySymbols()来获得某一对象上所有的Symbol类型。该方法会返回一个数组,数组中便是对应的Symbol。

        let name = Symbol("name");
        let age = Symbol("age");
        let obj = {
            [name]: 'lm',
            [age]: 19,
            gender: 'man',
            hobby: "play"
        }
        obj[Symbol('name')] = "zs";
        let arr = Object.getOwnPropertySymbols(obj);
        console.log(arr);
        console.log(arr[0]);
        console.log(obj[arr[0]]);
    

image.png

  • 最后补充一下,我们可以知道如果是一个数值类型的值,其是可以被转换为字符与布尔类型的,例如0,其可以是'0',也可以是false,那么Symbol也是可以转换的。但是其只可以转换为字符与布尔类型。

        let str = Symbol('str');
        console.log(typeof str);
        console.log(String(str));
        console.log(Boolean(str));
        // 不可以转化为数值型
        console.log(Number(str));
    

image.png

总结一下Symbol这个基本数据类型的特点:

  1. 其是用来表示一个独一无二值的,常常被用来作为解决对象中属性名重复时属性值被覆盖的情况

  2. 我们不能使用for in遍历对象时将其遍历出

  3. 我们可以使用如obj[Symbol()]的形式调用对象的属性值

  4. 我们可以通过Object.getOwnPropertySymbols()方法来获得某一对象上所有的Symbol,其会返回一个数组

  5. 我们可以将Symbol转换为字符型以及布尔型

第五部分

包含:1.ES6类的基本使用     2.类之间的继承

ES6类的基本使用:

  • ES6之前用构造函数来定义一个类,实例方法与属性直接写在构造函数里便好。静态方法与属性写在构造函数外面。

    function Person(myName,myAge){ // 实例属性与实例方法 this.myName = myName; this.myAge = myAge; this.say = function(){ console.log(this.myName,this.myAge,'hey'); } } // 静态属性与静态方法 Person.id = '#888888'; Person.play = function(){ console.log('paly'); } // 调用静态方法与属性 console.log(Person.id); Person.play(); // 调用实例方法与属性 let xm = new Person('xiaoming',18); console.log(xm.myAge); console.log(xm.myName); xm.say(); // 调用实例方法访问实例属性

image.png

  • **ES6引入了类这个概念,作为对象的模板,其是ES5的语法糖,**其中的绝大部分功能ES5都已实现。

  • ES6的类是一种特殊的函数,但其不具有变量提升

    class Person{

    } console.log(typeof Person); // function

  • ES6将实例属性写在constructor里,实例方法直接写在class中(只要写在class中的都是实例属性与方法)。

  • 静态属性与方法需要在定义前加static,也可以直接写在外面

      class Person{
        constructor(myName,myAge){
          this.myName = myName;
          this.myAge = myAge;
        }
        say(){
          Person.play(); // 调用静态方法
          console.log(this.myName,this.myAge,'say'); // 在实例方法中调用实例属性
        }
        static play(){
          console.log('play');
        }
      }
    Person.xxx = "xxxxx";
    let xm = new Person('xiaoming',18);
    console.log(xm.myAge);
    xm.say();
    Person.play();
    console.log(Person.id);
    console.log(Person.xxx);
    console.log(xm.xxx);
    

image.png

注意:

  • 实例属性写在constructor中

  • static关键字属于静态方法,ES6明确规定在class内部没有静态属性

  • 实例方法写在construcor外面,默认是添加在原型上

    class Point { constructor(){ // ... }

    toString(){ // ... }

    toValue(){ // ... } }

    // 等同于 Point.prototype = { toString(){}, toValue(){} };

ES5的继承:

首先我们要明确我们要继承父构造函数的什么?我们继承的有两个点:

  • 父构造函数中的属性
  • 父原型对象上的方法

首先对于父构造函数中的属性我们使用call方法巧妙的在子构造函数中执行一遍父构造函数就好了。

        function Father(faName){
            this.faName = faName;
        }
        Father.prototype.say = function(){
            console.log('我是爸爸');
        }
        function Son(faName,myName){
            Father.call(this,faName);
            this.myName = myName
        }
        let son1 = new Son('father','son');
        console.log(son1.faName);
        console.log(son1.myName);

image.png

继承第一版:

我们知道某一实例对象查找某一属性与方法时,首先会在自身上查找(相当于其构造函数的内部),之后会沿着原型链.__prop__去构造函数的原型对象上查找,然后再沿着原型链查找直到找到空为止,那么我们只需要让Son.prototype._

proto_

= Father.prototype便好。

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}

Cat.prototype.__proto__ = Animal.prototype

注意:instanceof的原理是如果A沿着原型链A.__proto__可以找到B.prototype那么A instanceof B为true

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}

Cat.prototype.__proto__ = Animal.prototype

// 添加 eat 函数
Animal.prototype.eat = function(){
    console.log('eat')
}

var cat = new Cat('Tom')
// 查看 name 属性是否成功挂载到 cat 对象上
console.log(cat.name) // Tom
// 查看是否能访问到 eat 函数
cat.eat() // eat 
// 查看 Animal.prototype 是否位于原型链上
console.log(cat instanceof Animal) // true
// 查看 Cat.prototype 是否位于原型链上
console.log(cat instanceof Cat) //true

直接使用__proto__来改变原型链非常方便但是其有两个缺点:

  • 比较消耗性能

  • __proto__在ES6才作为规范,存在兼容性问题,不推荐使用

继承第二版:

我们知道一个构造函数的实例对象其.__proto__属性指向的是其构造函数的prototype,我让Son.prototype = new Father()便好。

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat

我们知道每一个构造函数的原型对象上都有一个constructor属性其指向的是构造函数,所以当我们让Cat.prototype = new Animal()时,其constructor属性被覆盖,指向了Animal所以我们需要让其指回Cat。

但这样的缺点也很明显,我们需要多执行一次父构造函数,父构造函数中的属性会绑定到实例对象上,我们需要的是父原型对象上的方法,这样会造成子原型对象上的属性混乱。

继承第三版:

我们的解决办法也非常简单,我们需要使用一个临时的构造函数,这个临时构造函数是空的,我们让我们的子构造函数的原型对象指向这个空构造函数的实例对象,我们只需要让这个实例对象沿着原型链指向的是父构造函数德尔原型对象便好,最后让我们子构造函数原型对象的constructor指回子构造函数便好。

直接看代码比较好理解:我们需要解决问题的本质是子实例对象沿着原型链可以找到父原型对象也就是

Son.prototype.__proto__ === Father.prototype

function Animal(name){
    this.name = name
}

function Cat(name){
    Animal.call(this,name)
}

function Func(){}
Func.prototype = Animal.prototype

Cat.prototype = new Func()
Cat.prototype.constructor = Cat

ES6继承:

ES6的基础反而简单很多:使用extends继承原型方法,在constructor中使用super继承实例属性(个人理解)。

class Father{
            constructor(name){
                this.name = name;
            }
            say(){
                console.log('我是爸爸');
            }
        }
        class Son extends Father{
            constructor(faName,myName){
                super(faName); // 继承实例属性
                this.myName = myName;
            }
            mySay(){
              super.say(); // 在子实例方法里调用父实例方法
              console.log('mySay');
            }
        }
        let son1 = new Son('father','son');
        console.log(son1.name);
        console.log(son1.myName);
        son1.say();
                son1.mySay();

image.png

总结一下:

  • 不管是ES5还是ES6要继承的永远是两部分,实例属性与原型方法,实例属性ES5使用call改变this指向且调用父构造函数来完成,原型方法通过使用一个中间构造函数来完成
  • ES6原型方法使用extends来完成,实例属性通过super来完成。(只是为了记忆,实际中两者缺一不可)