常考面试题

手写代码系列

死磕 36 个 JS 手写题(搞懂后,提升真的大)

复杂版深克隆:基于简单版的基础上,还考虑了内置对象比如 Date、RegExp 等对象和函数以及解决了循环引用的问题。

const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;

function deepClone(target, map = new WeakMap()) {
    if (map.get(target)) {
        return target;
    }
    // 获取当前值的构造函数:获取它的类型
    let constructor = target.constructor;
    // 检测当前对象target是否与正则、日期格式对象匹配
    if (/^(RegExp|Date)$/i.test(constructor.name)) {
        // 创建一个新的特殊对象(正则类/日期类)的实例
        return new constructor(target);  
    }
    if (isObject(target)) {
        map.set(target, true);  // 为循环引用的对象做标记
        const cloneTarget = Array.isArray(target) ? [] : {};
        for (let prop in target) {
            if (target.hasOwnProperty(prop)) {
                cloneTarget[prop] = deepClone(target[prop], map);
            }
        }
        return cloneTarget;
    } else {
        return target;
    }
}

1、防抖与节流

1、函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。 这可以使用在一些点击请求或者input输入的事件上,避免因为用户的多次点击向后端发送多次请求。

function debounce(fn) {
    let timeout = null
    return function(arguments) {
        // 重新计时
        clearTimeout(timeOut)
        timeout = setTimeout(() => {
            fn.apply(this, arguments)
        }, 500)
    }
    
}

function successFn(val) {console.log(val)}
 
let test = debounce(successFn)

inputEl.addEventListener('input', function(e) {
    test(e.target.value)
}, false)

2、函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如 果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数或者窗口缩放的 事件监听上,通过事件节流来降低事件调用的频率。

function throttle() {
    // 创建一个闭包标记
    let flag = true
    return function() {
        if(!flag) {
            return false
        }
        flag = false
        setTimeout(() => {
            fn.apply(this, arguments)
            flag = true
            
        }, 500)
    }
}

function throtten(fn, delay) {
    let preTime = Date.now()

	return function(arguments) {
		let nowTime = Date.now()
		let context = this

		if(nowTime - preTime > delay) {
			return	fn.apply(context, arguments)
			preTime = nowTime
		}
	}
}

function sF() {alert('节流成功')}

window.addEventListener('resize', throttle(sF), false)

2、程序效果,停顿指定的时间,例:5秒执行5次

function sleep(timeout) {
    return new Promise((resolve, reject) => {
        setTimeout(resolve, timeout)
    })
}

async function fn() {
    for(let i = 0; i < 5; i++) {
        console.log(i)
       await sleep(1000)
    }
}

fn()

3、手写new

function F(name) {
    this.name = name
}

F.prototype.sayName = function() {
    console.log(this.name)
}

function _new(fn, ...args) {
    let obj = Object.create(fn.prorotype)

    // 修改this指向
    let o = fn.apply(obj, args)
    // 判断为null 或者 undefined 返回obj 否者新对象
    return o instanceof Object ? o : obj
}

1、创建空对象obj = {}

2、修改obj.__proto__( obj.constructor.prototype) = fn.prototype

3、修改this指向并传参

4、返回null和undefined 不处理

4、模拟实现一个Promise.finally 

Promise符合A+规范源码,以及Promise所有相关API源码

/**
**@description 原型方法finally
**@param {*} 回调函数
**/

Promise.prototype.finally = function(callback) {
    // Promise.resolve(callback()) 处理回调函数是promise的情况
    return this.then(res => {
        // 成功了继续向下传递成功
        return Promise.resolve(callback()).then(() => res)
    }, err => {
    // 失败了继续向下抛出错误
        return Promise.resolve(callback()).then(() => {throw err})
    })
}

5、使用 JavaScript Proxy 实现简单的数据绑定

<input type="text" id="input" />
<div id="show">展示输入内容</div>
<button id="button">添加todoList</button>
list内容:
<ul id="ul"></ul>


        const input = document.getElementById('input')
		const showText = document.getElementById('show')
		const button = document.getElementById('button')
		const ul = document.getElementById('ul')

		const inputObj = new Proxy({}, {
			get(target, key, value, receiver) {
				return Reflect.get(target, key, value, receiver)
			},
			set(target, key, value, receiver) {
				if(key === 'text') {
					input.value = value
					show.innerHTML = value
				}
				return Reflect.set(target, key, value, receiver)
			}
		})

		class Render{
			constructor(arr) {
				this.arr = arr
			}
			init() {
				const fragment = document.createDocumentFragment()
				let len = this.arr.length

				for(let i = 0; i < len; i++) {
					let li = document.createElement('li')
					li.textContent = this.arr[i]
					fragment.appendChild(li)
				}
				ul.appendChild(fragment)
			}

			addList(text) {
				let li = document.createElement('li')
				li.textContent = text
				ul.appendChild(li)
			}
		}

		const todoList = new Proxy([], {
			get(target, key, value, receiver) {
				return Reflect.get(target, key, value, receiver)
			},
			set(target, key, value, receiver) {
				if(key !== 'length') {
					render.addList(value)
				}
				return Reflect.set(target, key, value, receiver)
			}
		})

		render = new Render([])
		render.init()

		input.addEventListener('keyup', function(e) {
			let value = e.target.value
			inputObj.text = value
		}, false)

		button.addEventListener('click', function() {
			todoList.push(inputObj.text)
			inputObj.text = ''
		})

6、在输入框中如何判断输入的是一个正确的网址。

function isUrl(url) {
    const a = document.createElement('a')
    a.href = url
    return (
        [
            /^(http|https):$/.test(a.protocal),
            a.host,
            a.pathname !==  url,
            a.pathname !== `/${url}`
        ].find(item => !item) === undefined
    )
}

7、实现 convert 方法,把原始 list 转换成树形结构

要求尽可能降低时间复杂度

以下数据结构中,id 代表部门编号,name 是部门名称,parentId 是父部门编

号,为 0 代表一级部门,现在要求实现一个 convert 方法,把原始 list 转换
成树形结构, parentId 为多少就挂载在该 id 的属性 children 数组下,结构如
原始list
const list = [
			{ id: 1, name: '部门 A', parentId: 0 }, 
			{ id: 2, name: '部门 B', parentId: 0 }, 
			{ id: 3, name: '部门 C', parentId: 1 }, 
			{ id: 4, name: '部门 D', parentId: 1 }, 
			{ id: 5, name: '部门 E', parentId: 2 }, 
			{ id: 6, name: '部门 F', parentId: 3 }, 
			{ id: 7, name: '部门 G', parentId: 2 }, 
			{ id: 8, name: '部门 H', parentId: 4 }
		]


转换后的

let result = [ 
    { 
        id: 1, 
        name: '部门 A', 
        parentId: 0, 
        children: [ 
            { 
                id: 3, 
                name: '部门 C', 
                parentId: 1, 
                children: [ 
                    { 
                        id: 6, 
                        name: '部门 F', 
                        parentId: 3 
                    }, 
                    {
                        id: 16, 
                        name: '部门 L',
                        parentId: 3 
                    } 
                ] 
            },
    { 
        id: 4, 
        name: '部门 D', 
        parentId: 1, 
        children: [
             { 
                id: 8, 
                name: '部门 H', 
                parentId: 4 
            } 
        ] 
    }
]
转换函数
function convert(list) {
    const res = []
    const map = list.reduce((res, cur) => {
        res[cur.id] = cur;
        return res
    }, {})
    
    for(let item of list) {
        if(item.parentId == 0) {
            res.push(item)
            continue
        }
        if(item.parentId in map) {
            const parent = map[item.parentId]
            parent.children = parent.children || []
            parent.children.push(item)
        }
    }
    return res
}



/**
 * 数组转树形结构
 * @param list 源数组
 * @param tree 树
 * @param parentId 父ID
 */
const listToTree = (list, tree, parentId) => {
  list.forEach(item => {
    // 判断是否为父级菜单
    if (item.parentId === parentId) {
      const child = {
        ...item,
        key: item.key || item.name,
        children: []
      }
      // 迭代 list, 找到当前菜单相符合的所有子菜单
      listToTree(list, child.children, item.id)
      // 删掉不存在 children 值的属性
      if (child.children.length <= 0) {
        delete child.children
      }
      // 加入到树中
      tree.push(child)
    }
  })
}

写一个通用的事件侦听函数

const UntilEvent = {
    // 添加事件
        addEvent(el, type, fn) {
            if(el.addEventListener) {
                el.addEventListener(type, fn, false)
            } else if(el.attachEvent) {
                el.attachEvent('on'+type, fn)
            } else {
                el['on' + type] = fn
            }
        },
        // 移除事件
        removeEvent(el, type, fn) {
            if(el.removeEventListener) {
                el.removeEventListener(type, fn, false)
            } else if(el.detachEvent) {
                el.detachEvent('on'+type, fn)
            } else {
                el['on'+type] = fn
            }
        },
        // 获取事件目标
        getTarget(event) {
            return event.target || event.srcElement
        },
        // 获取event对象的引用, 取到事件的所有信息,确保随时能用到event
        getEvent(event) {
            return event || window.event
        },
        // 阻止冒泡
        stopPropagation(event) {
            if(event.stopPropagation) {
                event.stopPagation()
            } else {
                event.cancelBubble = false
            }
        },
        // 取消事假的默认行为
        preventDefault(event) {
            if(event.preventDefault) {
                event.preventDefault()
            } else {
                event.returnValue = false
            }
        }

}

设计实现promise.race

 Promise.myrace = function (iterator) {
            return new Promise((resolve, reject) => {
                try {
                    let it = iterator[Symbol.iterator]()
                    while(true) {
                        let res = it.next()
                        if(res.done) {
                            break
                        }
                        if(res.value instanceof Promise) {
                            res.value.then(resolve, reject)
                        } else {
                            resolve(res.value)
                        }
                    }
                } catch (error) {
                    reject(error)
                }
            })
        }



        Promise._race = (arr) => {
            return new Promise((resolve, reject) => {
                arr.forEach(promise => {
                    promise.then(resolve, reject)
                })
            })
        }

给定一组 url 实现并发请求

1、使用promiseAll

const urls = ['./1.json', './2.json', './3.json'];
function getData(url) {
  // 返回一个 Promise 利用 Promise.all 接受
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.responseType = 'json';
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(xhr.response);
        }
      }
    };
    xhr.open('GET', url, true);
    xhr.send(null);
  });
}
function getMultiData(urls) {
  // Promise.all 接受一个包含 promise 的数组,如果不是 promise 数组会被转成 promise
  Promise.all(urls.map(url => getData(url))).then(results => {
    console.log(results);
  });
}

2、不使用promiseAll

我们可以写一个方法,加个回调函数,等数据全部回来之后,触发回调函数传入得到的数据,那么数据全部回来的就是我们要考虑的核心问题,我们可以用个数组或者对象,然后判断一下数组的 length 和传入的 url 的长度是否一样来做判断

const urls = ['./1.json', './2.json', './3.json'];
function getAllDate(urls, cd) {
  const result = {};
  function getData(url, idx) {
    const xhr = new XMLHttpRequest();
    xhr.responseType = 'json';
    xhr.onreadystatechange = () => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          result[idx] = xhr.response;
          // 如果两者 length 相等说明都请求完成了
          if (Object.keys(result).length === urls.length) {
            // 给对象添加length属性,方便转换数组
            result.length = urls.length;
            cd && cd(Array.from(result));
          }
        }
      }
    };
  }
  // 触发函数执行
  urls.forEach((url, idx) => getData(url, idx));
}
// 使用
getAllDate(urls, data => {
  console.log(data);
});
function getGroupData(urls, cb) {
  const results = [];
  let count = 0;
  const getData = url => {
    const xhr = new XMLHttpRequest();
    xhr.responseType = 'json';
    xhr.onreadystatechange = _ => {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          results.push(xhr.response);
          if (++count === urls.length) {
            cb && cb(results);
          }
        }
      }
    };
    xhr.open('GET', url, true);
    xhr.send(null);
  };
  urls.forEach(url => getData(url));
}

getGroupData(urls, data => {
  console.log(data);
});

手写promise.all

Promise.prototype.all = function(promiseList) {
            return new Promise((resolve, reject) => {
                if(promiseList.length === 0) {
                    return resolve([])
                }
                let result = [],
                    count = 0;
                
                promiseList.forEach((promise, index) => {
                    Promise.resolve(promise).then(value => {
                        result[index] = value
                        if(++index === promiseList) {
                            resolve(result)
                        }

                    })
                })
            }, reason => reject(reason))

手写call,apply, bind

bind方法

JavaScript深入之bind的模拟实现 · Issue #12 · mqyqingfeng/Blog · GitHub

// 第三版
Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
        // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性
        // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}

但是在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转:

// 第四版
Function.prototype.bind2 = function (context) {

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

call, apply

 JavaScript深入之call和apply的模拟实现 · Issue #11 · mqyqingfeng/Blog · GitHub

Function.prototype.call2 = function (context) {
    var context = context || window;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}
Function.prototype.apply = 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;
}

发布订阅事件

class EventEmitter {
    constructor() {
        this.events = {}
    }
    on(type, cb) {
        const events = this.events[type] || []
        events.push(cb)
        this.events[type] = events
        return this
    }
    off(type, cb) {
        const events = this.events[type]
        this.events[type] = events && events.filter(callback => callback !== cb)
        return this
    }
    emit(type, ...args) {
        const events = this.events[type]
        this.events[type].forEach(cb => cb(...args))
        return this
    }
    once(type, cb) {
        const that = this
        let wrapFn = function(...args) {
            cb(...args)
            that.off(type, fn)
        }
        this.on(type, wrapFn)
        return this;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值