写点什么

社招前端常考手写面试题总结

  • 2022-12-16
    浙江
  • 本文字数:11491 字

    阅读完需:约 38 分钟

手写 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); }};
复制代码

手写 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;};
复制代码

实现节流函数(throttle)

节流函数原理:指频繁触发事件时,只会在指定的时间段内执行事件回调,即触发事件间隔大于等于指定的时间才会执行回调函数。总结起来就是: 事件,按照一段时间的间隔来进行触发



像 dom 的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多


手写简版


使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行


时间戳方式:


// func是用户传入需要防抖的函数// wait是等待时间const throttle = (func, wait = 50) => {  // 上一次执行该函数的时间  let lastTime = 0  return function(...args) {    // 当前时间    let now = +new Date()    // 将当前时间和上一次执行函数时间对比    // 如果差值大于设置的等待时间就执行函数    if (now - lastTime > wait) {      lastTime = now      func.apply(this, args)    }  }}
setInterval( throttle(() => { console.log(1) }, 500), 1)
复制代码


定时器方式:


使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数


function throttle(func, delay){  var timer = null;  returnfunction(){    var context = this;    var args = arguments;    if(!timer){      timer = setTimeout(function(){        func.apply(context, args);        timer = null;      },delay);    }  }}
复制代码


适用场景:


  • DOM 元素的拖拽功能实现(mousemove

  • 搜索联想(keyup

  • 计算鼠标移动的距离(mousemove

  • Canvas 模拟画板功能(mousemove

  • 监听滚动事件判断是否到页面底部自动加载更多

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动

  • 缩放场景:监控浏览器resize

  • 动画场景:避免短时间内多次触发动画引起性能问题


总结


  • 函数防抖 :将几次操作合并为一次操作进行。原理是维护一个计时器,规定在 delay 时间后触发函数,但是在 delay 时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

  • 函数节流 :使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

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

Promise.race

Promise.race = function(promiseArr) {  return new Promise((resolve, reject) => {    promiseArr.forEach(p => {      // 如果不是Promise实例需要转化为Promise实例      Promise.resolve(p).then(        val => resolve(val),        err => reject(err),      )    })  })}
复制代码

实现非负大整数相加

JavaScript 对数值有范围的限制,限制如下:


Number.MAX_VALUE // 1.7976931348623157e+308Number.MAX_SAFE_INTEGER // 9007199254740991Number.MIN_VALUE // 5e-324Number.MIN_SAFE_INTEGER // -9007199254740991
复制代码


如果想要对一个超大的整数(> Number.MAX_SAFE_INTEGER)进行加法运算,但是又想输出一般形式,那么使用 + 是无法达到的,一旦数字超过 Number.MAX_SAFE_INTEGER 数字会被立即转换为科学计数法,并且数字精度相比以前将会有误差。


实现一个算法进行大数的相加:


function sumBigNumber(a, b) {  let res = '';  let temp = 0;
a = a.split(''); b = b.split('');
while (a.length || b.length || temp) { temp += ~~a.pop() + ~~b.pop(); res = (temp % 10) + res; temp = temp > 9 } return res.replace(/^0+/, '');}
复制代码


其主要的思路如下:


  • 首先用字符串的方式来保存大数,这样数字在数学表示上就不会发生变化

  • 初始化 res,temp 来保存中间的计算结果,并将两个字符串转化为数组,以便进行每一位的加法运算

  • 将两个数组的对应的位进行相加,两个数相加的结果可能大于 10,所以可能要仅为,对 10 进行取余操作,将结果保存在当前位

  • 判断当前位是否大于 9,也就是是否会进位,若是则将 temp 赋值为 true,因为在加法运算中,true 会自动隐式转化为 1,以便于下一次相加

  • 重复上述操作,直至计算结束


参考 前端进阶面试题详细解答

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

实现一个 call

call 做了什么:


  • 将函数设为对象的属性

  • 执行 &删除这个函数

  • 指定 this 到函数并传入给定参数执行函数

  • 如果不传入参数,默认指向为 window


// 模拟 call bar.mycall(null);//实现一个call方法:Function.prototype.myCall = function(context) {  //此处没有考虑context非object情况  context.fn = this;  let args = [];  for (let i = 1, len = arguments.length; i < len; i++) {    args.push(arguments[i]);  }  context.fn(...args);  let result = context.fn(...args);  delete context.fn;  return result;};
复制代码

使用 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;}
复制代码

实现一个管理本地缓存过期的函数

封装一个可以设置过期时间的localStorage存储函数


class Storage{  constructor(name){      this.name = 'storage';  }  //设置缓存  setItem(params){      let obj = {          name:'', // 存入数据  属性          value:'',// 属性值          expires:"", // 过期时间          startTime:new Date().getTime()//记录何时将值存入缓存,毫秒级      }      let options = {};      //将obj和传进来的params合并      Object.assign(options,obj,params);      if(options.expires){      //如果options.expires设置了的话      //以options.name为key,options为值放进去          localStorage.setItem(options.name,JSON.stringify(options));      }else{      //如果options.expires没有设置,就判断一下value的类型          let type = Object.prototype.toString.call(options.value);          //如果value是对象或者数组对象的类型,就先用JSON.stringify转一下,再存进去          if(Object.prototype.toString.call(options.value) == '[object Object]'){              options.value = JSON.stringify(options.value);          }          if(Object.prototype.toString.call(options.value) == '[object Array]'){              options.value = JSON.stringify(options.value);          }          localStorage.setItem(options.name,options.value);      }  }  //拿到缓存  getItem(name){      let item = localStorage.getItem(name);      //先将拿到的试着进行json转为对象的形式      try{          item = JSON.parse(item);      }catch(error){      //如果不行就不是json的字符串,就直接返回          item = item;      }      //如果有startTime的值,说明设置了失效时间      if(item.startTime){          let date = new Date().getTime();          //何时将值取出减去刚存入的时间,与item.expires比较,如果大于就是过期了,如果小于或等于就还没过期          if(date - item.startTime > item.expires){          //缓存过期,清除缓存,返回false              localStorage.removeItem(name);              return false;          }else{          //缓存未过期,返回值              return item.value;          }      }else{      //如果没有设置失效时间,直接返回值          return item;      }  }  //移出缓存  removeItem(name){      localStorage.removeItem(name);  }  //移出全部缓存  clear(){      localStorage.clear();  }}
复制代码


用法


let storage = new Storage();storage.setItem({  name:"name",  value:"ppp"})
复制代码


下面我把值取出来


let value = storage.getItem('name');console.log('我是value',value);
复制代码


设置 5 秒过期


let storage = new Storage();storage.setItem({  name:"name",  value:"ppp",  expires: 5000})
复制代码


// 过期后再取出来会变为 falselet value = storage.getItem('name');console.log('我是value',value);
复制代码

实现一个 JS 函数柯里化

预先处理的思想,利用闭包的机制

  • 柯里化的定义:接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数

  • 函数柯里化的主要作用和特点就是参数复用提前返回延迟执行


  • 柯里化把多次传入的参数合并,柯里化是一个高阶函数

  • 每次都返回一个新函数

  • 每次入参都是一个


当柯里化函数接收到足够参数后,就会执行原函数,如何去确定何时达到足够的参数呢?


有两种思路:


  • 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数

  • 在调用柯里化工具函数时,手动指定所需的参数个数


将这两点结合一下,实现一个简单 curry 函数


通用版


// 写法1function curry(fn, args) {  var length = fn.length;  var args = args || [];  return function(){      newArgs = args.concat(Array.prototype.slice.call(arguments));      if (newArgs.length < length) {          return curry.call(this,fn,newArgs);      }else{          return fn.apply(this,newArgs);      }  }}
复制代码


// 写法2// 分批传入参数// redux 源码的compose也是用了类似柯里化的操作const curry = (fn, arr = []) => {// arr就是我们要收集每次调用时传入的参数  let len = fn.length; // 函数的长度,就是参数的个数
return function(...args) { let newArgs = [...arr, ...args] // 收集每次传入的参数
// 如果传入的参数个数等于我们指定的函数参数个数,就执行指定的真正函数 if(newArgs.length === len) { return fn(...newArgs) } else { // 递归收集参数 return curry(fn, newArgs) } }}
复制代码


// 测试function multiFn(a, b, c) {  return a * b * c;}
var multi = curry(multiFn);
multi(2)(3)(4);multi(2,3,4);multi(2)(3,4);multi(2,3)(4)
复制代码


ES6 写法


const curry = (fn, arr = []) => (...args) => (  arg => arg.length === fn.length    ? fn(...arg)    : curry(fn, arg))([...arr, ...args])
复制代码


// 测试let curryTest=curry((a,b,c,d)=>a+b+c+d)curryTest(1,2,3)(4) //返回10curryTest(1,2)(4)(3) //返回10curryTest(1,2)(3,4) //返回10
复制代码


// 柯里化求值// 指定的函数function sum(a,b,c,d,e) {  return a + b + c + d + e}
// 传入指定的函数,执行一次let newSum = curry(sum)
// 柯里化 每次入参都是一个参数newSum(1)(2)(3)(4)(5)
// 偏函数newSum(1)(2)(3,4,5)
复制代码


// 柯里化简单应用// 判断类型,参数多少个,就执行多少次收集function isType(type, val) {  return Object.prototype.toString.call(val) === `[object ${type}]`}
let newType = curry(isType)
// 相当于把函数参数一个个传了,把第一次先缓存起来let isString = newType('String')let isNumber = newType('Number')
isString('hello world')isNumber(999)
复制代码

实现每隔一秒打印 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);}
复制代码

版本号排序的方法

题目描述:有一组版本号如下 ['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。现在需要对其进行排序,排序的结果为 ['4.3.5','4.3.4.5','2.3.3','0.302.1','0.1.1']


arr.sort((a, b) => {  let i = 0;  const arr1 = a.split(".");  const arr2 = b.split(".");
while (true) { const s1 = arr1[i]; const s2 = arr2[i]; i++; if (s1 === undefined || s2 === undefined) { return arr2.length - arr1.length; }
if (s1 === s2) continue;
return s2 - s1; }});console.log(arr);
复制代码

二叉树深度遍历

// 二叉树深度遍历
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++ } // 前序遍历 preorderTraversal(visitor) { const traversal = node=>{ if(node === null) return visitor.visit(node.element) traversal(node.left) traversal(node.right) } traversal(this.root) } // 中序遍历 inorderTraversal(visitor) { const traversal = node=>{ if(node === null) return traversal(node.left) visitor.visit(node.element) traversal(node.right) } traversal(this.root) } // 后序遍历 posterorderTraversal(visitor) { const traversal = node=>{ if(node === null) return traversal(node.left) traversal(node.right) visitor.visit(node.element) } traversal(this.root) } // 反转二叉树:无论先序、中序、后序、层级都可以反转 invertTree() { const traversal = node=>{ if(node === null) return let temp = node.left node.left = node.right node.right = temp traversal(node.left) traversal(node.right) } traversal(this.root) return this.root }}
复制代码


先序遍历



二叉树的遍历方式



// 测试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})
// 先序遍历// console.log(bst.preorderTraversal(),'先序遍历')// console.log(bst.inorderTraversal(),'中序遍历')// ![](http://img-repo.poetries.top/images/20210522214837.png)// console.log(bst.posterorderTraversal(),'后序遍历')

// 深度遍历:先序遍历、中序遍历、后续遍历// 广度遍历:层次遍历(同层级遍历)// 都可拿到树中的节点
// 使用访问者模式class Visitor { constructor() { this.visit = function (elem) { elem.age = elem.age*2 } }}
// bst.posterorderTraversal({// visit(elem) {// elem.age = elem.age*10// }// })
// 不能通过索引操作 拿到节点去操作// bst.posterorderTraversal(new Visitor())
console.log(bst.invertTree(),'反转二叉树')
复制代码

查找字符串中出现最多的字符和个数

例: abbcccddddd -> 字符最多的是 d,出现了 5 次


let str = "abcabcabcbbccccc";let num = 0;let char = '';
// 使其按照一定的次序排列str = str.split('').sort().join('');// "aaabbbbbcccccccc"
// 定义正则表达式let re = /(\w)\1+/g;str.replace(re,($0,$1) => { if(num < $0.length){ num = $0.length; char = $1; }});console.log(`字符最多的是${char},出现了${num}次`);

复制代码

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

实现数组的乱序输出

主要的实现思路就是:


  • 取出数组的第一个元素,随机产生一个索引值,将该第一个元素和这个索引对应的元素进行交换。

  • 第二次取出数据数组第二个元素,随机产生一个除了索引为 1 的之外的索引值,并将第二个元素与该索引值对应的元素进行交换

  • 按照上面的规律执行,直到遍历完成


var arr = [1,2,3,4,5,6,7,8,9,10];for (var i = 0; i < arr.length; i++) {  const randomIndex = Math.round(Math.random() * (arr.length - 1 - i)) + i;  [arr[i], arr[randomIndex]] = [arr[randomIndex], arr[i]];}console.log(arr)
复制代码


还有一方法就是倒序遍历:


var arr = [1,2,3,4,5,6,7,8,9,10];let length = arr.length,    randomIndex,    temp;  while (length) {    randomIndex = Math.floor(Math.random() * length--);    temp = arr[length];    arr[length] = arr[randomIndex];    arr[randomIndex] = temp;  }console.log(arr)
复制代码

验证是否是邮箱

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

实现有并行限制的 Promise 调度器

题目描述:JS 实现一个带并发限制的异步调度器 Scheduler,保证同时运行的任务最多有两个


addTask(1000,"1"); addTask(500,"2"); addTask(300,"3"); addTask(400,"4"); 的输出顺序是:2 3 1 4
整个的完整执行流程:
一开始1、2两个任务开始执行500ms时,2任务执行完毕,输出2,任务3开始执行800ms时,3任务执行完毕,输出3,任务4开始执行1000ms时,1任务执行完毕,输出1,此时只剩下4任务在执行1200ms时,4任务执行完毕,输出4
复制代码


实现代码如下:


class Scheduler {  constructor(limit) {    this.queue = [];    this.maxCount = limit;    this.runCounts = 0;  }  add(time, order) {    const promiseCreator = () => {      return new Promise((resolve, reject) => {        setTimeout(() => {          console.log(order);          resolve();        }, time);      });    };    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 scheduler = new Scheduler(2);const addTask = (time, order) => {  scheduler.add(time, order);};addTask(1000, "1");addTask(500, "2");addTask(300, "3");addTask(400, "4");scheduler.taskStart();
复制代码

对象数组列表转成树形结构(处理菜单)

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


用户头像

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

还未添加个人简介

评论

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