[面试4]--JS下

110 阅读39分钟

this

1.先搞清楚函数的调用问题

var obj = {
    hi: function(){
        console.log(this);
        return ()=>{
            console.log(this);
        }
    },
    sayHi: function(){
        return function() {
            console.log(this);
            return ()=>{
                console.log(this);
            }
        }
    }
}


let hi = obj.hi();  //如果函数后面有括号,则调用,也就是说这里执行的时obj.hi()
                    //然后把函数的返回值赋给hi 
                                       
hi();               //输出obj对象 这里执行的时obj.hi()的返回值
                    //也就是这一段代码  ()=>{
                                            console.log(this);
                                        }
                                                       
let sayHi = obj.sayHi();//这里没有输出,因为obj.sayHi()函数里面没有语句只有返回值
                        //相当于把obj.sayHi()的返回值赋给sayHi
                         function() {
                            console.log(this);
                            return ()=>{
                                    console.log(this);
                                        }
                                    }

let fun1 = sayHi(); //先调用一次sayHi函数,再把sayHi()的返回值赋给fun1,相当于这段代码
                            ()=>{
                                    console.log(this);
                                }

注意:let hi = obj.hi;//如果函数后面没有括号,则不执行,相当于把obj.hi这个函数换个名字叫hi

2. this指向

this 就是一个指针,指向调用函数的对象

2.1 默认绑定

在调用Hi()时,应用了默认绑定,this指向全局对象(非严格模式下),严格模式下,this指向undefined,undefined上没有this对象,会抛出错误。

function sayHi(){
    console.log('Hello,', this.name);
}
var name = 'YvetteLau';
sayHi();//结果是Hello,YvetteLau

2.2 隐式绑定

XX.fn()

是person调用的函数 ,this指向person

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
person.sayHi();//Hello,YvetteLau.  

只有最后一层会确定this指向的是什么,不管有多少层,在判断this的时候,我们只关注最后一层,即此处的friend

function sayHi(){
    console.log('Hello,', this.name);
}
var person2 = {
    name: 'Christina',
    sayHi: sayHi
}
var person1 = {
    name: 'YvetteLau',
    friend: person2
}
person1.friend.sayHi();//Hello, Christina.

绑定丢失: 这里只是将person.sayHi赋给了Hi,其实跟person就一点关系都没有了,真正调用的是默认绑定

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = person.sayHi;
Hi();       //Hello,Wiliam.

绑定丢失也可能发生在回调中

function sayHi(){
    console.log('Hello,', this.name);
}
var person1 = {
    name: 'YvetteLau',
    sayHi: function(){
        setTimeout(function(){
            console.log('Hello,',this.name);
        })
    }
}
var person2 = {
    name: 'Christina',
    sayHi: sayHi
}
var name='Wiliam';
person1.sayHi();
setTimeout(person2.sayHi,100);
setTimeout(function(){
    person2.sayHi();
},200);

结果为

Hello, Wiliam
Hello, Wiliam
Hello, Christina

  • 1.输出很容易理解,setTimeout的回调函数中,this使用的是默认绑定,非严格模式下,执行的是全局对象
  • 2.setTimeout(fn,delay){ fn(); },相当于是将person2.sayHi赋值给了一个变量,最后执行了变量,这个时候,sayHi中的this显然和person2就没有关系了。
  • 3.虽然也是在setTimeout的回调中,但是我们可以看出,这是执行的是person2.sayHi()使用的是隐式绑定,因此这是this指向的是person2,跟当前的作用域没有任何关系

2.3 显式绑定

call,apply和bind的第一个参数,就是对应函数的this所指向的对象

使用硬绑定明确将this绑定在了person上

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = person.sayHi;
Hi.call(person); //Hi.apply(person)    结果是Hello, YvetteLau

显式绑定也会发生绑定丢失

虽然call把person绑定在Hi上,但是在执行fn的时候,相当于直接调用了sayHi方法(记住: person.sayHi已经被赋值给fn了,隐式绑定也丢了),没有指定this的值,对应的是默认绑定

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
    fn();
}
Hi.call(person, person.sayHi);   //Hello, Wiliam.

现在,我们希望绑定不会丢失,要怎么做?很简单,调用fn的时候,也给它硬绑定

因为person被绑定到Hi函数中的this上,fn又将这个对象绑定给了sayHi的函数。这时,sayHi中的this指向的就是person对象

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'YvetteLau',
    sayHi: sayHi
}
var name = 'Wiliam';
var Hi = function(fn) {
    fn.call(this);
}
Hi.call(person, person.sayHi); //Hello, YvetteLau

2.4 new绑定

使用new来调用函数的时候,就会新对象绑定到这个函数的this上

function _new() {
    let target = {}; //创建的新对象
    //第一个参数是构造函数
    let [constructor, ...args] = [...arguments];
    //执行[[原型]]连接;target 是 constructor 的实例
    target.__proto__ = constructor.prototype;
    //执行构造函数,将属性或方法添加到创建的空对象上
    let result = constructor.apply(target, args);
    if (result && (typeof (result) == "object" || typeof (result) == "function")) {
        //如果构造函数执行的结构返回的是一个对象,那么返回这个对象
        return result;
    }
    //如果构造函数返回的不是一个对象,返回创建的新对象
    return target;
}

在var Hi = new sayHi('Yevtte');这一步,会将sayHi中的this绑定到Hi对象上。

function sayHi(name){
    this.name = name;
	
}
var Hi = new sayHi('Yevtte');
console.log('Hello,', Hi.name);//Hello, Yevtte

2.5 绑定优先级

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

2.6 箭头函数

箭头函数没有自己的this,它的this继承于外层代码库中的this

var obj = {
    hi: function(){
        console.log(this);
        return ()=>{
            console.log(this);
        }
    },
    sayHi: function(){
        return function() {
            console.log(this);
            return ()=>{
                console.log(this);
            }
        }
    },
    say: ()=>{
        console.log(this);
    }
}
let hi = obj.hi();  //输出obj对象
hi();               //输出obj对象
let sayHi = obj.sayHi();
let fun1 = sayHi(); //输出window
fun1();             //输出window
obj.say();          //输出window

    1. obj.hi(); 对应了this的隐式绑定规则,this绑定在obj上,所以输出obj
    1. hi(); 这一步执行的就是箭头函数,箭头函数继承上一个代码库的this,刚刚我们得出上一层的this是obj,显然这里的this就是obj.
    1. 执行sayHi();前面说过这种隐式绑定丢失的情况,这个时候this执行的是默认绑定,this指向的是全局对象window.
    1. fun1(); 这一步执行的是箭头函数,按照箭头函数的this是继承于外层代码库的this,this指向的是window,因此这儿的输出结果是window.
    1. obj.say(); 执行的是箭头函数,当前的代码块obj中是不存在this的,只能往上找,就找到了全局的this,指向的是window

3. call/apply/bind

apply、call、bind 都是可以改变 this 的指向的,但是这三个函数稍有不同

3.1 apply 和 call 的区别

  • apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。

  • call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。

    var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }

    var b = a.fn;
    b.apply(a,[1,2])     // 3
    var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }

    var b = a.fn;
    b.call(a,1,2)       // 3

3.2 bind

bind()方法创建一个新的函数, 当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列

    var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }

    var b = a.fn;
    b.bind(a,1,2) //没有输出

bind 是创建一个新的函数,我们必须要手动去调用

    var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }

    var b = a.fn;
    b.bind(a,1,2)()           // 3

3.3 手写实现

3.3.1 call

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。

  • 处理传入的参数,截取第一个参数后的所有参数。

  • 将函数作为上下文对象的一个属性。

  • 使用上下文对象来调用这个方法,并保存返回结果。

  • 删除刚才新增的属性。

  • 返回结果。

Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
    result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

3.3.2 apply

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  • 判断传入上下文对象是否存在,如果不存在,则设置为 window 。

  • 将函数作为上下文对象的一个属性。

  • 判断参数值是否传入

  • 使用上下文对象来调用这个方法,并保存返回结果。

  • 删除刚才新增的属性

  • 返回结果

Function.prototype.myApply = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  let result = null;
  // 判断 context 是否存在,如果未传入则为 window
  context = context || window;
  // 将函数设为对象的方法
  context.fn = this;
  // 调用方法
  if (arguments[1]) {
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }
  // 将属性删除
  delete context.fn;
  return result;
};

3.3.3 bind

  • 判断调用对象是否为函数,即使是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况。

  • 保存当前函数的引用,获取其余传入参数值。

  • 创建一个函数返回

  • 函数内部使用 apply 来绑定函数调用,需要判断函数作为构造函数的情况,这个时候需要传入当前函数的 this 给 apply 调用,其余情况都传入指定的上下文对象。

Function.prototype.myBind = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 获取参数
  var args = [...arguments].slice(1),
    fn = this;
  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

对象与继承

1.创建对象的方式有哪些

js和一般的面向对象的语言不用,ES6之前没有类的概念,但是可以使用函数来进行模拟,从而产生可复用的对象创建方式

1.1 工厂模式

用函数来封装创建对象的细节,通过调用函数来达到复用的目的。

但是创建出来的对象无法和某个类型联系起来,只是简单的封装了复用代码,没有建立起对象和类型间的关系

1.2 构造函数模式

js中每一个函数都可以作为构造函数,只要一个函数是通过new来调用的,就可以称之为构造函数

执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。

因为 this 的值指向了新建的对象,因此可以使用 this 给对象赋值。

构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此可以通过原型来识别对象的类型。但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间

1.3 原型模式

利用函数的prototype属性,使用原型对象来添加公用属性和方法。

但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。

1.4 组合使用构造函数和原型

构造函数初始化对象的属性,原型对象实现函数方法的复用

1.5 动态原型模式

将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。

1.6 寄生构造函数模式

基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。

2. 继承

2.1 原型链继承

将父类实例作为子类原型

优点: 方法复用

缺点:创建子类实例的时候不能传参;子类实例共享了父类构造函数的引用属性(例如arr属性)

function Parent(name) {
    this.name = name || '⽗亲'; // 实例基本属性 (该属性,强调私有,不共享) 
    this.arr = [1]; // (该属性,强调私有)
}

Parent.prototype.say = function() { // -- 将需要复⽤、共享的⽅法定义在⽗类原型上
    console.log('hello')
}
function Child(like) { 
    this.like = like;
}
Child.prototype = new Parent() // 核⼼,但此时Child.prototype.constructor==Parent 

Child.prototype.constructor = Child // 修正constructor指向

let boy1 = new Child() 
let boy2 = new Child()

// 优点:共享了⽗类构造函数的say⽅法

// 缺点1:不能向⽗类构造函数传参

// 缺点2: ⼦类实例共享了⽗类构造函数的引⽤属性,⽐如arr属性

boy1.arr.push(2);

// 修改了boy1的arr属性,boy2的arr属性,也会变化,因为两个实例的原型上(Child.prototype)有了⽗类构造函数的实例属性arr; 
所以只要修改了boy1.arr,boy2.arr的属性也会变化

console.log(boy2.arr); // [1,2]

注意1:修改boy1的name属性,是不会影响到boy2.name。因为设置boy1.name相当于在⼦类实例新增了name属性。
注意2console.log(boy1.constructor); // Parent 你会发现实例的构造函数居然是Parent。
⽽实际上,我们希望⼦类实例的构造函数是Child,所以要记得修复构造函数指向。
修复如下:Child.prototype.constructor = Child;

2.2 借用构造函数

核⼼:借⽤⽗类的构造函数来增强⼦类实例,等于是复制⽗类的实例属性给⼦类。

优点:1.实例之间独⽴。

2.创建⼦类实例,可以向⽗类构造函数传参数。⼦类实例不共享⽗类构造函数的引⽤属性。如arr属性 

3.可实现多继承(通过多个call或者apply继承多个⽗类)

缺点:1.⽗类的⽅法不能复⽤;由于⽅法在⽗构造函数中定义,导致⽅法不能复⽤(因为每次创建⼦类实例都要创建⼀遍⽅法)。⽐如say⽅法。(⽅法应该要复⽤、共享)

2.⼦类实例,继承不了⽗类原型上的属性。(因为没有⽤到原型)

function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
    this.arr = [1]; // (该属性,强调私有)
    this.say = function() { // 实例引⽤属性 (该属性,强调复⽤,需要共享)
        console.log('hello')
    }
}
function Child(name,like) {
    Parent.call(this,name); // 核⼼ 拷⻉了⽗类的实例属性和⽅法
    this.like = like;
}

let boy1 = new Child(' ⼩ 红 ','apple'); 
let boy2 = new Child('⼩明', 'orange ');

// 优点1:可向⽗类构造函数传参

console.log(boy1.name, boy2.name); // ⼩红, ⼩明

// 优点2:不共享⽗类构造函数的引⽤属性

boy1.arr.push(2); 
console.log(boy1.arr,boy2.arr);// [1,2] [1]

// 缺点1:⽅法不能复⽤

console.log(boy1.say === boy2.say) // false (说明,boy1和boy2的say⽅法是独⽴,不是共享的)

// 缺点2:不能继承⽗类原型上的⽅法

Parent.prototype.walk = function () {    // 在⽗类的原型对象上定义⼀个walk⽅法。
    console.log('我会⾛路')
}

boy1.walk; // undefined (说明实例,不能获得⽗类原型上的⽅法)

2.3 组合继承

核⼼:通过调⽤⽗类构造函数,继承⽗类的属性并保留传参的优点;然后通过将⽗类实例作为⼦类原型,实现函数复⽤。

优点: 1.保留构造函数的优点:创建⼦类实例,可以向⽗类构造函数传参数。

2.保留原型链的优点:⽗类的⽅法定义在⽗类的原型对象上,可以实现⽅法复⽤。

3.不共享⽗类的引⽤属性。⽐如arr属性

缺点:由于调⽤了2次⽗类的构造⽅法,会存在⼀份多余的⽗类实例属性

注意:'组合继承'这种⽅式,要记得修复Child.prototype.constructor指向

function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
    this.arr = [1]; // (该属性,强调私有)
}

Parent.prototype.say = function() { // --- 将需要复⽤、共享的⽅法定义在⽗类原型上
    console.log('hello')
}

function Child(name,like) {
    Parent.call(this,name,like)   //核心   第一次
    this.like = like;
}

Child.prototype = new Parent() // 核⼼  第二次

Child.prototype.constructor = Child // 修正constructor指向

let boy1 = new Child('⼩红','apple')
let boy2 = new Child('⼩明','orange')

// 优点1:可以向⽗类构造函数传参数

console.log(boy1.name,boy1.like); // ⼩红,apple

// 优点2:可复⽤⽗类原型上的⽅法

console.log(boy1.say === boy2.say) // true

// 优点3:不共享⽗类的引⽤属性,如arr属性

boy1.arr.push(2)

console.log(boy1.arr,boy2.arr); // [1,2] [1] 可以看出没有共享arr属性。

// 缺点1:由于调⽤了2次⽗类的构造⽅法,会存在⼀份多余的⽗类实例属性

2.4 原型式继承(组合继承的优化)

核⼼

通过这种⽅式,砍掉⽗类的实例属性,这样在调⽤⽗类的构造函数的时候,就不会初始化两次实例,避免组合继承的缺点。

优点: 1.只调⽤⼀次⽗类构造函数。

2.保留构造函数的优点:创建⼦类实例,可以向⽗类构造函数传参数。

3.保留原型链的优点:⽗类的实例⽅法定义在⽗类的原型对象上,可以实现⽅法复⽤。

缺点

1.修正构造函数的指向之后,⽗类实例的构造函数指向,同时也发⽣变化(这是我们不希望的)

注意:'组合继承优化1'这种⽅式,要记得修复Child.prototype.constructor指向

原因是:不能判断⼦类实例的直接构造函数,到底是⼦类构造函数还是⽗类构造函数。

function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享) 
    this.arr = [1]; // (该属性,强调私有)
}

Parent.prototype.say = function() { // --- 将需要复⽤、共享的⽅法定义在⽗类原型上
    console.log('hello')
}

function Child(name,like) {
    Parent.call(this,name,like) // 核⼼
    this.like = like;
}

Child.prototype = Parent.prototype // 核⼼ ⼦类原型和⽗类原型,实质上是同⼀个

<!--这⾥是修复构造函数指向的代码-->

Child.prototype.constructor = Child

let boy1 = new Child('⼩红','apple')
let boy2 = new Child('⼩明','orange') 
let p1 = new Parent('⼩爸爸')

// 优点1:可以向⽗类构造函数传参数

console.log(boy1.name,boy1.like); // ⼩红,apple

// 优点2:可复⽤⽗类原型上的⽅法

console.log(boy1.say === boy2.say) // true

// 缺点1:当修复⼦类构造函数的指向后,⽗类实例的构造函数指向也会跟着变了。

没修复之前:console.log(boy1.constructor); // Parent
修复代码:Child.prototype.constructor = Child
修复之后 :console.log(boy1.constructor); // 
            Child console.log(p1.constructor);// Child 这⾥就是存在的问题(我们希望是Parent)
                        
具体原因:因为是通过原型来实现继承的,Child.prototype的上⾯是没有constructor属性的, 
就会往上找,这样就找到了Parent.prototype上⾯的constructor属性;
当你修改了⼦类实例的construtor属性,所有的constructor的指向都会发⽣变化。

2.5 寄生组合继承--完美方式

function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享) 
    this.arr = [1]; // (该属性,强调私有)
}

Parent.prototype.say = function() { // --- 将需要复⽤、共享的⽅法定义在⽗类原型上
    console.log('hello')
}

function Child(name,like) { 
    Parent.call(this,name,like) // 核⼼this.like = like;
}

// 核⼼ 通过创建中间对象,⼦类原型和⽗类原型,就会隔离开。不是同⼀个啦,有效避免了⽅式4的缺点。

Child.prototype = Object.create(Parent.prototype)

// 这⾥是修复构造函数指向的代码

Child.prototype.constructor = Child


let boy1 = new Child('⼩红','apple') 
let boy2 = new Child('⼩明','orange') 
let p1 = new Parent('⼩爸爸')

注意:这种⽅法也要修复构造函数的
修复代码:Child.prototype.constructor = Child
修复之后:console.log(boy1.constructor); // Child
console.log(p1.constructor); // Parent 完美

异步

1. 同步和异步

同步:执行某段代码时,在没有得到返回结果之前,其他代码暂时无法执行。也就是说,在此段代码执行完未返回结果之前,会阻塞之后的代码执行

异步:某一代码执行异步过程调用发出后,这段代码不会立刻得到返回结果,而是在异步调用发出之后,一般通过回调函数处理这个调用之后拿到结果。异步调用发出后,不会阻塞后面代码的执行

那为什么单线程的JavaScript还能实现异步呢?其实只是把一些操作交给了其他线程处理,然后采用了事件循环的机制来处理返回结果

2. 回调函数

最基本的层面上,JS的异步编程是通过回调实现的,回调的是函数,可以传给其他函数,而其他函数会在满足某个条件时调用这个函数。

2.1 定时器

setTimeout(asyncAdd(1, 2), 8000)

setTimeout()方法的第一个参数是一个函数,第二个参数是以毫秒为单位的时间间隔。asyncAdd()方法可能是一个回调函数,而setTimeout()方法就是注册回调函数的函数。它还代指在什么异步条件下调用回调函数。setTimeout()方法只会调用一次回调函数

2.2 事件监听

document.getElementById('#myDiv').addEventListener('click', (e) => {
  console.log('我被点击了')
}, false);

这里使用addEventListener注册了回调函数,这个方法的第一个参数是一个字符串,指定要注册的事件类型,如果用户点击了指定的元素,浏览器就会调用回调函数,并给他传入一个对象,其中包含着事件的详细信息。

这里(e)=> 的箭头函数就是回调函数

2.3 网络请求

const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", SERVER_URL, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);       //回调函数
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 发送 Http 请求
xhr.send(null);

2.4 Node中的回调与事件

const fs = require('fs');
let options = {}

//  读取配置文件,调用回调函数
fs.readFile('config.json', 'utf8', (err, data) => {
    if(err) {
      throw err;
    }else{
    	Object.assign(options, JSON.parse(data))
    }
		startProgram(options)
});

fs.readFile()方法以接收两个参数的回调函数作为最后一个参数。它会异步读取指定文件,如果读取成功就会将第二个参数传递给回调的第二个参数,如果发生错误,就会将错误传递给回调的第一个参数

3.Promise

3.1 Promise的概念

Promise是一个对象,表示异步操作的结果,可以让嵌套回调以一种更线性的链式形式表达出来

Promise标准化了异步错误处理,提供一种让错误正确传播的途径

实际上,Promise就是一个容器,里面保存着某个未来才会结束的事件(通常是异步操作)的结果。从语法上说,Promise 是一个对象,它可以获取异步操作的消息。Promise 提供了统一的 API,各种异步操作都可以用同样的方法进行处理

3.1.1 三个状态

  • pending 状态:表示进行中。Promise 实例创建后的初始态

  • fulfilled 状态:表示成功完成。在执行器中调用 resolve 后达成的状态

  • rejected 状态:表示操作失败。在执行器中调用 reject 后达成的状态

3.1.2 两个过程

  • pending -> fulfilled : Resolved(已完成)

  • pending -> rejected:Rejected(已拒绝)

3.1.3 特点

  • 一旦状态改变就不会再变,promise对象的状态改变,只有两种可能:从pending变为fulfilled,从pending变为rejected。当 Promise 实例被创建时,内部的代码就会立即被执行,而且无法从外部停止。比如无法取消超时或消耗性能的异步调用,容易导致资源的浪费;

  • 如果不设置回调函数,Promise内部抛出的错误,不会反映到外部;

  • Promise 处理的问题都是“一次性”的,因为一个 Promise 实例只能 resolve 或 reject 一次,所以面对某些需要持续响应的场景时就会变得力不从心。比如上传文件获取进度时,默认采用的就是事件监听的方式来实现

const https = require('https');

function httpPromise(url){
  return new Promise((resolve,reject) => {
    https.get(url, (res) => {//promise接受一个执行器,promise创建后执行器会立刻执行
      resolve(data);
    }).on("error", (err) => {
      reject(error);
    });
  })
}

httpPromise().then((data) => {//成功后走入then方法
  console.log(data)
}).catch((error) => {
  console.log(error)
})

3.2 创建promise

const promise = new Promise((resolve, reject) => {
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

function testPromise(ready) {
  return new Promise(resolve,reject) => {
    if(ready) {
      resolve("hello world");
    }else {
      reject("No thanks");
    }
  });
};

testPromise(true).then((msg) => {
  console.log(msg);
},(error) => {
  console.log(error);
});

上面的代码给testPromise方法传递一个参数,返回一个promise对象,如果为true,那么调用Promise对象中的resolve()方法,并且把其中的参数传递给后面的then第一个函数内,因此打印出 “hello world”, 如果为false,会调用promise对象中的reject()方法,则会进入then的第二个函数内,会打印No thanks

3.3 promise的作用

Promise 利用了三大技术手段来解决回调地狱:回调函数延迟绑定、返回值穿透、错误冒泡

let readFilePromise = (filename) => {
  fs.readFile(filename, (err, data) => {
    if(err) {
      reject(err);
    }else {
      resolve(data);
    }
  })
}
readFilePromise('1.json').then(data => {
  return readFilePromise('2.json')
});

回调函数不是直接声明的,而是通过后面的 then 方法传入的,即延迟传入,这就是回调函数延迟绑定
let x = readFilePromise('1.json').then(data => {
  return readFilePromise('2.json')  //这是返回的Promise
});
x.then()

根据 then 中回调函数的传入值创建不同类型的 Promise,
然后把返回的 Promise 穿透到外层,以供后续的调用。
这里的 x 指的就是内部返回的 Promise,然后在 x 后面可以依次完成链式调用。
这便是返回值穿透的效果
readFilePromise('1.json').then(data => {
    return readFilePromise('2.json');
}).then(data => {
    return readFilePromise('3.json');
}).then(data => {
    return readFilePromise('4.json');
}).catch(err => {
  // xxx
})

这样前面产生的错误会一直向后传递,被 catch 接收到,就不用频繁地检查错误了

3.4 promise的方法

3.4.1 then()

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

`then`方法接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为`resolved`时调用,
第二个回调函数是Promise对象的状态变为`rejected`时调用。其中第二个参数可以省略
let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
        resolve(res);
    })
})
promise.then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    
})

`then`方法返回的是一个新的Promise实例。因此可以采用链式写法
即`then`方法后面再调用另一个then方法。当写有顺序的异步事件时,需要串行时,可以这样写

3.4.2 catch()

p.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
}); 

catch方法相当于`then`方法的第二个参数,指向`reject`的回调函数
`catch`方法还有一个作用,就是在执行`resolve`回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入`catch`方法中

3.4.3 all()

let promise1 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(1);
	},2000)
});
let promise2 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject)=>{
	setTimeout(()=>{
       resolve(3);
	},3000)
});

Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);  //结果为:[1,2,3] 
})

`all`方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个`promise`对象
当数组中所有的`promise`的状态都达到`resolved`时,`all`方法的状态就会变成`resolved`
如果有一个状态变成了`rejected`,那么`all`方法的状态就会变成`rejected`

调用`all`方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象`resolve`执行时的值

3.4.4 race()

let promise1 = new Promise((resolve,reject) => {
	setTimeout(() =>  {
       reject(1);
	},2000)
});
let promise2 = new Promise((resolve,reject) => {
	setTimeout(() => {
       resolve(2);
	},1000)
});
let promise3 = new Promise((resolve,reject) => {
	setTimeout(() => {
       resolve(3);
	},3000)
});
Promise.race([promise1,promise2,promise3]).then(res => {
	console.log(res); //结果:2
},rej => {
    console.log(rej)};
)

`race`方法和`all`一样,接受的参数是一个每项都是`promise`的数组,
但与`all`不同的是,当最先执行完的事件执行完之后,就直接返回该`promise`对象的值

如果第一个`promise`对象状态变成`resolved`,那自身的状态变成了`resolved`;
反之,第一个`promise`变成`rejected`,那自身状态就会变成`rejected`

3.4.5 finally()

promise.then(result => {···})
			 .catch(error => {···})
       .finally(() => {···});

`finally`方法用于指定不管 Promise 对象最后状态如何,都会执行的操作
不管`promise`最后的状态如何,在执行完`then``catch`指定的回调函数以后,都会执行`finally`方法指定的回调函数

`finally`方法的回调函数不接受任何参数,这意味着没有办法知道前面的 Promise 状态到底是`fulfilled`还是`rejected`
这表明,`finally`方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果

3.4.6 allSettled()

const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
// 返回结果:
// [
//    { status: 'fulfilled', value: 2 },
//    { status: 'rejected', reason: -1 }
// ]

Promise.allSettled 的语法及参数跟 Promise.all 类似,其参数接受一个 Promise 的数组,返回一个新的 Promise
唯一的不同在于,执行完之后不会失败,也就是说当 Promise.allSettled 全部处理完成后,
我们可以拿到每个 Promise 的状态,而不管其是否处理成功

3.4.7 any()

const resolved = Promise.resolve(2);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.any([resolved, rejected]);
allSettledPromise.then(function (results) {
  console.log(results);
});
// 返回结果:2

any 方法返回一个 Promise,只要参数 Promise 实例有一个变成 fullfilled 状态,
最后 any 返回的实例就会变成 fullfilled 状态;
如果所有参数 Promise 实例都变成 rejected 状态,包装实例就会变成 rejected 状态

3.5 异常处理

const exe = (flag) => () => new Promise((resolve, reject) => {
    console.log(flag);
    setTimeout(() => {
        flag ? resolve("yes") : reject("no");
    }, 1000);
});
Promise.resolve()
       .then(exe(false))
       .then(exe(true));
上面的代码中,flag 参数用来控制流程是顺利执行还是发生错误
在错误发生的时候,no 字符串会被传递给 reject 函数,进一步传递给调用链
上面的调用链,在执行的时候,第二行就传入了参数 false,它就已经失败了,异常抛出了
因此第三行的 exe 实际没有得到执行

这就说明,通过这种方式,调用链被中断了,下一个正常逻辑 exe(true) 没有被执行
但是,有时候需要捕获错误,而继续执行后面的逻辑,该怎样做?--使用catch
Promise.resolve()
       .then(exe(false))
       .catch((info) => { console.log(info); })
       .then(exe(true));
这种方式下,异常信息被捕获并打印,而调用链的下一步,也就是第四行的 exe(true) 可以继续被执行

3.6 手写promise

3.6.1 promise

const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";

function MyPromise(fn) {
  // 保存初始化状态
  var self = this;

  // 初始化状态
  this.state = PENDING;

  // 用于保存 resolve 或者 rejected 传入的值
  this.value = null;

  // 用于保存 resolve 的回调函数
  this.resolvedCallbacks = [];

  // 用于保存 reject 的回调函数
  this.rejectedCallbacks = [];

  // 状态转变为 resolved 方法
  function resolve(value) {
    // 判断传入元素是否为 Promise 值,如果是,则状态改变必须等待前一个状态改变后再进行改变
    if (value instanceof MyPromise) {
      return value.then(resolve, reject);
    }

    // 保证代码的执行顺序为本轮事件循环的末尾
    setTimeout(() => {
      // 只有状态为 pending 时才能转变,
      if (self.state === PENDING) {
        // 修改状态
        self.state = RESOLVED;

        // 设置传入的值
        self.value = value;

        // 执行回调函数
        self.resolvedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 状态转变为 rejected 方法
  function reject(value) {
    // 保证代码的执行顺序为本轮事件循环的末尾
    setTimeout(() => {
      // 只有状态为 pending 时才能转变
      if (self.state === PENDING) {
        // 修改状态
        self.state = REJECTED;

        // 设置传入的值
        self.value = value;

        // 执行回调函数
        self.rejectedCallbacks.forEach(callback => {
          callback(value);
        });
      }
    }, 0);
  }

  // 将两个方法传入函数执行
  try {
    fn(resolve, reject);
  } catch (e) {
    // 遇到错误时,捕获错误,执行 reject 函数
    reject(e);
  }
}

MyPromise.prototype.then = function(onResolved, onRejected) {
  // 首先判断两个参数是否为函数类型,因为这两个参数是可选参数
  onResolved =
    typeof onResolved === "function"
      ? onResolved
      : function(value) {
          return value;
        };

  onRejected =
    typeof onRejected === "function"
      ? onRejected
      : function(error) {
          throw error;
        };

  // 如果是等待状态,则将函数加入对应列表中
  if (this.state === PENDING) {
    this.resolvedCallbacks.push(onResolved);
    this.rejectedCallbacks.push(onRejected);
  }

  // 如果状态已经凝固,则直接执行对应状态的函数

  if (this.state === RESOLVED) {
    onResolved(this.value);
  }

  if (this.state === REJECTED) {
    onRejected(this.value);
  }
};

3.6.2 Promise.then

then 方法返回一个新的 promise 实例,为了在 promise 状态发生变化时(resolve / reject 被调用时)再执行 then 里的函数,我们使用一个 callbacks 数组先把传给then的函数暂存起来,等状态改变时再调用。

那么,怎么保证后一个 then 里的方法在前一个 then(可能是异步)结束之后再执行呢?

可以将传给 then 的函数和新 promiseresolve 一起 push 到前一个 promisecallbacks 数组中,达到承前启后的效果:

  • 承前:当前一个 promise 完成后,调用其 resolve 变更状态,在这个 resolve 里会依次调用 callbacks 里的回调,这样就执行了 then 里的方法了
  • 启后:上一步中,当 then 里的方法执行完成后,返回一个结果,如果这个结果是个简单的值,就直接调用新 promiseresolve,让其状态变更,这又会依次调用新 promisecallbacks 数组里的方法,循环往复。。如果返回的结果是个 promise,则需要等它完成之后再触发新 promiseresolve,所以可以在其结果的 then 里调用新 promiseresolve
then(onFulfilled, onReject){
    // 保存前一个promise的this
    const self = this; 
    return new MyPromise((resolve, reject) => {
      // 封装前一个promise成功时执行的函数
      let fulfilled = () => {
        try{
          const result = onFulfilled(self.value); // 承前
          return result instanceof MyPromise? result.then(resolve, reject) : resolve(result); //启后
        }catch(err){
          reject(err)
        }
      }
      // 封装前一个promise失败时执行的函数
      let rejected = () => {
        try{
          const result = onReject(self.reason);
          return result instanceof MyPromise? result.then(resolve, reject) : reject(result);
        }catch(err){
          reject(err)
        }
      }
      switch(self.status){
        case PENDING: 
          self.onFulfilledCallbacks.push(fulfilled);
          self.onRejectedCallbacks.push(rejected);
          break;
        case FULFILLED:
          fulfilled();
          break;
        case REJECT:
          rejected();
          break;
      }
    })
   }

注意:

  • 连续多个 then 里的回调方法是同步注册的,但注册到了不同的 callbacks 数组中,因为每次 then 都返回新的 promise 实例(参考上面的例子和图)
  • 注册完成后开始执行构造函数中的异步事件,异步完成之后依次调用 callbacks 数组中提前注册的回调

4. Generator

4.1 概述

4.1.1 Generator

Generator 是一个带星号的函数(它并不是真正的函数),可以配合 yield 关键字来暂停或者执行函数

function* gen() {
  console.log("enter");
  let a = yield 1;
  let b = yield (function () {return 2})();
  return 3;
}
var g = gen()           // 阻塞,不会执行任何语句
console.log(typeof g)   // 返回 object 这里不是 "function"
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next()) 

Generator 中配合使用 yield 关键词可以控制函数执行的顺序,
每当执行一次 next 方法,Generator 函数会执行到下一个存在 yield 关键词的位置
  • 调用 gen() 后,程序会阻塞,不会执行任何语句;
  • 调用 g.next() 后,程序继续执行,直到遇到 yield 关键词时执行暂停;
  • 一直执行 next 方法,最后返回一个对象,其存在两个属性:value 和 done

4.1.2 yield

yield 关键词最后返回一个迭代器对象,该对象有 value 和 done 两个属性,其中 done 属性代表返回值以及是否完成。yield 配合着 Generator,再同时使用 next 方法,可以主动控制 Generator 执行进度

function* gen1() {
    yield 1;
    yield* gen2();
    yield 4;
}
function* gen2() {
    yield 2;
    yield 3;
}
var g = gen1();
console.log(g.next())//{ value: 1, done: false }
console.log(g.next())//{ value: 2, done: false }
console.log(g.next())//{ value: 3, done: false }
console.log(g.next())//{ value: 4, done: false }

4.1.3 生成器原理

其实,在生成器内部,如果遇到 yield 关键字,那么 V8 引擎将返回关键字后面的内容给外部,并暂停该生成器函数的执行。生成器暂停执行后,外部的代码便开始执行,外部代码如果想要恢复生成器的执行,可以使用 result.next 方法。

协程是—种比线程更加轻量级的存在。我们可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。比如,当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行; 同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。每一时刻,该线程只能执行其中某一个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

4.2 Generator 和 thunk 结合

let isType = (type) => {
  return (obj) => {
    return Object.prototype.toString.call(obj) === `[object ${type}]`;
  }
}
let isString = isType('String');
let isArray = isType('Array');
isString("123");    // true
isArray([1,2,3]);   // true

像 isType 这样的函数称为 thunk 函数,它的基本思路都是接收一定的参数,
会生产出定制化的函数,最后使用定制化的函数去完成想要实现的功能**
const readFileThunk = (filename) => {
  return (callback) => {
    fs.readFile(filename, callback);
  }
}
const gen = function* () {
  const data1 = yield readFileThunk('1.txt')
  console.log(data1.toString())
  const data2 = yield readFileThunk('2.txt')
  console.log(data2.toString)
}
let g = gen();
g.next().value((err, data1) => {
  g.next(data1).value((err, data2) => {
    g.next(data2);
  })
})
readFileThunk 就是一个 thunk 函数,上面的这种编程方式就让 Generator 和异步操作关联起来了
上面第三段代码执行起来嵌套的情况还算简单,如果任务多起来,就会产生很多层的嵌套,可读性不强
function run(gen){
  const next = (err, data) => {
    let res = gen.next(data);
    if(res.done) return;
    res.value(next);
  }
  next();
}
run(g);

run 函数和上面的执行效果其实是一样的。代码虽然只有几行,但其包含了递归的过程,
解决了多层嵌套的问题,并且完成了异步操作的一次性的执行效果。
这就是通过 thunk 函数完成异步操作的情况

4.3 Generator 和 Promise 结合

const readFilePromise = (filename) => {
  return new Promise((resolve, reject) => {
    fs.readFile(filename, (err, data) => {
      if(err) {
        reject(err);
      }else {
        resolve(data);
      }
    })
  }).then(res => res);
}
// 这块和上面 thunk 的方式一样
const gen = function* () {
  const data1 = yield readFilePromise('1.txt')
  console.log(data1.toString())
  const data2 = yield readFilePromise('2.txt')
  console.log(data2.toString)
}
// 这里和上面 thunk 的方式一样
function run(gen){
  const next = (err, data) => {
    let res = gen.next(data);
    if(res.done) return;
    res.value(next);
  }
  next();
}
run(g);

thunk 函数的方式和通过 Promise 方式执行效果本质上是一样的,
只不过通过 Promise 的方式也可以配合 Generator 函数实现同样的异步操作

5. Async/Await

5.1 概念

改进了生成器的缺点,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力。其实 async/await 是 Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的

从字面上来看,async是“异步”的简写,await则为等待,所以 async 用来声明异步函数,这个关键字可以用在函数声明、函数表达式、箭头函数和方法上。因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力,使用await关键字可以暂停异步代码的执行,等待Promise解决。async 关键字可以让函数具有异步特征,但总体上代码仍然是同步求值的

async function httpRequest() {
  let res1 = await httpPromise(url1)
  console.log(res1)
}

await关键字会接收一个期约并将其转化为一个返回值或一个抛出的异常
通过情况下,我们不会使用await来接收一个保存期约的变量,
更多的是把他放在一个会返回期约的函数调用面前,比如上述例子
这里的关键就是,await关键字并不会导致程序阻塞,代码仍然是异步的,
而await只是掩盖了这个事实,这就意味着任何使用await的代码本身都是异步的

async 函数返回的是 Promise 对象。如果异步函数使用return关键字返回了值(如果没有return则会返回undefined),这个值则会被 Promise.resolve() 包装成 Promise 对象。异步函数始终返回Promise对象

5.2 await的理解

function getSomething() {
    return "something";
}
async function testAsync() {
    return Promise.resolve("hello async");
}
async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}
test(); // something hello async

等的实际是一个返回值
await 不仅用于等 Promise 对象,它可以等任意表达式的结果
await 后面实际是可以接普通函数调用或者直接量的

await 表达式的运算结果取决于它等的是什么:

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的内容;
  • 如果它等到的是一个 Promise 对象,await 就会阻塞后面的代码,等着 Promise 对象 resolve,然后将得到的值作为 await 表达式的运算结果
function testAsy(x){
   return new Promise(resolve=>{setTimeout(() => {
       resolve(x);
     }, 3000)
    }
   )
}
async function testAwt(){    
  let result =  await testAsy('hello world');
  console.log(result);    // 3秒钟之后出现hello world
  console.log('cuger')   // 3秒钟之后出现cuger
}
testAwt();
console.log('cug')  //立即输出cug

这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,
它内部所有的阻塞都被封装在一个 Promise 对象中异步执行
await暂停当前async的执行,所以'cug'最先输出,hello world'和 cuger 是3秒钟后同时出现的

5.3 async/await的优势

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的理解负担;

  • Promise传递中间值很麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅;

  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获比较冗余;

  • 调试友好,Promise的调试很差,由于没有代码块,不能在⼀个返回表达式的箭头函数中设置断点,如果在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步

5.4 异常处理

const exe = (flag) => () => new Promise((resolve, reject) => {
    console.log(flag);
    setTimeout(() => {
        flag ? resolve("yes") : reject("no");
    }, 1000);
});
const run = async () => {
	try {
		await exe(false)();
		await exe(true)();
	} catch (e) {
		console.log(e);
	}
}
run();

这里定义一个异步方法 run,由于 await 后面需要直接跟 Promise 对象
因此通过额外的一个方法调用符号 () 把原有的 exe 方法内部的 Thunk 包装拆掉
即执行 exe(false)() 或 exe(true)() 返回的就是 Promise 对象
在 try 块之后,使用 catch 来捕捉。运行代码会得到这样的输出

false
no

这个 false 就是 exe 方法对入参的输出,而这个 no 就是 setTimeout 方法 reject 的回调返回
它通过异常捕获并最终在 catch 块中输出
就像我们所认识的同步代码一样,第四行的 exe(true) 并未得到执行

6.碎银几两

并发与并行的区别?

  • 并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
  • 并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。

什么是回调函数?回调函数有什么缺点?

  1. 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
  2. 嵌套函数一多,就很难处理错误

当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return

setTimeout、setInterval、requestAnimationFrame 各有什么特点?

总结

1.JS 异步编程进化史:callback -> promise -> generator -> async + await

2.async/await 函数的实现,就是将 Generator 函数和自动执行器,包装在一个函数里。

3.async/await可以说是异步终极解决方案了。

(1) async/await函数相对于Promise,优势体现在

  • 处理 then 的调用链,能够更清晰准确的写出代码
  • 并且也能优雅地解决回调地狱问题。

当然async/await函数也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低,代码没有依赖性的话,完全可以使用 Promise.all 的方式。

(2) async/await函数对 Generator 函数的改进,体现在以下三点

  • 内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行
  • 更广的适用性。 co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)
  • 更好的语义。 async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。

垃圾回收内存泄漏

1. 浏览器垃圾回收机制

1.1 垃圾回收概念

垃圾回收:当变量不在参与运行时,需要系统收回被占用的内存空间

回收机制

  • 自动垃圾回收机制,定期对不再使用的变量或对象占用的内存进行释放
  • 全局变量生命周期持续到页面卸载,局部变量声明在函数中,函数执行结束后即清除
  • 当局部变量被外部函数使用时,例如闭包,函数执行结束后不会回收

1.2 垃圾回收的方式

  • 标记清除:当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放

  • 引用计数: 是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。

1.3 减少垃圾回收

  • 对数组进行优化: 可以将数组长度设置为0,以此来达到清空数组的目的

  • 对object进行优化: 对象尽量复用,不在使用的对象就将其设置为null

  • 对函数进行优化: 循环中的函数表达式,如果可以复用,尽量放在函数的外面

2. 内存泄漏

  • 意外的全局变量:使用未声明的变量,默认为全局变量,导致一直在内存中无法回收

  • 遗忘的计时器或回调函数: 忘记取消计时器,或者循环函数有对外部变量的引用,都会一直留在内存中

  • 脱离DOM的引用: 获取一个DOM元素的引用,而后这个元素被删除,但是一直保留了对这个元素的引用

  • 闭包: 不合理的使用闭包