本文是前端面试必须掌握的手写题系列的最后一篇,这个系列几乎将我整理和遇到的题目都包含到了,这里还是想强调一下,对于特别常见的题目最好能“背”下来,不要眼高手低,在面试的时候不需要再进行推导分析直接一把梭,后续会整理分享一些其他的信息,希望对你能有所帮助
🔥请求并发控制
多次遇到的题目,而且有很多变种,主要就是同步改异步
1function getUrlByFetch() {
2 let idx = maxLoad;
3
4 function getContention(index) {
5 fetch(pics[index]).then(() => {
6 idx++;
7 if(idx < pics.length){
8 getContention(idx);
9 }
10 });
11 }
12 function start() {
13 for (let i = 0; i < maxLoad; i++) {
14 getContention(i);
15 }
16 }
17 start();
18}
🔥带并发限制的promise异步调度器
上一题的其中一个变化
1function taskPool() {
2 this.tasks = [];
3 this.pool = [];
4 this.max = 2;
5}
6
7taskPool.prototype.addTask = function(task) {
8 this.tasks.push(task);
9 this.run();
10}
11
12taskPool.prototype.run = function() {
13 if(this.tasks.length === 0) {
14 return;
15 }
16 let min = Math.min(this.tasks.length, this.max - this.pool.length);
17 for(let i = 0; i<min;i++) {
18 const currTask = this.tasks.shift();
19 this.pool.push(currTask);
20 currTask().finally(() => {
21 this.pool.splice(this.pool.indexOf(currTask), 1);
22 this.run();
23 })
24 }
25}
🔥🔥🔥实现lazy链式调用: person.eat().sleep(2).eat()
解法其实就是将所有的任务异步化,然后存到一个任务队列里
1function Person() {
2 this.queue = [];
3 this.lock = false;
4}
5
6Person.prototype.eat = function () {
7 this.queue.push(() => new Promise(resolve => { console.log('eat'); resolve(); }));
8
9 return this;
10}
11
12Person.prototype.sleep = function(time, flag) {
13 this.queue.push(() => new Promise(resolve => {
14 setTimeout(() => {
15 console.log('sleep', flag);
16 resolve();
17 }, time * 1000)
18 }));
19
20 return this;
21}
22
23Person.prototype.run = async function() {
24 if(this.queue.length > 0 && !this.lock) {
25 this.lock = true;
26 const task = this.queue.shift();
27 await task();
28 this.lock = false;
29 this.run();
30 }
31}
32
33const person = new Person();
34person.eat().sleep(1, '1').eat().sleep(3, '2').eat().run();
方法二
1class Lazy {
2
3 #cbs = [];
4 constructor(num) {
5
6 this.res = num;
7 }
8
9
10 #add(num) {
11 this.res += num;
12 console.log(this.res);
13 }
14
15
16 #multipy(num) {
17 this.res *= num;
18 console.log(this.res)
19 }
20
21 add(num) {
22
23
24
25 this.#cbs.push({
26 type: 'function',
27 params: num,
28 fn: this.#add
29 })
30 return this;
31 }
32 multipy(num) {
33
34
35 this.#cbs.push({
36 type: 'function',
37 params: num,
38 fn: this.#multipy
39 })
40 return this;
41 }
42 top (fn) {
43
44
45 this.#cbs.push({
46 type: 'callback',
47 fn: fn
48 })
49 return this;
50 }
51 delay (time) {
52
53
54 this.#cbs.push({
55 type: 'delay',
56
57
58 fn: () => {
59 return new Promise(resolve => {
60 console.log(`等待${time}ms`);
61 setTimeout(() => {
62 resolve();
63 }, time);
64 })
65 }
66 })
67 return this;
68 }
69
70
71
72
73 async output() {
74 let cbs = this.#cbs;
75 for(let i = 0, l = cbs.length; i < l; i++) {
76 const cb = cbs[i];
77 let type = cb.type;
78 if (type === 'function') {
79 cb.fn.call(this, cb.params);
80 }
81 else if(type === 'callback') {
82 cb.fn.call(this, this.res);
83 }
84 else if(type === 'delay') {
85 await cb.fn();
86 }
87 }
88
89
90 this.#cbs = [];
91 }
92}
93function lazy(num) {
94 return new Lazy(num);
95}
96
97const lazyFun = lazy(2).add(2).top(console.log).delay(1000).multipy(3)
98console.log('start');
99console.log('等待1000ms');
100setTimeout(() => {
101 lazyFun.output();
102}, 1000);
🔥函数柯里化
毫无疑问,需要记忆
1function curry(fn, args) {
2 let length = fn.length;
3 args = args || [];
4
5 return function() {
6 let subArgs = args.slice(0);
7 subArgs = subArgs.concat(arguments);
8 if(subArgs.length >= length) {
9 return fn.apply(this, subArgs);
10 } else {
11 return curry.call(this, fn, subArgs);
12 }
13 }
14}
15
16
17function curry(func, arity = func.length) {
18 function generateCurried(preArgs) {
19 return function curried(nextArgs) {
20 const args = [...preArgs, ...nextArgs];
21 if(args.length >= arity) {
22 return func(...args);
23 } else {
24 return generateCurried(args);
25 }
26 }
27 }
28 return generateCurried([]);
29}
es6实现方式
1
2function curry(fn, ...args) {
3 return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
4}
5
lazy-load实现
img标签默认支持懒加载只需要添加属性 loading=“lazy”,然后如果不用这个属性,想通过事件监听的方式来实现的话,也可以使用IntersectionObserver来实现,性能上会比监听scroll好很多
1const imgs = document.getElementsByTagName('img');
2const viewHeight = window.innerHeight || document.documentElement.clientHeight;
3
4let num = 0;
5
6function lazyLoad() {
7 for (let i = 0; i < imgs.length; i++) {
8 let distance = viewHeight - imgs[i].getBoundingClientRect().top;
9 if(distance >= 0) {
10 imgs[i].src = imgs[i].getAttribute('data-src');
11 num = i+1;
12 }
13 }
14}
15window.addEventListener('scroll', lazyLoad, false);
实现简单的虚拟dom
给出如下虚拟dom的数据结构,如何实现简单的虚拟dom,渲染到目标dom树
1
2let demoNode = ({
3 tagName: 'ul',
4 props: {'class': 'list'},
5 children: [
6 ({tagName: 'li', children: ['douyin']}),
7 ({tagName: 'li', children: ['toutiao']})
8 ]
9});
构建一个render函数,将demoNode对象渲染为以下dom
1<ul class="list">
2 <li>douyin</li>
3 <li>toutiao</li>
4</ul>
通过遍历,逐个节点地创建真实DOM节点
1function Element({tagName, props, children}){
2
3 if(!(this instanceof Element)){
4 return new Element({tagName, props, children})
5 }
6 this.tagName = tagName;
7 this.props = props || {};
8 this.children = children || [];
9}
10
11Element.prototype.render = function(){
12 var el = document.createElement(this.tagName),
13 props = this.props,
14 propName,
15 propValue;
16 for(propName in props){
17 propValue = props[propName];
18 el.setAttribute(propName, propValue);
19 }
20 this.children.forEach(function(child){
21 var childEl = null;
22 if(child instanceof Element){
23 childEl = child.render();
24 }else{
25 childEl = document.createTextNode(child);
26 }
27 el.appendChild(childEl);
28 });
29 return el;
30};
31
32
33var elem = Element({
34 tagName: 'ul',
35 props: {'class': 'list'},
36 children: [
37 Element({tagName: 'li', children: ['item1']}),
38 Element({tagName: 'li', children: ['item2']})
39 ]
40});
41document.querySelector('body').appendChild(elem.render());
实现SWR 机制
SWR 这个名字来自于 stale-while-revalidate:一种由 HTTP RFC 5861 推广的 HTTP 缓存失效策略
1const cache = new Map();
2
3async function swr(cacheKey, fetcher, cacheTime) {
4 let data = cache.get(cacheKey) || { value: null, time: 0, promise: null };
5 cache.set(cacheKey, data);
6
7
8 const isStaled = Date.now() - data.time > cacheTime;
9 if (isStaled && !data.promise) {
10 data.promise = fetcher()
11 .then((val) => {
12 data.value = val;
13 data.time = Date.now();
14 })
15 .catch((err) => {
16 console.log(err);
17 })
18 .finally(() => {
19 data.promise = null;
20 });
21 }
22
23 if (data.promise && !data.value) await data.promise;
24 return data.value;
25}
26
27const data = await fetcher();
28const data = await swr('cache-key', fetcher, 3000);
实现一个只执行一次的函数
1
2function once(fn) {
3 let called = false;
4 return function _once() {
5 if (called) {
6 return _once.value;
7 }
8 called = true;
9 _once.value = fn.apply(this, arguments);
10 }
11}
12
13
14Reflect.defineProperty(Function.prototype, 'once', {
15 value () {
16 return once(this);
17 },
18 configurable: true,
19})
20
LRU 算法实现
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
1class LRUCahe {
2 constructor(capacity) {
3 this.cache = new Map();
4 this.capacity = capacity;
5 }
6
7 get(key) {
8 if (this.cache.has(key)) {
9 const temp = this.cache.get(key);
10 this.cache.delete(key);
11 this.cache.set(key, temp);
12 return temp;
13 }
14 return undefined;
15 }
16
17 set(key, value) {
18 if (this.cache.has(key)) {
19 this.cache.delete(key);
20 } else if (this.cache.size >= this.capacity) {
21
22 this.cache.delete(this.cache.keys().next().value);
23 }
24 this.cache.set(key, value);
25 }
26}
🔥发布-订阅
发布者不直接触及到订阅者、而是由统一的第三方来完成实际的通信的操作,叫做发布-订阅模式。
1class EventEmitter {
2 constructor() {
3
4 this.handlers = {}
5 }
6
7
8 on(eventName, cb) {
9
10 if (!this.handlers[eventName]) {
11
12 this.handlers[eventName] = []
13 }
14
15
16 this.handlers[eventName].push(cb)
17 }
18
19
20 emit(eventName, ...args) {
21
22 if (this.handlers[eventName]) {
23
24 const handlers = this.handlers[eventName].slice()
25
26 handlers.forEach((callback) => {
27 callback(...args)
28 })
29 }
30 }
31
32
33 off(eventName, cb) {
34 const callbacks = this.handlers[eventName]
35 const index = callbacks.indexOf(cb)
36 if (index !== -1) {
37 callbacks.splice(index, 1)
38 }
39 }
40
41
42 once(eventName, cb) {
43
44 const wrapper = (...args) => {
45 cb(...args)
46 this.off(eventName, wrapper)
47 }
48 this.on(eventName, wrapper)
49 }
50}
观察者模式
1const queuedObservers = new Set();
2
3const observe = fn => queuedObservers.add(fn);
4const observable = obj => new Proxy(obj, {set});
5
6function set(target, key, value, receiver) {
7 const result = Reflect.set(target, key, value, receiver);
8 queuedObservers.forEach(observer => observer());
9 return result;
10}
单例模式
核心要点: 用闭包和Proxy属性拦截
1function getSingleInstance(func) {
2 let instance;
3 let handler = {
4 construct(target, args) {
5 if(!instance) instance = Reflect.construct(func, args);
6 return instance;
7 }
8 }
9 return new Proxy(func, handler);
10}
11
洋葱圈模型compose函数
1function compose(middleware) {
2 return function(context, next) {
3 let index = -1;
4 return dispatch(0);
5 function dispatch(i) {
6
7 if(i <= index) return Promise.reject(new Error('next() called multiple times'));
8
9 index = i;
10 let fn = middle[i];
11
12 if(i === middle.length) fn = next;
13 if(!fn) return Promsie.resolve();
14 try{
15 return Promise.resove(fn(context, dispatch.bind(null, i+1)));
16 }catch(err){
17 return Promise.reject(err);
18 }
19 }
20 }
21}
总结
当你看到这里的时候,几乎前端面试中常见的手写题目基本都覆盖到了,对于社招的场景下,其实手写题的题目是越来越务实的,尤其是真的有hc的情况下,一般出一些常见的场景题的可能性更大,所以最好理解➕记忆,最后欢迎评论区分享一些你遇到的题目
至此,手写题系列分享结束,希望对你有所帮助