登录
原创

前端面试题解析

发布于 2020-10-12 阅读 117
  • JavaScript
原创

1、10个 Ajax 同时发起请求,全部返回展示结果,并且最多允许三次失败,说出设计思路

这个问题很多人会想到用 Promise.all ,但是这个函数有一个局限在于如果失败一次就返回了,这样实现会有一些问题,需要变通下。来看下下面的两种实现思路

// 以下是不完整的代码,着重于思路  非 Promise 写法

let successCount = 0
let errorCount = 0
late datas = []

ajax(url, (res) => {
  if (success) {
    successCount++
    if (successCount + errorCount === 10) {
      console.log(datas)
    } else {
      datas.push(res.data)
    }
  } else {
    errorCount++
    if (errorCount > 3) {
      // 失败次数大于 3 次就应该报错了
      throw Error('失败 3 次')
    }
  }
})

// Promise 写法
let errorCount = 0
let p = new Promise((resolve, reject) => {
  if (success) {
    resolve(res.data)
  } else {
    errorCount++
    if (errorCount > 3) {
      // 失败次数大于 3 次就应该报错了
      reject(error)
    } else {
      resolve(error)
    }
  }
})

Promise.all([p]).then(v => {
  console.log(v)
})

2、基于 Localstorage 设计一个 1M 的缓存系统,需要实现缓存淘汰机制

设计思路:

  • 储存的每个对象需要添加两个属性:分别是过期时间和储存时间
  • 利用一个属性保存系统中目前所占空间大小,每次存储都增加该属性,,当该属性值大于 1M 时,需要按照时间排序系统中的数据,删除一定量的数据,保证能够储存下目前需要存储的数据
  • 每次取数据时,需要判断该缓存数据是否过期,如果过期就删除

以下是代码的实现,实现了思路,这种设计题一般是给出设计思路和部分代码,不会需要写出一个完整的代码

ckass Store {
  constructor () {
    let store = localStorage.getItem('cache')
    if (!store) {
      store = {
        maxSize: 1024 * 1024,
        size: 0
      }
      this.store = store
    } else {
      this.store = JSON.parse(store)
    }
  }

  set (key, value, expire) {
    this.store[key] = {
      date: Date.now(),
      expire,
      value
    },
    let size = this.sizeOf(JSON.stringify(this.store[key]))

    if (this.store.maxSize < size + this.store.size) {
      console.log('超了.....')
      var keys = Object.keys(this.store)

      // 时间排序
      keys = keys.sort((a, b) => {
        let item1 = this.store[a], item2 = this.store[b]
        return item2.date - item1.date
      })

      while (size + this.store.size > this.store.maxSize) {
        let index = keys[keys.length - 1]
        this.store.size -= this.sizeOf(JSON.stringify(this.store[index]))
        delete this.store[index]
      }
    }
    
    this.store.size += size
  
    localStorage.setItem('cache', JSON.stringify(this.store))
  }

  get (key) {
    let d = this.store[key]
    if (!d) {
       console.log('找不到该属性')
       return
    }
    if (d.expire > Date.now) {
       console.log('过期删除')
       delete this.store[key]
       localStorage.setItem('cache', JSON.stringify(this.store))
    } else {
      return d.value
    }
  }

  sizeOf (str, charset) {
    var total = 0,
      charCode,
      i,
      len

    charset = charset ? charset.toLowerCase() : ''
    if (charset === 'utf-16' || charset === 'utf16') {
      for (i = 0; len = str.length; i < len; i++) {
        charCode = str.charCodeAt(i)
        if (charCode <= 0xffff) {
          total += 2
        } else {
          total += 4
        }
      }
    } else {
      for (i = 0; len = str.length; i < len; i++) {
        charCode = str.charCodeAt(i)
        if (charCode <= 0x007f) {
          total += 1
        } else if (charCode <= 0x07ff) {
          total += 2
        } else if (charCode <= 0xffff) {
          total += 3
        } else {
          total += 4
        }
      }
    }
    return total
  }
}

3、闭包

什么是闭包?

MDN的定义:函数与对其状态即词法环境的引用共同构成闭包。也就是说,闭包可以让你从内部函数访问到外部函数作用域

javaScript 中,函数在每次创建时生成闭包。另一种解释:闭包是指有权访问另一个函数作用域中的变量函数

看下边例子:

// 这个就是一个闭包简单的闭包案例
function saySomething () {
  var name = 'wex'
  return function () {
    console.log(name)
  }
}

var say = saySomething()
say()

闭包产生的原因?

根据 JS 的垃圾回收机制(不提新生代和老生代),根据可达性算法:不可达就会被回收

什么是不可达?简答来说:堆内存中没有在栈内存中方引用(即:没有指针指向堆)就视为不可达

上面案例代码中:saySomething方法的返回值的引用存在了say 变量中,所以可达,所以引用不会被销毁,从而产生闭包

说一个闭包的使用场景?

案例一:请求出错的提示框(多个请求同事出错一般都只有一个提示框)

实现思路:使用设计模式之单例模式

上代码:

const Singleton = (function () {
  var _instance
  return function (obj) {
    return _instance || (_instance = obj)
  }
})()

var a = new Singleton({ x: 1 })
var b = new Singleton({ y: 2 })

console.log(a === b)

上边例子有一个有点:_intance 是私有的,外部不能更改(保证安全无污染/可信)

案例二:解决 varfor + setTimeout 混合场景中的 BUG

BUG 展示

for(var i = 1; i <= 5; i++) {
  setTimeout(function () {
     console.log(i)
  }i * 300)
}

案例二会打印:6 6 6 6 6

因为 var 是函数作用域,而 setTimeout 是异步执行,所以:当 console.log执行的时候 i 已经等于 6 了

在没有 let 和 const 的年代,常用的解决办法就是闭包

for(var i = 1; i <= 5; i++) {
  (function (j) {
    setTimeout(function () {
      console.log(j)
    }, j * 300) 
  })(i)
}

闭包的缺点

缺点:

1.性能考量:闭包在处理速度和内存消耗方面对脚本性能具有负面影响(多执行了一个函数,多了一个内存指向)
2.可能内存溢出。(比如:在闭包中的 addEventListener 没有被 removeEventListener

函数的 this 指向问题

1.函数的形式调用(this 指向 window)

function fn() {
  console.log(this, 'fn')
  function subFn() { 
    console.log(this, 'subFn')
  }
  sunbFn() // window
}
fn() // window

2.以方法的形式调用(this 指向调用函数的对象)

var x = 'abc'
var obj = {
  x: 123,
  fn: function () {
    console.log(this.x)
  }
}

obj.fn() // 123
var fn = obj.fn
fn() // abc

3.以构造函数调用(指向实例)

new 的实例是构造函数中 return 的对象 || this

// 构造函数中有 return 对象的情况
fucntion A() {
  return {
    a: 1
  }
}

A.prototype.say = function () {
  console.log(this, 'xx')
}

var a  = new A()
a = { a: 1 }
a.say === undefined

// 构造函数中没有 return 对象的情况
function A() {
  // 可以手动 return this
}
A.prototype.say = function () {
  console.log(this, 'xxx')
}
var a = new A()
a.say()
// A {} 'xxx'

4.箭头函数的 this 指向

箭头函数的 this,就是定义是所在的对象
一旦绑定了上下文,就不可改变

let obj = {
  x () {
    let y = () => {
      console.log(this === obj)
    }
    y() // true
    // call apply bind 都不能改变韩式内部 this 指向
    y.call(window) // true
  }
}

由于 this 指向问题,所以:箭头函数不能当做构造函数,不能使用 new 命令
箭头函数每有 arguments,需要手动使用 …args 参数代替
箭头函数不能用作 Generator 函数

说一下尾递归

尾递归就是:函数最后 单纯 return 函数,尾递归来说,由于只存在一个调用记录,所以永远不会发生‘占溢出’错误

ES6 出现的尾递归,可以将复杂度O(n)的调用记录,换为复杂度O(1)d调用记录

测试:不使用尾递归

function Fibonacci (n) {
  if (n <= 1) { return 1 }
  // return 四则运算
  return Fibonacci(n - 1) + Fibonacci(n - 2)
}
Fibonacci(10) // 89
Fibonacci(100) // 超时

测试:使用尾递归

function Fibonacci(n, ac1 = 1, ac2 = 1) {
  if (n <= 1) { return ac2 }
  return Fibonacci(n - 1, ac2, ac1 + ac2)
}
Fibonacci(100) // 573147844013817200000
Fibonacci(1000) // 7.0330367711422765e+208
Fibonacci(10000) // Infinity

蹦床函数(协程):解决递归栈溢出问题,将函数变成循环

function trampoline(f) {
  while (f && f intanceof Function){
   f = f()
  }
  return f
}

尾递归优化:

function tco(f) {
  var value
  var active = false
  var accumulated = []

  return function accoumulator () {
    accumulated.push(arguments)
    // 除了第一次执行,其他的执行都是为了传参
    if (!active) { // 很重要,如果不使用 active 关闭后续进入,sum 函数超过会溢出
      // 在第一次进入递归优化时激活,关闭后续进入
      active = true
      // 有参数就执行
      while (accumulated.length) {
        // 调用 f ,顺便清除参数
        value = f.apply(this, accumulated.shift())
        // 由于 while 中又调用 f,f 调用 sum,然后 sum 在执行时给 accumulated 塞了一个参数
        // 所以 while 循环会在 sum 返回结果前一种执行,知道递归完成
      }
      active = false
      return value
    }
  }
}

var sum = tco(function (x, y) {
  if (y > 0) {
    // 此时的 sum 是 accumulator
    // 执行 sum 等于给 accumulator 传参
    return sum(x + 1, y -1)
  } else {
    return x
  }
})

sum(1, 10000)

手写一个防抖函数

防抖函数:

function debounce(fn, wait) {
  let timer = null
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, wait)
  }
}

使用场景:输入框校验

手写一个节流函数

节流函数:

function throttle(fn, wait = 300) {
  let flag = true
  return (...args) => {
    if (!flag) return
    flag = false
    setTimeout(() => {
      fn.apply(this, args)
      flag - true
    }, wait)
  }
}

使用场景:
1.延迟防抖函数:onscroll 时触发事件
2.立即执行防抖函数:按钮点击事件(魔种情况下 once 函数更合适)

评论区

wex
2粉丝

雾锁山头山锁雾,天连水尾水连天

1

0

1