更新面试题系列三

66 阅读9分钟

1、什么是闭包?闭包的应用

1、闭包:指有权另访问另一个函数作用域中变量的函数;

2、形成原因:内部函数存在外部作用域的引用就会形成闭包;

3、闭包的作用:
   - 保护函数的私有变量不被外部干扰,形成不销毁的栈内存;
   - 保存一些函数内的值,闭包可以实现方法和属性的私有化;
   
4、闭包的使用:
   函数内部返回一个函数;
   自执行函数;
   函数作为参数;
   循环赋值。

2、原型和原型链

1、原型:
    原型是一个可以复制的类,是一个对象模板,原型定义了一些公用的属性和方法,利用原型创建出来的
新对象实例会共享原型的所有属性和方法;

2、原型对象:
    我们创建的每一个函数都有一个prototype属性,它指向一个对象。
    prototype就是通过该构造函数创建的某个实例的原型对象。
    好处:所有的对象实例都可以共享它包含的属性和方法。
    
3、原型链:
   每个对象都有一个__proto__的属性,指向该对象构造函数的原型。
   它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,就回去它的
   __proto__属性所指向的父对象去查找,直到找到终点null,这条查找的链路就是原型链。       

3、继承的方式

1、原型链继承
2、借用构造函数
3、组合继承
4、原型式继承
5、寄生式继承
6、寄生组合式继承
   这个继承方式式引用类型的理想继承方式:
   1、它使用超类型原型的副本作为子类型的原型;
   2、它的优点是只调用一次超类构造函数,避免创造多余的属性。
   

4、用原型实现一个new

new操作符创建了一个全新的对象:

function objectFactory(){
    const obj = new Object();
    const Constructor = [].shift.call(arguments);
    obj.__proto__ = Constructor.prototype;
    const ret = Constructor.apply(obj,arguments);
    return typeof ret === 'object' ? ret:obj;
}

5、class是为了解决什么问题出现的?

class作为对象的模板引入,可以通过class关键字定义类。
它的本质是一个函数,可以看作一个语法糖,让对象原型的写法更加清晰、更像面向对象编程的语法。
特点:
1、在class中声明方法,不需要function关键字;
2class存在暂时性死区,在声明之前不能调用;
3class默认有constructor、static方法;
4class构造时必须使用new关键字;
5class通过extends关键字实现类的继承;
6、通过super关键字进行拓展父类构造器或方法;
   子类使用构造器constructor的时候,必须使用super关键字,用来扩展构造器;
   子类同名方法会覆盖父类同名方法,使用super关键字后则可以调用到父类的同名构造函数。
7static关键字是类的方法,只能通过类名来调用,不能被实例对象调用,也可以被继承。

6、var\const\let的区别,还有哪个可以声明提前?

1var:变量提升机制
   在全局或者局部作用域中,使用var关键字声明的变量
,都会被提升到该作用域的最顶部。
2let声明:
   无变量提升,只在当前作用域中有效,是块级作用域;
   禁止重复声明,会报错;
3const声明
   声明常量,必须初始化值,一旦定义不能修改值;
   只在当前作用域有效,是块级作用域;
   不能重复定义,不存在变量提升;
   const定义对象,可以修改对象里的属性值,不能重写整个对象。
   
   原因:对象是引用数据类型,它的值是同时保存在栈内存和堆内存的对象,栈区保存了对象在堆区的地址。
   const声明的只是栈区内容不变,那么在栈区保存的引用数据类型的地址不可改变,对象就不能
   重写,可以修改对象的属性值,不影响栈区地址的变化。
   扩展:
   基本数据类型:stringnumberbooleannullundefined,基本数据类型的变量是保存在
   栈区中的,基本数据类型的值直接在栈内存中存储,值与值之间是独立存在的,修改一个变量不会影响其他的变量。 
   

7、js的事件循环

因为js是单线程的,每个线程都有一个事件循环,主线程上是同步任务,异步任务会被放入异步队列中,
而异步任务又分为宏任务和微任务。
执行顺序优先级是:同步任务>微任务>宏任务
常见的宏任务(macrotasks):
整体代码script、setTimeout、setInterval、setImmediate、I/O、UI renderingnew;
常见的微任务(microtasks):
promise.then(new promise的构造函数是同步任务)、process.nextTick、mutationObserver

8、setTimeout第二个参数是0,他在什么时间执行?

setTimeout是宏任务,它的执行在主线程和微任务之后;
var fuc = [1,2,3];
for(var i in fuc){
  setTimeout(function(){console.log(fuc[i])},0);
  console.log(fuc[i]);
}
chrome的打印结果:
1
2
3
3个3

9、promise的方法和实现原理?

promise的由来:
在js中要处理异步操作,经历了以下的过程:
多层回调函数——>解决回调地狱的promise——>generator——>async/await;
generator是基于promise来实现的,而async/await又是generator的语法糖;
promise返回resolve、reject回调;
实现方法:then、race、all;
三种状态:pending(未完成)、fulfilled(履行)、rejected(拒绝)
// promise/a+规定的三种状态

const PENDING = 'pending';//未完成

const FULFILLED = 'fulfilled';//履行

const REJECTED = 'rejected';//拒绝

\


class MyPromise {

  // 构造方法接收一个回调

  constructor(executor) {

    this._status = PENDING//promise的状态

    this._value = undefined//存储then回调return的值

    this._resolveQueue = []; //成功队列,resolve时触发

    this._rejectQueue = []; //失败队列,reject时触发

\


    // 由于resolve/reject是在executor内部被调用,因此需要使用箭头函数固定this指向,否则找不到this._resolveQueue

    let _resolve = (val) => {

      //把resolve执行回调的操作封装成一个函数,放进setTimeout里,以兼容executor是同步代码的情况

      const run = () => {

        if (this._status !== PENDINGreturn// 对应规范中的"状态只能由pending到fulfilled或rejected"

        this._status = FULFILLED//状态变更

        this._value = val; //存储当前value

        // 使用队列来储存回调,为了实现then方法可以被同一个promise调用多次

        while (this._resolveQueue.length) {

          const callback = this._resolveQueue.shift();//删除队列中的第一个元素并返回

          callback(val);

        }

      }

      setTimeout(run)

    }

    // 实现同resolve

    let _reject = (val) => {

      const run = () => {

        if (this._status !== PENDINGreturn;

        this._status = REJECTED;

        this._value = val;

        while (this._rejectQueue.length) {

          const callback = this._rejectQueue.shift();

          callback(val);

        }

      }

      setTimeout(run);

    }

    // new promise()时立即执行executor,并传入resolve和reject

    executor(_resolve, _reject)

  }

\


  // then方法,接收一个成功的回调和一个失败的回调

  then(resolveFn, rejectFn) {

    // 根据规范,如果then的参数不是function,则我们需要忽略它,让链式调用继续往下执行

    typeof resolveFn !== 'function' ? resolveFn = value => value : null;

    typeof rejectFn !== 'function' ? rejectFn = reason => {

      throw new Error(reason instanceof Error ? reason.message : reason);

    } : null;

\


    // 返回一个新的promise

    return new MyPromise((resolve, reject) => {

      // 把resolveFn重新包装一下,再push进resolve执行队列,这是为了获取回调的返回值进行分类讨论

      const fulfilledFn = value => {

        try {

          // 执行第一个promise的成功回调,并获取返回值

          let x = resolveFn(value);

          // 如果是promise,那么等待状态变更,否则直接resolve

          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);

        } catch (error) {

          reject(error)

        }

      }

\


      // reject

      const rejectedFn = error => {

        try {

          let x = rejectFn(error);

          x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);

        } catch{

          reject(error);

        }

      }

\


      switch (this._status) {

        // 当状态为pending时,把then回调push进resolve/reject执行队列,等待执行

        case PENDING:

          this._resolveQueue.push(fulfilledFn);

          this._rejectQueue.push(rejectedFn);

          break;

        // 当状态已经变为resolve/reject时,直接执行then回调

        case FULFILLED:

          fulfilledFn(this._value);

          break;

        case REJECTED:

          rejectedFn(this._value);

          break;

      }

    })

  }

\


  // catch方法其实就是执行一下then的第二个回调

  catch(rejectFn) {

    return this.then(undefined, rejectFn)

  }

\


  // finally方法:

  finally(callback) {

    return this.then(

      value => MyPromise.resolve(callback()).then(() => value),

      reason => MyPromise.resolve(callback()).then(() => { throw reason })

    )

  }

\


  // 静态的resolve方法

  static resolve(value) {

    // 根据规范,如果参数是promise实例,直接return这个实例

    if (value instanceof MyPromisereturn value;

    return new MyPromise(resolve => resolve(value))

  }

\


  // 静态reject方法

  static reject(reason) {

    return new MyPromise((resolve, reject) => reject(reason))

  }

\


  // 静态的all方法

  static all(promiseArr) {

    let index = 0;

    let result = [];

    return new MyPromise((resolve, reject) => {

      promiseArr.forEach((p, i) => {

        // promise.resolve(p)用于处理传入值不为promise的情况

        MyPromise.resolve(p).then(

          val => {

            index++;

            result[i] = val;

            if (index === promiseArr.length) {

              resolve(result)

            }

          },

          err => {

            reject(err)

          }

        )

      })

    })

  }

\


  // 静态方法race

  static race(promiseArr) {

    return new MyPromise((resolve, reject) => {

      // 同时执行promise,如果有一个promise的状态发生改变,就更新mypromise的状态

      for (let p of promiseArr) {

        MyPromise.resolve(p).then(

          value => {

            resolve(value)

          },

          err => {

            reject(err)

          }

        )

      }

    })

  }

}

\

10、async await为什么可以同步方式写异步?

 async await实际上是对generator的封装,是一个语法糖。它可以通过yield关键字,把函数的执行流挂起,
 通过next()方法可以切换到下一个状态。
 
 那么generator函数是如何暂停执行程序的呢?
 generator是通过协程来控制程序执行的,generator函数是一个生成器,执行它会返回一个迭代器,
 这个迭代器同时也是一个协程。一个线程中可以有多个协程,但是同时只能有一个协程在执行。
 协程的执行由程序控制,通过调用生成器的next()方法可以让该协程执行,通过yeild关键字可以
 让该协程暂停,交出主线程控制权,通过return关键字可以让该协程结束。

async是通过promise+generator来实现的generator是通过线程来控制程序调度的。
在协程中执行异步任务时,先用promise封装该异步任务,如果异步任务完成,会将其结果放入微任务队列中,
然后通过yeild让出主线程执行权,继续执行主线程js,主线程执行完成之后,会去查找微任务,有任务则执行,
这时通过调用迭代器的next(result)方法,将主线程的执行权转交給该协程继续执行,并且将result赋值給
yield表达式左边的变量,从而以同步的方式实现了异步编程。

11、 $nextTick()是宏任务还是微任务?

首先来说下什么是nextTick?
在下次dom更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的dom。
原理:
使用nextTick接收传入的回调函数,将回调函数暂时存放到一个队列中,开启异步更新。为了考虑浏览器的兼容,
在异步更新时还存在一个降级的过程,会优先使用promise微任务去执行队列中的所有回调,在下一次事件循环时,
更新页面。
promise->mutationObserver->setImmediate->setTimeout
在vue的不同版本中,针对各种渲染和dom事件问题进行了多次调整,在最新的版本中为微任务。