下面是B站找回来的资源
32个手写JS,巩固你的JS基础(面试高频) 作为前端开发,JS是重中之重,最近结束了面试的高峰期,基本上offer也定下来了就等开奖,趁着这个时间总结下32个手写JS问题,这些都是高频面试题,希望对你能有所帮助。
关于源码都紧遵规范,都可跑通MDN示例
01.数组扁平化 数组扁平化是指将一个多维数组变为一个一维数组
1 2 const arr = [1 , [2 , [3 , [4 , 5 ]]], 6 ];
方法一:使用flat() 1 const res1 = arr.flat (Infinity );
方法二:利用正则 1 const res2 = JSON .stringify (arr).replace (/\[|\]/g , '' ).split (',' );
但数据类型都会变为字符串
方法三:正则改良版本 1 const res3 = JSON .parse ('[' + JSON .stringify (arr).replace (/\[|\]/g , '' ) + ']' );
方法四:使用reduce 1 2 3 4 5 6 const flatten = arr => { return arr.reduce ((pre, cur ) => { return pre.concat (Array .isArray (cur) ? flatten (cur) : cur); }, []) } const res4 = flatten (arr);
方法五:函数递归 1 2 3 4 5 6 7 8 9 10 11 const res5 = [];const fn = arr => { for (let i = 0 ; i < arr.length ; i++) { if (Array .isArray (arr[i])) { fn (arr[i]); } else { res5.push (arr[i]); } } } fn (arr);
02.数组去重 1 2 const arr = [1 , 1 , '1' , 17 , true , true , false , false , 'true' , 'a' , {}, {}];
方法一:利用Set 1 const res1 = Array .from (new Set (arr));
方法二:两层for循环+splice 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const unique1 = arr => { let len = arr.length ; for (let i = 0 ; i < len; i++) { for (let j = i + 1 ; j < len; j++) { if (arr[i] === arr[j]) { arr.splice (j, 1 ); len--; j--; } } } return arr; }
方法三:利用indexOf 1 2 3 4 5 6 7 const unique2 = arr => { const res = []; for (let i = 0 ; i < arr.length ; i++) { if (res.indexOf (arr[i]) === -1 ) res.push (arr[i]); } return res; }
当然也可以用include、filter,思路大同小异。
方法四:利用include 1 2 3 4 5 6 7 const unique3 = arr => { const res = []; for (let i = 0 ; i < arr.length ; i++) { if (!res.includes (arr[i])) res.push (arr[i]); } return res; }
方法五:利用filter 1 2 3 4 5 const unique4 = arr => { return arr.filter ((item, index ) => { return arr.indexOf (item) === index; }); }
方法六:利用Map 1 2 3 4 5 6 7 8 9 10 11 const unique5 = arr => { const map = new Map (); const res = []; for (let i = 0 ; i < arr.length ; i++) { if (!map.has (arr[i])) { map.set (arr[i], true ) res.push (arr[i]); } } return res; }
03.类数组转化为数组 类数组是具有length 属性,但不具有数组原型上的方法。常见的类数组有arguments 、DOM操作方法返回的结果。
方法一:Array.from 1 Array .from (document .querySelectorAll ('div' ))
方法二:Array.prototype.slice.call() 1 Array .prototype .slice .call (document .querySelectorAll ('div' ))
方法三:扩展运算符 1 [...document .querySelectorAll ('div' )]
方法四:利用concat 1 Array .prototype .concat .apply ([], document .querySelectorAll ('div' ));
04.Array.prototype.filter()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Array .prototype .filter = function (callback, thisArg ) { if (this == undefined ) { throw new TypeError ('this is null or not undefined' ); } 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) { if (callback.call (thisArg, O[i], i, O)) { res.push (O[i]); } } } return res; }
05.Array.prototype.map()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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; }
06.Array.prototype.forEach() forEach
跟map类似,唯一不同的是forEach
是没有返回值的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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++; } }
07.Array.prototype.reduce() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 Array .prototype .reduce = function (callback, initialValue ) { if (this == undefined ) { throw new TypeError ('this is null or not defined' ); } if (typeof callback !== 'function' ) { throw new TypeError (callbackfn + ' is not a function' ); } const O = Object (this ); const len = this .length >>> 0 ; let accumulator = initialValue; let k = 0 ; if (accumulator === undefined ) { while (k < len && !(k in O)) { k++; } if (k >= len) { throw new TypeError ('Reduce of empty array with no initial value' ); } accumulator = O[k++]; } while (k < len) { if (k in O) { accumulator = callback.call (undefined , accumulator, O[k], k, O); } k++; } return accumulator; }
08.Function.prototype.apply() 第一个参数是绑定的this,默认为window
,第二个参数是数组或类数组
1 2 3 4 5 6 7 8 9 10 11 Function .prototype .apply = function (context = window , args ) { if (typeof this !== 'function' ) { throw new TypeError ('Type Error' ); } const fn = Symbol ('fn' ); context[fn] = this ; const res = context[fn](...args); delete context[fn]; return res; }
09.Function.prototype.call 于call
唯一不同的是,call()
方法接受的是一个参数列表
1 2 3 4 5 6 7 8 9 10 11 Function .prototype .call = function (context = window , ...args ) { if (typeof this !== 'function' ) { throw new TypeError ('Type Error' ); } const fn = Symbol ('fn' ); context[fn] = this ; const res = context[fn](...args); delete context[fn]; return res; }
10.Function.prototype.bind 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Function .prototype .bind = function (context, ...args ) { if (typeof this !== 'function' ) { throw new Error ("Type Error" ); } var self = this ; return function F ( ) { if (this instanceof F) { return new self (...args, ...arguments ) } return self.apply (context, [...args, ...arguments ]) } }
11.debounce(防抖) 触发高频时间后n秒内函数只会执行一次,如果n秒内高频时间再次触发,则重新计算时间。
1 2 3 4 5 6 7 8 9 const debounce = (fn, time ) => { let timeout = null ; return function ( ) { clearTimeout (timeout) timeout = setTimeout (() => { fn.apply (this , arguments ); }, time); } };
防抖常应用于用户进行搜索输入节约请求资源,window
触发resize
事件时进行防抖只触发一次。
12.throttle(节流) 高频时间触发,但n秒内只会执行一次,所以节流会稀释函数的执行频率。
1 2 3 4 5 6 7 8 9 10 11 const throttle = (fn, time ) => { let flag = true ; return function ( ) { if (!flag) return ; flag = false ; setTimeout (() => { fn.apply (this , arguments ); flag = true ; }, time); } }
节流常应用于鼠标不断点击触发、监听滚动事件。
13.函数珂里化
指的是将一个接受多个参数的函数 变为 接受一个参数返回一个函数的固定形式,这样便于再次调用,例如f(1)(2)
经典面试题:实现add(1)(2)(3)(4)=10;
、 add(1)(1,2,3)(2)=9;
1 2 3 4 5 6 7 8 9 10 11 function add ( ) { const _args = [...arguments ]; function fn ( ) { _args.push (...arguments ); return fn; } fn.toString = function ( ) { return _args.reduce ((sum, cur ) => sum + cur); } return fn; }
14.模拟new操作 3个步骤:
以ctor.prototype
为原型创建一个对象。
执行构造函数并将this绑定到新创建的对象上。
判断构造函数执行返回的结果是否是引用数据类型,若是则返回构造函数执行的结果,否则返回创建的对象。
1 2 3 4 5 6 7 8 9 10 11 function newOperator (ctor, ...args ) { if (typeof ctor !== 'function' ) { throw new TypeError ('Type Error' ); } const obj = Object .create (ctor.prototype ); const res = ctor.apply (obj, args); const isObject = typeof res === 'object' && res !== null ; const isFunction = typeof res === 'function' ; return isObject || isFunction ? res : obj; }
15.instanceof instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。
1 2 3 4 5 6 7 8 9 10 const myInstanceof = (left, right ) => { if (typeof left !== 'object' || left === null ) return false ; let proto = Object .getPrototypeOf (left); while (true ) { if (proto === null ) return false ; if (proto === right.prototype ) return true ; proto = Object .getPrototypeOf (proto); } }
16.原型继承 这里只写寄生组合继承了,中间还有几个演变过来的继承但都有一些缺陷
1 2 3 4 5 6 7 8 9 function Parent ( ) { this .name = 'parent' ; } function Child ( ) { Parent .call (this ); this .type = 'children' ; } Child .prototype = Object .create (Parent .prototype );Child .prototype .constructor = Child ;
17.Object.is Object.is
解决的主要是这两个问题:
1 2 3 4 5 6 7 8 9 10 11 12 +0 === -0 NaN === NaN const is = (x, y ) => { if (x === y) { return x !== 0 || y !== 0 || 1 /x === 1 /y; } else { return x !== x && y !== y; } }
18.Object.assign Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象(请注意这个操作是浅拷贝)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 Object .defineProperty (Object , 'assign' , { value : function (target, ...args ) { if (target == null ) { return new TypeError ('Cannot convert undefined or null to object' ); } const to = Object (target); for (let i = 0 ; i < args.length ; i++) { const nextSource = args[i]; if (nextSource !== null ) { for (const nextKey in nextSource) { if (Object .prototype .hasOwnProperty .call (nextSource, nextKey)) { to[nextKey] = nextSource[nextKey]; } } } } return to; }, enumerable : false , writable : true , configurable : true , })
19.深拷贝 递归的完整版本(考虑到了Symbol属性):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const cloneDeep1 = (target, hash = new WeakMap () ) => { if (typeof target !== 'object' || target === null ) { return target; } if (hash.has (target)) return hash.get (target); const cloneTarget = Array .isArray (target) ? [] : {}; hash.set (target, cloneTarget); const symKeys = Object .getOwnPropertySymbols (target); if (symKeys.length ) { symKeys.forEach (symKey => { if (typeof target[symKey] === 'object' && target[symKey] !== null ) { cloneTarget[symKey] = cloneDeep1 (target[symKey]); } else { cloneTarget[symKey] = target[symKey]; } }) } for (const i in target) { if (Object .prototype .hasOwnProperty .call (target, i)) { cloneTarget[i] = typeof target[i] === 'object' && target[i] !== null ? cloneDeep1 (target[i], hash) : target[i]; } } return cloneTarget; }
20.Promise 实现思路:Promise源码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 const PENDING = 'PENDING' ; const FULFILLED = 'FULFILLED' ; const REJECTED = 'REJECTED' ; class Promise { constructor (exector ) { this .status = PENDING ; this .value = undefined ; this .reason = undefined ; this .onFulfilledCallbacks = []; this .onRejectedCallbacks = []; const resolve = value => { if (this .status === PENDING ) { this .status = FULFILLED ; this .value = value; this .onFulfilledCallbacks .forEach (fn => fn (this .value )); } } const reject = reason => { if (this .status === PENDING ) { this .status = REJECTED ; this .reason = reason; this .onRejectedCallbacks .forEach (fn => fn (this .reason )) } } try { exector (resolve, reject); } catch (e) { reject (e); } } then (onFulfilled, onRejected ) { onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value; onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw new Error (reason instanceof Error ? reason.message :reason) } const self = this ; return new Promise ((resolve, reject ) => { if (self.status === PENDING ) { self.onFulfilledCallbacks .push (() => { try { setTimeout (() => { const result = onFulfilled (self.value ); result instanceof Promise ? result.then (resolve, reject) : resolve (result); }) } catch (e) { reject (e); } }); self.onRejectedCallbacks .push (() => { try { setTimeout (() => { const result = onRejected (self.reason ); result instanceof Promise ? result.then (resolve, reject) : reject (result); }) } catch (e) { reject (e); } }) } else if (self.status === FULFILLED ) { try { setTimeout (() => { const result = onFulfilled (self.value ); result instanceof Promise ? result.then (resolve, reject) : resolve (result); }); } catch (e) { reject (e); } } else if (self.status === REJECTED ){ try { setTimeout (() => { const result = onRejected (self.reason ); result instanceof Promise ? result.then (resolve, reject) : reject (result); }) } catch (e) { reject (e); } } }); } catch (onRejected) { return this .then (null , onRejected); } static resolve (value ) { if (value instanceof Promise ) { return value; } else { return new Promise ((resolve, reject ) => resolve (value)); } } static reject (reason ) { return new Promise ((resolve, reject ) => { reject (reason); }) } }
21.Promise.all Promise.all
是支持链式调用的,本质上就是返回了一个Promise实例,通过resolve
和reject
来改变实例状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Promise .myAll = function (promiseArr ) { return new Promise ((resolve, reject ) => { const ans = []; let index = 0 ; for (let i = 0 ; i < promiseArr.length ; i++) { promiseArr[i] .then (res => { ans[i] = res; index++; if (index === promiseArr.length ) { resolve (ans); } }) .catch (err => reject (err)); } }) }
22.Promise.race 1 2 3 4 5 6 7 8 9 10 11 Promise .race = function (promiseArr ) { return new Promise ((resolve, reject ) => { promiseArr.forEach (p => { Promise .resolve (p).then ( val => resolve (val), err => reject (err), ) }) }) }
23.Promise并行限制 就是实现有并行限制的Promise调度器问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 class Scheduler { constructor ( ) { this .queue = []; this .maxCount = 2 ; this .runCounts = 0 ; } add (promiseCreator ) { 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 timeout = time => new Promise (resolve => { setTimeout (resolve, time); }) const scheduler = new Scheduler (); const addTask = (time,order ) => { scheduler.add (() => timeout (time).then (()=> console .log (order))) } addTask (1000 , '1' );addTask (500 , '2' );addTask (300 , '3' );addTask (400 , '4' );scheduler.taskStart ()
24.JSONP script标签不遵循同源协议,可以用来进行跨域请求 ,优点就是兼容性好但仅限于GET请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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); } }) }
25.AJAX 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const getJSON = function (url ) { return new Promise ((resolve, reject ) => { const xhr = XMLHttpRequest ? new XMLHttpRequest () : new ActiveXObject ('Mscrosoft.XMLHttp' ); xhr.open ('GET' , url, false ); xhr.setRequestHeader ('Accept' , 'application/json' ); xhr.onreadystatechange = function ( ) { if (xhr.readyState !== 4 ) return ; if (xhr.status === 200 || xhr.status === 304 ) { resolve (xhr.responseText ); } else { reject (new Error (xhr.responseText )); } } xhr.send (); }) }
26.event模块 实现node中回调函数的机制,node中回调函数其实是内部使用了观察者模式 。
观察者模式:定义了对象间一种一对多的依赖关系,当目标对象Subject发生改变时,所有依赖它的对象Observer都会得到通知。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 function EventEmitter ( ) { this .events = new Map (); } const wrapCallback = (fn, once = false ) => ({ callback : fn, once });EventEmitter .prototype .addListener = function (type, fn, once = false ) { const hanlder = this .events .get (type); if (!hanlder) { this .events .set (type, wrapCallback (fn, once)); } else if (hanlder && typeof hanlder.callback === 'function' ) { this .events .set (type, [hanlder, wrapCallback (fn, once)]); } else { hanlder.push (wrapCallback (fn, once)); } } EventEmitter .prototype .removeListener = function (type, listener ) { const hanlder = this .events .get (type); if (!hanlder) return ; if (!Array .isArray (this .events )) { if (hanlder.callback === listener.callback ) this .events .delete (type); else return ; } for (let i = 0 ; i < hanlder.length ; i++) { const item = hanlder[i]; if (item.callback === listener.callback ) { hanlder.splice (i, 1 ); i--; if (hanlder.length === 1 ) { this .events .set (type, hanlder[0 ]); } } } } EventEmitter .prototype .once = function (type, listener ) { this .addListener (type, listener, true ); } EventEmitter .prototype .emit = function (type, ...args ) { const hanlder = this .events .get (type); if (!hanlder) return ; if (Array .isArray (hanlder)) { hanlder.forEach (item => { item.callback .apply (this , args); if (item.once ) { this .removeListener (type, item); } }) } else { hanlder.callback .apply (this , args); if (hanlder.once ) { this .events .delete (type); } } return true ; } EventEmitter .prototype .removeAllListeners = function (type ) { const hanlder = this .events .get (type); if (!hanlder) return ; this .events .delete (type); }
27.图片懒加载 可以给img标签统一自定义属性data-src='default.png'
,当检测到图片出现在窗口之后再补充src 属性,此时才会进行图片资源加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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);
28.滚动加载 原理就是监听页面滚动事件,分析clientHeight 、scrollTop 、scrollHeight 三者的属性关系。
1 2 3 4 5 6 7 8 9 window .addEventListener ('scroll' , function ( ) { const clientHeight = document .documentElement .clientHeight ; const scrollTop = document .documentElement .scrollTop ; const scrollHeight = document .documentElement .scrollHeight ; if (clientHeight + scrollTop >= scrollHeight) { } }, false );
29.渲染几万条数据不卡住页面 渲染大数据时,合理使用createDocumentFragment 和requestAnimationFrame ,将操作切分为一小段一小段执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 setTimeout (() => { const total = 100000 ; const once = 20 ; const loopCount = Math .ceil (total / once); let countOfRender = 0 ; const ul = document .querySelector ('ul' ); function add ( ) { const fragment = document .createDocumentFragment (); for (let i = 0 ; i < once; i++) { const li = document .createElement ('li' ); li.innerText = Math .floor (Math .random () * total); fragment.appendChild (li); } ul.appendChild (fragment); countOfRender += 1 ; loop (); } function loop ( ) { if (countOfRender < loopCount) { window .requestAnimationFrame (add); } } loop (); }, 0 )
30.打印出当前网页使用了多少种HTML元素 一行代码可以解决:
1 2 3 const fn = ( ) => { return [...new Set ([...document .querySelectorAll ('*' )].map (el => el.tagName ))].length ; }
值得注意的是:DOM操作返回的是类数组 ,需要转换为数组之后才可以调用数组的方法。
31.将VirtualDom转化为真实DOM结构 这是当前SPA应用的核心概念之一
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 function render (vnode, container ) { container.appendChild (_render (vnode)); } function _render (vnode ) { if (typeof vnode === 'number' ) { vnode = String (vnode); } if (typeof vnode === 'string' ) { return document .createTextNode (vnode); } const dom = document .createElement (vnode.tag ); if (vnode.attrs ) { Object .keys (vnode.attrs ).forEach (key => { const value = vnode.attrs [key]; dom.setAttribute (key, value); }) } vnode.children .forEach (child => render (child, dom)); return dom; }
32.字符串解析问题 1 2 3 4 5 6 7 var a = { b : 123 , c : '456' , e : '789' , } var str=`a{a.b}aa{a.c}aa {a.d}aaaa` ;
实现函数使得将str字符串中的{}
内的变量替换,如果属性不存在保持原样(比如{a.d}
)
类似于模版字符串,但有一点出入,实际上原理大差不差
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 const fn1 = (str, obj ) => { let res = '' ; let flag = false ; let start; for (let i = 0 ; i < str.length ; i++) { if (str[i] === '{' ) { flag = true ; start = i + 1 ; continue ; } if (!flag) res += str[i]; else { if (str[i] === '}' ) { flag = false ; res += match (str.slice (start, i), obj); } } } return res; } const match = (str, obj ) => { const keys = str.split ('.' ).slice (1 ); let index = 0 ; let o = obj; while (index < keys.length ) { const key = keys[index]; if (!o[key]) { return `{${str} }` ; } else { o = o[key]; } index++; } return o; }