面试题总结
HTML/CSS
- em 和 rem 的区别是什么,rem 适配的原理是什么?
REM 是相对于根元素字体大小的一个单位。原理:利用媒体查询或 JS 动态检测设备的宽度,不同宽度下动态设置对应的根元素(HTML)字体大小,设备宽度发生变化(根元素字体大小也会变化),那么所有使用根元素的那些内容自然就跟着变了。
flexible.js
- HTML 的语义化你是怎么理解的?
是什么:合适的内容使用合适的标签。
好处是什么:见名知意;利于 SEO。
- 实现一个盒子水平垂直居中?
知道自身宽高可以用 margin-top / margin-left 负值,更建议使用 transform: translate(-50%, -50%);
,它能做到在不知道自身尺寸的情况下也能位移自身的一半。
-
margin-top 的百分比相对谁,例如 margin-top: 50% 是什么意思?
-
说一下 Less 你用过哪些特性(除了变量、嵌套、计算,再找几条)?
Extend 继承
before
.box-shadow {
box-shadow: 0 0 9px 3px #ccc;
border-radius: 6px;
}
.box1 {
height: 200px;
width: 400px;
margin: 50px auto 0;
&:extend(.box-shadow);
}
.box2 {
height: 100px;
width: 400px;
margin: 30px auto 0;
&:extend(.box-shadow);
}
after
.box-shadow,
.box1,
.box2 {
box-shadow: 0 0 9px 3px #ccc;
border-radius: 6px;
}
.box1 {
height: 200px;
width: 400px;
margin: 50px auto 0;
}
.box2 {
height: 100px;
width: 400px;
margin: 30px auto 0;
}
Mixin 混入
before
// #1 如果这儿带了括号,此 .box-shadow 不会生成
.box-shadow(@radius: 6px) {
box-shadow: 0 0 9px 3px #ccc;
border-radius: @radius;
}
.box1 {
height: 200px;
width: 400px;
margin: 50px auto 0;
.box-shadow();
}
.box2 {
height: 100px;
width: 400px;
margin: 30px auto 0;
// #2 可以传递参数
.box-shadow(50px);
}
// #3 相比较于继承,.box1 和 .box2 公用的样式并没有被整合
after
.box1 {
height: 200px;
width: 400px;
margin: 50px auto 0;
box-shadow: 0 0 9px 3px #ccc;
border-radius: 6px;
}
.box2 {
height: 100px;
width: 400px;
margin: 30px auto 0;
box-shadow: 0 0 9px 3px #ccc;
border-radius: 50px;
}
Func 函数
.box-shadow(@radius: 6px) {
box-shadow: 0 0 9px 3px #ccc;
border-radius: @radius;
}
.box1 {
height: 200px;
width: 400px;
margin: 50px auto 0;
.box-shadow();
// return a color which is 10% *lighter* than @color
background-color: lighten(red, 20%);
}
- 实现一个左侧固定,右侧自适应的布局?
父元素 display: flex;
子元素左边固定宽度,右边 flex: 1;
,了解:flex: 1 是什么意思?flex-shrink、flex-grow、flex-basis。
JavaScript
- JS 数据类型(8 种,按照 MDN 上面去说)?
number
、string
、boolean
、undefined
、symbol
、bigint
、null
(null 是一个 Bug,通过 typeof 去检测确实是一个 object)
object( function、object、array、regexp、date... )
- 数组的 push 和 pop 方法的返回值是什么?
push:新增之后的数组长度;pop:删除的那个元素;map:加工之后的新数组,不影响原数据。
<script>
// Polyfill: 填充 / 替代
// forEach
// Array.prototype.forEach = Array.prototype.forEach || function () { }
// 自己造一个 forEach
// const o = { age: 18 };
// 第一个参数是一个函数
// 第二个参数是内部 this 的指向
/* ['a', 'b', 'c'].forEach(function (item, index, originArr) {
console.log(item, index, originArr, this)
}, o); */
/* Array.prototype.forEach2 = function (callback, _this) {
// 调用 callback
// this => ['a', 'b', 'c']
for (let i = 0; i < this.length; i++) {
callback.call(_this, this[i], i, this) // window.callback()
}
};
['a', 'b', 'c'].forEach2(function (item, index, originArr) {
console.log(item, index, originArr, this)
}, o); */
/* Array.prototype.map2 = function (callback) {
const arr = []
for (let i = 0; i < this.length; i++) {
arr.push(callback(this[i], i, this))
}
return arr
}
// map
const newArr = ['a', 'b', 'c'].map2((item, index, originArr) => {
// 把这个函数返回的结果放到新数组
return '~' + item + '~'
});
console.log(newArr) */
/* Array.prototype.filter2 = function (callback) {
const arr = []
for (let i = 0; i < this.length; i++) {
if (callback(this[i], i, this)) {
arr.push(this[i])
}
}
return arr
}
// map
const newArr = [1, 3, 5].filter2((item, index, originArr) => {
// 当这个函数的返回结果是 true 的时候,就保留 item 到新数组
return item >= 3
});
console.log(newArr) */
</script>
- 你知道怎么退出 forEach 循环吗?
退不出,除非抛出异常,throw new Error('异常信息')
。
-
undefined + 2
和null + 2
的结果是什么? -
函数传参?
传递简单数据类型,传递是值本身,内部修改不会影响外部;
传递复杂数据类型传递的是引用地址,内部【内容的】修改,修改的是同一个内存空间,会相互影响;引用修改也不会相互影响;
/* let a = 8
// 传递简单数据类型,传递是值本身,不会影响外部
function fn(a) {
// 局部变量 let a;形参也是局部变量;
a = 9
}
fn(a)
console.log(a) // 8 */
/* let a = 8
function fn() {
a = 9
}
fn(a)
console.log(a) // 9 */
/* let obj = {
age: 8
}
// 引用类型,参数传递,传递的是引用地址,内部的修改,修改的是同一个内存空间,所以修改里面的内容会相互影响
function fn(obj) {
obj.age = 9
}
fn(obj)
console.log(obj.age) // 9 */
/* let obj = {
age: 8
}
// 引用类型,参数传递,传递的是引用地址,内部的修改,修改的是同一个内存空间,所以修改里面的【内容】会相互影响
function fn(a) {
a.age = 9
}
fn(obj)
console.log(obj.age) // 9 */
let obj = {
age: 8
}
function fn(a) {
// 重新指向了一个新空间,当然不会影响原来的 obj 了呀
a = {
age: 9
}
}
fn(obj)
console.log(obj.age) // 8
- 说一下 bind、apply、call 的区别?
相同点:三个都是用来改变 this 指向的(第一个参数都是用来改变 this 执行的)
bind 和 [apply/call] 的区别:bind 不会直接调用函数,而是返回一个新函数;apply 和 call 会直接调用函数;
apply 和 call 的区别:call 可以传递任意多个参数,apply 只能传 2 个,第二个参数要是类数组。
// const arr = [1, 3, 5]
// console.log(Math.max(...[1, 3, 5]))
// 应用
/* console.log(Math.max.apply(Math, [1, 3, 5]))
function fn(a, b, c) {
console.log(a, b, c)
}
fn.apply(undefined, [1, 3, 5]) */
console.log([1, 3, 5])
console.log.apply(console, [1, 3, 5])
- 假如有 10000 个元素需要添加到页面上,你觉得怎么操作性能最好(考察文档碎片)?
// 往页面上加 10000 个元素
// 【最慢】
/* console.time('耗时')
for (let i = 1; i <= 1000; i++) {
document.body.innerHTML += `<div>${i}</div>`
}
console.timeEnd('耗时') // 1260 ms */
// 【很快】
/* console.time('耗时')
let str = ''
for (let i = 1; i <= 1000; i++) {
str += `<div>${i}</div>`
}
// 一次性添加
document.body.innerHTML = str
console.timeEnd('耗时') // 4 ms */
// 【很快,理论上来说内存占用少一些】
/* console.time('耗时')
const arr = []
for (let i = 1; i <= 1000; i++) {
arr.push(`<div>${i}</div>`)
}
// 一次性添加
document.body.innerHTML = arr.join('')
console.timeEnd('耗时') // 3 ms */
// 问题:不灵活
/* console.time('耗时')
const arr = []
for (let i = 1; i <= 1000; i++) {
// 需求,每一个 div 绑定事件,执行对应的业务逻辑
// 问题:不太方便写业务逻辑,例如绑定事件
arr.push(`<div>${i}</div>`)
}
// 一次性添加
document.body.innerHTML = arr.join('')
console.timeEnd('耗时') // 3 ms */
/* console.time('耗时')
// 【createElement => 灵活 => 性能也 ok】
// 需求,每一个 div 绑定事件,执行对应的业务逻辑
for (let i = 1; i <= 1000; i++) {
const oDiv = document.createElement('div');
oDiv.onclick = function () {
// ...
};
oDiv.style.backgroundColor = i === 769 ? 'red' : ''
oDiv.innerHTML = i;
document.body.appendChild(oDiv)
}
console.timeEnd('耗时') // 14 ms */
console.time('耗时')
// 【文档碎片,篮子】
// #1 准备篮子
const oFrag = document.createDocumentFragment()
for (let i = 1; i <= 1000; i++) {
const oDiv = document.createElement('div');
oDiv.onclick = function () {
// ...
};
oDiv.style.backgroundColor = i === 769 ? 'red' : ''
oDiv.innerHTML = i;
// #2 装篮子
oFrag.appendChild(oDiv)
}
document.body.appendChild(oFrag)
console.timeEnd('耗时') // 存在大量元素添加的时候,在低版本 IE 确实能提升性能,在现代浏览器意义不大了
- 什么是伪数组,伪数组怎么转真数组,Array.from、{...伪数组}(三个点只能处理可迭代数据)?
伪数组:具有 length 属性;能够通过下表访问元素;不具备真数组的方法。
const aDiv = document.querySelectorAll('div')
/* const arrDiv = Array.from(aDiv)
arrDiv.push(8)
console.log(arrDiv) */
// console.log([...aDiv].push(8))
const arrDiv = [...aDiv]
arrDiv.push(8)
console.log(arrDiv)
Array.from 更通用,例如:
const obj = {
0: 'a',
1: 'b',
length: 2
}
console.log(Array.from(obj))
// iterable 可迭代,下去自己了解?
console.log([...obj])
const aDiv = document.querySelectorAll('div')
// 第三种转换的方法,了解
/* const arr = Array.prototype.slice.call(aDiv)
arr.push(8)
console.log(arr) */
// “借用”方法
function sum() {
// arguments
Array.prototype.push.call(arguments, 8)
console.log(arguments)
}
sum(1, 3, 4)
-
arguments 是什么?
-
函数传参,传递复杂数据类型和简单数据类型有什么区别?
-
new 的执行过程?
开辟一个空间;把 this 指向这个空间;执行构造函数中的代码;返回 this;
function Fn(name, age) {
this.name = name;
this.age = age
// return this // 默认有这一行
// 如果返回简单数据类型,会忽略
// return 8
// 如果返回简单复杂类型,此时 new 得到的结果就不再是实例对象了,而是这个复杂数据类型
return { address: '武汉' }
}
const ngls = new Fn('尼古拉斯', 18)
console.log(ngls)
- 防抖节流?
防抖和节流都是性能优化的一种手段。
是什么:持续触发(事件)不执行,不触发的一段时间后再执行。
举个生活中的例子:王者荣耀英雄回城。
实际怎么应用:根据输入的内容发请求。
能封装一个吗:自己封装或使用 lodash 里面的 _.debounce。
<style>
div {
margin: 0 auto;
width: 500px;
height: 200px;
background-color: deeppink;
}
</style>
<div id="oBox"></div>
<script src="./node_modules/lodash/lodash.min.js"></script>
<script>
// 第一版
/* oBox.onmousemove = function (e) {
const left = e.pageX - this.offsetLeft;
const top = e.pageY - this.offsetTop;
this.innerHTML = `left: ${left} top: ${top}`;
}; */
// 希望频率低一点
// 防抖:持续触发(事件)不执行,不触发的一段时间后再执行。
// #1
/* let timer = null
oBox.onmousemove = function (e) {
// #2
clearTimeout(timer)
// #3
timer = setTimeout(() => {
const left = e.pageX - this.offsetLeft;
const top = e.pageY - this.offsetTop;
this.innerHTML = `left: ${left} top: ${top}`;
}, 100);
}; */
// 生活中的防抖:英雄回城
// 应用场景:根据输入的内容发请求
// 封装一个防抖函数
/* function debounce(callback, time) {
let timer = null
return function (e) {
// this => oBox
clearTimeout(timer)
timer = setTimeout(() => {
callback.call(this, e) // window.callback(e)
}, time);
}
} */
/* function debounce(callback, time) {
let timer = null
return function (e) {
// this => oBox
clearTimeout(timer)
timer = setTimeout(() => {
callback.apply(this, arguments) // window.callback(e)
}, time);
}
} */
// 实际开发你怎么用?
oBox.onmousemove = _.debounce(function (e) {
const left = e.pageX - this.offsetLeft;
const top = e.pageY - this.offsetTop;
this.innerHTML = `left: ${left} top: ${top}`;
}, 100);
</script>
节流
<style>
div {
margin: 0 auto;
width: 500px;
height: 200px;
background-color: deeppink;
}
</style>
<div id="oBox"></div>
<script src="./node_modules/lodash/lodash.min.js"></script>
<script>
// 节流:持续触发也执行,只不过执行的频率变低了。
// 生活中的例子:一直扣着扳机打散弹枪
// #1
/* let bBar = true
oBox.onmousemove = function (e) {
// #2
if (bBar) {
// #3
bBar = false
setTimeout(() => {
const left = e.pageX - this.offsetLeft;
const top = e.pageY - this.offsetTop;
this.innerHTML = `left: ${left} top: ${top}`;
// #4
bBar = true
}, 1000)
}
}; */
function throttle(callback, time) {
// #1
let bBar = true
return function () {
// #2
if (!bBar) return false
// #3
bBar = false
setTimeout(() => {
callback.apply(this, arguments)
// #4
bBar = true
}, 1000)
}
}
oBox.onmousemove = throttle(function (e) {
const left = e.pageX - this.offsetLeft;
const top = e.pageY - this.offsetTop;
this.innerHTML = `left: ${left} top: ${top}`;
}, 1000);
// 实际开发你怎么用?
/* oBox.onmousemove = _.throttle(function (e) {
const left = e.pageX - this.offsetLeft;
const top = e.pageY - this.offsetTop;
this.innerHTML = `left: ${left} top: ${top}`;
}, 1000); */
</script>
- JS 中怎么实现继承(组合继承 = Call 式继承 + 原型继承)?
function Person(name, age) {
this.name = name;
this.age = age
}
Person.prototype.show = function () {
console.log('~~~')
}
function Star(name, age) {
// #1 构造函数继承 => 继承的是属性
Person.apply(this, [name, age])
}
// #2 原型继承 => 继承是方法
Star.prototype = new Person()
Star.prototype.constructor = Star;
const s = new Star('AngelaBaby', 30);
console.log(s)
s.show()
// #3 组合继承 = 构造函数继承 + 原型继承
ES6 继承
class Person {
constructor(name, age) {
this.name = name;
this.age = age
}
show() {
console.log('~~~')
}
}
// ES6 的 extends 继承
class Star extends Person { }
const s = new Star('AngelaBaby', 30);
console.log(s)
s.show()
- 你平常怎么判断数据类型,你觉得怎样判断才是比较准确的?
typeof 不能准确区分复杂数据类型,例如数组和对象或 null
instanceof 判断左边的能不能通过原型链找到右边的 prototype
,看一下在不在同一条原型链
const arr = []
const obj = {}
console.log(arr instanceof Object)
console.log(arr.__proto__.__proto__ === Object.prototype)
console.log(obj instanceof Object)
console.log(obj.__proto__ === Object.prototype)
Object.prototype.toString.call(数据)
,目前最佳!
/* console.log(Object.prototype.toString.call(null)) // '[object Null]'
console.log(Object.prototype.toString.call(undefined)) // '[object Undefined]'
console.log(Object.prototype.toString.call([])) // '[object Array]'
console.log(Object.prototype.toString.call({})) // '[object Object]'
console.log(Object.prototype.toString.call(function () { })) // '[object Function]'
console.log(Object.prototype.toString.call('')) // '[object String]' */
function isType(c) {
let r = Object.prototype.toString.call(c)
// let r = {}.toString.call(c)
// let r = {}.__proto__.toString.call(c)
if (r === '[object Null]') {
return 'null 类型'
} else if (r === '[object Undefined]') {
return 'undefined 类型'
} else if (r === '[object Array]') {
return 'array 类型'
}
}
const arr = []
console.log(isType(arr))
- 扁平化?
// 对此数组进行【扁平化】后【去重】后【排序】
const arr = [[1, 6, [9, [99, [88, [77, [101, [333, [22]]]]]]]], 1, [5], [3, [0, [9, 84]]]];
// flat
// arr.flat(Infinity)
// console.log(new Set(arr.flat(Infinity))) // 去重后的 set 数据结构
// Array.from(new Set(arr.flat(Infinity))) // 去重后的数组结构
const result = Array.from(new Set(arr.flat(Infinity))).sort((num1, num2) => num1 - num2) // 去重后的数组结构
console.log(result)
- 说一下 Promise?有哪些方法?
实例方法:then catch`
静态方法:race、all
、any
-
如何并发多个请求,按顺序拿到结果?中间有一个出错了会怎样?catch 之后还会触发 then 吗?
-
说一下 DOM 事件流?捕获、冒泡、目标、
e.stopPropagation()
-
说一下事件委托?
如何按需触发事件,可以通过 e.target 拿到当前点击的自定义属性来进行判断。
- 说一下 Event Loop / 事件循环 / 事件环 / JS 的执行机制?
- JS 代码的执行分为同步代码(任务)和异步代码(任务,宏任务+微任务);
- 当碰到同步代码的时候直接在执行栈中执行;
- 当碰到异步代码并且时机符合的时候(例如定时器时间到了),就把异步代码添加到任务队列当中;
- 当执行栈中的同步代码执行完毕后;
- 就去任务队列中把异步代码拿到执行栈中执行;
- 这种反复轮训任务队列并把异步代码拿到执行栈中执行的操作就是 Event Loop;
setTimeout(function() {
// 不一定 1s 执行
// 能不能保证 1s 执行呢?
// Web Worker // 开启多线程
}, 1000)
- 原型链?
多个对象之间通过 __proto__
链接起来的这种关系就是原型链。
function Person(name, age) {
this.name = name;
this.age = age;
}
const ifer = new Person('ifer', 18)
// 是对象就有 __proto__
console.log(ifer.__proto__ === Person.prototype)
console.log(Person.prototype.__proto__ === Object.prototype)
console.log(ifer.__proto__.__proto__ === Object.prototype)
// console.log(Function.prototype.__proto__ === Object.prototype)
// console.log(Function.__proto__ === Function.prototype)
你平常写代码有用到过原型链这个机制吗?
const arr = []
// arr 并没有 push,你怎么能用呢?因为原型链的机制,它会顺着 __proto__ 找到 Array.prototype 上的 push
arr.push(9)
- 什么是重绘和回流,如何减少重绘和回流?
重绘:外观属性发生变化(颜色、透明度、阴影、outline)
回流:几何属性
- 前端安全了解吗,说一下 XSS 和 CSRF,以及怎么规避?
XSS:跨站脚本攻击,来自第三方信息需要进行过滤/转义。
CSRF:跨站请求伪造,模拟用户(利用已经登录的用户的 token)自身去进行一些操作,解决方式,验证码。
- 说一下 localstorage/sessionStorage/cookie 之间的差异?
生命周期不一样
生效范围不一样
存储大小不一样
Cookie 相比较那两者会随着请求头自动携带到后端
- 移动端 click 300ms 延迟是怎么回事,你是怎么处理的?
fastclick.js
-
做移动端开发时,有没有碰到过什么问题(考察移动端的兼容性,搜索一下,找几条)?
-
移动端 1px 的问题了解吗?
<div class="box1"></div>
<div class="box2"></div>
<style>
.box1 {
height: 1px;
background-color: red;
}
.box2 {
margin-top: 50px;
height: 1px;
background-color: red;
transform: scale(1, 0.5);
}
</style>
-
说一下 AJAX 的状态码?MDN
-
说一下 HTTP 的状态码,400、401、403、405、301、302、304?
400(请求参数错误)
401(认证失败,一般是由于 Token 过期造成的认证失败/没有权限)
403(forbidden,禁止访问,没有权限,普通用户却访问了管理员的东西)
405(请求方法/方式错误)
-
图片是怎么上传和预览的?
-
讲一下 set 和 map 区别,讲一下深浅拷贝有哪些?zh.javascript.info/map-set
-
for of / for in。
-
康康老师 有空可以讲一下深拷贝和浅拷贝吗。
Node
- 说一下你对 Node 的了解?
Node 是什么?基于 Chrome V8 引擎的运行环境
能干什么?API、前端的命令行工具都是基于 Node 的、做 API 的接口转发聚合、Electron(Node)...
Vue
- nextTick
<template>
<ul>
<li v-for="item in arr" :key="item">{{ item }}</li>
</ul>
<button @click="add">add</button>
</template>
<script>
import { nextTick, reactive } from 'vue'
export default {
name: 'App',
setup() {
const arr = reactive([1, 2, 3])
const add = () => {
arr.push(Math.random())
arr.push(Math.random())
arr.push(Math.random())
// Vue 中数据更新是同步的
console.log(arr.length) // 6
// Vue 中,【内存】中的 DOM 更新是【异步,表现形式】的?为什么呢?性能
// DOM 更新和 DOM 渲染是不是一回事?不是一回事!
// 上面的【异步】是真的异步吗?
const oUl = document.querySelector('ul')
console.log(oUl.children.length) // 3
/* Promise.resolve().then(() => {
// 同一个 loop 等同步代码执行完毕,再到这儿
// DOM 更新也完事了。
// console.log(oUl.children.length, 'test') // 6
// 同步代码 => 微任务 => DOM 渲染 => 宏任务
alert(oUl.children.length) // 6
}) */
// nextTick => 内部封装了 Promise.then => MutationObserver => setImmdiate => setTimeout
nextTick().then(() => {
console.log(oUl.children.length, 'test')
})
}
return { arr, add }
},
}
</script>
- 你会在哪个阶段发请求,为什么?
created => 请求发的时机越早越好;请求之前可能需要依赖 data 里面的数据或者 methods 里面的方法;
beforeDestroy 干什么事?解绑事件;清除定时器。
父子组件,我期望子组件 mounted 后,父组件才需要做一些事情?
在子组件的 mounted 里面 this.$emit(父亲的自定义事件)
;
<v-chart @hook:mounted="loading = false"/>
- 你说一下 Vue 中有哪些内置组件?说一下什么是动态组件和异步组件?
component 配合 is 属性可以实现动态组件
、slot
、keep-alive
异步组件:只有用到了才去加载,目的:性能,语法如下:
export default {
component: {
Hello: () => import('./Hello')
}
}
传统写法
import Hello from './Hello'
export default {
component: {
Hello
}
}
- 说一下你对 Vuex 的理解,Vuex 中能在 mutation 的某个方法中触发另一个 mutation 中的方法吗?
是什么
怎么用,分两方面去说:一个是配置项(6 plugins)以及其作用;触发流程,把那个图说出来。
mutation 里面可以写异步吗?可以写,严格模式下会有警告,但是即便可以写也不建议写?为什么呢?
其实目的是为了形成数据快照,配合 devtools 进行调试。
dispatch action 的时候, action 函数的第一个参数是什么?
- 创建 store 实例的时候,除了 state、mutations、actions、getters、mudules,plugins 选项是干什么的,你用过吗(讲过 vuex-persistedstate 这个插件可以做 vuex 中的数据持久化,百度一下)?
plugins 是配置插件的,例如我最常用的是那个持久 vuex 数据的插件 persistedstate
Vuex 中的数据刷新会丢失吗?为什么?
路由传参的数据刷新会丢失吗?不会
-
你知道和 Vue 相关的性能优化有哪些?链接
-
Vue 组件中的 data 为什么需要是一个函数?
为了避免多个组件实例之间的数据相互影响。为什么是函数就不会相互影响了呀?
data 必须是一个函数,返回一个新对象,这样再实例化的时候能保证自己的实例对应的是独立的一个对象空间,不会和其他的产生关系。
- 你进行过组件封装吗,用到了哪些技术(
Element UI 举例
)?
业务组件
UI 组件(传值和校验、插槽和作用域插槽、自定义事件)
基于 UI 组件进行二次封装
- Vue2 在哪些情况下操作不是响应式的?为什么?怎么解决?Vue3 呢为什么就可以了?
给对象不存在的 key 进行赋值(后续添加的 key 进行赋值);为什么?因为 Object.defineProperty 劫持的是对象中的每一个属性,后续添加的当然就没有劫持到,所以不是响应式的。
this.$set(obj, 'age', 18)
Vue.set(obj, 'age', 18)
通过索引修改数组的内容也不是响应式的; 明确:Object.defineProperty 确确实实是支持对数组的劫持的,但是作者出于性能的原因没有这样做。
this.$set(数组/对象, key/索引, 值)
splice
Vue3 不存在这个问题了,因为 Vue3 的响应式原理换成了 Proxy,而 Proxy 它直接劫持的是整个对象,也就无所谓对象里面的属性是不是后续添加的了(因为后续添加的也是属于整个对象啊),所以给对象后续添加的 key 也是响应式的。再一个来说 Proxy 对数组劫持也不存在性能问题了。
- 为什么 Vue3 把实现双向数据绑定的 API Object.defineProperty 换成了 Proxy,出于什么考虑?能具体说一下吗?
Vue2
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="oP"></p>
<input type="text" id="oInput">
<script>
const data = {
name: '张三丰',
age: 18,
};
// console.log(data)
// 数据劫持:就是把数据都变成 getter/setter 形式,能做到对数据操作的时候,也能添加一些额外的功能(例如修改视图)
// 对象、属性、描述/配置
const copyData = { ...data };
// ['name', 'age']
Object.keys(data).forEach(key => {
Object.defineProperty(data, key, {
get() {
// !这儿给了你自由,获取数据的时候想干啥干啥
// 获取 name 的时候,会触发这儿
return copyData[key]
},
set(newValue) {
// #1
oP.innerHTML = newValue
oInput.value = newValue
// 设置 name 的时候,会触发这儿
copyData[key] = newValue
}
});
})
oInput.oninput = function (e) {
// 边输入,边设置,边触发 #1 处的 set
data.name = e.target.value
}
</script>
</body>
</html>
Proxy
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p id="oP"></p>
<input type="text" id="oInput">
<script>
const data = {};
// proxyData => 代理对象
// 对代理的操作 proxyData => 会影响到原始值 data
const proxyData = new Proxy(data, {
get(target, attr) {
// 获取数据的时候会触发这儿
// target => 原数据
// attr => 属性
// console.log(target === data)
return target[attr]
},
set(target, attr, newValue) {
oP.innerHTML = newValue
oInput.value = newValue
// 设置数据的时候会触发这儿
target[attr] = newValue
}
})
oInput.oninput = function (e) {
proxyData.address = e.target.value
}
</script>
</body>
</html>
核心就是性能。
如果对象有 100 个属性,Object.defineProperty 要循环 100 次来劫持每一个属性;而由于 Proxy 直接可以代理整个对象,一次就可以搞定。
Proxy 对后续新增的属性也具有响应式。
- 说一下对 Vue3 的了解?
- 性能更高了(Proxy、VNode 算法进行了改进,静态标记)
- 体积更小了(按需导入,能配合 Webpack 支持 Tree Shaking;删除了一些不常用的功能 API,filter、EventBus...)
- 对 TS 支持更好了(源码)
- Composition API(小项目挺好的,一个萝卜一个坑,一目了然,但是!对于大型项目,同一功能的数据和操作数据的业务逻辑太过于分散,不方便维护;不方便复用。)
- 新特性(Fragment、Teleport...script setup 语法糖,更简洁)
- 说一下 Vue 组件通信?
-
父传子的第一种方式
父亲通过自定义属性传递;儿子通过 props 接收;
传递过来的数据可以被修改吗?
简单数据类型不能改,赋值数据类型,引用地址不能改,内容可以改,即便内容可以改,也不建议改,因为单项数据流?
单项数据流:数据在哪来的就在哪修改,目的方便追溯错误;祖先的数据被改了,后代也会改变。
怎么办?通过 $emit 触发组件(父组件)的自定义事件,在自定义事件对应的回调里面修改。
-
父传子的第二种方式
父亲通过自定义属性传递,儿子通过 $attrs 非 props 属性进行接收
-
父传子的第三种方式
ref:获取子组件实例的同时,调用子组件的方法并传参。
-
父传子的第四种方式
this.$children:获取子组件实例的同时,调用子组件的方法并传参。
-
子传父第一种方式
儿子通过 $emit 触发父亲的自定义事件并传参
-
子传父第二种方式
父亲传递一个方法,儿子接收并调用这个方法的同时并传参
-
子传父的第三种方式
this.$parent
可以获取父亲的实例,直接修改父亲的数据 -
子传父的第四种方式
this.$listeners 获取父亲的自定义事件并调用并传参
-
兄弟传值第一种方式
状态提升(把数据提升到公共的父组件里面,变成子父、父子之间的传递)
-
兄弟传值第二种方式
EventBus、事件中心,EventBus 在 Vue3 种被干掉了,为什么干掉呢?
它可以实现任意两个组件间通信,太过于灵活,例如一个组件进行了 on 监听,出现错误了不方便追溯;EventBus 本身实现起来很简单,自己也可以实现,Vue 处于自身体积更小的原因就干掉了;作者推荐的也有相关的包去实现;包。
干了你还怎么实现兄弟通信呀?状态提升
-
跨层级,
依赖注入
祖先通过 Provide 提供数据,后代通过 Inject 消费/使用数据。
- v-for / v-if 为什么不建议放一行?如何处理?Vue3 呢?
Vue2 v-for 优先级比较高,会不管三七二十一把所有的元素循环出来,再把不符合条件的销毁,多了一些不必要的创建和销毁的操作,性能不好。
计算属性提前处理。
- 父子组件嵌套,父子组件的生命周期触发顺序是怎样的?
父子子父
。
初始化:父 beforeCreate、父 created、父 beforeMount、儿子走完、父 mounted
- keep-alive 了解吗,说一下?
做组件缓存的,相关的两个钩子,相关的配置属性;
-
你项目中是怎么组织接口请求的,axios 封装,你都封装了哪些东西?
-
怎么配置 vue-cli 这个工具呢,你都配置过哪些东西(vue.config.js)?了解 Webpack 吗?
webpack 是什么?配置项有哪些?不过,实际开发中,我更愿意用基于webpack 的 vue-cli 去创建项目,它提供了 vue.config.js 可以对 webpack 进行配置...
-
script setup 语法糖。
-
Proxy 和 Object.defineProperty
-
nextick
小程序
- 做过小程序开发吗,说一下和网页开发的区别?
开发方式不一样,官网提供了开发和提示的工具
宿主环境不一样
小程序在生命周期(应用级别、页面级别的、组件级别在)
小程序在适配,提供好了 rpx 单位,可以直接进行适配
小程序开发(原生 + wepy + taro+uni-app...)
项目
- 说一下你最近负责的一个项目,有碰到什么问题吗,你认为的难点是什么?
- 路由缓存问题
- 数据不更新的问题
- 大数字问题,后端返回的数据本质上都是 json 格式的字符串,前端用 axios 请求,为什么可以当做对象使用呢?
- 因为 axios 内部使用 JSON.parse 进行转换。
- 但是,当这个 json 格式的字符串里面包含大数字的时候,JSON.parse 就搞不定了,转出来的结果就不准确了
- 所以当我用这个 id 去请求的时候就出现了 404
- 解决方式1:后端那个 id 不要用数字表示,用字符串
- 解决方式2:配置 axios 的 transformResponse =>
json-bigint
-
说一下页面访问控制和权限管理你是怎么做的?
-
开发项目的时候你都优化了哪些东西?
补充
- 怎样控制代码在1 秒后执行(定时器做不到这点): 利用 webwork
var worker = new Worker("task.js");
worker.onmessage = function(event) {
// 消息文本放置在data属性中,
// oBox.innerHTML = event.data;
alert(event.data);
}
worker.postMessage(500000);
- 你有什么想问的吗? ( 问面试官 -> 业务流程 团队建设 技术栈 ; 如果是人事, 可以问薪资体系 )
- 箭头函数传参: 箭头函数没有arguments对象,获取参数列表需要用扩展符,获取的参数列表是数组类型
let arrow = (...args) => {
console.log('参数列表:',args, args instanceof Array)
}
arrow(1,2,3) // 参数列表;[1,2,3] true
-
es5 和 es6 继承的区别
- ES5的继承时通过prototype或构造函数机制来实现。ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.apply(this)。
- ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。