JavaScript面试碰到手写笔试题该怎么办?高频前端手写代码笔试题汇总

防抖和节流的实现

function debounce(fn, delay) {
  var timer;
  return function () {
    var _this = this;
    var args = arguments;
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(function () {
      fn.apply(_this, args);
    }, delay);
  };
}

function throttle(fn, delay) {
  var timer;
  return function () {
    var _this = this;
    var args = arguments;
    if (timer) {
      return;
    }
    timer = setTimeout(function() {
      fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args);
      timer = null;
    }, delay);
  }; 
}

JSONP的实现

// jsonp原理:因为jsonp发送的并不是ajax请求,其实是动态创建script标签
// script标签是没有同源限制的,把script标签的src指向请求的服务端地址。
function jsonp(url, data = {}, callback = 'callback') {
  // 处理json对象,拼接url
  data.callback = callbak
  let params = []
  for (let key in data) {
    params.push(key + '=' + data[key])
  }
  let script = document.creatElement('script')
  script.src = url + '?' + params.join('&')
  document.body.appendChild(script)
  // 返回Promise
  return new Promise((resolve, reject) => {
    window[callback] = (data) => {
      try {
        resolve(data)
      } catch (e) {
        reject(e)
      } finally {
        // 移除script元素
        script.parentNode.removeChild(script)
        console.log(script)
      }
    }
  })
}
// 请求数据
jsonp('http://photo.sina.cn/aj/index', {
  page: 1,
  cate: 'recommend',
}, 'jsonCallback').then(data => {
  console.log(data)
})

柯里化:实现 sum 支持sum(1)、sum(1)(2,3)调用

函数的柯里化,是 Javascript 中函数式编程的一个重要概念。它返回的,是一个函数的函数。其实现方式,需要依赖参数以及递归,通过拆分参数的方式,来调用一个多参数的函数方法,以达到减少代码冗余,增加可读性的目的。

function curry(fn, currArgs) {
  return function () {
    let args = [].slice.call(arguments);
    // 首次调用时,若未提供最后一个参数currArgs,则不用进行args的拼接
    if (currArgs !== undefined) {
      args = args.concat(currArgs);
    }
    // 递归调用
    if (args.length < fn.length) {
      return curry(fn, args);
    }
    // 递归出口
    return fn.apply(null, args);
  }
}

解析一下 curry 函数的写法:
首先,它有 2 个参数,fn 指的就是本文一开始的源处理函数 sum。currArgs 是调用 curry 时传入的参数列表,比如 (1, 2)(3) 这样的。
再看到 curry 函数内部,它会整个返回一个匿名函数。
再接下来的 let args = [].slice.call(arguments);,意思是将 arguments 数组化。arguments 是一个类数组的结构,它并不是一个真的数组,所以没法使用数组的方法。我们用了 call 的方法,就能愉快地对 args 使用数组的原生方法了。
currArgs !== undefined 的判断,是为了解决递归调用时的参数拼接。
最后,判断 args 的个数,是否与 fn (也就是 sum )的参数个数相等,相等了就可以把参数都传给 fn,进行输出;否则,继续递归调用,直到两者相等。

实现两个字符串数字相加

function add(a, b) {
  // 小数前面补0
  const absLen = Math.abs(a.length - b.length)
  if (a.length > b.length) {
    b = Array(absLen).fill(0).join('') + b
  } else if (a.length < b.length) {
    a = Array(absLen).fill(0).join('') + a
  }
  const arrA = a.split('').reverse()
  const arrB = b.split('').reverse()
  let result = []
  let carry = 0 // 进位

  for (let i = 0; i < a.length; ++i) {
    let sum = parseInt(arrA[i]) + parseInt(arrB[i]) + carry
    result[i] = sum % 10
    carry = sum > 9 ? 1 : 0
  }
  if (carry === 1) {
    result[a.length] = 1
  }

  return result.reverse().join('')
 }

JS 扁平化(flatten) 数组

var array = [[1, 2, 3], 4, 5, 6, [[7]], []]
var result = flatten(array)

console.log(result)

// 递归
function flatten(arr, result = []) {
  for (let item of arr) {
    if (Array.isArray(item))
      flatten(item, result)
    else
      result.push(item)
  }
  return result
}

// 迭代器实现
// 这里我们给数组的迭代器函数重新定义了 next 方法,
// 实现了一个 getFirst 用来递归取真正的第一个数组元素(无论嵌套多少层),
// 在对数组进行迭代操作的时候,会自动调用迭代器的 next 方法,获得我们一个个基本元素。
Array.prototype[Symbol.iterator] = function () {
  let arr = [].concat(this)
  const getFirst = function (array) {
    let first = array[0]
    // 去掉为 [] 的元素
    while (Array.isArray(array[0]) && array.length === 0) {
      array.shift()
    }
    if (Array.isArray(first)) {
      // 即将是 []
      if (first.length === 1) array.shift()
      return getFirst(first)
    } else {
      array.shift()
      return first
    }
  }
  return {
    next: function () {
      let item = getFirst(arr)
      if (item) {
        return {
          value: item,
          done: false,
        }
      } else {
        return {
          done: true,
        }
      }
    },
  }
}
// 生成器实现
function* flat(arr) {
  for (let item of arr) {
    if (Array.isArray(item))
      yield* flat(item)
    else
      yield item
  }
}

function flatten(arr) {
  let result = []
  for (let val of flat(arr)) {
    result.push(val)
  }
  return result
}


// reduce
function flatten(arr) {
  return arr.reduce((flat, toFlatten) => {
    return flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten);
  }, []);
}

一个字符串不重复的最长子序列

// 12343210 => 43210
var lengthOfLongestSubstring = function (s) {
  let arr = [], max = 0
  for (let i = 0; i < s.length; i++) {
    let index = arr.indexOf(s[i])
    if (index !== -1) {
      arr.splice(0, index + 1);
    }
    arr.push(s.charAt(i))
    max = Math.max(arr.length, max)
  }
  return max
};
// chain = new Chain, chain.eat().sleep(5).eat().sleep(6).work()
Class Chain{
  Task = Promise.resolve();
  Eat() {
    this.Task = this.Task.then(_ => { console.log(eat) })
    Return this;
  }

  Sleep(t) {
    this.task = this.Task.then(_ => new Promise(r => {
      Console.log()
      setTimeout(r, t)
    }))
    Return this
  }
}

Generator的最小实现

function generator(cb) {
  return (function () {
    var object = {
      next: 0,
      stop: function () { }
    };

    return {
      next: function () {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}

Promise的最小实现核心功能:

  • new Promise(fn) 其中 fn 只能为函数,且要立即执行
  • promise.then(success, fail)中的 success 是函数,且会在 resolve 被调用的时候执行,fail
    同理

实现思路:

  1. then(succeed, fail) 先把成功失败回调放到一个回调数组 callbacks[] 上
  2. resolve() 和 reject() 遍历 callbacks
  3. resolve() 读取成功回调 / reject() 读取失败回调,并异步执行 callbacks
    里面的成功和失败回调(放到本轮的微任务队列中)
class Promise2 {
  state = "pending";
  callbacks = [];
  constructor(fn) {
    if (typeof fn !== "function") {
      throw new Error("must pass function");
    }
    fn(this.resolve.bind(this), this.reject.bind(this));
  }
  resolve(result) {
    if (this.state !== "pending") return;
    this.state = "fulfilled";
    nextTick(() => {
      this.callbacks.forEach((handle) => {
        if (typeof handle[0] === "function") {
          handle[0].call(undefined, result);
        }
      });
    });
  }
  reject(reason) {
    if (this.state !== "pending") return;
    this.state = "rejected";
    nextTick(() => {
      this.callbacks.forEach((handle) => {
        if (typeof handle[1] === "function") {
          handle[1].call(undefined, reason);
        }
      });
    });
  }
  then(succeed, fail) {
    const handle = [];
    if (typeof succeed === "function") {
      handle[0] = succeed;
    }
    if (typeof fail === "function") {
      handle[1] = fail;
    }
    this.callbacks.push(handle);
  }
}

function nextTick(fn) {
  if (process !== undefined && typeof process.nextTick === "function") {
    return process.nextTick(fn);
  } else {
    // 用MutationObserver实现浏览器上的nextTick
    var counter = 1;
    var observer = new MutationObserver(fn);
    var textNode = document.createTextNode(String(counter));

    observer.observe(textNode, {
      characterData: true,
    });
    counter += 1;
    textNode.data = String(counter);
  }
}

这是个观察者模式,这种收集依赖 -> 触发通知 -> 取出依赖执行 的方式,被广泛运用于观察者模式的实现,在Promise里,执行顺序是then收集依赖 -> 异步触发resolve -> resolve执行依赖。

Promise A+规范

Promise本质是一个状态机,且状态只能为以下三种:Pending(等待态)、Fulfilled(执行态)、Rejected(拒绝态),状态的变更是单向的,只能从Pending -> Fulfilled 或 Pending -> Rejected,状态变更不可逆
then方法接收两个可选参数,分别对应状态改变时触发的回调。then方法返回一个promise。then 方法可以被同一个 promise 调用多次。
我们思考一下如何实现这种链式调用:

  1. 显然.then()需要返回一个Promise,这样才能找到then方法,所以我们会把then方法的返回值包装成Promise。
  2. .then()的回调需要拿到上一个.then()的返回值
  3. .then()的回调需要顺序执行,以上面这段代码为例,虽然中间return了一个Promise,但执行顺序仍要保证是1->2->3。我们要等待当前Promise状态变更后,再执行下一个then收集的回调,这就要求我们对then的返回值分类讨论
  4. 值穿透:根据规范,如果 then()
    接收的参数不是function,那么我们应该忽略它。如果没有忽略,当then()回调不为function时将会抛出异常,导致链式调用中断
  5. 处理状态为resolve/reject的情况:其实我们上边 then()
    的写法是对应状态为padding的情况,但是有些时候,resolve/reject 在 then()
    之前就被执行(比如Promise.resolve().then()),如果这个时候还把then()回调push进resolve/reject的执行队列里,那么回调将不会被执行,因此对于状态已经变为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 !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
        this._status = FULFILLED // 变更状态
        this._value = val // 储存当前value

        // 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
        // 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
        while (this._resolveQueue.length) {
          const callback = this._resolveQueue.shift()
          callback(val)
        }
      }
      setTimeout(run)
    }
    // 实现同resolve
    let _reject = (val) => {
      const run = () => {
        if (this._status !== PENDING) return // 对应规范中的"状态只能由pending到fulfilled或rejected"
        this._status = REJECTED // 变更状态
        this._value = val // 储存当前value
        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
    // return一个新的promise
    return new MyPromise((resolve, reject) => {
      // 把resolveFn重新包装一下,再push进resolve执行队列,这是为了能够获取回调的返回值进行分类讨论
      const fulfilledFn = value => {
        try {
          // 执行第一个(当前的)Promise的成功回调,并获取返回值
          let x = resolveFn(value)
          // 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
          // 这里resolve之后,就能被下一个.then()的回调获取到返回值,从而实现链式调用
          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 (error) {
          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) // this._value是上一个then回调return的值(见完整版代码)
          break;
        case REJECTED:
          rejectedFn(this._value)
          break;
      }
    });
  }; 
}

Promise.Finally()

//finally()方法返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。在finally之后,还可以继续then。并且会将值原封不动的传递给后面的then
//finally方法
finally(callback) {
	return this.then(
		value => MyPromise.resolve(callback()).then(() => value), // MyPromise.resolve执行回调,并在then中return结果传递给后面的Promise
		reason => MyPromise.resolve(callback()).then(() => { throw reason }
	) // reject同理
)}
// MyPromise.resolve(callback())的意义,这里补充解释一下:这个写法其实涉及到一个finally()的使用细节,finally()如果return了一个reject状态的Promise,将会改变当前Promise的状态,这个MyPromise.resolve就用于改变Promise状态,在finally()没有返回reject态Promise或throw错误的情况下,去掉MyPromise.resolve也是一样的

Promise.all()

// Promise.all(iterable)方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve)
// 如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。

//静态的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
          //所有then执行后, resolve结果
          if (index === promiseArr.length) {
            resolve(result)
          }
        },
        err => {
          //有一个Promise被reject时,MyPromise的状态变为reject
          reject(err)
        }
      )
    })
  })
}

Promise.race()

// Promise.race(iterable)方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

static race(promiseArr) {
  return new MyPromise((resolve, reject) => {
    //同时执行Promise,如果有一个Promise的状态发生改变,就变更新MyPromise的状态
    for (let p of promiseArr) {
      MyPromise.resolve(p).then( //Promise.resolve(p)用于处理传入值不为Promise的情况
        value => {
          resolve(value) //注意这个resolve是上边new MyPromise的
        },
        err => {
          reject(err)
        }
      )
    }
  })
}

Promise 和 async await实现

function asyncToGenerator(generatorFn) {
  return function () {
    var gen = generatorFn.apply(this, arguments);
    return new Promise(function (resolve, reject) {
      function step(key, arg) {
        try {
          var info = gen[key](arg);
          var value = info.value;
        } catch (error) {
          reject(error);
          return;
        }

        if (info.done) {
          resolve(value);
        } else {
          return Promise.resolve(value).then(function (value) {
            step('next', value);
          }, function (err) {
            step('throw', err);
          });
        }
      }
      return step('next');
    });
  }
}

手写bind的实现

// 高级:支持 new,例如 new (funcA.bind(thisArg, args))
function bind_3(asThis) {
  var slice = Array.prototype.slice;
  var args1 = slice.call(arguments, 1);
  var fn = this;
  if (typeof fn !== "function") {
    throw new Error("Must accept function");
  }
  function resultFn() {
    var args2 = slice.call(arguments, 0);
    return fn.apply(
      resultFn.prototype.isPrototypeOf(this) ? this : asThis, // 用来绑定 this
      args1.concat(args2)
    );
  }
  resultFn.prototype = fn.prototype;
  return resultFn;
}

手写深拷贝

class DeepClone {
  constructor() {
    this.cacheList = [];
  }
  clone(source) {
    if (source instanceof Object) {
      const cache = this.findCache(source);
      if (cache) return cache; // 如果找到缓存,直接返回
      else {
        let target;
        if (source instanceof Array) {
          target = new Array();
        } else if (source instanceof Function) {
          target = function () {
            return source.apply(this, arguments);
          };
        } else if (source instanceof Date) {
          target = new Date(source);
        } else if (source instanceof RegExp) {
          target = new RegExp(source.source, source.flags);
        }
        this.cacheList.push([source, target]); // 把源对象和新对象放进缓存列表
        for (let key in source) {
          if (source.hasOwnProperty(key)) { // 不拷贝原型上的属性,太浪费内存
            target[key] = this.clone(source[key]); // 递归克隆
          }
        }
        return target;
      }
    }
    return source;
  }
  findCache(source) {
    for (let i = 0; i < this.cacheList.length; ++i) {
      if (this.cacheList[i][0] === source) {
        return this.cacheList[i][1]; // 如果有环,返回对应的新对象
      }
    }
    return undefined;
  }
}

手写New

function _new(obj, ...rest) {
  // 基于Obj的原型创建一个新的对象
  const newObj = Object.create(obj.prototype)
  // 添加属性到新创建的newObj上, 并获取obj函数执行的结果.
  const result = obj.apply(newObj, rest);
  // 如果执行结果有返回值并且是一个对象, 返回执行的结果, 否则, 返回新创建的对象
  return typeof result === 'object' ? result : newObj;
}

手写 EventHub(发布-订阅)

核心思路是:

  1. 使用一个对象作为缓存
  2. on 负责把方法发布到缓存的 EventName 对应的数组
  3. emit 负责遍历触发(订阅) EventName 下的方法数组
  4. off 找方法的索引,并删除
class EventHub {
  cache = {};
  on(eventName, fn) {
    this.cache[eventName] = this.cache[eventName] || [];
    this.cache[eventName].push(fn);
  }
  emit(eventName) {
    this.cache[eventName].forEach((fn) => fn());
  }
  off(eventName, fn) {
    const index = indexOf(this.cache[eventName], fn); // 这里用this.cache[eventName].indexOf(fn) 完全可以,封装成函数是为了向下兼容
    if (index === -1) return;
    this.cache[eventName].splice(index, 1);
  }
}
// 兼容 IE 8 的 indexOf
function indexOf(arr, item) {
  if (arr === undefined) return -1;
  let index = -1;
  for (let i = 0; i < arr.length; ++i) {
    if (arr[i] === item) {
      index = i;
      break;
    }
  }
  return index;
}

手写Promise.any

Promise.any = promises => {
  return new Promise((resolve, reject) => {
    let hasOneResolve = false;
    let remaining = promise.length;
    const errors = [];
    for (let index in Promises) {
      Promises[index].then(data => {
        if (hasOneResolved) return
        hasOneResolved = true
        resolve(data)
      }, err => {
        if (hasOneResolved) return;
        remaining--;
        errors[index] = err;
        remaining || reject(errors)
      })
    }
  })
}

如何中断Promise

promise.then(fn).then()
// 补充fn
//fn
return new Promise((_, _) => { })

BFS和DFS

function deepTraversal(node) {
  let nodes = [];
  if (node != null) {
    nodes.push[node];
    let childrens = node.children
    for (let i = 0; i < childrens.length; i++) {
      deepTraversal(childrens[i]);
    }
  }
  return nodes;
}
function deepTraversal(node) {
  let nodes = []
  if (node != null) {
    //存放将来要访问的节点
    const stack = [];
    stack.push(node);
    while (stack.length != 0) {
      let item = stack.pop()
      nodes.push(item);
      let childrens = item.children;
      for (let i = children.length - 1; i >= 0; i--) {
        stack.push(childrens[i])
      }
    }
  }
  return nodes;
}

function wideTraversal(node) {
  let nodes = [];
  let i = 0;
  if (node != null) {
    nodes.push(node.nextElementSibling)
    node = nodes[i++]
    wideTraversal(node.firstElementChild)
  }
  return nodes;
}

function wideTraversal(node) {
  let nodes = [];
  let i = 0;
  while (node != null) {
    nodes.push(node)
    node = nodes[i++]
    let childrens = node.children;
    for (let i = 0; i < childrens.length; i++) {
      nodes.push(childrens[i])
    }
  }
  return nodes;
}
  • 0
    点赞
  • 0
    评论
  • 1
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:游动-白 设计师:我叫白小胖 返回首页

打赏作者

Silent Riddler

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值