关于前端:高级前端手写面试题

2次阅读

共计 11366 个字符,预计需要花费 29 分钟才能阅读完成。

树形构造转成列表(解决菜单)

[
    {
        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 很类似,不多赘述

// 模仿 apply
Function.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 的结尾)
  • 所以咱们将这里的正则拆分一下:
  • '(^|)'示意的就是获取每一项的结尾,因为咱们晓得如果 ^ 不是放在 [] 里的话就是示意结尾匹配。所以这里 (^|) 的意思其实就被拆分为 (^) 示意的匹配 username 这种状况,它后面什么都没有是一个空串 (你能够把(^) 了解为 ^ 它前面还有一个暗藏的 '');而| 示意的就是或者是一个 " "(为了匹配user-id 结尾的这种状况)
  • +name+这没什么好说的
  • =([^;]*)这里匹配的就是 = 前面的值了,比方 poetry;刚刚说了^ 要是放在 [] 里的话就示意 "除了 ^ 前面的内容都能匹配",也就是非的意思。所以这里([^;]*) 示意的是除了 ";" 这个字符串别的都匹配 (* 应该都晓得什么意思吧,匹配 0 次或屡次)
  • 有的大佬等号前面是这样写的 '=([^;]*)(;|$)',而最初为什么能够把'(;|$)' 给省略呢?因为其实最初一个 cookie 项是没有 ';' 的,所以它能够合并到 =([^;]*) 这一步。
  • 最初获取到的 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;
}
正文完
 0