前言
今日阳光明媚,多云转晴,正是大展身手的好日子。
叮铃铃~~~ ,小魏45度角望向天际,双鬓前的秀发好像又倔强了几分。
小魏:“您好,哪位?”
面试官:“昆仑山火星分山集团面试官,看了你的简历,挺能吹的,很符合我们公司气质”
小魏:“会一会?”
面试官:“光明顶!”
切磋
1、类型判断篇
面试官:如何准确判断变量类型?
小魏:小意思
/**
* @description 判断数据类型
* @param {*} variable
* @returns {string}
*/
function getType(variable) {
return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
}
2、防抖、节流篇
面试官:防抖如何理解,如何实现?
小魏:玩过LOL吗,按B回城不管按多少次,都是最后一次再过8秒才能回
/**
* @description 防抖
* @param {Function} fn
* @param {Number} t
* @returns
*/
function debounce(fn, t) {
let timer = null;
return function() {
// 每次调用都初始化定时器
timer && clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this);
}, t);
}
}
面试官:节流呢(挺能编啊,有本事再编)
小魏:玩过快乐风男吗,体验过被问号追赶的快乐吗,无论e按的多快,仍然是按cd来执行
/**
* @description 节流
* @param {Function} fn
* @param {Number} t
* @returns
*/
function throttle(fn, t) {
let timer = null;
return function() {
// 每次调用,如果定时器存在,则返回
if(timer) return;
fn.apply(this);
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
}, t);
}
}
3、深拷贝篇
面试官:会深拷贝吗?
小魏:JSON.parse(JSON.stringify(obj))
面试官:我的大刀早已饥。。。
小魏:先把刀放下
/**
* @description 深拷贝
* @param {*} target
* @returns
*/
function deepClone(target, cache = new WeakMap()) {
// 基本数据类型统一直接返回原值
if(typeof target !== "Object") return target;
// 数组对象区分初始值
const result = Array.isArray(target) ? [] : {};
// Map缓存一下,避免自调用如target.target = target 致使程序崩溃
if(cache.get(target)) return cache.get(target);
cache.set(target, result);
for(const k in target) {
result[k] = deepClone(target[k], cache);
}
return result;
}
4、洗牌算法篇
面试官:当过荷官吗,啊呸,实现一个洗牌算法(差点被这小子带偏了)
小魏:性感荷官在线发牌吗
/**
* @description 洗牌
* @param {Array} _arr
* @returns
*/
function shuffle(_arr) {
let arr = _arr;
const newArr = [];
while (arr.length) {
const i = random(0, arr.length - 1);
newArr.push(arr[i]);
arr = arr.filter(j => arr[i] !== j);
}
return newArr;
}
5、并发请求限制篇
面试官:光明顶有4个厕所,但是现在有几百号弟子要上厕所,你给解决一下
小魏:你说的是限制并发吧(一脸正经)
面试官:小伙子有两把刷子
此处会有两个想法:
1、将请求列表按最大并发数分为n等份,每份使用promise.all,遍历n等份,最终完成执行回调。
弊端:如果一份中其中一个请求特别慢,就会很明显的看到,此时的并发数为1而不是最大并发数,这样很显然是在浪费资源
2、维护一个长度为最大并发数的异步队列,利用递归来解决
第一次直接启动最大并发数个请求,随后每完成一个,就再启动一个,这样可以保证在请求列表充足的情况下永远保持最大并发数
so,采取第二种方法实现
/**
* @description 并发请求限制
* @param {Array} urlList
* @param {Number} maxNum
* @param {Function} callback
*/
function limitRequest(urlList, maxNum, callback) {
const resultMap = {}; // 结果存储
let currentIndex = 0; // 当前请求序号
const handlerRequest = (url) => {
console.log(`${url} - start`);
if(currentIndex < maxNum) { // 若请求数未达到最大并发数,就直接启动下一个请求
currentIndex += 1;
handlerRequest(urlList[currentIndex]);
}
request(url).then(res => {
// 一个请求结束(成功)
console.log(`${url} - success end`);
resultMap[url] = res; // 收集结果
currentIndex += 1;
// 若还有请求,则继续开启下一个
if(urlList[currentIndex]) {
handlerRequest(urlList[currentIndex]);
}
// 若结果存储器长度等于请求数了,则证明所有请求已处理完毕,执行完成回调
if(Object.keys(resultMap).length >= urlList.length) {
callback && callback(resultMap);
}
}).catch(error => {
// 一个请求结束(失败),与成功同理
console.log(`${url} - error end`);
resultMap[url] = error;
currentIndex += 1;
if(urlList[currentIndex]) {
handlerRequest(urlList[currentIndex]);
}
if(Object.keys(resultMap).length >= urlList.length) {
callback && callback(resultMap);
}
});
}
// 手动执行第一次请求
handlerRequest(urlList[0]);
}
/**
* @description min max 之间随机整数
* @param {Number} min
* @param {Number} max
* @returns
*/
function random(min, max) {
return min + Math.floor(Math.random()*(max + 1 - min));
}
/**
* @description 模拟请求
* @param {String} url
* @returns
*/
function request(url){
return new Promise((resolve, reject) => {
setTimeout(() => {
random(1, 10) > 7 ? reject("error" + url) : resolve(url);
}, random(1, 6) * 1000);
});
}
6、数组扁平化篇
面试官:实现一个数组扁平化
小魏:(假装思索)
/**
* @description 数组扁平化
* @param {*} arr
* @returns
*/
function flat(arr) {
let newArr = [];
for(const i of arr) {
if(Array.isArray(i)) {
// 数组递归取值
newArr = [...newArr, ...flat(i)];
} else {
// 非数组直接push原值
newArr.push(i);
}
}
return newArr;
}
面试官:用reduce实现,小样
小魏:(白眼一翻,并不想说话)
/**
* @description 数组扁平化 reduce 方式实现
* @param {*} arr
* @returns
*/
function flatByReduce(arr) {
return arr.reduce((pre, cur) => {
if(Array.isArray(cur)) {
return [...pre, ...cur];
}
return pre.push(cur);
}, []);
}
7、观察者模式、发布-订阅模式篇
面试官:了解观察者模式吗,实现一下
小魏:(捋了捋眼角上方的秀发)
/**
* @description 观察者模式实现
*/
// 目标对象
class Subject {
constructor() {
// 维护一个观察者集合
this.observerList = [];
}
// 添加观察者
addObserver(observer) {
// 避免重复添加观察者
if(this.observerList.includes(observer)) return;
this.observerList.push(observer);
}
// 删除指定观察者,不支持全部删除,风险较高
removeObserver(observer) {
if(!observer) return;
this.observerList = this.observerList.filter(i => i !== observer);
}
// 通知观察者
notify() {
// 若无观察者,也就不通知了
if(!this.observerList) return;
// 遍历调用观察者的 update 方法
this.observerList.forEach(observer => {
if(observer.update && Object.prototype.toString.call(observer.update).slice(8, -1) === "Function") {
observer.update();
}
});
}
}
// 观察者
class Observer {
update() {
console.log("我观察到了目标对象的变化");
}
}
面试官:发布-订阅呢?写一下
小魏:(眉头一皱)
/**
* @description 发布订阅实现
*/
class Event {
constructor() {
// 维护一个事件对象
this.eventHandler = {};
}
// 事件订阅
on(key, fn) {
if(Object.prototype.toString.call(fn).slice(8, -1) !== "Function") return;
// 若没有,则创建事件执行数组
if(!this.eventHandler[key]) {
this.eventHandler[key] = [];
}
// 避免重复添加
if(this.eventHandler[key].includes(fn)) return;
this.eventHandler[key].push(fn);
}
// 事件发布
emit(key) {
if(!this.eventHandler[key] || !this.eventHandler[key].length) return;
this.eventHandler[key].forEach(fn => fn());
}
// 取消订阅
remove(key, fn) {
// 若不传事件 key 则返回
if(!key || !this.eventHandler[key]) return;
// 若不传事件订阅的具体方法则取消该事件的所有订阅
if(!fn) {
this.eventHandler[key] = [];
return;
}
// 取消该事件的某个订阅
this.eventHandler[key] = this.eventHandler[key].filter(i => i !== fn);
}
}
面试官:他俩之间有什么异同吗
小魏:(😭就猜到要问这个),他们两者呢,其实大体的思想是很相像的,只是抽象的维度略有不同。
观察者模式注重与观察与被观察者两者之间的关系,被观察者直接通知观察者更新状态,也没有更多的事件key概念,比较纯粹。
发布-订阅模式是由发布者、订阅者、与事件处理枢纽来组成,个人理解为发布订阅通过事件枢纽的建造来完成观察与被观察者之间的解耦,从而让功能更加专注,只需处理好这个中间枢纽即可,再者也有了事件概念,发布订阅着可以通过不同的事件进行交互,也更加灵活。
当然这只是我的个人理解,一万个人心里就有一万个哈姆雷特,用自己的理解正确的运用,解决适当的问题才是最关键的(个人观点不代表大众,甩锅保平安😂)
8、vue的响应式实现篇
面试官:vue是怎样实现数据响应的
小魏:数据劫持加观察者模式
面试官:怎么个劫持法?
小魏:(我。。。我祝你生日快乐【引自·西虹市首富】)vue2中使用了Object.defineProperty来劫持数据,vue3使用proxy代理来实现
/**
* @description Object.defineProperty 实现响应式
*/
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
set(newValue) {
console.log(`有人将${key}属性从${value}改成了${newValue}`);
value = newValue;
},
get() {
console.log(`有人在读取${key}属性`);
return value;
}
});
}
function observer(data) {
for(const k in data) {
defineReactive(data, k, data[k]);
}
}
const data = {
name: "小明",
age: 24
}
observer(data);
data.age = 25;
console.log(data.age);
/**
* @description proxy 实现响应式
*/
function observerByProxy(data) {
return new Proxy(data, {
get(target, key) {
console.log("get", target, key);
return target[key];
},
set(target, key, value) {
console.log("set", target, key, value);
target[key] = value;
}
});
}
const obj = observerByProxy({
name: "小明",
age: 24
});
obj.age = 25;
console.log(obj.age);
obj.father = "me";
obj.brother = "you";
console.log(obj.father);
console.log(obj.brother);
// 此处小魏内涵了一波昆仑山面试官,认真的看官才能发现,发现的评论区集合😂
面试官:(wf,臭小子内涵我),来说说两者的差异
小魏:(😭自作孽不可活啊)。
Object.defineProperty 为对象的所有属性值增加 set get方法,但也因只能对已有的属性进行劫持所以在 vue2中对于对象的新增属性是实现不了响应式的,此时只有通过 Vue.set 处理才能达到响应式的作用。而proxy就不一样了,他代理的是整个对象,也就是说无论是已有属性值的修改还是新增属性都是可以感知到的,也就是更牛逼了。但是proxy也有一个很大的弊端,就是proxy是es6才提出的,兼容性和defineProperty没得比,且很多问题通过 poilyfill 是无法解决的,有舍才有得,对于二者的选用,就是一个利弊取舍。
面试官:(糟糕,让他装到了),好,还行,你刚提到的 Vue.set干了什么事
小魏:Vue.set传参有 obj,key,value,三个,其实就是检测一下key属性在obj中是否是响应式的即,判断下key是否是已有属性,是否是数组属性等等,如果不是响应式的,那就使用defineReactive去增加一下set get方法让其变为可响应属性
面试官:对于数组操作,"arr[0] = 3",在vue中并不能响应到,为什么?
小魏:其实我自己尝试过,数组和对象类似,defineProperty实际上试可以劫持到的,但因为数组其实是一个不可控的东西,这其中造成的一些性能浪费是不可预期的,所以,在用户体验与性能浪费的权衡中,vue放弃了对数组索引操作的响应,转而用数组方法变异处理的方式优雅的解决了数组的问题
面试官:(不赖不赖,但还是得高冷)嗯,还行,那我们今天先到这里,随后再联系
小魏:(风中摇曳的秀发不再倔强,但不代表他没有自己的锋芒)
未完待续~~~
结语
本故事纯属虚构,如有雷同纯属巧合
致敬每一个前端coder;Respect;