写点什么

高级前端手写面试题

  • 2022 年 9 月 17 日
    浙江
  • 本文字数:8464 字

    阅读完需:约 28 分钟

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

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


前端手写面试题详细解答

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

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

实现 ES6 的 extends

function B(name){  this.name = name;};function A(name,age){  //1.将A的原型指向B  Object.setPrototypeOf(A,B);  //2.用A的实例作为this调用B,得到继承B之后的实例,这一步相当于调用super  Object.getPrototypeOf(A).call(this, name)  //3.将A原有的属性添加到新实例上  this.age = age;   //4.返回新实例对象  return this;};var a = new A('poetry',22);console.log(a);
复制代码

实现 apply 方法

apply 原理与 call 很相似,不多赘述


// 模拟 applyFunction.prototype.myapply = function(context, arr) {  var context = Object(context) || window;  context.fn = this;
var result; if (!arr) { result = context.fn(); } else { var args = []; for (var i = 0, len = arr.length; i < len; i++) { args.push("arr[" + i + "]"); } result = eval("context.fn(" + args + ")"); }
delete context.fn; return result;};
复制代码

手写 Promise.race

该方法的参数是 Promise 实例数组, 然后其 then 注册的回调方法是数组中的某一个 Promise 的状态变为 fulfilled 的时候就执行. 因为 Promise 的状态只能改变一次, 那么我们只需要把 Promise.race 中产生的 Promise 对象的 resolve 方法, 注入到数组中的每一个 Promise 实例中的回调函数中即可.


Promise.race = function (args) {  return new Promise((resolve, reject) => {    for (let i = 0, len = args.length; i < len; i++) {      args[i].then(resolve, reject)    }  })}
复制代码

debounce(防抖)

触发高频时间后 n 秒内函数只会执行一次,如果 n 秒内高频时间再次触发,则重新计算时间。


const debounce = (fn, time) => {  let timeout = null;  return function() {    clearTimeout(timeout)    timeout = setTimeout(() => {      fn.apply(this, arguments);    }, time);  }};
复制代码


防抖常应用于用户进行搜索输入节约请求资源,window触发resize事件时进行防抖只触发一次。

实现数组的 push 方法

let arr = [];Array.prototype.push = function() {    for( let i = 0 ; i < arguments.length ; i++){        this[this.length] = arguments[i] ;    }    return this.length;}
复制代码

实现深拷贝

  • 浅拷贝: 浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用 Object.assign 和展开运算符来实现。

  • 深拷贝: 深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败

(1)JSON.stringify()

  • JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用JSON.stringifyjs对象序列化(JSON 字符串),再使用JSON.parse来反序列化(还原)js对象。

  • 这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。


let obj1 = {  a: 0,              b: {                 c: 0                 }            };let obj2 = JSON.parse(JSON.stringify(obj1));obj1.a = 1;obj1.b.c = 1;console.log(obj1); // {a: 1, b: {c: 1}}console.log(obj2); // {a: 0, b: {c: 0}}
复制代码

(2)函数库 lodash 的_.cloneDeep 方法

该函数库也有提供_.cloneDeep 用来做 Deep Copy


var _ = require('lodash');var obj1 = {    a: 1,    b: { f: { g: 1 } },    c: [1, 2, 3]};var obj2 = _.cloneDeep(obj1);console.log(obj1.b.f === obj2.b.f);// false
复制代码

(3)手写实现深拷贝函数

// 深拷贝的实现function deepCopy(object) {  if (!object || typeof object !== "object") return;
let newObject = Array.isArray(object) ? [] : {};
for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = typeof object[key] === "object" ? deepCopy(object[key]) : object[key]; } }
return newObject;}
复制代码

递归反转链表

// 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())
复制代码

实现一个 padStart()或 padEnd()的 polyfil

String.prototype.padStartString.prototype.padEndES8中新增的方法,允许将空字符串或其他字符串添加到原始字符串的开头或结尾。我们先看下使用语法:


String.padStart(targetLength,[padString])
复制代码


用法:


'x'.padStart(4, 'ab') // 'abax''x'.padEnd(5, 'ab') // 'xabab'
// 1. 若是输入的目标长度小于字符串原本的长度则返回字符串本身'xxx'.padStart(2, 's') // 'xxx'
// 2. 第二个参数的默认值为 " ",长度是为1的// 3. 而此参数可能是个不确定长度的字符串,若是要填充的内容达到了目标长度,则将不要的部分截取'xxx'.padStart(5, 'sss') // ssxxx
// 4. 可用来处理日期、金额格式化问题'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"
复制代码


polyfill 实现:


String.prototype.myPadStart = function (targetLen, padString = " ") {  if (!targetLen) {    throw new Error('请输入需要填充到的长度');  }  let originStr = String(this); // 获取到调用的字符串, 因为this原本是String{},所以需要用String转为字符串  let originLen = originStr.length; // 调用的字符串原本的长度  if (originLen >= targetLen) return originStr; // 若是 原本 > 目标 则返回原本字符串  let diffNum = targetLen - originLen; // 10 - 6 // 差值  for (let i = 0; i < diffNum; i++) { // 要添加几个成员    for (let j = 0; j < padString.length; j++) { // 输入的padString的长度可能不为1      if (originStr.length === targetLen) break; // 判断每一次添加之后是否到了目标长度      originStr = `${padString[j]}${originStr}`;    }    if (originStr.length === targetLen) break;  }  return originStr;}console.log('xxx'.myPadStart(16))console.log('xxx'.padStart(16))
复制代码


还是比较简单的,而padEnd的实现和它一样,只需要把第二层for循环里的${padString[j]}${orignStr}换下位置就可以了。

Array.prototype.map()

Array.prototype.map = function(callback, thisArg) {  if (this == undefined) {    throw new TypeError('this is null or not defined');  }  if (typeof callback !== 'function') {    throw new TypeError(callback + ' is not a function');  }  const res = [];  // 同理  const O = Object(this);  const len = O.length >>> 0;  for (let i = 0; i < len; i++) {    if (i in O) {      // 调用回调函数并传入新数组      res[i] = callback.call(thisArg, O[i], i, this);    }  }  return res;}
复制代码

用正则写一个根据 name 获取 cookie 中的值的方法

function getCookie(name) {  var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]*)'));  if (match) return unescape(match[2]);}
复制代码


  1. 获取页面上的cookie可以使用 document.cookie


这里获取到的是类似于这样的字符串:


'username=poetry; user-id=12345; user-roles=home, me, setting'
复制代码


可以看到这么几个信息:


  • 每一个 cookie 都是由 name=value 这样的形式存储的

  • 每一项的开头可能是一个空串''(比如username的开头其实就是), 也可能是一个空字符串' '(比如user-id的开头就是)

  • 每一项用";"来区分

  • 如果某项中有多个值的时候,是用","来连接的(比如user-roles的值)

  • 每一项的结尾可能是有";"的(比如username的结尾),也可能是没有的(比如user-roles的结尾)


  1. 所以我们将这里的正则拆分一下:


  • '(^| )'表示的就是获取每一项的开头,因为我们知道如果^不是放在[]里的话就是表示开头匹配。所以这里(^| )的意思其实就被拆分为(^)表示的匹配username这种情况,它前面什么都没有是一个空串(你可以把(^)理解为^它后面还有一个隐藏的'');而|表示的就是或者是一个" "(为了匹配user-id开头的这种情况)

  • +name+这没什么好说的

  • =([^;]*)这里匹配的就是=后面的值了,比如poetry;刚刚说了^要是放在[]里的话就表示"除了^后面的内容都能匹配",也就是非的意思。所以这里([^;]*)表示的是除了";"这个字符串别的都匹配(*应该都知道什么意思吧,匹配 0 次或多次)

  • 有的大佬等号后面是这样写的'=([^;]*)(;|$)',而最后为什么可以把'(;|$)'给省略呢?因为其实最后一个cookie项是没有';'的,所以它可以合并到=([^;]*)这一步。


  1. 最后获取到的match其实是一个长度为 4 的数组。比如:


[  "username=poetry;",  "",  "poetry",  ";"]
复制代码


  • 第 0 项:全量

  • 第 1 项:开头

  • 第 2 项:中间的值

  • 第 3 项:结尾


所以我们是要拿第 2 项match[2]的值。


  1. 为了防止获取到的值是%xxx这样的字符序列,需要用unescape()方法解码。

滚动加载

原理就是监听页面滚动事件,分析 clientHeightscrollTopscrollHeight 三者的属性关系。


window.addEventListener('scroll', function() {  const clientHeight = document.documentElement.clientHeight;  const scrollTop = document.documentElement.scrollTop;  const scrollHeight = document.documentElement.scrollHeight;  if (clientHeight + scrollTop >= scrollHeight) {    // 检测到滚动至页面底部,进行后续操作    // ...  }}, false);
复制代码

使用 reduce 求和

arr = [1,2,3,4,5,6,7,8,9,10],求和


let arr = [1,2,3,4,5,6,7,8,9,10]arr.reduce((prev, cur) => { return prev + cur }, 0)
复制代码


arr = [1,2,3,[[4,5],6],7,8,9],求和


let arr = [1,2,3,4,5,6,7,8,9,10]arr.flat(Infinity).reduce((prev, cur) => { return prev + cur }, 0)
复制代码


arr = [{a:1, b:3}, {a:2, b:3, c:4}, {a:3}],求和


let arr = [{a:9, b:3, c:4}, {a:1, b:3}, {a:3}] 
arr.reduce((prev, cur) => { return prev + cur["a"];}, 0)
复制代码

实现 AJAX 请求

AJAX 是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。


创建 AJAX 请求的步骤:


  • 创建一个 XMLHttpRequest 对象。

  • 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。

  • 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发 onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。

  • 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。


const SERVER_URL = "/server";let xhr = new XMLHttpRequest();// 创建 Http 请求xhr.open("GET", SERVER_URL, true);// 设置状态监听函数xhr.onreadystatechange = function() {  if (this.readyState !== 4) return;  // 当请求成功时  if (this.status === 200) {    handle(this.response);  } else {    console.error(this.statusText);  }};// 设置请求失败时的监听函数xhr.onerror = function() {  console.error(this.statusText);};// 设置请求头信息xhr.responseType = "json";xhr.setRequestHeader("Accept", "application/json");// 发送 Http 请求xhr.send(null);
复制代码

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

实现 Object.freeze

Object.freeze冻结一个对象,让其不能再添加/删除属性,也不能修改该对象已有属性的可枚举性、可配置可写性,也不能修改已有属性的值和它的原型属性,最后返回一个和传入参数相同的对象


function myFreeze(obj){  // 判断参数是否为Object类型,如果是就封闭对象,循环遍历对象。去掉原型属性,将其writable特性设置为false  if(obj instanceof Object){    Object.seal(obj);  // 封闭对象    for(let key in obj){      if(obj.hasOwnProperty(key)){        Object.defineProperty(obj,key,{          writable:false   // 设置只读        })        // 如果属性值依然为对象,要通过递归来进行进一步的冻结        myFreeze(obj[key]);        }    }  }}
复制代码

类数组转化为数组

类数组是具有 length 属性,但不具有数组原型上的方法。常见的类数组有 arguments、DOM 操作方法返回的结果。

方法一:Array.from
Array.from(document.querySelectorAll('div'))
复制代码
方法二:Array.prototype.slice.call()
Array.prototype.slice.call(document.querySelectorAll('div'))
复制代码
方法三:扩展运算符
[...document.querySelectorAll('div')]
复制代码
方法四:利用 concat
Array.prototype.concat.apply([], document.querySelectorAll('div'));
复制代码

图片懒加载

可以给 img 标签统一自定义属性data-src='default.png',当检测到图片出现在窗口之后再补充 src 属性,此时才会进行图片资源加载。


function lazyload() {  const imgs = document.getElementsByTagName('img');  const len = imgs.length;  // 视口的高度  const viewHeight = document.documentElement.clientHeight;  // 滚动条高度  const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;  for (let i = 0; i < len; i++) {    const offsetHeight = imgs[i].offsetTop;    if (offsetHeight < viewHeight + scrollHeight) {      const src = imgs[i].dataset.src;      imgs[i].src = src;    }  }}
// 可以使用节流优化一下window.addEventListener('scroll', lazyload);
复制代码

实现数组去重

给定某无序数组,要求去除数组中的重复数字并且返回新的无重复数组。


ES6 方法(使用数据结构集合):


const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
Array.from(new Set(array)); // [1, 2, 3, 5, 9, 8]
复制代码


ES5 方法:使用 map 存储不重复的数字


const array = [1, 2, 3, 5, 1, 5, 9, 1, 2, 8];
uniqueArray(array); // [1, 2, 3, 5, 9, 8]
function uniqueArray(array) { let map = {}; let res = []; for(var i = 0; i < array.length; i++) { if(!map.hasOwnProperty([array[i]])) { map[array[i]] = 1; res.push(array[i]); } } return res;}
复制代码


用户头像

还未添加个人签名 2022.07.31 加入

还未添加个人简介

评论

发布
暂无评论
高级前端手写面试题_JavaScript_helloworld1024fd_InfoQ写作社区