1.引言
最近的机会貌似又多了起来,决定再准备准备,直接魔怔人一波,目标八股,刚好今天和大厂返校的好兄弟聊了聊,发现确实有很多不会的。又面了几个,但是越面越感觉自己就是前端人海中的微不足道的一个,现在什么demo等的基本上没什么了,人太多了,去年秋招前端进的人简直是非常幸运了,现在甚至有点怀疑自己这几年的前端白学了。不说了不说了,总结一些遇到的一些问题,手上还有几个笔试链接,害,珍惜每一次机会即使可能不属于自己。另外,我笔试中也遇到了这种问题:官网防作弊做的太差了,或者笔试简直开卷,我没有抄的习惯,但…不知道友友们怎么看。
2.薄弱的手写篇
如果这些代码不会手写,那面了还是白面了。
(1)手写promise,其实就是考察对于promise的理解,promise是对异步函数的封装,不过认为设置了三种状态,这里可以回想一下自己写js异步调用的时候的callback怎么用的就行,promise则类似于将多个promise封装了,具体实现代码如下(确实有点长
):
const isFunction = variable => typeof variable === 'function'
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
class MyPromise {
constructor(handle) {
if (!isFunction(handle)) {
throw new Error('MyPromise must accept a function as a parameter')
}
this._status = PENDING
this._value = undefined
this._fulfilledQueues = []
this._rejectedQueues = []
try {
handle(this._resolve.bind(this), this._reject.bind(this))
} catch (err) {
this._reject(err)
}
}
// 添加resovle时执行的函数
_resolve(val) {
const run = () => {
if (this._status !== PENDING) return
const runFulfilled = (value) => {
while (this._fulfilledQueues.length) {
this._fulfilledQueues.shift()(value);
}
}
const runRejected = (error) => {
while (this._rejectedQueues.length) {
this._rejectedQueues.shift()(error);
}
}
//判断是否是resolve的参数为Promise对象
if (val instanceof MyPromise) {
val.then(value => {
this._value = value
this._status = FULFILLED
runFulfilled(value)
}, err => {
this._value = err
this._status = REJECTED
runRejected(err)
})
} else {
this._value = val
this._status = FULFILLED
runFulfilled(val)
}
}
// 为了支持同步的Promise,这里采用异步调用
setTimeout(run, 0)
}
// 添加reject时执行的函数
_reject(err) {
if (this._status !== PENDING) return
const run = () => {
this._status = REJECTED
this._value = err
while (this._rejectedQueues.length) {
this._rejectedQueues.shift()(err)
}
}
// 为了支持同步的Promise,这里采用异步调用
setTimeout(run, 0)
}
then(onFulfilled, onRejected) {
const { _value, _status } = this
// 返回一个新的Promise对象
return new MyPromise((onFulfilledNext, onRejectedNext) => {
// 封装一个成功时执行的函数
let fulfilled = value => {
try {
if (!isFunction(onFulfilled)) {
onFulfilledNext(value)
} else {
let res = onFulfilled(value);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后再执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
// 封装一个失败时执行的函数
let rejected = error => {
try {
if (!isFunction(onRejected)) {
onRejectedNext(error)
} else {
let res = onRejected(error);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后再执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
switch (_status) {
// 当状态为pending时,将then方法回调函数加入执行队列等待执行
case PENDING:
this._fulfilledQueues.push(fulfilled)
this._rejectedQueues.push(rejected)
break
// 当状态已经改变时,立即执行对应的回调函数
case FULFILLED:
fulfilled(_value)
break
case REJECTED:
rejected(_value)
break
}
})
}
// 添加catch方法
catch(onRejected) {
return this.then(undefined, onRejected)
}
// 添加静态resolve方法
static resolve(value) {
// 如果参数是MyPromise实例,直接返回这个实例
if (value instanceof MyPromise) return value
return new MyPromise(resolve => resolve(value))
}
// 添加静态reject方法
static reject(value) {
return new MyPromise((resolve, reject) => reject(value))
}
// 添加静态all方法
static all(list) {
return new MyPromise((resolve, reject) => {
let values = []
let count = 0
for (let [i, p] of list.entries()) {
// 数组参数如果不是MyPromise实例,先调用MyPromise.resolve
this.resolve(p).then(res => {
values[i] = res
count++
// 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled
if (count === list.length) resolve(values)
}, err => {
// 有一个被rejected时返回的MyPromise状态就变成rejected
reject(err)
})
}
})
}
// 添加静态race方法
static race(list) {
return new MyPromise((resolve, reject) => {
for (let p of list) {
// 只要有一个实例率先改变状态,新的MyPromise的状态就跟着改变
this.resolve(p).then(res => {
resolve(res)
}, err => {
reject(err)
})
}
})
}
finally(cb) {
return this.then(
value => MyPromise.resolve(cb()).then(() => value),
reason => MyPromise.resolve(cb()).then(() => { throw reason })
);
}
}
(2)递归实现字符串的反转,不用递归直接str.split('').reverse().join("");
可别人就是要用递归:
//利用二分的思想
function strReverse(str) {
let len = str.length();
if (len <= 1) {
return str;
}
let left = str.substring(0, len / 2);
let right = str.substring(len / 2, len);
return strReverse(right) + strReverse(left);
}
console.log("输出数据:", strReverse("hello world!"))
(3)计算一个字符串里面出现次数最多的并统计次数(默认多个字符相同取第一个,考察目标es6的map
),注意要是多次用数组循环遍历估计淘汰的就是你:
function getMaxChar(str) {
let map = new Map();
for (let i = 0, len = str.length; i < len; i++) {
if (map.has(str[i])) {
map.set(str[i], map.get(str[i]) + 1);
} else {
map.set(str[i], 1);
}
}
let char = "", num = 0;
map.forEach((value, key) => {
if (value > num) {
num = value;
char = key;
}
})
return { char, num };
}
console.log("输出查询结果:",getMaxChar("zhangsan"));//{char:'a',num:2}
(4)手写深拷贝,这里首先提出对于JSON.parse(JSON.stringify())
的认知纠错:
a. 如果对象里面存在时间对象,拷贝之后,时间对象变成了字符串。
b. 如果对象里有RegExp、Error对象,则序列化的结果将只得到空对象。
c. 如果对象里有函数,undefined,则序列化的结果会把函数, undefined丢失。
d. 如果对象里有NaN和Infinity,则序列化的结果会变成null。
e JSON.stringify()只能序列化对象的可枚举的自有属性,会丢弃对象的constructor;
f. 如果对象中存在循环引用的情况也无法正确实现深拷贝。
好直接放上deepCopy的经典递归代码:
function deepCopy(obj) {
let newObj = null
if (typeof obj === 'object' && obj !== null) {
newObj = obj instanceof Array ? [] : {}
for (let i in obj) {
newObj[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i]
}
} else {
newObj = obj
}
return newObj
}
(5)防抖(“回城”,点一次又重新“回城”)与节流(“貂蝉放大招,放了就得一次操作完”),形象比喻还得是掘金:
//防抖
function debounce(fn, wait, immediate = true) {
let timer = null;
let isInvoke = false;
return function (...args) {
let context = this;
if (timer) {
clearTimeout(timer);
timer = null;
}
//第一次进入不用等待,至于传值的promise封装我这里就不展开了
if (immediate && !isInvoke) {
fn.apply(context, args);
isInvoke = true;
}
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
}
}
//节流
function throttle(fn, delay) {
let curTime = 0;
return function (...args) {
let context = this;
nowTime = Date.now();
if (nowTime - curTime >= delay) {
curTime = Date.now();
return fn.apply(context, args);
}
}
}
(6)手写New操作符,考察原型的使用以及New的实现原理:
//原理说明
//(1)创建一个新对象
//(2)将原型指针指向函数原型
//(3)修改函数的this对象,改到构造的对象上去
function myNew(fn,...args){
const obj={};
obj.__proto__=fn.prototype;
fn.apply(obj,args);
return obj;
}
3.粗糙的原理篇
虽然很少有面试的机会,但是应该花更多的时间在面试上,笔试有一部分是看简历的,面试准备不充分被刷特别难受。总结如下知识:
(1)css样式重叠题(出自鼎甲科技):
咋一看有点蒙,如果html基础知识忘记的话,其实不难,记忆如下知识:
1、第一等:代表内联样式,如: style=””,权值为1000。
2、第二等:代表ID选择器,如:#content,权值为0100。
3、第三等:代表类,伪类和属性选择器,如.content,权值为0010。
4、第四等:代表类型选择器和伪元素选择器,如div p,权值为0001。
5、通配符、子选择器、相邻选择器等的。如*、>、+,权值为0000。
6、继承的样式没有权值。
个人觉得这个理论不好,毕竟现在都是用class,很少用兄弟和子代,容易造成不必要的麻烦,直接用less,scss或者叠加class倒是直观。
(2)JWT
(web json token),翻译就是服务端和客户端进行数据交换的安全令牌;具体包含header、payload以及secret,header里面包含的是token的类型以及加解密使用的算法,payload里面存放有效信息,secret里面存放的是header指示的如下信息:
HMACSHA256(//加密算法
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret//自己设置的盐值,加解密都需要使用到
)
(3)webpack的热更新原理:
在启动webpack的本地服务之前,调用了updateCompiler(this.compiler)方法,该方法修改了webpack.config.js的entry配置:在entry中新增两个js文件,一个是负责与本地服务建立webSocket通信。另一个负责浏览器在收到本地服务消息后进行热更新检查及更新操作。因为这两个操作都需要在浏览器端完成,所以需要随入口文件一起打包,这样就能在浏览器的环境中运行。webpack依赖express启动了一个本地服务器,可以访问本地静态资源。生成了compiler对象,可以对本地文件的修改进行监听,当文件发生变化时重新进行编译,然后将编译后的文件打包内存。当一次webpack编译结束,就会调用_sendStats方法通过websocket给浏览器发送通知,浏览器做检查更新逻辑。webpack与vite的区别
:webpack是分析依赖=> 编译打包=> 交给本地服务器进行渲染随着模块的增多,会造成打出的 bundle 体积过大,进而会造成热更新速度明显拖慢。而vite:是启动服务器=> 请求模块时按需动态编译显示。是先启动开发服务器,请求某个模块时再对该模块进行实时编译,因为现代游览器本身支持ES-Module,所以会自动向依赖的Module发出请求。所以vite就将开发环境下的模块文件作为浏览器的执行文件,而不是像webpack进行打包后交给本地服务器,vite更快但是生态没有webpack完善。
(4)scoped的原理(防止样式污染):给样式加上hash值,hash的生成依赖于组件的位置。
(5)盒子模型+“多态”样式:
标准盒模型(content-box):margin + border + padding + content,
IE盒模型(border-box):margin + content(border + padding),给出一道鼎甲科技的题目(求盒子真实内容的宽高):
div {
margin: 10px 0;
padding: 10px 5px 30x;
border: 5px solid red;
width: 100px;
height: 100px;
}
由于这里没有指定具体的盒子默认是标准盒模型,一个参数即上下左右全部都是一样的,两个参数分别是上下和左右,三个参数是上、左右、下,四个参数是下右下左;也就是120px的宽,170px的高;另外需要额外注意的是margin的重叠,包含如下几种情况:
a. 相邻兄弟元素的marin-bottom和margin-top的值发生重叠;
b. 父级和第一个/最后一个子元素的margin失效,一般建议还是使用padding比较好;
(6)Vue2与Vue3的区别,目的是看看你是否了解原理而不是别人用这个所以你也用,还有就是方便更清晰的区分避免在编程的时候Vue23混用,不是面试也得理解:
(1)将es5的object.defineProperty()对对象属性进行劫持、结合发布订阅模式的双向绑定模式改成了es6的proxy api来实现,能够真正实现对对象实现包括新增属性监听,增加了对数组的监听;
(2)周期函数上,去掉了beforeCreate和created,改用setup,将BeforeDestroy和destroyed改成了beforeUnmount和unMounted,其他的周期函数都加上了on前缀;
(3)去掉了vue2的选择是api,改用合成式api setup,使代码更加简洁,在setup中使用响应式数据的时候需要使用.value来访问数据,其他函数不用,setup中的函数只支持同步函数;
(4)增加了对fragments碎片的支持,通俗来说就是组件里面可以含有多个根节点;
(5)setup函数的第二个参数中中包含有emit,可以直接使用不需要用this.$emit来调用;
(6)新增了Teleport组件瞬移有点类似于插槽,能够对将组件挂载到指定位置与不局限于#app;
4.写在后面
“此时无声胜有声!”