前言
本系列主要整理博主2023秋招面试情况,此次为兴业数金前端一面,面试时间为2022-8-18
问题总结
1. 自我介绍
2. vue的工作原理
答:1、建立虚拟DOM Tree,通过document.createDocumentFragment(),遍历指定根节点内部节点,根据{{ prop }}、v-model等规则进行compile;
2、通过 Object.defineProperty() 进行数据变化拦截;
3、截取到的数据变化,通过发布者-订阅者模式,触发Watcher,从而改变虚拟DOM中的具体数据;
4、通过改变虚拟DOM元素值,从而改变最后渲染dom树的值,完成双向绑定。
其中面试官着重问了Object.defineProperty(),详见我之前的博客响应式与双向数据绑定。
3. 双向数据绑定
双向数据绑定就是多了 View 变化会通知到 Model 层。即 MVVM 的具体实现。无论 Model 还是 View 中的值发生变化,都会通过 ViewModel 通知到对方,实现同步。实际应用就是 v-model 双向数据绑定。
4. nextTick()方法详细说一说?
什么是Vue.nextTick()?
定义:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
所以就衍生出了这个获取更新后的DOM的Vue方法。所以放在Vue.nextTick()回调函数中的执行的应该是会对DOM进行操作的 js代码;
理解:nextTick(),是将回调函数延迟在下一次dom更新数据后调用,简单的理解是:当数据更新了,在dom中渲染后,自动执行该函数。
什么时候需要用的Vue.nextTick()?
1、Vue生命周期的created()钩子函数进行的DOM操作一定要放在Vue.nextTick()的回调函数中,原因是在created()钩子函数执行的时候DOM 其实并未进行任何渲染,而此时进行DOM操作无异于徒劳,所以此处一定要将DOM操作的js代码放进Vue.nextTick()的回调函数中。与之对应的就是mounted钩子函数,因为该钩子函数执行时所有的DOM挂载已完成。
2、当项目中你想在改变DOM元素的数据后基于新的dom做点什么,对新DOM一系列的js操作都需要放进Vue.nextTick()的回调函数中;通俗的理解是:更改数据后当你想立即使用js操作新的视图的时候需要使用它
Vue.nextTick(callback) 使用原理:
原因是,Vue是异步执行dom更新的,一旦观察到数据变化,Vue就会开启一个队列,然后把在同一个事件循环 (event loop) 当中观察到数据变化的 watcher 推送进这个队列。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲行为可以有效的去掉重复数据造成的不必要的计算和DOm操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
当你设置 vm.someData = ‘new value’,DOM 并不会马上更新,而是在异步队列被清除,也就是下一个事件循环开始时执行更新时才会进行必要的DOM更新。如果此时你想要根据更新的 DOM 状态去做某些事情,就会出现问题。。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。
5. Webpack是怎么配置的?
根目录下新增 webpack 的配置文件webpack.config.js:
const { resolve } = require('path'); //node内置核心模块,用来设置路径。
module.exports = {
entry: './src/js/app.js', // 入口文件配置(精简写法)
/*完整写法:
entry:{
main:'./src/js/app.js'
}
*/
output: { //输出配置
filename: './js/app.js',//输出文件名
path: resolve(__dirname, 'build')//输出文件路径(绝对路径)
},
mode: 'development' //开发环境(二选一)
//mode: 'production' //生产环境(二选一)
};
6. Webpack里面的loader,css-loader和style-loader顺序?
css-loader用于将css文件打包到js中, 常常配合style-loader一起使用,将css文件打包并插入到页面中。
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
};
其中style-loader为什么有这个呢?原因是css-loader只负责加载css文件,但是并不负责将css具体样式嵌入到文档中。这个时候,我们还需要一个style-loader帮助我们处理。style-loader需要放在css-loader的前面。这是因为webpack在读取使用的loader的过程中,是按照从右向左的顺序读取的。
7. 深拷贝是怎么实现的?
8. 跨域问题?你使用过吗?怎么配置?
首先一个url是由:协议、域名、端口 三部分组成。(一般端口默认80)如:https://blog.moonlet.cn:80
当一个请求url的协议、域名、端口三者之间的任意一个与当前页面url不同即为跨域。
在webpack.config.js中配置(当然还有其他的跨域解决的办法,这里只介绍Webpack-dev-server的proxy用法)
使用一:
mmodule.exports = {
//...
devServer: {
proxy: {
'/api': 'http://localhost:3000'
}
}
};
请求到 /api/xxx 现在会被代理到请求 http://localhost:3000/api/xxx, 例如 /api/user 现在会被代理到请求 http://localhost:3000/api/user
使用二:
如果你想要代码多个路径代理到同一个target下, 你可以使用由一个或多个「具有 context 属性的对象」构成的数组:
module.exports = {
//...
devServer: {
proxy: [{
context: ['/auth', '/api'],
target: 'http://localhost:3000',
}]
}
};
使用三:
如果你不想始终传递 /api ,则需要重写路径:
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
pathRewrite: {'^/api' : ''}
}
}
}
};
请求到 /api/xxx 现在会被代理到请求 http://localhost:3000/xxx, 例如 /api/user 现在会被代理到请求 http://localhost:3000/user
使用四:
默认情况下,不接受运行在 HTTPS 上,且使用了无效证书的后端服务器。如果你想要接受,只要设置 secure: false 就行。修改配置如下:
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'https://other-server.example.com',
secure: false
}
}
}
};
使用五:
有时你不想代理所有的请求。可以基于一个函数的返回值绕过代理。
在函数中你可以访问请求体、响应体和代理选项。必须返回 false 或路径,来跳过代理请求。
例如:对于浏览器请求,你想要提供一个 HTML 页面,但是对于 API 请求则保持代理。你可以这样做:
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
bypass: function(req, res, proxyOptions) {
if (req.headers.accept.indexOf('html') !== -1) {
console.log('Skipping proxy for browser request.');
return '/index.html';
}
}
}
}
}
};
解决跨域原理
上面的参数列表中有一个changeOrigin参数, 是一个布尔值, 设置为true, 本地就会虚拟一个服务器接收你的请求并代你发送该请求,
module.exports = {
//...
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
}
}
}
};
9. 常见了几种继承方式?class-extends是怎么实现的?
- 原型链继承
- 构造函数继承(借助 call)
- 组合继承(前两种组合)
- 原型式继承
- 寄生式继承
- 寄生组合式继承
- ES6 的 extends 关键字实现逻辑
- Class的方法都加在原型链上
JS的原型链继承的本质是根据__proto__一层一层往上找
继承的时候只需要把子类的原型对象prototype里的__proto__属性指向父类的prototype即可
这就好理解Extends了
- Class的方法都加在原型链上
class A {
// constructor也是定义在A的原型链上
constructor(x,y){
this.x = x;
this.y = y;
}
// 直接加在了function A 的原型链式
one(){
return 1;
}
}
class B extends A{
constructor(){
//相当于A.prototype.constructor.call(this)
super(1,2)
}
}
var x = new B()
console.log(x)
总结:
通过 Object.create 来划分不同的继承方式,最后的寄生式组合继承方式是通过组合继承改造之后的最优继承方式,而 extends 的语法糖和寄生组合继承的方式基本类似。
10. 判断数组和对象?为什么Object.prototype.toString.call()可以准确判断数据类型?
js中有一句话叫,万物皆对象。而Object.prototype.toString() 这个函数作用就是,返回当前调用者的对象类型。
Object.prototype.toString()会返回[object, [[class]]]的字符串
其中[[class]]会返回es定义的对象类型,包含"Arguments", “Array”, “Boolean”, “Date”, “Error”, “Function”, “JSON”, “Math”, “Number”, “Object”, “RegExp”, 和 “String”;再加上es5新增加的返回[object Undefined]和[object Null]
Object.prototype.toString.call()为什么要加call();
因为Object.prototype.toString()返回的是调用者的类型。不论你toString()本身的入参写的是什么,在Object.prototype.toString()中,它的调用者永远都是Object.prototype;所以,在不加call()情况下,我们的出来的结果永远都是 ‘[object Object]’
call()是为了改变Object.prototype.toString这个函数都指向。让Object.prototype.toString这个方法指向我们所传入的数据。
总结:
- js中所有的数据类型,本质上都是对象,而这些数据类型不过是对象的一种类型而已。
- Object.prototype.toString这个方法是用于返回当前调用者的对象类型的
- call是为了让Object.prototype.toString方法指向我们指定的数据。否则返回永远都是[object Object]
11. 数组常用的方法?哪些改变了数组?
不会改变原来数组的有:
- concat()
concat() 方法用于连接两个或多个字符串。
该方法没有改变原有字符串,但是会返回连接两个或多个字符串新字符串。 - every()
every() 方法用于检测数组所有元素是否都符合指定条件(通过函数提供)。
every() 方法使用指定函数检测数组中的所有元素:
如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测。如果所有元素都满足条件,则返回 true。 - some()
some() 方法用于检测数组中的元素是否满足指定条件(函数提供)。
some() 方法会依次执行数组的每个元素:
如果有一个元素满足条件,则表达式返回true , 剩余的元素不会再执行检测。
如果没有满足条件的元素,则返回false。
注意: some() 不会对空数组进行检测。
注意: some() 不会改变原始数组。 - filter()
filter() 方法创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。
注意: filter() 不会对空数组进行检测。
注意: filter() 不会改变原始数组。 - map()
map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。
map() 方法按照原始数组元素顺序依次处理元素。
注意: map() 不会对空数组进行检测。
注意: map() 不会改变原始数组。 - slice()
slice() 方法可从已有的数组中返回选定的元素。
slice()方法可提取字符串的某个部分,并以新的字符串返回被提取的部分。
注意: slice() 方法不会改变原始数组。
会改变原来数组的有:
- pop()
pop() 方法用于删除数组的最后一个元素并返回删除的元素。
注意:此方法改变数组的长度!
提示: 移除数组第一个元素,请使用 shift() 方法。 - push()
push() 方法可向数组的末尾添加一个或多个元素,并返回新的长度。
注意: 新元素将添加在数组的末尾。
注意: 此方法改变数组的长度。
提示: 在数组起始位置添加元素请使用 unshift() 方法。 - shift()
shift() 方法用于把数组的第一个元素从其中删除,并返回第一个元素的值。
注意: 此方法改变数组的长度!
提示: 移除数组末尾的元素可以使用 pop() 方法。 - unshift()
unshift() 方法可向数组的开头添加一个或更多元素,并返回新的长度。
注意: 该方法将改变数组的数目。
提示: 将新项添加到数组末尾,请使用 push() 方法。 - reverse()
reverse() 方法用于颠倒数组中元素的顺序。 - sort()
sort() 方法用于对数组的元素进行排序。
排序顺序可以是字母或数字,并按升序或降序。
默认排序顺序为按字母升序。
注意: 这种方法会改变原始数组!。 - splice()
splice() 方法用于添加或删除数组中的元素。