本文记录前端面试中的一些常见题型,如果有幸让你看到了这篇文章,欢迎探讨学习!
编程题 实现 call 函数 call
函数的作用,可以改变调用者函数内部的 this
指向,立即执行,第二个参数为 rest
类型参数。如果没有指定第一个参数,则 this
指向 window。
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 Function .prototype .myCall = function (con ) { let context = con || window , _fn = Symbol ("_fn" ); context[_fn] = this ; let arg = [...arguments ].slice (1 ); let res = context[_fn](...arg); delete context[_fn]; return res; }; function a (num ) { console .log (this .foo + num); } let foo = 4 ;let obj = { foo : 5 };a.myCall (obj, 1 ); a.call (obj, 1 );
我们在使用 _fn
作为临时属性时,使用的是 Symbol
类型,这是为了避免与上下文环境中的其他属性重复而发生冲突。
实现 apply 函数 apply
函数的作用,可以改变调用者函数内部的 this
指向,立即执行,第二个参数为数组类型。如果没有指定第一个参数,则 this
指向 window。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 Function .prototype .myApply = function (con, arr = [] ) { let context = con || window , _fn = Symbol ("_fn" ); context[_fn] = this ; let res = context[_fn](...arr); delete context[_fn]; return res; }; function a (num1, num2, num3 ) { console .log (this .foo + num1 + num2 + num3); } var foo = 2 ;var obj = { foo : 4 };a.myApply (obj, [1 , 2 , 3 ]); a.apply (obj, [1 , 2 , 3 ]);
实现 bind 函数 bind
函数的作用,返回新的函数,该函数内的 this
指向 bind
函数的第一个参数对象,第二个参数为传入新函数内的 rest 参数。如果没有指定第一个参数,则 this
指向 window。
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 Function .prototype .myBind = function (con ) { if (typeof this !== "function" ) throw new Error ("error" ); var context = con || window ; var _this = this ; var args = [...arguments ].slice (1 ); return function Fn ( ) { if (this instanceof Fn ) { return new _this (...args, ...arguments ); } return _this.apply (context, [...args, ...arguments ]); }; }; var foo = 1 ;var obj = { foo : 2 };function a (num1, num2 ) { console .log (this .foo + num1 + num2); } var fn1 = a.bind (obj, 2 , 2 );fn1 (3 , 5 );var fn2 = a.myBind (obj, 2 , 2 );fn2 (3 , 5 );
对多维数组进行降维(扁平化) 实现的效果类似这样:
1 let arr = [[1 , [2 ]], [3 ], [4 ]];
扁平化后,如下所示:
1 let flatArr = [1 , 2 , 3 , 4 ];
方式 1:
1 2 3 let arr1 = [1 , [2 , [4 , [5 ]]], 3 ].flat (Infinity );console .log ("方式1" , arr1);
方式 2:
1 2 3 4 5 6 7 8 9 const flattenDeep = arr => { return Array .isArray (arr) ? arr.reduce ((a, b ) => [...a, ...flattenDeep (b)], []) : [arr]; }; let arr2 = flattenDeep ([1 , [[2 ], [3 , [4 ]], 5 ]]);console .log ("方式2" , arr2);
方式 3:
使用 generater
函数 和 for ...of
循环 或 使用 扩展运算符 ...
也行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function * iterArr (arr ) { if (Array .isArray (arr)) { for (let i = 0 ; i < arr.length ; i++) { yield * iterArr (arr[i]); } } else { yield arr; } } let newArr = [], arr3 = [1 , [[2 ], [3 , [4 ]], 5 ]]; for (let v of iterArr (arr3)) { newArr.push (v); } console .log ("方式3" , newArr);console .log ("方式3(扩展运算符)" , [...iterArr (arr3)]);
实现支持注册、分发和解绑的事件类 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 class Event { constructor ( ) { this ._cache = {}; } on (type, callback ) { this ._cache [type] = this ._cache [type] || []; let fns = this ._cache [type]; if (fns.indexOf (callback) === -1 ) { fns.push (callback); } return this ; } trigger (type, ...data ) { let fns = this ._cache [type]; if (Array .isArray (fns)) { fns.forEach (fn => { fn (...data); }); } return this ; } off (type, callback ) { let fns = this ._cache [type]; if (Array .isArray (fns)) { if (callback) { let index = fns.indexOf (callback); if (index !== -1 ) { fns.splice (index, 1 ); } } else { fns = []; } } return this ; } } const event = new Event ();event .on ("test" , a => { console .log (a); }) .trigger ("test" , "hello" );
实现斐波那契数列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 function * fib ( ) { let [prev, curr] = [1 , 1 ]; for (;;) { yield curr; [prev, curr] = [curr, prev + curr]; } } for (let v of fib ()) { if (v > 55 ) break ; console .log (v); }
利用 Generater
函数 和 for ...of
循环可以很巧妙的实现,注意,generator
函数在被执行后,里面的代码并不会立即执行,需要依靠遍历器驱动 yield
执行。
所以,for(;;){}
并不会有什么性能问题。
实现防抖和节流函数
防抖
功能:触发高频事件后 interval 毫秒内函数只会执行一次,如果 interval 毫秒 内高频事件再次被触发,则重新计算时间。
会清除定时器,对于高频率触发的动作,会限制其频率降低。
1 2 3 4 5 6 7 8 9 10 11 function debounce (fn, interval = 300 ) { let timer = null ; return function ( ) { clearTimeout (timer); timer = setTimeout (() => { fn.apply (this , arguments ); }, interval); }; }
节流
功能:高频事件触发,但在 interval 毫秒内只会执行一次,所以节流会稀释函数的执行频率。
不会清除定时器,对于高频率触发的动作,会减少其触发次数。
1 2 3 4 5 6 7 8 9 10 11 12 13 function throttle (fn, interval = 1000 ) { let canRun = true ; return function ( ) { if (!canRun) return ; canRun = false ; setTimeout (() => { fn.apply (this , arguments ); canRun = true ; }, interval); }; }
实现对字符串进行金额格式化的功能函数 显示类似如下的字符串格式转换功能。
1 2 let num = 52545455454 ;num.toLocaleString ();
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function toPriceRight (str, gap, type ) { return String (str) .split ("" ) .reduce ((init, v, i ) => { if (i % gap == gap - 1 ) { init += type + v; } else { init += v; } return init; }, "" ); } console .log (toPriceRight ("52545455454" , 3 , "," ));
对指定数组生成树形结构数据 题目:对于如下数据,pid
表示 父节点的值,如果没有 pid
则表示该对象对象为根节点,请将其格式化为树形数据结构,要求可以达到无限深度。
1 2 3 4 5 6 7 8 9 10 11 let origin = [ { id : 1 }, { id : 2 }, { id : 3 }, { id : 11 , pid : 1 }, { id : 12 , pid : 1 }, { id : 111 , pid : 11 }, { id : 112 , pid : 11 }, { id : 21 , pid : 2 }, { id : 31 , pid : 3 } ];
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function tree (arr, ori ) { if (arr.length === 0 ) arr = ori.filter (v => !("pid" in v)); arr.forEach (v => { let childs = ori.filter (item => item.pid === v.id ); if (childs.length > 0 ) { v.children = tree (childs, ori); } }); return arr; } console .log (tree ([], origin));
实现倒计时功能 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const endtime = +new Date ("2019/12/15 0:0:0" ); function daojishi ( ) { let nowtime = +new Date (), leftime = (endtime - nowtime) / 1000 , d = parseInt (leftime / 60 / 60 / 24 ), h = parseInt ((leftime / 60 / 60 ) % 60 ), m = parseInt ((leftime / 60 ) % 60 ), s = parseInt (leftime % 60 ); console .log (`距离结束还有:${d} 天 ${h} 小时 ${m} 分钟 ${s} 秒` ); if (nowtime <= endtime) setTimeout (daojishi, 1000 ); } daojishi ();
给定一个数组,对里面所有的奇数求和 1 2 3 4 5 6 7 8 let arr = ["1" , "2" , "3" , 6 , 4 , -99 , -101 ];let res = arr.reduce ((init, v ) => { if (v % 2 !== 0 ) init += Number .parseFloat (v); return init; }, 0 ); console .log (res);
函数柯里化 题目:完成 bindLeft 实现函数参数的部分绑定功能。
1 2 3 4 function bindLeft ( ) { }
使用方法如下:
1 2 3 let fn1 = (a, b, c, d ) => a - b * c + d;let fn2 = bindLeft (fn1, 1 , 2 ); console .log (fn2 (3 , 4 ));
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 let fn1 = (a, b, c, d ) => a - b * c + d;function bindLeft (fn ) { let args = [...arguments ].slice (1 ); return function ( ) { return fn.apply (this , [...args, ...arguments ]); }; } let fn2 = bindLeft (fn1, 1 , 2 );console .log (fn2 (3 , 4 ));console .log (1 - 2 * 3 + 4 );
使用冒泡排序对数组中所有正整数排序(非正整数位置保持不变) 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 let arr = [11 , -1 , 6 , 5 , -4 , -7 , 9 , 8 ];function bubbleSort (arr ) { let len = arr.length ; let ori = arr.reduce ((init, v, i ) => { if (v < 0 ) { init.push ({ value : v, index : i }); } return init; }, []); for (let i = 0 ; i < len; i++) { for (let j = 0 ; j < len - i - 1 ; j++) { if (arr[j] > arr[j + 1 ]) { [arr[j], arr[j + 1 ]] = [arr[j + 1 ], arr[j]]; } } } let index = arr.findIndex (v => v >= 0 ), right = arr.slice (index); ori.forEach (v => { right.splice (v.index , 0 , v.value ); }); return right; } console .log (bubbleSort (arr));
已知一个对象 obj ,在不知道第一个属性键名的情况下,如何获取第一个属性的值 1 2 3 let obj = { a : 1 , b : 2 };console .log (Object .values (obj)[0 ]);
将数组去除重复项并按降序排列 1 2 3 4 5 6 7 8 9 10 11 12 let arr = [2 , 0 , 1 , 8 , 0 , 2 , 1 , 5 ];let res = arr .sort ((a, b ) => b - a) .reduce ((init, v ) => { if (init.length === 0 || init[init.length - 1 ] !== v) { init.push (v); } return init; }, []); console .log (res);
看题作答
请写出下面程序的打印结果
1 2 3 4 5 6 7 8 9 10 var p = new Promise ((resolve, reject ) => { console .log ("a" ); resolve (); }); setTimeout (() => console .log ("d" ), 0 );p.then (() => console .log ("c" )); console .log ("b" );
对于以上这类型的题,看到 Promsie
和 setTimeout
这样的字眼,就知道肯定考察的是与 js 的运行机制 事件循环
相关的。
ok,我们捋一下上面代码的执行过程:
首先,new Promise()
构造函数被执行,console.log('a')
被率先执行。打印出 a
。
然后,遇到 setTimeout()
函数,其被执行后,回调函数被推入 宏任务
队列中,等待下一轮事件循环时执行。
紧接着,p.then()
被执行,其中的回调函数被推入 微任务
队列中,等到这一轮事件循环的执行栈为空时,再清空 微任务
队列。
接下来,同步代码 console.log('b')
被执行。打印出 b
此时,执行栈空,清空 微任务
队列,console.log('c')
被压入执行栈中执行。打印出 c
。
最后,开始第二轮事件循环,console.log('d')
出队,压入执行栈中执行,打印出 d
。程序结束。
所以最终的打印结果为:
请写出下面程序的打印结果
1 2 3 4 5 6 7 8 9 10 var x = 0 ;function test ( ) { console .log (this .x ); } var o = {};o.x = 1 ; o.m = test; o.m .apply (); test ();
ok!定眼一看,这是一道有关 this
问题的题目。所以,你的脑中应该迅速回忆起 this
相关的知识点。
该题涉及到 3
种 this
指向情况:
普通函数中的 this
对象方法中的 this
被 apply
改变过的 this
首先,代码中,在全局定义了 3 个变量:变量x
、test
函数、对象o
。
第一条执行语句,使用 apply
的方式调用了 o.m()
方法。这里只要使用了 apply
函数,没有指定 thisArg
的话,那么 o.m()
方法中的 this
就指向 window
。所以打印出 0
第二条执行语句,test()
被直接调用,其中的 this
指向全局 window
,打印出 0。