手写 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 数组中提前注册的回调

实现apply办法

思路: 利用this的上下文个性。apply其实就是改一下参数的问题
Function.prototype.myApply = function(context = window, args) {  // this-->func  context--> obj  args--> 传递过去的参数  // 在context上加一个惟一值不影响context上的属性  let key = Symbol('key')  context[key] = this; // context为调用的上下文,this此处为函数,将这个函数作为context的办法  // let args = [...arguments].slice(1)   //第一个参数为obj所以删除,伪数组转为数组  let result = context[key](...args); // 这里和call传参不一样  // 革除定义的this 不删除会导致context属性越来越多  delete context[key];   // 返回后果  return result;}
// 应用function f(a,b){ console.log(a,b) console.log(this.name)}let obj={ name:'张三'}f.myApply(obj,[1,2])  //arguments[1]

AJAX

const getJSON = function(url) {  return new Promise((resolve, reject) => {    const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');    xhr.open('GET', url, false);    xhr.setRequestHeader('Accept', 'application/json');    xhr.onreadystatechange = function() {      if (xhr.readyState !== 4) return;      if (xhr.status === 200 || xhr.status === 304) {        resolve(xhr.responseText);      } else {        reject(new Error(xhr.responseText));      }    }    xhr.send();  })}

手写 Object.create

思路:将传入的对象作为原型

function create(obj) {  function F() {}  F.prototype = obj  return new F()}

模仿Object.create

Object.create()办法创立一个新对象,应用现有的对象来提供新创建的对象的__proto__。

// 模仿 Object.createfunction create(proto) {  function F() {}  F.prototype = proto;  return new F();}

实现Event(event bus)

event bus既是node中各个模块的基石,又是前端组件通信的依赖伎俩之一,同时波及了订阅-公布设计模式,是十分重要的根底。

简略版:

class EventEmeitter {  constructor() {    this._events = this._events || new Map(); // 贮存事件/回调键值对    this._maxListeners = this._maxListeners || 10; // 设立监听下限  }}// 触发名为type的事件EventEmeitter.prototype.emit = function(type, ...args) {  let handler;  // 从贮存事件键值对的this._events中获取对应事件回调函数  handler = this._events.get(type);  if (args.length > 0) {    handler.apply(this, args);  } else {    handler.call(this);  }  return true;};// 监听名为type的事件EventEmeitter.prototype.addListener = function(type, fn) {  // 将type事件以及对应的fn函数放入this._events中贮存  if (!this._events.get(type)) {    this._events.set(type, fn);  }};

面试版:

class EventEmeitter {  constructor() {    this._events = this._events || new Map(); // 贮存事件/回调键值对    this._maxListeners = this._maxListeners || 10; // 设立监听下限  }}// 触发名为type的事件EventEmeitter.prototype.emit = function(type, ...args) {  let handler;  // 从贮存事件键值对的this._events中获取对应事件回调函数  handler = this._events.get(type);  if (args.length > 0) {    handler.apply(this, args);  } else {    handler.call(this);  }  return true;};// 监听名为type的事件EventEmeitter.prototype.addListener = function(type, fn) {  // 将type事件以及对应的fn函数放入this._events中贮存  if (!this._events.get(type)) {    this._events.set(type, fn);  }};// 触发名为type的事件EventEmeitter.prototype.emit = function(type, ...args) {  let handler;  handler = this._events.get(type);  if (Array.isArray(handler)) {    // 如果是一个数组阐明有多个监听者,须要顺次此触发外面的函数    for (let i = 0; i < handler.length; i++) {      if (args.length > 0) {        handler[i].apply(this, args);      } else {        handler[i].call(this);      }    }  } else {    // 单个函数的状况咱们间接触发即可    if (args.length > 0) {      handler.apply(this, args);    } else {      handler.call(this);    }  }  return true;};// 监听名为type的事件EventEmeitter.prototype.addListener = function(type, fn) {  const handler = this._events.get(type); // 获取对应事件名称的函数清单  if (!handler) {    this._events.set(type, fn);  } else if (handler && typeof handler === "function") {    // 如果handler是函数阐明只有一个监听者    this._events.set(type, [handler, fn]); // 多个监听者咱们须要用数组贮存  } else {    handler.push(fn); // 曾经有多个监听者,那么间接往数组里push函数即可  }};EventEmeitter.prototype.removeListener = function(type, fn) {  const handler = this._events.get(type); // 获取对应事件名称的函数清单  // 如果是函数,阐明只被监听了一次  if (handler && typeof handler === "function") {    this._events.delete(type, fn);  } else {    let postion;    // 如果handler是数组,阐明被监听屡次要找到对应的函数    for (let i = 0; i < handler.length; i++) {      if (handler[i] === fn) {        postion = i;      } else {        postion = -1;      }    }    // 如果找到匹配的函数,从数组中革除    if (postion !== -1) {      // 找到数组对应的地位,间接革除此回调      handler.splice(postion, 1);      // 如果革除后只有一个函数,那么勾销数组,以函数模式保留      if (handler.length === 1) {        this._events.set(type, handler[0]);      }    } else {      return this;    }  }};
实现具体过程和思路见实现event

参考 前端进阶面试题具体解答

手写 new 操作符

在调用 new 的过程中会产生以上四件事件:

(1)首先创立了一个新的空对象

(2)设置原型,将对象的原型设置为函数的 prototype 对象。

(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象增加属性)

(4)判断函数的返回值类型,如果是值类型,返回创立的对象。如果是援用类型,就返回这个援用类型的对象。

function objectFactory() {  let newObject = null;  let constructor = Array.prototype.shift.call(arguments);  let result = null;  // 判断参数是否是一个函数  if (typeof constructor !== "function") {    console.error("type error");    return;  }  // 新建一个空对象,对象的原型为构造函数的 prototype 对象  newObject = Object.create(constructor.prototype);  // 将 this 指向新建对象,并执行函数  result = constructor.apply(newObject, arguments);  // 判断返回对象  let flag = result && (typeof result === "object" || typeof result === "function");  // 判断返回后果  return flag ? result : newObject;}// 应用办法objectFactory(构造函数, 初始化参数);

实现apply办法

apply原理与call很类似,不多赘述

// 模仿 applyFunction.prototype.myapply = function(context, arr) {  var context = Object(context) || window;  context.fn = this;  var result;  if (!arr) {    result = context.fn();  } else {    var args = [];    for (var i = 0, len = arr.length; i < len; i++) {      args.push("arr[" + i + "]");    }    result = eval("context.fn(" + args + ")");  }  delete context.fn;  return result;};

实现 jsonp

// 动静的加载js文件function addScript(src) {  const script = document.createElement('script');  script.src = src;  script.type = "text/javascript";  document.body.appendChild(script);}addScript("http://xxx.xxx.com/xxx.js?callback=handleRes");// 设置一个全局的callback函数来接管回调后果function handleRes(res) {  console.log(res);}// 接口返回的数据格式handleRes({a: 1, b: 2});

手写 Promise.all

1) 外围思路

  1. 接管一个 Promise 实例的数组或具备 Iterator 接口的对象作为参数
  2. 这个办法返回一个新的 promise 对象,
  3. 遍历传入的参数,用Promise.resolve()将参数"包一层",使其变成一个promise对象
  4. 参数所有回调胜利才是胜利,返回值数组与参数程序统一
  5. 参数数组其中一个失败,则触发失败状态,第一个触发失败的 Promise 错误信息作为 Promise.all 的错误信息。

2)实现代码

一般来说,Promise.all 用来解决多个并发申请,也是为了页面数据结构的不便,将一个页面所用到的在不同接口的数据一起申请过去,不过,如果其中一个接口失败了,多个申请也就失败了,页面可能啥也出不来,这就看以后页面的耦合水平了

function promiseAll(promises) {  return new Promise(function(resolve, reject) {    if(!Array.isArray(promises)){        throw new TypeError(`argument must be a array`)    }    var resolvedCounter = 0;    var promiseNum = promises.length;    var resolvedResult = [];    for (let i = 0; i < promiseNum; i++) {      Promise.resolve(promises[i]).then(value=>{        resolvedCounter++;        resolvedResult[i] = value;        if (resolvedCounter == promiseNum) {            return resolve(resolvedResult)          }      },error=>{        return reject(error)      })    }  })}// testlet p1 = new Promise(function (resolve, reject) {    setTimeout(function () {        resolve(1)    }, 1000)})let p2 = new Promise(function (resolve, reject) {    setTimeout(function () {        resolve(2)    }, 2000)})let p3 = new Promise(function (resolve, reject) {    setTimeout(function () {        resolve(3)    }, 3000)})promiseAll([p3, p1, p2]).then(res => {    console.log(res) // [3, 1, 2]})

手写防抖函数

函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则从新计时。这能够应用在一些点击申请的事件上,防止因为用户的屡次点击向后端发送屡次申请。

// 函数防抖的实现function debounce(fn, wait) {  let timer = null;  return function() {    let context = this,        args = arguments;    // 如果此时存在定时器的话,则勾销之前的定时器从新记时    if (timer) {      clearTimeout(timer);      timer = null;    }    // 设置定时器,使事件间隔指定事件后执行    timer = setTimeout(() => {      fn.apply(context, args);    }, wait);  };}

实现JSONP办法

利用<script>标签不受跨域限度的特点,毛病是只能反对 get 申请
  • 创立script标签
  • 设置script标签的src属性,以问号传递参数,设置好回调函数callback名称
  • 插入到html文本中
  • 调用回调函数,res参数就是获取的数据
function jsonp({url,params,callback}) {  return new Promise((resolve,reject)=>{  let script = document.createElement('script')    window[callback] = function (data) {      resolve(data)      document.body.removeChild(script)    }    var arr = []    for(var key in params) {      arr.push(`${key}=${params[key]}`)    }    script.type = 'text/javascript'    script.src = `${url}?callback=${callback}&${arr.join('&')}`    document.body.appendChild(script)  })}
// 测试用例jsonp({  url: 'http://suggest.taobao.com/sug',  callback: 'getData',  params: {    q: 'iphone手机',    code: 'utf-8'  },}).then(data=>{console.log(data)})
  • 设置 CORS: Access-Control-Allow-Origin:*
  • postMessage

解析 URL Params 为对象

let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';parseParam(url)/* 后果{ user: 'anonymous',  id: [ 123, 456 ], // 反复呈现的 key 要组装成数组,能被转成数字的就转成数字类型  city: '北京', // 中文需解码  enabled: true, // 未指定值得 key 约定为 true}*/
function parseParam(url) {  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 前面的字符串取出来  const paramsArr = paramsStr.split('&'); // 将字符串以 & 宰割后存到数组中  let paramsObj = {};  // 将 params 存到对象中  paramsArr.forEach(param => {    if (/=/.test(param)) { // 解决有 value 的参数      let [key, val] = param.split('='); // 宰割 key 和 value      val = decodeURIComponent(val); // 解码      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字      if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则增加一个值        paramsObj[key] = [].concat(paramsObj[key], val);      } else { // 如果对象没有这个 key,创立 key 并设置值        paramsObj[key] = val;      }    } else { // 解决没有 value 的参数      paramsObj[param] = true;    }  })  return paramsObj;}

实现事件总线联合Vue利用

Event Bus(Vue、Flutter 等前端框架中有出镜)和 Event Emitter(Node中有出镜)出场的“剧组”不同,然而它们都对应一个独特的角色—— 全局事件总线

全局事件总线,严格来说不能说是观察者模式,而是公布-订阅模式。它在咱们日常的业务开发中利用十分广。

如果只能选一道题,那这道题肯定是 Event Bus/Event Emitter 的代码实现——我都说这么分明了,这个知识点到底要不要把握、须要把握到什么水平,就看各位本人的了。

在Vue中应用Event Bus来实现组件间的通信

Event Bus/Event Emitter 作为全局事件总线,它起到的是一个沟通桥梁的作用。咱们能够把它了解为一个事件核心,咱们所有事件的订阅/公布都不能由订阅方和公布方“私下沟通”,必须要委托这个事件核心帮咱们实现。

在Vue中,有时候 A 组件和 B 组件中距离了很远,看似没什么关系,但咱们心愿它们之间可能通信。这种状况下除了求助于 Vuex 之外,咱们还能够通过 Event Bus 来实现咱们的需要。

创立一个 Event Bus(实质上也是 Vue 实例)并导出:

const EventBus = new Vue()export default EventBus

在主文件里引入EventBus,并挂载到全局:

import bus from 'EventBus的文件门路'Vue.prototype.bus = bus

订阅事件:

// 这里func指someEvent这个事件的监听函数this.bus.$on('someEvent', func)

公布(触发)事件:

// 这里params指someEvent这个事件被触发时回调函数接管的入参this.bus.$emit('someEvent', params)
大家会发现,整个调用过程中,没有呈现具体的发布者和订阅者(比方下面的PrdPublisherDeveloperObserver),全程只有bus这个货色一个人在疯狂刷存在感。这就是全局事件总线的特点——所有事件的公布/订阅操作,必须经由事件核心,禁止所有“私下交易”!

上面,咱们就一起来实现一个Event Bus(留神看正文里的解析):

class EventEmitter {  constructor() {    // handlers是一个map,用于存储事件与回调之间的对应关系    this.handlers = {}  }  // on办法用于装置事件监听器,它承受指标事件名和回调函数作为参数  on(eventName, cb) {    // 先检查一下指标事件名有没有对应的监听函数队列    if (!this.handlers[eventName]) {      // 如果没有,那么首先初始化一个监听函数队列      this.handlers[eventName] = []    }    // 把回调函数推入指标事件的监听函数队列里去    this.handlers[eventName].push(cb)  }  // emit办法用于触发指标事件,它承受事件名和监听函数入参作为参数  emit(eventName, ...args) {    // 查看指标事件是否有监听函数队列    if (this.handlers[eventName]) {      // 如果有,则一一调用队列里的回调函数      this.handlers[eventName].forEach((callback) => {        callback(...args)      })    }  }  // 移除某个事件回调队列里的指定回调函数  off(eventName, cb) {    const callbacks = this.handlers[eventName]    const index = callbacks.indexOf(cb)    if (index !== -1) {      callbacks.splice(index, 1)    }  }  // 为事件注册单次监听器  once(eventName, cb) {    // 对回调函数进行包装,使其执行结束主动被移除    const wrapper = (...args) => {      cb.apply(...args)      this.off(eventName, wrapper)    }    this.on(eventName, wrapper)  }}
在日常的开发中,大家用到EventBus/EventEmitter往往提供比这五个办法多的多的多的办法。但在面试过程中,如果大家可能残缺地实现出这五个办法,曾经十分能够阐明问题了,因而楼上这个EventBus心愿大家能够熟练掌握。学有余力的同学

实现数组的flat办法

function _flat(arr, depth) {  if(!Array.isArray(arr) || depth <= 0) {    return arr;  }  return arr.reduce((prev, cur) => {    if (Array.isArray(cur)) {      return prev.concat(_flat(cur, depth - 1))    } else {      return prev.concat(cur);    }  }, []);}

实现some办法

Array.prototype.mySome=function(callback, context = window){             var len = this.length,                 flag=false,           i = 0;             for(;i < len; i++){                if(callback.apply(context, [this[i], i , this])){                    flag=true;                    break;                }              }             return flag;        }        // var flag=arr.mySome((v,index,arr)=>v.num>=10,obj)        // console.log(flag);

循环打印红黄绿

上面来看一道比拟典型的问题,通过这个问题来比照几种异步编程办法:红灯 3s 亮一次,绿灯 1s 亮一次,黄灯 2s 亮一次;如何让三个灯一直交替反复亮灯?

三个亮灯函数:

function red() {    console.log('red');}function green() {    console.log('green');}function yellow() {    console.log('yellow');}

这道题简单的中央在于须要“交替反复”亮灯,而不是“亮完一次”就完结了。

(1)用 callback 实现

const task = (timer, light, callback) => {    setTimeout(() => {        if (light === 'red') {            red()        }        else if (light === 'green') {            green()        }        else if (light === 'yellow') {            yellow()        }        callback()    }, timer)}task(3000, 'red', () => {    task(2000, 'green', () => {        task(1000, 'yellow', Function.prototype)    })})

这里存在一个 bug:代码只是实现了一次流程,执行后红黄绿灯别离只亮一次。该如何让它交替反复进行呢?

下面提到过递归,能够递归亮灯的一个周期:

const step = () => {    task(3000, 'red', () => {        task(2000, 'green', () => {            task(1000, 'yellow', step)        })    })}step()

留神看黄灯亮的回调里又再次调用了 step 办法 以实现循环亮灯。

(2)用 promise 实现

const task = (timer, light) =>     new Promise((resolve, reject) => {        setTimeout(() => {            if (light === 'red') {                red()            }            else if (light === 'green') {                green()            }            else if (light === 'yellow') {                yellow()            }            resolve()        }, timer)    })const step = () => {    task(3000, 'red')        .then(() => task(2000, 'green'))        .then(() => task(2100, 'yellow'))        .then(step)}step()

这里将回调移除,在一次亮灯完结后,resolve 以后 promise,并仍然应用递归进行。

(3)用 async/await 实现

const taskRunner =  async () => {    await task(3000, 'red')    await task(2000, 'green')    await task(2100, 'yellow')    taskRunner()}taskRunner()

手写 instanceof 办法

instanceof 运算符用于判断构造函数的 prototype 属性是否呈现在对象的原型链中的任何地位。

实现步骤:

  1. 首先获取类型的原型
  2. 而后取得对象的原型
  3. 而后始终循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null

具体实现:

function myInstanceof(left, right) {  let proto = Object.getPrototypeOf(left), // 获取对象的原型      prototype = right.prototype; // 获取构造函数的 prototype 对象  // 判断构造函数的 prototype 对象是否在对象的原型链上  while (true) {    if (!proto) return false;    if (proto === prototype) return true;    proto = Object.getPrototypeOf(proto);  }}

函数柯里化的实现

函数柯里化指的是一种将应用多个参数的一个函数转换成一系列应用一个参数的函数的技术。

function curry(fn, args) {  // 获取函数须要的参数长度  let length = fn.length;  args = args || [];  return function() {    let subArgs = args.slice(0);    // 拼接失去现有的所有参数    for (let i = 0; i < arguments.length; i++) {      subArgs.push(arguments[i]);    }    // 判断参数的长度是否曾经满足函数所需参数的长度    if (subArgs.length >= length) {      // 如果满足,执行函数      return fn.apply(this, subArgs);    } else {      // 如果不满足,递归返回科里化的函数,期待参数的传入      return curry.call(this, fn, subArgs);    }  };}// es6 实现function curry(fn, ...args) {  return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);}

用正则写一个依据name获取cookie中的值的办法

function getCookie(name) {  var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]*)'));  if (match) return unescape(match[2]);}
  1. 获取页面上的cookie能够应用 document.cookie

这里获取到的是相似于这样的字符串:

'username=poetry; user-id=12345; user-roles=home, me, setting'

能够看到这么几个信息:

  • 每一个cookie都是由 name=value 这样的模式存储的
  • 每一项的结尾可能是一个空串''(比方username的结尾其实就是), 也可能是一个空字符串' '(比方user-id的结尾就是)
  • 每一项用";"来辨别
  • 如果某项中有多个值的时候,是用","来连贯的(比方user-roles的值)
  • 每一项的结尾可能是有";"的(比方username的结尾),也可能是没有的(比方user-roles的结尾)
  • 所以咱们将这里的正则拆分一下:
  • '(^| )'示意的就是获取每一项的结尾,因为咱们晓得如果^不是放在[]里的话就是示意结尾匹配。所以这里(^| )的意思其实就被拆分为(^)示意的匹配username这种状况,它后面什么都没有是一个空串(你能够把(^)了解为^它前面还有一个暗藏的'');而|示意的就是或者是一个" "(为了匹配user-id结尾的这种状况)
  • +name+这没什么好说的
  • =([^;]*)这里匹配的就是=前面的值了,比方poetry;刚刚说了^要是放在[]里的话就示意"除了^前面的内容都能匹配",也就是非的意思。所以这里([^;]*)示意的是除了";"这个字符串别的都匹配(*应该都晓得什么意思吧,匹配0次或屡次)
  • 有的大佬等号前面是这样写的'=([^;]*)(;|$)',而最初为什么能够把'(;|$)'给省略呢?因为其实最初一个cookie项是没有';'的,所以它能够合并到=([^;]*)这一步。
  • 最初获取到的match其实是一个长度为4的数组。比方:
[  "username=poetry;",  "",  "poetry",  ";"]
  • 第0项:全量
  • 第1项:结尾
  • 第2项:两头的值
  • 第3项:结尾

所以咱们是要拿第2项match[2]的值。

  1. 为了避免获取到的值是%xxx这样的字符序列,须要用unescape()办法解码。