树形结构转成列表(处理菜单)
[ { 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.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层级  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 //  // 刚开始反转的是最后两个 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;}
复制代码
评论