内推
字节飞书内推地址,欢迎大家投递
杭州:job.toutiao.com/s/iNcUyd1p
北京:job.toutiao.com/s/iNcUqwQw
引言
对于前端面试而言,手撕代码一定是一道面试必考的题目,通常面试给出的手写代码题目大体分为两种, 一种是根据前端基础知识引申出的各种手写代码题目,比如手写Promise、手写防抖节流,深浅拷贝等,另外一种就是纯算法题,本篇文章将主要介绍第一种形式的手写代码题,对面试中常见的手写代码题目进行归纳和总结。
本文将持续更新,敬请期待~
常见手写代码题目总结
Promise相关
手写Promise
以下代码已通过Promise A+规范测试
class Promise{
constructor(executor){
this.state = 'pending' // 状态
this.value = null // 成功的终值
this.reason = null // 失败的拒因
this.onFulfilledCallbacks = []
this.onRejectedCallbacks = []
const resolve = (value) =>{
this.state = 'fulfilled'
this.value = value
this.onFulfilledCallbacks.forEach(fn=>fn())
}
const reject = (reason) =>{
this.state = 'rejected';
this.reason = reason
this.onRejectedCallbacks.forEach(fn=>fn())
}
try{
executor(resolve,reject)
}catch(e){
reject(e)
}
}
then(onFulfilled,onRejected){
// 判断是否是函数
if(typeof onFulfilled !== 'function'){
onFulfilled = function(value){
return value
}
}
if(typeof onRejected !== 'function'){
onRejected = function(reason){
throw reason
}
}
let promise2 = new Promise((resolve,reject)=>{
if(this.state === 'fulfilled'){
setTimeout(()=>{
try{
// promise的值作为onFulfilled的参数
let x = onFulfilled(this.value)
resolvePromsie(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
}
if(this.state === 'rejected'){
setTimeout(()=>{
try{
// promise的值作为onFulfilled的参数
let x = onRejected(this.reason)
resolvePromsie(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
}
if(this.state === 'pending'){
this.onFulfilledCallbacks.push(()=>{
setTimeout(()=>{
try{
// promise的值作为onFulfilled的参数
let x = onFulfilled(this.value)
resolvePromsie(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
})
this.onRejectedCallbacks.push(()=>{
setTimeout(()=>{
try{
// promise的值作为onFulfilled的参数
let x = onRejected(this.reason)
resolvePromsie(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
})
}
})
return promise2
}
}
function resolvePromsie(promise2,x,resolve,reject){
if(x === promise2){
return reject(new TypeError('chaining cycle'))
}
let called;
if(x !== null && (typeof x === 'object' || typeof x === 'function')){
try{
let then = x.then
if(typeof then === 'function'){
then.call(x,value=>{
if(called)return
called = true
resolvePromsie(promise2,value,resolve,reject)
},err=>{
if(called)return
called = true
reject(err)
})
}else{
resolve(x)
}
}catch(e){
if(called)return
called = true
reject(e)
}
}else{
resolve(x)
}
}
Promise.resolve
Promise.resolve = function(val){
return new Promise((resolve,reject)=>{
resolve(val)
})
}
Promise.reject
Promise.reject = function(val){
return new Promise((resolve,reject)=>{
reject(val)
})
}
Promise.race
Promise.race = function(promises){
return new Promise((resolve,reject)=>{
for(let i=0;i<promises.length;i++){
promises[i].then(resolve,reject)
}
})
}
Promise.all
Promise.all = function(promiseArray){
return new Promise((resolve,reject)=>{
// 参数类型的判断
if(!Array.isArray(promiseArray)){
return reject(new TypeError('arguments must be array'))
}
const promiseNum = promiseArray.length
const res = []
let counter = 0
for(let i=0;i<promiseNum;i++){
// 注意数组元素类型
Promise.resolve(promiseArray[i]).then(value=>{
count++
// 不能用push,会导致顺序不一致,因为push是每次加在最后面
// res.push(value)
res[i] = value
// 用counter计数 不能用res.length 判断 因为如果[1,2,3] 3很快执行 那么会导致res[2]=3
// 此时哪怕1 2 没执行 res的长度也已经是3了
if(counter === promiseNum){
resolve(res)
}
},err=>{
reject(error)
}).catch(e=>{
reject(e)
})
}
})
}
new的实现
function objectFactory(){
// 创建一个新对象
let obj = new Object()
let Constructor = [].shift.call(arguments)
// 对象__proto__属性指向构造函数原型
obj.__proto__ = Constructor.prototype
// 绑定this,考虑存在返回值的情况
let result = Constructor.apply(obj,arguments)
// 判断:如果返回值是一个对象 那实例只能访问对象的属性
return typeof result === 'object' ? result : obj
}
apply call bind的实现
call的模拟实现
Function.prototype.call = function (context) {
var context = context || window
context.fn = this
var args = []
for(var i=1;i<arguments.length;i++){
args.push('argument['+i+']')
}
var result = eval('context.fn('+args+')')
delete context.fn
return result
}
// es6 实现
Function.prototype.call = function(context,...args){
let ctx = context
ctx.fn = this
let result = ctx.fn(...args)
delete ctx.fn
return result
}
apply的模拟实现
Function.prototype.apply = function (context, arr) {
var context = 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 + ')')
// es6实现
// let result = context.fn(...arr)
}
delete context.fn
return result;
}
bind的模拟实现
Function.prototype.bind = function (context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
//bind时传参
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fBound = function () {
// bind返回函数执行时也可以传参
var bindArgs = Array.prototype.slice.call(arguments);
// bind返回函数作为构造函数时 this失效,但传入参数有效
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
// 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
柯里化函数
function curry(fn,args){
var length = fn.length
args = args || []
return function(){
var _args = args.slice(0);
for(var i=0;i<arguments.length;i++){
_args.push(arguments[i])
}
if(_arg.length < length){
return curry.call(this,fn,_args)
}else{
return fn.apply(this,_args)
}
}
}
ES6
function curry(fn) {
let judge = (...args) => {
if (args.length == fn.length) return fn(...args)
return (...arg) => judge(...args, ...arg)
}
return judge
}
实现一个compose函数
function fn1(x){
return x+1
}
function fn2(x){
return x+2
}
function fn3(x){
return x+3
}
function fn4(x){
return x+4
}
const a = compose(fn1,fn2,fn3,fn4)
console.log(a(1))
function compose(){
const argFnList = [...arguments]
return (num)=>{
return argFnList.reduce((prev,next)=>{
return next(prev)
},num)
}
}
实现一个基于洋葱模型的compose
// 洋葱模型
function compose(middlewares){
const copyMiddlewares = [...middlewares]
let index = 0
const fn = ()=>{
if(index > copyMiddlewares.length){
return
}
const middleware = copyMiddlewares[index]
index++
return middleware(fn)
}
return fn
}
实现一个异步任务调度并发控制器
const urls = [
{
info:'link1',
time:3000,
priority:1
},
{
info:'link2',
time:2000,
priority:1
},
{
info:'link3',
time:5000,
priority:2
},
{
info:'link4',
time:1000,
priority:1
},
{
info:'link5',
time:1200,
priority:1
},
{
info:'link6',
time:2000,
priority:5
},
{
info:'link7',
time:800,
priority:1
},
{
info:'link8',
time:3000,
priority:1
},
]
class PromiseQueue{
constructor(options = {}){
this.concurrency = options.concurrency || 1
this.currentCount = 0;
this.pendingList = [];
}
add(task){
this.pendingList.push(task)
this.run()
}
run(){
if(this.pendingList.length === 0) return
if(this.currentCount === this.concurrency) return
this.currentCount++
// 优先级排序
const {fn} = this.pendingList.sort((a,b)=> b.priority - a.priority).shift()
const promise = fn()
promise.then(this.completeOne.bind(this)).catch(this.completeOne.bind(this))
}
completeOne(){
this.currentCount--
this.run()
}
}
const q = new PromiseQueue({
concurrency:3 // 最大并发数量
})
const formatTask = (url)=>{
return{
fn:()=>loadImg(url),
priority:url.priority
}
}
urls.forEach(url=>{
q.add(formatTask(url))
})
const highPriorityTask = {
priority:10,
info:'high priority',
time:2000
}
q.add(formatTask(highPriorityTask))
function loadImg(url){
return new Promise((resolve,reject)=>{
console.log('---- '+url.info + ' start!!')
setTimeout(()=>{
console.log(url.info+' OK!!')
resolve()
},url.time)
})
}
实现数组扁平化
递归实现
function flatten(arr){
let result = []
for(let i=0,len=arr.length;i<len;i++){
if(Array.isArray(arr[i])){
result = result.concat(flatten(arr[i]))
}else{
result.push(arr[i])
}
}
return result
},
toString方式
这种方式只适用于数字类型的数组
function flatten(arr){
return arr.toString().split(',').map(function (item) {
return parseInt(item)
// return +item
})
}
reduce方式实现
function flatten(arr){
return arr.reduce((prev,next)=>{
return prev.concat(Array.isArray(next) ? this.flatten(next): next)
},[])
}
ES6扩展运算符
function flatten(arr){
while(arr.some(item=>Array.isArray(item))){
arr = [].concat(...arr)
}
return arr
}
防抖和节流
防抖 防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行
function debounce(func,wait){
let timeout;
return function(){
let context = this
let args = arguments;
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context,args)
},wait)
}
}
立即执行版本:有时候我们需要立即执行函数,然后等待停止触发n秒后,在重新执行
function debounce(func,wait,immediate){
let timeout;
return function(){
let context = this
let args = arguments;
if(timeout) clearTimeout(timeout)
if(immediate){
var callNow = !timeout
timeout = setTimeout(function(){
timeout = null
},wait)
if(callNow) func.apply(context, args)
}else{
timeout = setTimeout(function(){
func.apply(context,args)
},wait)
}
}
}
节流
函数节流会用在比input, keyup更频繁触发的事件中,如resize, touchmove, mousemove, scroll。throttle 会强制函数以固定的速率执行。因此这个方法比较适合应用于动画相关的场景
使用时间戳:
function throttle(func,wait){
var context,args
var previous = 0
return function(){
var now = +new Date()
context = this
args = arguments
if(now-previous > wait){
fn.apply(context,args)
previous = now
}
}
}
使用定时器
function throttle(func,wait){
var context,args
var timeout
return function(){
context = this
args = arguments
if(!timeout){
timeout = setTimeout(function(){
timeout = null
func.apply(context,args)
},wait)
}
}
}
所以比较两个方法:
第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件
深浅拷贝的实现
浅拷贝实现
Object.assign()
let obj = {
name:"rudy",
info:{
"age":18,
"sex":"male"
}
}
let obj2 = Object.assign({},obj);
obj2.info.age = 24;
console.log(obj.info.age) // 24
Array.prototype.concat()
let arr = [1,2,{
name:"rudy"
}];
let arr2 = arr.concat();
arr2[2].name = "tony";
console.log(arr[2]) // {name:"tony"}
Array.prototype.slice()
let arr = [1,2,{
name:"rudy"
}];
let arr3 = arr.slice();
arr3[2].name = "tony";
console.log(arr[2]) // {name:"tony"}
手动实现
var shallowCopy = function(obj){
if(typeof obj !== 'object') return
var newObj = obj instanceof Array ? [] : {}
for(var key in obj){
if(obj.hasOwnProperty(key)){
newObj[key] = obj[key]
}
}
return newObj
}
深拷贝实现
JSON.parse(JSON.stringify())
let obj = {
name:"rudy",
info:{
sex:"male",
age:18
}
}
let deepObj = JSON.parse(JSON.stringify(obj));
deepObj.info.age = 24;
console.log(obj.info.age) // 18
简单版本
var deepCopy = function(obj){
if(typeof obj !== 'object') return
var newObj = obj instanceof Array ? [] : {}
for(var key in obj){
if(obj.hasOwnProperty(key)){
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]
}
}
return newObj
}
考虑多种类型
function deepCopy(obj,hash = new WeakMap()){
if(typeof obj === null){
return
}
if( obj instanceof Date){
return new Date(obj)
}
if(obj instanceof RegExp){
return new RegExp(obj)
}
if(typeof obj !== 'object'){
return obj
}
if(hash.has(obj)){
return hash.get(obj)
}
const resObj = Array.isArray(obj) ? [] : {}
hash.set(obj,resObj)
Reflect.ownKeys(obj).forEach(key=>{
resObj[key] = deepCopy(obj[key],hash)
})
return resObj
}
编写一个深度克隆函数,满足以下需求
function deepClone(obj) {}
// deepClone 函数测试效果
const objA = {
name: 'jack',
birthday: new Date(),
pattern: /jack/g,
body: document.body,
others: [123,'coding', new Date(), /abc/gim,]
};
const objB = deepClone(objA);
console.log(objA === objB); // 打印 false
console.log(objA, objB); // 对象内容一样
实现:
const deepCopy = (sourceObj) => {
// 如果不是对象则退出(可停止递归)
if(typeof sourceObj !== 'object') return;
// 深拷贝初始值:对象/数组
let newObj = (sourceObj instanceof Array) ? [] : {};
// 使用 for-in 循环对象属性(包括原型链上的属性)
for (let key in sourceObj) {
// 只访问对象自身属性
if (sourceObj.hasOwnProperty(key)) {
// 当前属性还未存在于新对象中时
if(!(key in newObj)){
if (sourceObj[key] instanceof Date) {
// 判断日期类型
newObj[key] = new Date(sourceObj[key].getTime());
} else if (sourceObj[key] instanceof RegExp) {
// 判断正则类型
newObj[key] = new RegExp(sourceObj[key]);
} else if ((typeof sourceObj[key] === 'object') && sourceObj[key].nodeType === 1 ) {
// 判断 DOM 元素节点
let domEle = document.getElementsByTagName(sourceObj[key].nodeName)[0];
newObj[key] = domEle.cloneNode(true);
} else {
// 当元素属于对象(排除 Date、RegExp、DOM)类型时递归拷贝
newObj[key] = (typeof sourceObj[key] === 'object') ? deepCopy(sourceObj[key]) : sourceObj[key];
}
}
}
}
return newObj;
}
图片懒加载的实现
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lazyload</title>
<style>
.image-item {
display: block;
margin-bottom: 50px;
height: 200px;
}
</style>
</head>
<body>
<img src="" class="image-item" lazyload="true" data-original="images/1.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/2.png"/>
<img src="" class="image-item" lazyload="true" data-original="images/3.png"/>
<script>
const viewHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight; //获取可视区高度
function lazyload(){
const eles=document.querySelectorAll('img[data-original][lazyload]');
Array.prototype.forEach.call(eles,function(item,index){
if(item.dataset.original==="") {
return;
}
const rect=item.getBoundingClientRect(); // 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置
if(rect.bottom>=0 && rect.top < viewHeight){
let img=new Image()
img.src=item.dataset.original
img.onload=function(){
item.src=img.src
}
item.removeAttribute("data-original")//移除属性,下次不再遍历
item.removeAttribute("lazyload");
}
})
}
lazyload()//刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
document.addEventListener("scroll",lazyload)
</script>
</body>
</html>
实现一个响应式函数,对能一个对象内的所有key添加响应式
const render = (key, val) => {
console.log(`SET key=${key} val=${val}`)
}
const defineReactive = (obj, key, val) => {
reactive(val)
Object.defineProperty(obj, key, {
get() {
return val
},
set(newVal) {
if (val === newVal) return
val = newVal
render(key, val)
}
})
}
const reactive = obj => {
if (typeof obj === 'object') {
for (const key in obj) {
defineReactive(obj, key, obj[key])
}
}
}
const data = {
a: 1,
b: 2,
c: {
c1: {
af: 999
},
c2: 4
}
}
实现数组响应式
const render = (action,...args)=>{
console.log(`Action=${action} args=${args.join(',')}`)
}
const arrPrototype = Array.prototype
const newArrPrototype = Object.create(arrPrototype)
const methods = ['push','pop','shift','unshift','sort','splice','reverse']
methods.forEach(methodName=>{
newArrPrototype[methodName] = function(){
// 执行原数组的方法
arrPrototype[methodName].call(this,...arguments)
// 触发渲染
render(methodName,...arguments)
}
})
const reactive = obj=>{
if(Array.isArray(obj)){
obj.__proto__ = newArrPrototype
}
}
const data = [1,2,3,4]
reactive(data)
data.push(5) // Action = push, args = 5
data.splice(0,2) // Action=splice,args=0,2
实现delete操作的响应式处理
前置知识点:Proxy、Reflect
let observeStore = new Map()
function makeObservable(target){
let handleName = Symbol('handler')
observeStore.set(handleName,[])
target.observe = function(handler){
observeStore.get(handleName).push(handler)
}
const proxyHandler = {
get(target,property,receiver){
// 处理嵌套对象
if(typeof target[property] === 'object' && target[property] !== null){
return new Proxy(target[property],proxyHandler)
}
let success = Reflect.get(...arguments)
if(success){
observeStore.get(handleName).forEach(handler=>handler('GET',property,target[property]))
}
return success
},
set(target,property,value,receiver){
let success = Reflect.set(...arguments)
if(success){
observeStore.get(handleName).forEach(handler=>handler('SET',property,value))
}
},
deleteProperty(target,property){
let success = Reflect.deleteProperty(...arguments)
if(success){
observeStore.get(handleName).forEach(handler=>handler('DELETE',property))
}
}
}
// 创建proxy 拦截更改
return new Proxy(target,proxyHandler)
}
let user = {}
user = makeObservable(user)
user.observe((action,key,value)=>{
console.log(`${action} key=${key} value=${value || ''}`)
})
user.name = 'john' // SET key=name value=john
console.log(user.name) // GET key=name value=john
delete user.name // DELETE key=name value=
虚拟DOM转成真实DOM
// 将vnode转成真实DOM元素
const vnode = {
tag:'DIV',
attrs:{
id:'app'
},
children:[{
tag:'SPAN',
children:[{
tag:'A',
children:[]
}]
},
{
tag:'SPAN',
children:[
{
tag:'A',
children:[]
},
{
tag:'A',
children:[]
}
]
}]
}
function render(vnode){
if(typeof vnode === 'number'){
vnode = String(vnode)
}
if(typeof vnode === 'string'){
return document.createTextNode(vnode)
}
const element = document.createElement(vnode.tag)
if(vnode.attrs){
Object.keys(vnode.attrs).forEach(attrKey=>{
element.setAttribute(attrKey,vnode.attrs[attrKey])
})
}
if(vnode.children){
vnode.children.forEach(childNode=>{
element.appendChild(render(childNode))
})
}
return element
}
console.log(render(vnode))
如何让一个对象可以被 for ... of遍历
const obj = {
count:0,
[Symbol.iterator]:()=>{
return{
next:()=>{
obj.count++
if(obj.count <= 10){
return {
value:obj.count,
done:false
}
}else{
return{
value:undefined,
done:true
}
}
}
}
}
}
for(const item of obj){
console.log(item)
}
实现instanceof
function instanceOf(left,right){
if(typeof left !=='object' || left === null){
return false
}
while(true){
if(left === null) return false
if(left.__proto__ === right.prototype){
return true
}
left = left.__proto__
}
}
扁平数组转树
let input = [
{
id: 1,
val: "学校",
parentId: null,
},
{
id: 2,
val: "班级1",
parentId: 1,
},
{
id: 3,
val: "班级2",
parentId: 1,
},
{
id: 4,
val: "学生1",
parentId: 2,
},
{
id: 5,
val: "学生2",
parentId: 3,
},
{
id: 6,
val: "学生3",
parentId: 3,
},
];
function buildTree(arr, parentId, childrenArray) {
arr.forEach((item) => {
if (item.parentId === parentId) {
item.children = [];
buildTree(arr, item.id, item.children);
childrenArray.push(item);
}
});
}
function arrayToTree(input, parentId) {
const array = [];
buildTree(input, parentId, array);
return array.length > 0 ? (array.length > 1 ? array : array[0]) : {};
}
const obj = arrayToTree(input, null);
console.log(obj);
手写发布订阅
class EventEmitter {
constructor() {
// handlers是一个map,用于存储事件与回调之间的对应关系
this.handlers = {}
}
// on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
on(eventName, cb) {
// 先检查一下目标事件名有没有对应的监听函数队列
if (!this.handlers[eventName]) {
// 如果没有,那么首先初始化一个监听函数队列
this.handlers[eventName] = []
}
// 把回调函数推入目标事件的监听函数队列里去
this.handlers[eventName].push(cb)
}
// emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
emit(eventName, ...args) {
// 检查目标事件是否有监听函数队列
if (this.handlers[eventName]) {
// 这里需要对 this.handlers[eventName] 做一次浅拷贝,主要目的是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
const handlers = this.handlers[eventName].slice()
// 如果有,则逐个调用队列里的回调函数
handlers.forEach((callback) => {
callback(...args)
})
}
}
// 移除某个事件回调队列里的指定回调函数
off(eventName, cb) {
const callbacks = this.handlers[eventName]
const index = callbacks.indexOf(cb)
if (index !== -1) {
callbacks.splice(index, 1)
}
}
// 为事件注册单次监听器
once(eventName, cb) {
// 对回调函数进行包装,使其执行完毕自动被移除
const wrapper = (...args) => {
cb(...args)
this.off(eventName, wrapper)
}
this.on(eventName, wrapper)
}
}