循环打印红黄绿

上面来看一道比拟典型的问题,通过这个问题来比照几种异步编程办法:红灯 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()

手写 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);  }};

实现双向数据绑定

let obj = {}let input = document.getElementById('input')let span = document.getElementById('span')// 数据劫持Object.defineProperty(obj, 'text', {  configurable: true,  enumerable: true,  get() {    console.log('获取数据了')  },  set(newVal) {    console.log('数据更新了')    input.value = newVal    span.innerHTML = newVal  }})// 输出监听input.addEventListener('keyup', function(e) {  obj.text = e.target.value})

Array.prototype.filter()

Array.prototype.filter = function(callback, thisArg) {  if (this == undefined) {    throw new TypeError('this is null or not undefined');  }  if (typeof callback !== 'function') {    throw new TypeError(callback + 'is not a function');  }  const res = [];  // 让O成为回调函数的对象传递(强制转换对象)  const O = Object(this);  // >>>0 保障len为number,且为正整数  const len = O.length >>> 0;  for (let i = 0; i < len; i++) {    // 查看i是否在O的属性(会查看原型链)    if (i in O) {      // 回调函数调用传参      if (callback.call(thisArg, O[i], i, O)) {        res.push(O[i]);      }    }  }  return res;}

实现类数组转化为数组

类数组转换为数组的办法有这样几种:

  • 通过 call 调用数组的 slice 办法来实现转换
Array.prototype.slice.call(arrayLike);
  • 通过 call 调用数组的 splice 办法来实现转换
Array.prototype.splice.call(arrayLike, 0);
  • 通过 apply 调用数组的 concat 办法来实现转换
Array.prototype.concat.apply([], arrayLike);
  • 通过 Array.from 办法来实现转换
Array.from(arrayLike);

手写 call 函数

call 函数的实现步骤:

  1. 判断调用对象是否为函数,即便咱们是定义在函数的原型上的,然而可能呈现应用 call 等形式调用的状况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  3. 解决传入的参数,截取第一个参数后的所有参数。
  4. 将函数作为上下文对象的一个属性。
  5. 应用上下文对象来调用这个办法,并保留返回后果。
  6. 删除方才新增的属性。
  7. 返回后果。
// call函数实现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;};

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

实现一个迷你版的vue

入口

// js/vue.jsclass Vue {  constructor (options) {    // 1. 通过属性保留选项的数据    this.$options = options || {}    this.$data = options.data || {}    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el    // 2. 把data中的成员转换成getter和setter,注入到vue实例中    this._proxyData(this.$data)    // 3. 调用observer对象,监听数据的变动    new Observer(this.$data)    // 4. 调用compiler对象,解析指令和差值表达式    new Compiler(this)  }  _proxyData (data) {    // 遍历data中的所有属性    Object.keys(data).forEach(key => {      // 把data的属性注入到vue实例中      Object.defineProperty(this, key, {        enumerable: true,        configurable: true,        get () {          return data[key]        },        set (newValue) {          if (newValue === data[key]) {            return          }          data[key] = newValue        }      })    })  }}

实现Dep

class Dep {  constructor () {    // 存储所有的观察者    this.subs = []  }  // 增加观察者  addSub (sub) {    if (sub && sub.update) {      this.subs.push(sub)    }  }  // 发送告诉  notify () {    this.subs.forEach(sub => {      sub.update()    })  }}

实现watcher

class Watcher {  constructor (vm, key, cb) {    this.vm = vm    // data中的属性名称    this.key = key    // 回调函数负责更新视图    this.cb = cb    // 把watcher对象记录到Dep类的动态属性target    Dep.target = this    // 触发get办法,在get办法中会调用addSub    this.oldValue = vm[key]    Dep.target = null  }  // 当数据发生变化的时候更新视图  update () {    let newValue = this.vm[this.key]    if (this.oldValue === newValue) {      return    }    this.cb(newValue)  }}

实现compiler

class Compiler {  constructor (vm) {    this.el = vm.$el    this.vm = vm    this.compile(this.el)  }  // 编译模板,解决文本节点和元素节点  compile (el) {    let childNodes = el.childNodes    Array.from(childNodes).forEach(node => {      // 解决文本节点      if (this.isTextNode(node)) {        this.compileText(node)      } else if (this.isElementNode(node)) {        // 解决元素节点        this.compileElement(node)      }      // 判断node节点,是否有子节点,如果有子节点,要递归调用compile      if (node.childNodes && node.childNodes.length) {        this.compile(node)      }    })  }  // 编译元素节点,解决指令  compileElement (node) {    // console.log(node.attributes)    // 遍历所有的属性节点    Array.from(node.attributes).forEach(attr => {      // 判断是否是指令      let attrName = attr.name      if (this.isDirective(attrName)) {        // v-text --> text        attrName = attrName.substr(2)        let key = attr.value        this.update(node, key, attrName)      }    })  }  update (node, key, attrName) {    let updateFn = this[attrName + 'Updater']    updateFn && updateFn.call(this, node, this.vm[key], key)  }  // 解决 v-text 指令  textUpdater (node, value, key) {    node.textContent = value    new Watcher(this.vm, key, (newValue) => {      node.textContent = newValue    })  }  // v-model  modelUpdater (node, value, key) {    node.value = value    new Watcher(this.vm, key, (newValue) => {      node.value = newValue    })    // 双向绑定    node.addEventListener('input', () => {      this.vm[key] = node.value    })  }  // 编译文本节点,解决差值表达式  compileText (node) {    // console.dir(node)    // {{  msg }}    let reg = /\{\{(.+?)\}\}/    let value = node.textContent    if (reg.test(value)) {      let key = RegExp.$1.trim()      node.textContent = value.replace(reg, this.vm[key])      // 创立watcher对象,当数据扭转更新视图      new Watcher(this.vm, key, (newValue) => {        node.textContent = newValue      })    }  }  // 判断元素属性是否是指令  isDirective (attrName) {    return attrName.startsWith('v-')  }  // 判断节点是否是文本节点  isTextNode (node) {    return node.nodeType === 3  }  // 判断节点是否是元素节点  isElementNode (node) {    return node.nodeType === 1  }}

实现Observer

class Observer {  constructor (data) {    this.walk(data)  }  walk (data) {    // 1. 判断data是否是对象    if (!data || typeof data !== 'object') {      return    }    // 2. 遍历data对象的所有属性    Object.keys(data).forEach(key => {      this.defineReactive(data, key, data[key])    })  }  defineReactive (obj, key, val) {    let that = this    // 负责收集依赖,并发送告诉    let dep = new Dep()    // 如果val是对象,把val外部的属性转换成响应式数据    this.walk(val)    Object.defineProperty(obj, key, {      enumerable: true,      configurable: true,      get () {        // 收集依赖        Dep.target && dep.addSub(Dep.target)        return val      },      set (newValue) {        if (newValue === val) {          return        }        val = newValue        that.walk(newValue)        // 发送告诉        dep.notify()      }    })  }}

应用

<!DOCTYPE html><html lang="cn"><head>  <meta charset="UTF-8">  <meta name="viewport" content="width=device-width, initial-scale=1.0">  <meta http-equiv="X-UA-Compatible" content="ie=edge">  <title>Mini Vue</title></head><body>  <div id="app">    <h1>差值表达式</h1>    <h3>{{ msg }}</h3>    <h3>{{ count }}</h3>    <h1>v-text</h1>    <div v-text="msg"></div>    <h1>v-model</h1>    <input type="text" v-model="msg">    <input type="text" v-model="count">  </div>  <script src="./js/dep.js"></script>  <script src="./js/watcher.js"></script>  <script src="./js/compiler.js"></script>  <script src="./js/observer.js"></script>  <script src="./js/vue.js"></script>  <script>    let vm = new Vue({      el: '#app',      data: {        msg: 'Hello Vue',        count: 100,        person: { name: 'zs' }      }    })    console.log(vm.msg)    // vm.msg = { test: 'Hello' }    vm.test = 'abc'  </script></body></html>

数组扁平化

数组扁平化是指将一个多维数组变为一个一维数组

const arr = [1, [2, [3, [4, 5]]], 6];// => [1, 2, 3, 4, 5, 6]
办法一:应用flat()
const res1 = arr.flat(Infinity);
办法二:利用正则
const res2 = JSON.stringify(arr).replace(/\[|\]/g, '').split(',');

但数据类型都会变为字符串

办法三:正则改进版本
const res3 = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']');
办法四:应用reduce
const flatten = arr => {  return arr.reduce((pre, cur) => {    return pre.concat(Array.isArray(cur) ? flatten(cur) : cur);  }, [])}const res4 = flatten(arr);
办法五:函数递归
const res5 = [];const fn = arr => {  for (let i = 0; i < arr.length; i++) {    if (Array.isArray(arr[i])) {      fn(arr[i]);    } else {      res5.push(arr[i]);    }  }}fn(arr);

Promise并行限度

就是实现有并行限度的Promise调度器问题

class Scheduler {  constructor() {    this.queue = [];    this.maxCount = 2;    this.runCounts = 0;  }  add(promiseCreator) {    this.queue.push(promiseCreator);  }  taskStart() {    for (let i = 0; i < this.maxCount; i++) {      this.request();    }  }  request() {    if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) {      return;    }    this.runCounts++;    this.queue.shift()().then(() => {      this.runCounts--;      this.request();    });  }}const timeout = time => new Promise(resolve => {  setTimeout(resolve, time);})const scheduler = new Scheduler();const addTask = (time,order) => {  scheduler.add(() => timeout(time).then(()=>console.log(order)))}addTask(1000, '1');addTask(500, '2');addTask(300, '3');addTask(400, '4');scheduler.taskStart()// 2// 3// 1// 4

实现 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});

实现公布-订阅模式

class EventCenter{  // 1. 定义事件容器,用来装事件数组    let handlers = {}  // 2. 增加事件办法,参数:事件名 事件办法  addEventListener(type, handler) {    // 创立新数组容器    if (!this.handlers[type]) {      this.handlers[type] = []    }    // 存入事件    this.handlers[type].push(handler)  }  // 3. 触发事件,参数:事件名 事件参数  dispatchEvent(type, params) {    // 若没有注册该事件则抛出谬误    if (!this.handlers[type]) {      return new Error('该事件未注册')    }    // 触发事件    this.handlers[type].forEach(handler => {      handler(...params)    })  }  // 4. 事件移除,参数:事件名 要删除事件,若无第二个参数则删除该事件的订阅和公布  removeEventListener(type, handler) {    if (!this.handlers[type]) {      return new Error('事件有效')    }    if (!handler) {      // 移除事件      delete this.handlers[type]    } else {      const index = this.handlers[type].findIndex(el => el === handler)      if (index === -1) {        return new Error('无该绑定事件')      }      // 移除事件      this.handlers[type].splice(index, 1)      if (this.handlers[type].length === 0) {        delete this.handlers[type]      }    }  }}

JSONP

script标签不遵循同源协定,能够用来进行跨域申请,长处就是兼容性好但仅限于GET申请

const jsonp = ({ url, params, callbackName }) => {  const generateUrl = () => {    let dataSrc = '';    for (let key in params) {      if (Object.prototype.hasOwnProperty.call(params, key)) {        dataSrc += `${key}=${params[key]}&`;      }    }    dataSrc += `callback=${callbackName}`;    return `${url}?${dataSrc}`;  }  return new Promise((resolve, reject) => {    const scriptEle = document.createElement('script');    scriptEle.src = generateUrl();    document.body.appendChild(scriptEle);    window[callbackName] = data => {      resolve(data);      document.removeChild(scriptEle);    }  })}

实现千位分隔符

// 保留三位小数parseToMoney(1234.56); // return '1,234.56'parseToMoney(123456789); // return '123,456,789'parseToMoney(1087654.321); // return '1,087,654.321'
function parseToMoney(num) {  num = parseFloat(num.toFixed(3));  let [integer, decimal] = String.prototype.split.call(num, '.');  integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,');  return integer + '.' + (decimal ? decimal : '');}

手写类型判断函数

function getType(value) {  // 判断数据是 null 的状况  if (value === null) {    return value + "";  }  // 判断数据是援用类型的状况  if (typeof value === "object") {    let valueClass = Object.prototype.toString.call(value),      type = valueClass.split(" ")[1].split("");    type.pop();    return type.join("").toLowerCase();  } else {    // 判断数据是根本数据类型的状况和函数的状况    return typeof value;  }}

用正则写一个依据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()办法解码。

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

字符串最长的不反复子串

题目形容

给定一个字符串 s ,请你找出其中不含有反复字符的 最长子串 的长度。示例 1:输出: s = "abcabcbb"输入: 3解释: 因为无反复字符的最长子串是 "abc",所以其长度为 3。示例 2:输出: s = "bbbbb"输入: 1解释: 因为无反复字符的最长子串是 "b",所以其长度为 1。示例 3:输出: s = "pwwkew"输入: 3解释: 因为无反复字符的最长子串是 "wke",所以其长度为 3。     请留神,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。示例 4:输出: s = ""输入: 0

答案

const lengthOfLongestSubstring = function (s) {  if (s.length === 0) {    return 0;  }  let left = 0;  let right = 1;  let max = 0;  while (right <= s.length) {    let lr = s.slice(left, right);    const index = lr.indexOf(s[right]);    if (index > -1) {      left = index + left + 1;    } else {      lr = s.slice(left, right + 1);      max = Math.max(max, lr.length);    }    right++;  }  return max;};

Promise.all

Promise.all是反对链式调用的,实质上就是返回了一个Promise实例,通过resolvereject来扭转实例状态。

Promise.myAll = function(promiseArr) {  return new Promise((resolve, reject) => {    const ans = [];    let index = 0;    for (let i = 0; i < promiseArr.length; i++) {      promiseArr[i]      .then(res => {        ans[i] = res;        index++;        if (index === promiseArr.length) {          resolve(ans);        }      })      .catch(err => reject(err));    }  })}

Object.is

Object.is解决的次要是这两个问题:

+0 === -0  // trueNaN === NaN // false
const is= (x, y) => {  if (x === y) {    // +0和-0应该不相等    return x !== 0 || y !== 0 || 1/x === 1/y;  } else {    return x !== x && y !== y;  }}

手写 apply 函数

apply 函数的实现步骤:

  1. 判断调用对象是否为函数,即便咱们是定义在函数的原型上的,然而可能呈现应用 call 等形式调用的状况。
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window 。
  3. 将函数作为上下文对象的一个属性。
  4. 判断参数值是否传入
  5. 应用上下文对象来调用这个办法,并保留返回后果。
  6. 删除方才新增的属性
  7. 返回后果
// apply 函数实现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;};