写点什么

2022 前端常考手写面试题总结

  • 2022-11-10
    浙江
  • 本文字数:11700 字

    阅读完需:约 38 分钟

实现 JSON.parse

var json = '{"name":"cxk", "age":25}';var obj = eval("(" + json + ")");
复制代码


此方法属于黑魔法,极易容易被 xss 攻击,还有一种new Function大同小异。

封装异步的 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);})();
复制代码

树形结构转成列表(处理菜单)

[    {        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;}
复制代码

Function.prototype.call

call唯一不同的是,call()方法接受的是一个参数列表


Function.prototype.call = function(context = window, ...args) {  if (typeof this !== 'function') {    throw new TypeError('Type Error');  }  const fn = Symbol('fn');  context[fn] = this;
const res = context[fn](...args); delete context[fn]; return res;}
复制代码


参考:前端手写面试题详细解答

实现发布-订阅模式

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] } } }}
复制代码

验证是否是邮箱

function isEmail(email) {    var regx = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/;    return regx.test(email);}
复制代码

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]) }}
复制代码

实现每隔一秒打印 1,2,3,4

// 使用闭包实现for (var i = 0; i < 5; i++) {  (function(i) {    setTimeout(function() {      console.log(i);    }, i * 1000);  })(i);}// 使用 let 块级作用域for (let i = 0; i < 5; i++) {  setTimeout(function() {    console.log(i);  }, i * 1000);}
复制代码

手写常见排序

冒泡排序

冒泡排序的原理如下,从第一个元素开始,把当前元素和下一个索引元素进行比较。如果当前元素大,那么就交换位置,重复操作直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操作,但是此时最后一个元素已经是最大数了,所以不需要再比较最后一个元素,只需要比较到 length - 1 的位置。


function bubbleSort(list) {  var n = list.length;  if (!n) return [];
for (var i = 0; i < n; i++) { // 注意这里需要 n - i - 1 for (var j = 0; j < n - i - 1; j++) { if (list[j] > list[j + 1]) { var temp = list[j + 1]; list[j + 1] = list[j]; list[j] = temp; } } } return list;}
复制代码

快速排序

快排的原理如下。随机选取一个数组中的值作为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。然后将数组以基准值的位置分为两部分,继续递归以上操作


ffunction quickSort(arr) {  if (arr.length<=1){    return arr;  }  var baseIndex = Math.floor(arr.length/2);//向下取整,选取基准点  var base = arr.splice(baseIndex,1)[0];//取出基准点的值,  // splice 通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。  // slice方法返回一个新的数组对象,不会更改原数组  //这里不能直接base=arr[baseIndex],因为base代表的每次都删除的那个数  var left=[];  var right=[];  for (var i = 0; i<arr.length; i++){    // 这里的length是变化的,因为splice会改变原数组。    if (arr[i] < base){      left.push(arr[i]);//比基准点小的放在左边数组,    }  }else{    right.push(arr[i]);//比基准点大的放在右边数组,  }  return quickSort(left).concat([base],quickSort(right));}
复制代码

选择排序

function selectSort(arr) {  // 缓存数组长度  const len = arr.length;  // 定义 minIndex,缓存当前区间最小值的索引,注意是索引  let minIndex;  // i 是当前排序区间的起点  for (let i = 0; i < len - 1; i++) {    // 初始化 minIndex 为当前区间第一个元素    minIndex = i;    // i、j分别定义当前区间的上下界,i是左边界,j是右边界    for (let j = i; j < len; j++) {      // 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j      if (arr[j] < arr[minIndex]) {        minIndex = j;      }    }    // 如果 minIndex 对应元素不是目前的头部元素,则交换两者    if (minIndex !== i) {      [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];    }  }  return arr;}// console.log(selectSort([3, 6, 2, 4, 1]));
复制代码

插入排序

function insertSort(arr) {  for (let i = 1; i < arr.length; i++) {    let j = i;    let target = arr[j];    while (j > 0 && arr[j - 1] > target) {      arr[j] = arr[j - 1];      j--;    }    arr[j] = target;  }  return arr;}// console.log(insertSort([3, 6, 2, 4, 1]));
复制代码

递归反转链表

// 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层级 ![](http://img-repo.poetries.top/images/20210522115056.png) 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 } reverse() { const reverse = head=>{ if(head == null || head.next == null) { return head } let newHead = reverse(head.next) // 从这个链表的最后一个开始反转,让当前下一个元素的next指向自己,自己指向null // ![](http://img-repo.poetries.top/images/20210522161710.png) // 刚开始反转的是最后两个 head.next.next = head head.next = null
return newHead } return reverse(this.head) }}
let ll = new LinkedList()
ll.add(1)ll.add(2)ll.add(3)ll.add(4)
// console.dir(ll,{depth: 1000})
console.log(ll.reverse())
复制代码

图片懒加载

// <img src="default.png" data-src="https://xxxx/real.png">function isVisible(el) {  const position = el.getBoundingClientRect()  const windowHeight = document.documentElement.clientHeight  // 顶部边缘可见  const topVisible = position.top > 0 && position.top < windowHeight;  // 底部边缘可见  const bottomVisible = position.bottom < windowHeight && position.bottom > 0;  return topVisible || bottomVisible;}
function imageLazyLoad() { const images = document.querySelectorAll('img') for (let img of images) { const realSrc = img.dataset.src if (!realSrc) continue if (isVisible(img)) { img.src = realSrc img.dataset.src = '' } }}
// 测试window.addEventListener('load', imageLazyLoad)window.addEventListener('scroll', imageLazyLoad)// orwindow.addEventListener('scroll', throttle(imageLazyLoad, 1000))
复制代码

分片思想解决大数据量渲染问题

题目描述: 渲染百万条结构简单的大数据时 怎么使用分片思想优化渲染


let ul = document.getElementById("container");// 插入十万条数据let total = 100000;// 一次插入 20 条let once = 20;//总页数let page = total / once;//每条记录的索引let index = 0;//循环加载数据function loop(curTotal, curIndex) {  if (curTotal <= 0) {    return false;  }  //每页多少条  let pageCount = Math.min(curTotal, once);  window.requestAnimationFrame(function () {    for (let i = 0; i < pageCount; i++) {      let li = document.createElement("li");      li.innerText = curIndex + i + " : " + ~~(Math.random() * total);      ul.appendChild(li);    }    loop(curTotal - pageCount, curIndex + pageCount);  });}loop(total, index);
复制代码


扩展思考 :对于大数据量的简单 dom 结构渲染可以用分片思想解决 如果是复杂的 dom 结构渲染如何处理?


这时候就需要使用虚拟列表了,虚拟列表和虚拟表格在日常项目使用还是很多的

请实现一个 add 函数,满足以下功能

add(1);             // 1add(1)(2);      // 3add(1)(2)(3);// 6add(1)(2, 3); // 6add(1, 2)(3); // 6add(1, 2, 3); // 6
复制代码


function add(...args) {  // 在内部声明一个函数,利用闭包的特性保存并收集所有的参数值  let fn = function(...newArgs) {   return add.apply(null, args.concat(newArgs))  }
// 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回 fn.toString = function() { return args.reduce((total,curr)=> total + curr) }
return fn}
复制代码


考点:


  • 使用闭包, 同时要对 JavaScript 的作用域链(原型链)有深入的理解

  • 重写函数的 toSting()方法


// 测试,调用toString方法触发求值
add(1).toString(); // 1add(1)(2).toString(); // 3add(1)(2)(3).toString();// 6add(1)(2, 3).toString(); // 6add(1, 2)(3).toString(); // 6add(1, 2, 3).toString(); // 6
复制代码

二叉树搜索

// 二叉搜索树
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++ }}
复制代码




// 测试var bst = new BST((a,b)=>b.age-a.age) // 模拟sort方法
bst.add({age: 10})bst.add({age: 8})bst.add({age:19})bst.add({age:20})bst.add({age: 5})
console.log(bst)
复制代码

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

异步串行 | 异步并行

// 字节面试题,实现一个异步加法function asyncAdd(a, b, callback) {  setTimeout(function () {    callback(null, a + b);  }, 500);}
// 解决方案// 1. promisifyconst promiseAdd = (a, b) => new Promise((resolve, reject) => { asyncAdd(a, b, (err, res) => { if (err) { reject(err) } else { resolve(res) } })})
// 2. 串行处理async function serialSum(...args) { return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))}
// 3. 并行处理async function parallelSum(...args) { if (args.length === 1) return args[0] const tasks = [] for (let i = 0; i < args.length; i += 2) { tasks.push(promiseAdd(args[i], args[i + 1] || 0)) } const results = await Promise.all(tasks) return parallelSum(...results)}
// 测试(async () => { console.log('Running...'); const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12) console.log(res1) const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12) console.log(res2) console.log('Done');})()
复制代码

二叉树层次遍历

// 二叉树层次遍历
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方法
// ![](http://img-repo.poetries.top/images/20210522203619.png)// ![](http://img-repo.poetries.top/images/20210522211809.png)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 } }}
// ![](http://img-repo.poetries.top/images/20210523095515.png)console.log(bst.levelOrderTraversal(new Visitor()))
复制代码

实现 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__;  }}
复制代码

怎么在制定数据源里面生成一个长度为 n 的不重复随机数组 能有几种方法 时间复杂度多少(字节)

第一版 时间复杂度为 O(n^2)


function getTenNum(testArray, n) {  let result = [];  for (let i = 0; i < n; ++i) {    const random = Math.floor(Math.random() * testArray.length);    const cur = testArray[random];    if (result.includes(cur)) {      i--;      break;    }    result.push(cur);  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 10);
复制代码


第二版 标记法 / 自定义属性法 时间复杂度为 O(n)


function getTenNum(testArray, n) {  let hash = {};  let result = [];  let ranNum = n;  while (ranNum > 0) {    const ran = Math.floor(Math.random() * testArray.length);    if (!hash[ran]) {      hash[ran] = true;      result.push(ran);      ranNum--;    }  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 10);
复制代码


第三版 交换法 时间复杂度为 O(n)


function getTenNum(testArray, n) {  const cloneArr = [...testArray];  let result = [];  for (let i = 0; i < n; i++) {    debugger;    const ran = Math.floor(Math.random() * (cloneArr.length - i));    result.push(cloneArr[ran]);    cloneArr[ran] = cloneArr[cloneArr.length - i - 1];  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 14);
复制代码


值得一提的是操作数组的时候使用交换法 这种思路在算法里面很常见


最终版 边遍历边删除 时间复杂度为 O(n)


function getTenNum(testArray, n) {  const cloneArr = [...testArray];  let result = [];  for (let i = 0; i < n; ++i) {    const random = Math.floor(Math.random() * cloneArr.length);    const cur = cloneArr[random];    result.push(cur);    cloneArr.splice(random, 1);  }  return result;}const testArray = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];const resArr = getTenNum(testArray, 14);
复制代码

实现 add(1)(2)(3)

函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。


1)粗暴版


function add (a) {return function (b) {     return function (c) {      return a + b + c;     }}}console.log(add(1)(2)(3)); // 6
复制代码


2)柯里化解决方案


  • 参数长度固定


var add = function (m) {  var temp = function (n) {    return add(m + n);  }  temp.toString = function () {    return m;  }  return temp;};console.log(add(3)(4)(5)); // 12console.log(add(3)(6)(9)(25)); // 43
复制代码


对于 add(3)(4)(5),其执行过程如下:


  1. 先执行 add(3),此时 m=3,并且返回 temp 函数;

  2. 执行 temp(4),这个函数内执行 add(m+n),n 是此次传进来的数值 4,m 值还是上一步中的 3,所以 add(m+n)=add(3+4)=add(7),此时 m=7,并且返回 temp 函数

  3. 执行 temp(5),这个函数内执行 add(m+n),n 是此次传进来的数值 5,m 值还是上一步中的 7,所以 add(m+n)=add(7+5)=add(12),此时 m=12,并且返回 temp 函数

  4. 由于后面没有传入参数,等于返回的 temp 函数不被执行而是打印,了解 JS 的朋友都知道对象的 toString 是修改对象转换字符串的方法,因此代码中 temp 函数的 toString 函数 return m 值,而 m 值是最后一步执行函数时的值 m=12,所以返回值是 12。


  • 参数长度不固定


function add (...args) {    //求和    return args.reduce((a, b) => a + b)}function currying (fn) {    let args = []    return function temp (...newArgs) {        if (newArgs.length) {            args = [                ...args,                ...newArgs            ]            return temp        } else {            let val = fn.apply(this, args)            args = [] //保证再次调用时清空            return val        }    }}let addCurry = currying(add)console.log(addCurry(1)(2)(3)(4, 5)())  //15console.log(addCurry(1)(2)(3, 4, 5)())  //15console.log(addCurry(1)(2, 3, 4, 5)())  //15
复制代码


用户头像

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

还未添加个人简介

评论

发布
暂无评论
2022前端常考手写面试题总结_JavaScript_helloworld1024fd_InfoQ写作社区