一些JavaScript手写题目实现
开局
不按分类顺序排放,我是持续加进去的,想到一个写一个属于是~
浅拷贝和深拷贝(来源于某次面试的思考)
首先拷贝需要创建一个新的对象,浅拷贝即指把对象的属性拷贝一份,如果属性是基础类型,那么拷贝的是基础类型的值,如果属性是引用类型(对象),那么拷贝的是引用地址;而深拷贝与浅拷贝的区别是如果属性是引用类型,那么拷贝的是这个引用类型的浅拷贝,深拷贝的作用是创建对象完全的副本,互不影响! 其中浅拷贝需要考虑的东西相对比较简单,只需要判断一下类型即可;但是深度拷贝还需要考虑循环引用等情况
const isObject = (target) => {
return target !== null && typeof target === "object";
};
// 浅拷贝
const shallowCopy = (original) => {
/**
* 判断类型
* 基础类型 > 直接返回
* 引用类型 > copy
* */
if (!isObject(original)) return original;
const result = Array.isArray(original) ? [] : {};
for (const key in original) {
if (original.hasOwnProperty(key)) {
result[key] = original[key];
}
}
return result;
};
// 深拷贝
const deepCopy = (original) => {
// 用一个缓存来解决循环依赖的问题
const map = new Map();
const nestedCopy = () => {
if (!isObject(original)) return original;
const result = Array.isArray(original) ? [] : {};
// 设置缓存
map.set(original, result);
for (const key in original) {
if (original.hasOwnProperty(key)) {
const val = original[key];
if (isObject(val)) {
// TODO 还需要判断原生对象类型,比如Date,你需要new一个Date;这里就不实现了
if (map.has(val)) {
const cacheVal = map.get(val);
result[key] = cacheVal;
} else {
const copyVal = nestedCopy(val);
result[key] = copyVal;
}
} else {
result[key] = original[key];
}
}
}
return result;
};
const result = nestedCopy();
// 置空缓存
map.clear();
return result;
};
apply
// 我们先准备一个对象以及一个函数
const _t = {
name: 'panda'
}
function sayName(age, sex) {
console.log(this.name)
console.log(age)
console.log(sex)
}
Function.prototype.myApply = function (context, args) {
var t_obj = context || window;
// 将fn作为对象的一个属性
t_obj.fn = this;
// 然后从这个对象去访问这个方法并执行
const result = t_obj.fn(...args)
delete t_obj.fn;
// 别忘记返回函数返回的返回值
return result;
};
call
bind
这三个实现一个基本就都能实现了,bind相比于上边两个,实现起来相对比较麻烦,因为需要考虑的东西要多一些
数组去重
// 第一种 利用set的特性
const duplicateRemoval=(arr)=>{
// 另外一种写法:Array.from(new Set(arr))
return [...new Set(arr)]
}
// 第二种 双重循环,最笨方法(for+findIndex,for+find,filter+indexOf,for+includes是一样的原理)
const duplicateRemoval=(arr)=>{
const resultArr=[]
for(let i=0;i<arr.length;i++){
const node=arr[i]
let flag=false
for(let j=0;j<resultArr.length;j++){
const node2=resultArr[j]
if(node===node2){
flag=true
break;
}
}
if(!flag)resultArr.push(node)
}
return resultArr
}
// for + object(也可以用map)
const duplicateRemoval =(arr)=>{
// 利用对象属性名不能重复这一特点
const resultArr = []
const obj = {}
for(let i = 0;i<arr.length;i++){
if (!obj[arr[i]]) {
resultArr.push(arr[i])
obj[arr[i]] = 1
} else {
obj[arr[i]] ++
}
};
return resultArr
}
// 利用reduce
function duplicateRemoval (arr) {
let resultArr = []
return arr.reduce((prev, next,index, arr) => {
// 如果包含,就返回原数据,不包含,就把新数据追加进去
return newArr.includes(next) ? newArr : newArr.push(next)
}, 0)
}
数组打平,多维数组转换为一维数组
// 原生已经实现了
const expandArray=(array)=>{
return array.flat(Infinity);
}
// 递归实现,可设置层级
const expandArray = (array, level = Infinity) => {
const resultArr = []
let dep = 0
const run = (arr) => {
// 每当需要递归调用说明层级+1了
dep++
arr.forEach((node) => {
if (Array.isArray(node) && dep < level) {
run(node)
} else {
resultArr.push(node)
}
})
// 一轮执行完毕,应该将层级减去1,恢复到上一层级
dep--
}
run(array)
return resultArr
}
instanceof
这个操作符用来判断该对象是否在某一条原型链上,通常用来判断是不是某个对象的子集
const myInstanceof = (obj, { prototype }) => {
let p = Object.getPrototypeOf(obj)
while (true) {
if (p === null) {
return false
} else if (p === prototype) {
return true
}
p = Object.getPrototypeOf(p)
}
}
解析url参数
这块熟悉正则的话应该能玩的6一点
const getUrlParams = (url) => {
const params = {}
const paramsString = url.split('?')[1]
const kvArray = paramsString.split('&')
kvArray.forEach((kv) => {
const lr = kv.split('=')
params[lr[0]] = lr[1]
})
return params
}
Object.create
该函数用来创建一个干净的对象
function createObj(obj){
function Fn(){}
Fn.prototype = obj
return new Fn()
}
实现简易hash路由系统
class myRoute {
constructor() {
// 路由映射关系
this.routes = {}
// 当前hash值
this.currentHash = ''
window.addEventListener('load', this.updateRoute, false)
window.addEventListener('hashchange', this.updateRoute, false)
}
addRoute(path, cb) {
this.routes[path] = cb || function () { }
}
// 更新
updateRoute = () => {
this.currentHash = location.hash.slice(1) || '/'
this.routes[this.currentHash]()
}
}
节流
通常用来稀释函数执行的次数
const d = (fn, delay) => {
let timer = null;
return (...args) => {
if (timer) return;
timer = setTimeout(() => {
fn(...args);
timer = null;
}, delay);
};
};
防抖
在一段时间内只会执行最后一次调用
const t = (fn, delay) => {
let timer = null;
return (...args) => {
timer && clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
};
当防抖函数遇到先发起的请求响应巨慢的问题(其实这里防不防抖无所谓,主要是前边响应慢的问题)
来源于朋友某次面试题的思考,不废话,直接模拟场景
const t = (fn, delay) => {
let timer = null;
return (...args) => {
timer && clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
};
const req = () => {
// 请求发起时间戳
const now = Date.now();
setTimeout(
() => {
console.log(`请求响应了,本次预计返回的数据:${now}`);
// 假设返回的就是时间戳
data.count = now;
},
// 模拟第一次请求慢一点,第二次立即返回
i === 1 ? 2000 : 0
);
};
const data = {
count: 0,
};
// 防抖函数,间隔500
const reqT = t(req, 500);
// 分别发送两次请求,必须满足防抖下执行两次的时间间隔
let i = 0;
const timer = setInterval(() => {
i++;
reqT();
i === 2 && clearInterval(timer);
}, 700);
setTimeout(
() => {
console.log("最终结果:" + data.count);
},
// 最短需要700*2+2000才能看到最终结果
700 * 2 + 2000 + 1
);
通过上边的例子可以得出结果总是得到第一次响应慢的结果,而我们希望得到的最新发起请求的结果;怎么办呢?我想的是通过时间戳来判断,如果发起时间早于完成时间,就是无效请求.下边是解决方案:
const t = (fn, delay) => {
let timer = null;
return (...args) => {
timer && clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, delay);
};
};
/**
* 请求函数
* 需求:期望总是以最后发起请求后的响应为准,前边响应慢的请求为无效请求
*/
const req = (() => {
let requestDoneTime = 0;
return () => {
// 请求发起时间戳
const now = Date.now();
setTimeout(
() => {
console.log(`请求响应了,本次预计返回的数据:${now}`);
// 如果发起时间早于完成时间,就说明是过期请求;说明一下,如果要适应不同的请求,还需要一定的封装处理,使它们相互隔离.
if (now < requestDoneTime) return;
// 假设返回的就是时间戳
data.count = now;
// 重新设置请求成功时间
requestDoneTime = Date.now();
},
// 模拟第一次请求慢一点,第二次立即返回
i === 1 ? 2000 : 0
);
};
})();
const data = {
count: 0,
};
// 防抖函数,间隔500
const reqT = t(req, 500);
// 分别发送两次请求,必须满足防抖下执行两次的时间间隔
let i = 0;
const timer = setInterval(() => {
i++;
reqT();
i === 2 && clearInterval(timer);
}, 700);
setTimeout(
() => {
console.log("最终结果:" + data.count);
// 再次发送请求,查看是否影响后续请求
reqT();
setTimeout(() => {
console.log("最终结果2:" + data.count);
}, 700 + 1);
},
// 最短需要700*2+2000才能看到最终结果
700 * 2 + 2000 + 1
);
发布订阅
class EventBus {
constructor() {
this._map = new Map();
this.$on = this.on;
this.$once = this.once;
this.$off = this.off;
this.$emit = this.emit;
}
on(event, handler) {
const eventHandlers = this._map.get(event);
if (!eventHandlers || eventHandlers.length === 0) {
this._map.set(event, [handler]);
} else {
eventHandlers.push(handler);
}
}
once(event, handler) {
this.on(event, (..args) => {
handler(args);
this.off(event, handler);
});
}
emit(event, ...args) {
const eventHandlers = this._map.get(event);
if (!eventHandlers || eventHandlers.length === 0) return;
eventHandlers.forEach((node) => {
node(args);
});
}
off(event, handler) {
const eventHandlers = this._map.get(event);
if (!eventHandlers || eventHandlers.length === 0) return;
this._map.set(
event,
eventHandlers.filter((node) => {
return node !== handler;
})
);
}
}
async和await的手写模拟实现
async其实是帮我们自动的去调用生成器的next方法来实现像同步代码一样的风格,知道这个原理来手写一个其实也并不难,await你可以理解为用yeild包裹了一下异步函数(受限于js的特性,无法真正的手写实现)
const myAsync = (fn) => {
const g = fn();
const run = (data) => {
// 第一次next传参是无效的,他只用于启动
const result = g.next(data);
if (result.done) return result.value;
result.value.then((res) => {
run(res);
});
};
return run();
};
// 一个异步函数,需要返回一个promise对象
const handler = (val = 0) => {
return new Promise((reslove) => {
setTimeout(() => {
console.log(val + 1);
reslove(val + 1);
}, 1000);
});
};
myAsync(function* () {
// yield只能用于生成器内部,否则可以完美实现手写
const val1 = yield handler();
const val2 = yield handler(val1);
const val3 = yield handler(val2);
console.log({ val1, val2, val3 });
});
mini版本的vuex
import { inject, reactive } from 'vue'
// 注入时候的key,可以做当一个命名空间来理解,用户可以自行传入name做区分
const STORE_KEY = '__store__'
// hooks
function useStore(name = STORE_KEY) {
return inject(STORE_KEY)
}
// 创建实例
function createStore(options) {
return new Store(options)
}
class Store {
constructor(options) {
this.$options = options
this._state = reactive({
data: options.state
})
this._name = options.name || STORE_KEY
this._mutations = options.mutations
this._actions = options.actions
}
get state() {
return this._state.data
}
// 触发mutation
commit = (type, payload) => {
const mutation = this._mutations[type]
mutation && mutation(this.state, payload)
}
// 触发action
dispatch = (type, payload) => {
const action = this._actions[type]
action && action(this, payload)
}
// 注册的时候,vue会自动调用这个方法
install(app) {
// 这里的app就是vue的实例,将状态注入
app.provide(this._name, this)
}
}
export { createStore, useStore }
封装一个最大请求数的请求函数
来源于成都博智信息
的面试问题,他想要得是你封装一个函数,参数是一堆请求列表,然后可以设置最大同时请求数目,等待所有请求完成后返回这些列表的响应请求
// 模拟请求函数,响应时间随机,抛错随机
const req = (url,) => {
const time = Date.now()
return new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5 ? resolve({
url,
time,
done_time: Date.now(),
}) : reject(
{
url,
time,
done_time: Date.now(),
}
)
},
Math.random() * 3000
)
})
}
/**
* @param {*} urls 请求列表
* @param {*} max 同时请求数目
*/
const maxReq = (urls = [], max = 3) => {
const result = []
//请求完成数目,当前操作的索引
let count = 0, index = -1;
const len = urls.length
return new Promise((resolve) => {
const next = () => {
const index2 = ++index
if (index2 >= len) return
const url = urls[index2]
req(url).then((res) => {
result[index2] = res
next()
}).catch((err) => {
result[index2] = err
next()
}).finally(() => {
if (++count >= len) resolve(result);
})
}
for (let i = 0; i < (max < len ? max : len); i++) {
next()
}
})
}
const urls = ['url1', 'url2', 'url3', 'url4', 'url5', 'url6', 'url7']
// test
maxReq(urls).then((res) => {
console.log(res);
debugger
})
其实可以封装一个调度的类,那么代码就可以改为下面的方式:
class Scheduler {
// 用于判断是否是一个promise,灵感来源于vue源码
static isPromise(val) {
return (
val !== undefined && val !== null &&
typeof val.then === 'function' &&
typeof val.catch === 'function'
)
}
constructor(limit = 2, callback = () => { }) {
// 任务编号索引
this.index = -1;
// 任务队列
this.queue = [];
// 完成数目
this.doneCount = 0;
// 限制并发数
this.limit = limit;
// 结果数组
this.resultArray = []
// 所有请求完成之后的回调
this.callback = callback
}
// 添加任务
add = (task) => {
this.index++;
if (Scheduler.isPromise(task)) {
console.log("任务是promise类型");
this.queue.push(this.decoratTask(task));
} else if (typeof task === 'function') {
console.warn('你似乎传入的不是一个promise,这种情况请查看下使用方式.')
// 这里需要遵循一定的规范去使用,需要提前告知使用者,实际上还不如强制告诉使用者必须传入promise
this.queue.push(
this.decoratTask(
new Promise((resolve, reject) => {
task(resolve, reject);
})
));
}
}
// 开始任务
start = () => {
const count = this.limit > this.queue.length ? this.queue.length : this.limit;
for (let i = 0; i < count; i++) {
this.next();
}
}
// 下一个
next = () => {
if (this.queue.length) {
const task = this.queue.shift();
this.run(task)
}
}
// 装饰任务对象,其实这里的作用是为了保存结果数组的有序性
decoratTask = (task) => {
return {
index: this.index,
p: task
}
}
// 执行单次任务
run = (task) => {
console.log("run");
const { index, p } = task;
p.then((res) => {
this.resultArray[index] = res;
}).catch((err) => {
console.log(err);
this.resultArray[index] = err;
}).finally(() => {
console.log('will next');
this.doneCount++;
if (this.index + 1 === this.doneCount) {
this.callback(this.resultArray);
} else {
this.next();
}
});
}
}
const req = (url,) => {
const time = Date.now()
return new Promise((resolve,) => {
setTimeout(() => {
resolve({
url,
time,
done_time: Date.now(),
})
}, Math.random() * 3000
)
});
}
const scheduler = new Scheduler(99, (res) => {
console.log(res);
});
const urls = ['url1', 'url2', 'url3', 'url4', 'url5', 'url6', 'url7'];
urls.forEach((url) => {
scheduler.add(req(url));
});
scheduler.start();
封装一个管道函数(上海通办服务面试题)
实现一个高阶函数pipe
,让传递的函数依次执行,上一个函数的返回值是下一个函数的参数;形如这样调用:pipe(fn,fn,fn,fn,fn)(params)
const pipe = (...fns) => {
return (params) => {
let result = params;
[...fns].map((fn) => {
result = fn(result);
})
return result;
}
}
const res = pipe((a) => { return a + 1 }, (a) => { return a + 2 })(0)
console.log(res); //3
如果函数内有异步行为的话,那么返回的结果也会是一个promise,稍加改造即可。
手写Promise
Promise在我们日常的工作中,已经运用得非常广泛了,它配合async和await将js从回调地狱中拉到了人间。好,我们一步一步来实现一个符合A+规范的Promise。
首先我们都知道promise会有三个状态,那么我们先来定义三个状态,他们分别表示了‘等待’,‘解决’,‘拒绝’。
class MyPromise {
#PENDING = 'pending';
#FULFILLED = 'fulfilled';
#REJECTED = 'rejected';
}
好,接下来是构造器