高级前端进阶(id:FrontendGaoji)
作者:木易杨,资深前端工程师,前网易工程师,13K star Daily-Interview-Question 作者
半月刊第四期来啦,这段时间 Daily-Interview-Question 新增了 14 道高频面试题,今天就把最近半月汇总的面试题和部分答案发给大家,帮助大家查漏补缺。
更多更全的面试题和答案汇总在下面的项目中,点击查看。
项目地址:https://github.com/Advanced-Frontend/Daily-Interview-Question
第 40 题:在 Vue 中,子组件为何不可以修改父组件传递的 Prop
如果修改了,Vue 是如何监控到属性的修改并给出警告的。
解析:
子组件为何不可以修改父组件传递的 Prop
单向数据流,易于监测数据的流动,出现了错误可以更加迅速的定位到错误发生的位置。如果修改了,Vue 是如何监控到属性的修改并给出警告的。
if (process.env.NODE_ENV !== 'production') {
var hyphenatedKey = hyphenate(key);
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
("\"" + hyphenatedKey + "\" is a reserved attribute and cannot be used as component prop."),
vm
);
}
defineReactive$$1(props, key, value, function () {
if (!isRoot && !isUpdatingChildComponent) {
warn(
"Avoid mutating a prop directly since the value will be " +
"overwritten whenever the parent component re-renders. " +
"Instead, use a data or computed property based on the prop's " +
"value. Prop being mutated: \"" + key + "\"",
vm
);
}
});
}
在initProps的时候,在defineReactive时通过判断是否在开发环境,如果是开发环境,会在触发set的时候判断是否此key是否处于updatingChildren中被修改,如果不是,说明此修改来自子组件,触发warning提示。
需要特别注意的是,当你从子组件修改的prop属于基础类型时会触发提示。 这种情况下,你是无法修改父组件的数据源的, 因为基础类型赋值时是值拷贝。你直接将另一个非基础类型(Object, array)赋值到此key时也会触发提示(但实际上不会影响父组件的数据源), 当你修改object的属性时不会触发提示,并且会修改父组件数据源的数据。
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/60
第 41 题:下面代码输出什么
var a = 10;
(function () {
console.log(a)
a = 5
console.log(window.a)
var a = 20;
console.log(a)
})()
解析:
依次输出:undefined -> 10 -> 20
在立即执行函数中,var a = 20;
语句定义了一个局部变量 a
,由于js的变量声明提升机制,局部变量a
的声明会被提升至立即执行函数的函数体最上方,且由于这样的提升并不包括赋值,因此第一条打印语句会打印undefined
,最后一条语句会打印20
。
由于变量声明提升,a = 5;
这条语句执行时,局部的变量a
已经声明,因此它产生的效果是对局部的变量a
赋值,此时window.a
依旧是最开始赋值的10
。
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/61
第 42 题:实现一个 sleep 函数
比如 sleep(1000) 意味着等待1000毫秒,可从 Promise、Generator、Async/Await 等角度实现。
解析:4 种方式
//Promise
const sleep = time => {
return new Promise(resolve => setTimeout(resolve,time))
}
sleep(1000).then(()=>{
console.log(1)
})
//Generator
function* sleepGenerator(time) {
yield new Promise(function(resolve,reject){
setTimeout(resolve,time);
})
}
sleepGenerator(1000).next().value.then(()=>{console.log(1)})
//async
function sleep(time) {
return new Promise(resolve => setTimeout(resolve,time))
}
async function output() {
let out = await sleep(1000);
console.log(1);
return out;
}
output();
//ES5
function sleep(callback,time) {
if(typeof callback === 'function')
setTimeout(callback,time)
}
function output(){
console.log(1);
}
sleep(output,1000);
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/63
第 43 题:使用 sort() 对数组 [3, 15, 8, 29, 102, 22] 进行排序,输出结果
解析:
sort
函数,可以接收一个函数,返回值是比较两个数的相对顺序的值
默认没有函数 是按照
UTF-16
排序的,对于字母数字 你可以利用ASCII
进行记忆
[3, 15, 8, 29, 102, 22].sort();
// [102, 15, 22, 29, 3, 8]
带函数的比较
[3, 15, 8, 29, 102, 22].sort((a,b) => {return a - b});
返回值大于0 即a-b > 0 , a 和 b 交换位置
返回值大于0 即a-b < 0 , a 和 b 位置不变
返回值等于0 即a-b = 0 , a 和 b 位置不变
对于函数体返回
b-a
可以类比上面的返回值进行交换位置
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/66
第 44 题:介绍 HTTPS 握手过程
解析:
开始加密通信之前,客户端和服务器首先必须建立连接和交换参数,这个过程叫做握手(handshake)。
假定客户端叫做爱丽丝,服务器叫做鲍勃,整个握手过程可以用下图说明。
握手阶段分成五步。
第一步,爱丽丝给出协议版本号、一个客户端生成的随机数(Client random),以及客户端支持的加密方法。
第二步,鲍勃确认双方使用的加密方法,并给出数字证书、以及一个服务器生成的随机数(Server random)。
第三步,爱丽丝确认数字证书有效,然后生成一个新的随机数(Premaster secret),并使用数字证书中的公钥,加密这个随机数,发给鲍勃。
第四步,鲍勃使用自己的私钥,获取爱丽丝发来的随机数(即Premaster secret)。
第五步,爱丽丝和鲍勃根据约定的加密方法,使用前面的三个随机数,生成"对话密钥"(session key),用来加密接下来的整个对话过程。
参考:
图解SSL/TLS协议
SSL/TLS协议运行机制的概述
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/70
第 45 题:HTTPS 握手过程中,客户端如何验证证书的合法性
解析:
1、首先什么是HTTP协议?
http协议是超文本传输协议,位于tcp/ip四层模型中的应用层;通过请求/响应的方式在客户端和服务器之间进行通信;但是缺少安全性,http协议信息传输是通过明文的方式传输,不做任何加密,相当于在网络上裸奔;容易被中间人恶意篡改,这种行为叫做中间人攻击;2、加密通信:
为了安全性,双方可以使用对称加密的方式key进行信息交流,但是这种方式对称加密秘钥也会被拦截,也不够安全,进而还是存在被中间人攻击风险;
于是人们又想出来另外一种方式,使用非对称加密的方式;使用公钥/私钥加解密;通信方A发起通信并携带自己的公钥,接收方B通过公钥来加密对称秘钥;然后发送给发起方A;A通过私钥解密;双发接下来通过对称秘钥来进行加密通信;但是这种方式还是会存在一种安全性;中间人虽然不知道发起方A的私钥,但是可以做到偷天换日,将拦截发起方的公钥key;并将自己生成的一对公/私钥的公钥发送给B;接收方B并不知道公钥已经被偷偷换过;按照之前的流程,B通过公钥加密自己生成的对称加密秘钥key2;发送给A;
这次通信再次被中间人拦截,尽管后面的通信,两者还是用key2通信,但是中间人已经掌握了Key2;可以进行轻松的加解密;还是存在被中间人攻击风险;3、解决困境:权威的证书颁发机构CA来解决;
3.1制作证书:作为服务端的A,首先把自己的公钥key1发给证书颁发机构,向证书颁发机构进行申请证书;证书颁发机构有一套自己的公私钥,CA通过自己的私钥来加密key1,并且通过服务端网址等信息生成一个证书签名,证书签名同样使用机构的私钥进行加密;制作完成后,机构将证书发给A;
3.2校验证书真伪:当B向服务端A发起请求通信的时候,A不再直接返回自己的公钥,而是返回一个证书;
说明:各大浏览器和操作系统已经维护了所有的权威证书机构的名称和公钥。B只需要知道是哪个权威机构发的证书,使用对应的机构公钥,就可以解密出证书签名;接下来,B使用同样的规则,生成自己的证书签名,如果两个签名是一致的,说明证书是有效的;
签名验证成功后,B就可以再次利用机构的公钥,解密出A的公钥key1;接下来的操作,就是和之前一样的流程了;3.3:中间人是否会拦截发送假证书到B呢?
因为证书的签名是由服务器端网址等信息生成的,并且通过第三方机构的私钥加密中间人无法篡改; 所以最关键的问题是证书签名的真伪;
4、https主要的思想是在http基础上增加了ssl安全层,即以上认证过程。
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/74
第 46 题:输出以下代码执行的结果并解释为什么
var obj = {
'2': 3,
'3': 4,
'length': 2,
'splice': Array.prototype.splice,
'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
解析:
涉及知识点:
类数组(ArrayLike):
一组数据,由数组来存,但是如果要对这组数据进行扩展,会影响到数组原型,ArrayLike的出现则提供了一个中间数据桥梁,ArrayLike有数组的特性, 但是对ArrayLike的扩展并不会影响到原生的数组。
push方法:
push 方法有意具有通用性。该方法和 call() 或 apply() 一起使用时,可应用在类似数组的对象上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。
唯一的原生类数组(array-like)对象是 Strings,尽管如此,它们并不适用该方法,因为字符串是不可改变的。
对象转数组的方式:
Array.from()、splice()、concat()等。
题分析:
这个obj中定义了两个key值,分别为splice和push分别对应数组原型中的splice和push方法,因此这个obj可以调用数组中的push和splice方法,调用对象的push方法:push(1),因为此时obj中定义length为2,所以从数组中的第二项开始插入,也就是数组的第三项(下表为2的那一项),因为数组是从第0项开始的,这时已经定义了下标为2和3这两项,所以它会替换第三项也就是下标为2的值,第一次执行push完,此时key为2的属性值为1,同理:第二次执行push方法,key为3的属性值为2。此时的输出结果就是:
Object(4) [empty × 2, 1, 2, splice: ƒ, push: ƒ]---->
[
2: 1,
3: 2,
length: 4,
push: ƒ push(),
splice: ƒ splice()
]
因为只是定义了2和3两项,没有定义0和1这两项,所以前面会是empty。
如果讲这道题改为:
var obj = {
'2': 3,
'3': 4,
'length': 0,
'splice': Array.prototype.splice,
'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
此时的打印结果就是:
Object(2) [1, 2, 2: 3, 3: 4, splice: ƒ, push: ƒ]---->
[
0: 1,
1: 2,
2: 3,
3: 4,
length: 2,
push: ƒ push(),
splice: ƒ splice()
]
原理:此时length长度设置为0,push方法从第0项开始插入,所以填充了第0项的empty
至于为什么对象添加了splice属性后并没有调用就会变成类数组对象这个问题,这是控制台中 DevTools 猜测类数组的一个方式:
https://github.com/ChromeDevTools/devtools-frontend/blob/master/front_end/event_listeners/EventListenersUtils.js#L330
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/76
第 47 题:双向绑定和 vuex 是否冲突
解析:
当在严格模式中使用 Vuex 时,在属于 Vuex 的 state 上使用 v-model
会比较棘手:
<input v-model="obj.message">
假设这里的 obj
是在计算属性中返回的一个属于 Vuex store 的对象,在用户输入时,v-model
会试图直接修改 obj.message
。在严格模式中,由于这个修改不是在 mutation 函数中执行的, 这里会抛出一个错误。
用“Vuex 的思维”去解决这个问题的方法是:给 <input>
中绑定 value,然后侦听 input
或者change
事件,在事件回调中调用 action:
<input :value="message" @input="updateMessage">
// ...
computed: {
...mapState({
message: state => state.obj.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}
下面是 mutation 函数:
// ...
mutations: {
updateMessage (state, message) {
state.obj.message = message
}
}
双向绑定的计算属性
必须承认,这样做比简单地使用“v-model
+ 局部状态”要啰嗦得多,并且也损失了一些 v-model
中很有用的特性。另一个方法是使用带有 setter 的双向绑定计算属性:
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/81
第 48 题:call 和 apply 的区别是什么,哪个性能更好一些
解析:
Function.prototype.apply和Function.prototype.call 的作用是一样的,区别在于传入参数的不同;
第一个参数都是,指定函数体内this的指向;
第二个参数开始不同,apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数。
call比apply的性能要好,平常可以多用call, call传入参数的格式正是内部所需要的格式,参考 call和apply的性能对比
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/84
第 49 题:为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?
解析:
能够完成整个 HTTP 请求+响应(尽管不需要响应内容)
触发 GET 请求之后不需要获取和处理数据、服务器也不需要发送数据
跨域友好
执行过程无阻塞
相比 XMLHttpRequest 对象发送 GET 请求,性能上更好
GIF的最低合法体积最小(最小的BMP文件需要74个字节,PNG需要67个字节,而合法的GIF,只需要43个字节)
不会阻塞页面加载,影响用户的体验,只要new Image对象就好了;(排除JS/CSS文件资源方式上报)
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/87
第 50 题:实现 (5).add(3).minus(2) 功能。
例: 5 + 3 - 2,结果为 6
解析:
Number.prototype.add = function(n) {
return this.valueOf() + n;
};
Number.prototype.minus = function(n) {
return this.valueOf() - n;
};
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/88
第 51 题:Vue 的响应式原理中 Object.defineProperty 有什么缺陷?
为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?
解析:
Object.defineProperty本身有一定的监控到数组下标变化的能力:
Object.defineProperty本身是可以监控到数组下标的变化的,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性。具体我们可以参考 《记一次思否问答的问题思考:Vue为什么不能检测数组变动》这篇文章,文章底部配图中有尤大大的严肃回复截图; 下方的讨论区也很值得大家下去看一看,有对于 for / forEach / for .. in .. 几个循环方式的讨论。
关于 Vue 3.0 的其他信息我们可以参考 尤大大发布的 Vue 3.0 新特性预览PPT
直接通过数组的下标给数组设置值,不能实时响应。 为了解决这个问题,经过vue内部处理后可以使用以下几种方法来监听数组
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
由于只针对了以上几种方法进行了hack处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。
Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。
而要取代它的Proxy有以下两个优点;
可以劫持整个对象,并返回一个新对象
有13种劫持操作
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/90
第 52 题:怎么让一个 div 水平垂直居中
解析:
<div class="parent">
<div class="child"></div>
</div>
1.
div.parent {
display: flex;
justify-content: center;
align-items: center;
}
2.
div.parent {
position: relative;
}
div.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* 或者 */
div.child {
width: 50px;
height: 10px;
position: absolute;
top: 50%;
left: 50%;
margin-left: -25px;
margin-top: -5px;
}
/* 或 */
div.child {
width: 50px;
height: 10px;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
margin: auto;
}
3.
div.parent {
display: grid;
}
div.child {
justify-self: center;
align-self: center;
}
4.
div.parent {
font-size: 0;
text-align: center;
&::before {
content: "";
display: inline-block;
width: 0;
height: 100%;
vertical-align: middle;
}
}
div.parent{
display: inline-block;
vertical-align: middle;
}
未完待续,点击查看更多细节:https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/92
第 53 题:输出以下代码的执行结果并解释为什么
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
console.log(a.x)
console.log(b.x)
解析:
var a = {n: 1};
var b = a;
a.x = a = {n: 2};
a.x // --> undefined
b.x // --> {n: 2}
答案已经写上面了,这道题的关键在于
1、优先级。
.
的优先级高于=
,所以先执行a.x
,堆内存中的{n: 1}
就会变成{n: 1, x: undefined}
,改变之后相应的b.x
也变化了,因为指向的是同一个对象。2、赋值操作是
从右到左
,所以先执行a = {n: 2}
,a
的引用就被改变了,然后这个返回值又赋值给了a.x
,需要注意的是这时候a.x
是第一步中的{n: 1, x: undefined}
那个对象,其实就是b.x
,相当于b.x = {n: 2}