共计 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.stringify
将js
对象序列化(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.padStart
和 String.prototype.padEnd
是 ES8
中新增的办法,容许将空字符串或其余字符串增加到原始字符串的结尾或结尾。咱们先看下应用语法:
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]);
}
- 获取页面上的
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]
的值。
- 为了避免获取到的值是
%xxx
这样的字符序列,须要用unescape()
办法解码。
滚动加载
原理就是监听页面滚动事件,剖析 clientHeight、scrollTop、scrollHeight三者的属性关系。
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;
}