滴滴前端一面常考手写面试题合集
- 2023-01-04  浙江
- 本文字数:12886 字 - 阅读完需:约 42 分钟 
使用 Promise 封装 AJAX 请求
// promise 封装实现:function getJSON(url) {  // 创建一个 promise 对象  let promise = new Promise(function(resolve, reject) {    let xhr = new XMLHttpRequest();    // 新建一个 http 请求    xhr.open("GET", url, true);    // 设置状态的监听函数    xhr.onreadystatechange = function() {      if (this.readyState !== 4) return;      // 当请求成功或失败时,改变 promise 的状态      if (this.status === 200) {        resolve(this.response);      } else {        reject(new Error(this.statusText));      }    };    // 设置错误监听函数    xhr.onerror = function() {      reject(new Error(this.statusText));    };    // 设置响应的数据类型    xhr.responseType = "json";    // 设置请求头信息    xhr.setRequestHeader("Accept", "application/json");    // 发送 http 请求    xhr.send(null);  });  return promise;}
树形结构转成列表(处理菜单)
[    {        id: 1,        text: '节点1',        parentId: 0,        children: [            {                id:2,                text: '节点1_1',                parentId:1            }        ]    }]转成[    {        id: 1,        text: '节点1',        parentId: 0 //这里用0表示为顶级节点    },    {        id: 2,        text: '节点1_1',        parentId: 1 //通过这个字段来确定子父级    }    ...]
实现代码如下:
function treeToList(data) {  let res = [];  const dfs = (tree) => {    tree.forEach((item) => {      if (item.children) {        dfs(item.children);        delete item.children;      }      res.push(item);    });  };  dfs(data);  return res;}
实现 prototype 继承
所谓的原型链继承就是让新实例的原型等于父类的实例:
//父方法function SupperFunction(flag1){    this.flag1 = flag1;}
//子方法function SubFunction(flag2){    this.flag2 = flag2;}
//父实例var superInstance = new SupperFunction(true);
//子继承父SubFunction.prototype = superInstance;
//子实例var subInstance = new SubFunction(false);//子调用自己和父的属性subInstance.flag1;   // truesubInstance.flag2;   // false
将 VirtualDom 转化为真实 DOM 结构
这是当前 SPA 应用的核心概念之一
// vnode结构:// {//   tag,//   attrs,//   children,// }
//Virtual DOM => DOMfunction render(vnode, container) {  container.appendChild(_render(vnode));}function _render(vnode) {  // 如果是数字类型转化为字符串  if (typeof vnode === 'number') {    vnode = String(vnode);  }  // 字符串类型直接就是文本节点  if (typeof vnode === 'string') {    return document.createTextNode(vnode);  }  // 普通DOM  const dom = document.createElement(vnode.tag);  if (vnode.attrs) {    // 遍历属性    Object.keys(vnode.attrs).forEach(key => {      const value = vnode.attrs[key];      dom.setAttribute(key, value);    })  }  // 子数组进行递归操作  vnode.children.forEach(child => render(child, dom));  return dom;}
Promise
// 模拟实现Promise// Promise利用三大手段解决回调地狱:// 1. 回调函数延迟绑定// 2. 返回值穿透// 3. 错误冒泡
// 定义三种状态const PENDING = 'PENDING';      // 进行中const FULFILLED = 'FULFILLED';  // 已成功const REJECTED = 'REJECTED';    // 已失败
class Promise {  constructor(exector) {    // 初始化状态    this.status = PENDING;    // 将成功、失败结果放在this上,便于then、catch访问    this.value = undefined;    this.reason = undefined;    // 成功态回调函数队列    this.onFulfilledCallbacks = [];    // 失败态回调函数队列    this.onRejectedCallbacks = [];
    const resolve = value => {      // 只有进行中状态才能更改状态      if (this.status === PENDING) {        this.status = FULFILLED;        this.value = value;        // 成功态函数依次执行        this.onFulfilledCallbacks.forEach(fn => fn(this.value));      }    }    const reject = reason => {      // 只有进行中状态才能更改状态      if (this.status === PENDING) {        this.status = REJECTED;        this.reason = reason;        // 失败态函数依次执行        this.onRejectedCallbacks.forEach(fn => fn(this.reason))      }    }    try {      // 立即执行executor      // 把内部的resolve和reject传入executor,用户可调用resolve和reject      exector(resolve, reject);    } catch(e) {      // executor执行出错,将错误内容reject抛出去      reject(e);    }  }  then(onFulfilled, onRejected) {    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;    onRejected = typeof onRejected === 'function'? onRejected :      reason => { throw new Error(reason instanceof Error ? reason.message : reason) }    // 保存this    const self = this;    return new Promise((resolve, reject) => {      if (self.status === PENDING) {        self.onFulfilledCallbacks.push(() => {          // try捕获错误          try {            // 模拟微任务            setTimeout(() => {              const result = onFulfilled(self.value);              // 分两种情况:              // 1. 回调函数返回值是Promise,执行then操作              // 2. 如果不是Promise,调用新Promise的resolve函数              result instanceof Promise ? result.then(resolve, reject) : resolve(result);            })          } catch(e) {            reject(e);          }        });        self.onRejectedCallbacks.push(() => {          // 以下同理          try {            setTimeout(() => {              const result = onRejected(self.reason);              // 不同点:此时是reject              result instanceof Promise ? result.then(resolve, reject) : resolve(result);            })          } catch(e) {            reject(e);          }        })      } else if (self.status === FULFILLED) {        try {          setTimeout(() => {            const result = onFulfilled(self.value);            result instanceof Promise ? result.then(resolve, reject) : resolve(result);          });        } catch(e) {          reject(e);        }      } else if (self.status === REJECTED) {        try {          setTimeout(() => {            const result = onRejected(self.reason);            result instanceof Promise ? result.then(resolve, reject) : resolve(result);          })        } catch(e) {          reject(e);        }      }    });  }  catch(onRejected) {    return this.then(null, onRejected);  }  static resolve(value) {    if (value instanceof Promise) {      // 如果是Promise实例,直接返回      return value;    } else {      // 如果不是Promise实例,返回一个新的Promise对象,状态为FULFILLED      return new Promise((resolve, reject) => resolve(value));    }  }  static reject(reason) {    return new Promise((resolve, reject) => {      reject(reason);    })  }  static all(promiseArr) {    const len = promiseArr.length;    const values = new Array(len);    // 记录已经成功执行的promise个数    let count = 0;    return new Promise((resolve, reject) => {      for (let i = 0; i < len; i++) {        // Promise.resolve()处理,确保每一个都是promise实例        Promise.resolve(promiseArr[i]).then(          val => {            values[i] = val;            count++;            // 如果全部执行完,返回promise的状态就可以改变了            if (count === len) resolve(values);          },          err => reject(err),        );      }    })  }  static race(promiseArr) {    return new Promise((resolve, reject) => {      promiseArr.forEach(p => {        Promise.resolve(p).then(          val => resolve(val),          err => reject(err),        )      })    })  }}
封装异步的 fetch,使用 async await 方式来使用
(async () => {    class HttpRequestUtil {        async get(url) {            const res = await fetch(url);            const data = await res.json();            return data;        }        async post(url, data) {            const res = await fetch(url, {                method: 'POST',                headers: {                    'Content-Type': 'application/json'                },                body: JSON.stringify(data)            });            const result = await res.json();            return result;        }        async put(url, data) {            const res = await fetch(url, {                method: 'PUT',                headers: {                    'Content-Type': 'application/json'                },                data: JSON.stringify(data)            });            const result = await res.json();            return result;        }        async delete(url, data) {            const res = await fetch(url, {                method: 'DELETE',                headers: {                    'Content-Type': 'application/json'                },                data: JSON.stringify(data)            });            const result = await res.json();            return result;        }    }    const httpRequestUtil = new HttpRequestUtil();    const res = await httpRequestUtil.get('http://golderbrother.cn/');    console.log(res);})();
参考 前端进阶面试题详细解答
深拷贝
递归的完整版本(考虑到了 Symbol 属性):
const cloneDeep1 = (target, hash = new WeakMap()) => {  // 对于传入参数处理  if (typeof target !== 'object' || target === null) {    return target;  }  // 哈希表中存在直接返回  if (hash.has(target)) return hash.get(target);
  const cloneTarget = Array.isArray(target) ? [] : {};  hash.set(target, cloneTarget);
  // 针对Symbol属性  const symKeys = Object.getOwnPropertySymbols(target);  if (symKeys.length) {    symKeys.forEach(symKey => {      if (typeof target[symKey] === 'object' && target[symKey] !== null) {        cloneTarget[symKey] = cloneDeep1(target[symKey]);      } else {        cloneTarget[symKey] = target[symKey];      }    })  }
  for (const i in target) {    if (Object.prototype.hasOwnProperty.call(target, i)) {      cloneTarget[i] =        typeof target[i] === 'object' && target[i] !== null        ? cloneDeep1(target[i], hash)        : target[i];    }  }  return cloneTarget;}
实现一个函数判断数据类型
function getType(obj) {   if (obj === null) return String(obj);   return typeof obj === 'object'    ? Object.prototype.toString.call(obj).replace('[object ', '').replace(']', '').toLowerCase()   : typeof obj;}
// 调用getType(null); // -> nullgetType(undefined); // -> undefinedgetType({}); // -> objectgetType([]); // -> arraygetType(123); // -> numbergetType(true); // -> booleangetType('123'); // -> stringgetType(/123/); // -> regexpgetType(new Date()); // -> date
Array.prototype.reduce()
Array.prototype.reduce = function(callback, initialValue) {  if (this == undefined) {    throw new TypeError('this is null or not defined');  }  if (typeof callback !== 'function') {    throw new TypeError(callbackfn + ' is not a function');  }  const O = Object(this);  const len = this.length >>> 0;  let accumulator = initialValue;  let k = 0;  // 如果第二个参数为undefined的情况下  // 则数组的第一个有效值作为累加器的初始值  if (accumulator === undefined) {    while (k < len && !(k in O)) {      k++;    }    // 如果超出数组界限还没有找到累加器的初始值,则TypeError    if (k >= len) {      throw new TypeError('Reduce of empty array with no initial value');    }    accumulator = O[k++];  }  while (k < len) {    if (k in O) {      accumulator = callback.call(undefined, accumulator, O[k], k, O);    }    k++;  }  return accumulator;}
二叉树层次遍历
// 二叉树层次遍历
class Node {  constructor(element, parent) {    this.parent = parent // 父节点     this.element = element // 当前存储内容    this.left = null // 左子树    this.right = null // 右子树  }}
class BST {  constructor(compare) {    this.root = null // 树根    this.size = 0 // 树中的节点个数
    this.compare = compare || this.compare  }  compare(a,b) {    return a - b  }  add(element) {    if(this.root === null) {      this.root = new Node(element, null)      this.size++      return    }    // 获取根节点 用当前添加的进行判断 放左边还是放右边    let currentNode = this.root     let compare    let parent = null     while (currentNode) {      compare = this.compare(element, currentNode.element)      parent = currentNode // 先将父亲保存起来      // currentNode要不停的变化      if(compare > 0) {        currentNode = currentNode.right      } else if(compare < 0) {        currentNode = currentNode.left      } else {        currentNode.element = element // 相等时 先覆盖后续处理      }    }
    let newNode = new Node(element, parent)    if(compare > 0) {      parent.right = newNode    } else if(compare < 0) {      parent.left = newNode    }
    this.size++  }  // 层次遍历 队列  levelOrderTraversal(visitor) {    if(this.root == null) {      return    }    let stack = [this.root]    let index = 0 // 指针 指向0    let currentNode     while (currentNode = stack[index++]) {      // 反转二叉树      let tmp = currentNode.left      currentNode.left = currentNode.right      currentNode.right = tmp      visitor.visit(currentNode.element)      if(currentNode.left) {        stack.push(currentNode.left)      }      if(currentNode.right) {        stack.push(currentNode.right)      }    }  }}
// 测试var bst = new BST((a,b)=>a.age-b.age) // 模拟sort方法
// // bst.add({age: 10})bst.add({age: 8})bst.add({age:19})bst.add({age:6})bst.add({age: 15})bst.add({age: 22})bst.add({age: 20})
// 使用访问者模式class Visitor {  constructor() {    this.visit = function (elem) {      elem.age = elem.age*2    }  }}
// console.log(bst.levelOrderTraversal(new Visitor()))
使用 setTimeout 实现 setInterval
setInterval 的作用是每隔一段指定时间执行一个函数,但是这个执行不是真的到了时间立即执行,它真正的作用是每隔一段时间将事件加入事件队列中去,只有当当前的执行栈为空的时候,才能去从事件队列中取出事件执行。所以可能会出现这样的情况,就是当前执行栈执行的时间很长,导致事件队列里边积累多个定时器加入的事件,当执行栈结束的时候,这些事件会依次执行,因此就不能到间隔一段时间执行的效果。
针对 setInterval 的这个缺点,我们可以使用 setTimeout 递归调用来模拟 setInterval,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval 的问题。
实现思路是使用递归函数,不断地去执行 setTimeout 从而达到 setInterval 的效果
function mySetInterval(fn, timeout) {  // 控制器,控制定时器是否继续执行  var timer = {    flag: true  };  // 设置递归函数,模拟定时器执行。  function interval() {    if (timer.flag) {      fn();      setTimeout(interval, timeout);    }  }  // 启动定时器  setTimeout(interval, timeout);  // 返回控制器  return timer;}
Array.prototype.forEach()
Array.prototype.forEach = function(callback, thisArg) {  if (this == null) {    throw new TypeError('this is null or not defined');  }  if (typeof callback !== "function") {    throw new TypeError(callback + ' is not a function');  }  const O = Object(this);  const len = O.length >>> 0;  let k = 0;  while (k < len) {    if (k in O) {      callback.call(thisArg, O[k], k, O);    }    k++;  }}
实现防抖函数(debounce)
防抖函数原理:在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。
那么与节流函数的区别直接看这个动画实现即可。
手写简化版:
// 防抖函数const debounce = (fn, delay) => {  let timer = null;  return (...args) => {    clearTimeout(timer);    timer = setTimeout(() => {      fn.apply(this, args);    }, delay);  };};
适用场景:
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次 
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似 
生存环境请用 lodash.debounce
实现字符串的 repeat 方法
输入字符串 s,以及其重复的次数,输出重复的结果,例如输入 abc,2,输出 abcabc。
function repeat(s, n) {    return (new Array(n + 1)).join(s);}
递归:
function repeat(s, n) {    return (n > 0) ? s.concat(repeat(s, --n)) : "";}
对象数组列表转成树形结构(处理菜单)
[    {        id: 1,        text: '节点1',        parentId: 0 //这里用0表示为顶级节点    },    {        id: 2,        text: '节点1_1',        parentId: 1 //通过这个字段来确定子父级    }    ...]
转成[    {        id: 1,        text: '节点1',        parentId: 0,        children: [            {                id:2,                text: '节点1_1',                parentId:1            }        ]    }]
实现代码如下:
function listToTree(data) {  let temp = {};  let treeData = [];  for (let i = 0; i < data.length; i++) {    temp[data[i].id] = data[i];  }  for (let i in temp) {    if (+temp[i].parentId != 0) {      if (!temp[temp[i].parentId].children) {        temp[temp[i].parentId].children = [];      }      temp[temp[i].parentId].children.push(temp[i]);    } else {      treeData.push(temp[i]);    }  }  return treeData;}
Function.prototype.bind
Function.prototype.bind = function(context, ...args) {  if (typeof this !== 'function') {    throw new Error("Type Error");  }  // 保存this的值  var self = this;
  return function F() {    // 考虑new的情况    if(this instanceof F) {      return new self(...args, ...arguments)    }    return self.apply(context, [...args, ...arguments])  }}
实现一个链表结构
链表结构
 
 看图理解 next 层级
 
 // 链表 从头尾删除、增加 性能比较好// 分为很多类 常用单向链表、双向链表
// js模拟链表结构:增删改查
// node节点class Node {  constructor(element,next) {    this.element = element    this.next = next  } }
class LinkedList { constructor() {   this.head = null // 默认应该指向第一个节点   this.size = 0 // 通过这个长度可以遍历这个链表 } // 增加O(n) add(index,element) {   if(arguments.length === 1) {     // 向末尾添加     element = index // 当前元素等于传递的第一项     index = this.size // 索引指向最后一个元素   }  if(index < 0 || index > this.size) {    throw new Error('添加的索引不正常')  }  if(index === 0) {    // 直接找到头部 把头部改掉 性能更好    let head = this.head    this.head = new Node(element,head)  } else {    // 获取当前头指针    let current = this.head    // 不停遍历 直到找到最后一项 添加的索引是1就找到第0个的next赋值    for (let i = 0; i < index-1; i++) { // 找到它的前一个      current = current.next    }    // 让创建的元素指向上一个元素的下一个    // 看图理解next层级    current.next = new Node(element,current.next) // 让当前元素指向下一个元素的next  }
  this.size++; } // 删除O(n) remove(index) {  if(index < 0 || index >= this.size) {    throw new Error('删除的索引不正常')  }  this.size--  if(index === 0) {    let head = this.head    this.head = this.head.next // 移动指针位置
    return head // 返回删除的元素  }else {    let current = this.head    for (let i = 0; i < index-1; i++) { // index-1找到它的前一个      current = current.next    }    let returnVal = current.next // 返回删除的元素    // 找到待删除的指针的上一个 current.next.next     // 如删除200, 100=>200=>300 找到200的上一个100的next的next为300,把300赋值给100的next即可    current.next = current.next.next 
    return returnVal  } } // 查找O(n) get(index) {  if(index < 0 || index >= this.size) {    throw new Error('查找的索引不正常')  }  let current = this.head  for (let i = 0; i < index; i++) {    current = current.next  }  return current }}
var ll = new LinkedList()
ll.add(0,100) // Node { ellement: 100, next: null }ll.add(0,200) // Node { element: 200, next: Node { element: 100, next: null } }ll.add(1,500) // Node {element: 200,next: Node { element: 100, next: Node { element: 500, next: null } } }ll.add(300)ll.remove(0)
console.log(ll.get(2),'get')console.log(ll.head)
module.exports = LinkedList
交换 a,b 的值,不能用临时变量
巧妙的利用两个数的和、差:
a = a + bb = a - ba = a - b
实现 instanceOf
// 模拟 instanceoffunction instance_of(L, R) {  //L 表示左表达式,R 表示右表达式  var O = R.prototype; // 取 R 的显示原型  L = L.__proto__; // 取 L 的隐式原型  while (true) {    if (L === null) return false;    if (O === L)      // 这里重点:当 O 严格等于 L 时,返回 true      return true;    L = L.__proto__;  }}
实现一个迷你版的 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>

还未添加个人签名 2022-07-31 加入
还未添加个人简介










 
    
评论