给大家推荐一个实用面试题库
1、前端面试题库 (面试必备) 推荐:★★★★★
地址:web前端面试题库
Html5和CSS3
常见的水平垂直居中实现方案
- 最简单的方案当然是flex布局
.father { display: flex; justify-content: center; align-items: center; } .son { ... }
- 绝对定位配合margin:auto,的实现方案
.father { position: relative; } .son { position: absolute; top: 0; left: 0; bottom: 0; right: 0; margin: auto; }
- 绝对定位配合transform实现
.father { position: relative; } .son { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); }
BFC问题
BFC:块格式上下文,是一块独立的渲染区域,内部元素不会影响外部的元素。
flex:1; 是哪些属性的缩写,对应的属性代表什么含义
flex: 1;在浏览器中查看分别是flex-grow(设置了对应元素的增长系数)、flex-shrink(指定了对应元素的收缩规则,只有在所有元素的默认宽度之和大于容器宽度时才会触发)、flex-basis(指定了对应元素在主轴上的大小)
隐藏元素的属性有哪些
- display: none;
- visibility: hidden;
- opacity: 0;
Js相关
Js的基础类型,typeof和instanceof的区别
基础类型有:boolean、string、number、bigint、undefined、symbol、null。
typeof能识别所有的值类型,识别函数,能区分是否是引用类型。
const a = "str"; console.log("typeof a :>> ", typeof a); // typeof a :>> string const b = 999; console.log("typeof b :>> ", typeof b); // typeof b :>> number const c = BigInt(9007199254740991); console.log("typeof c :>> ", typeof c); // typeof c :>> bigint const d = false; console.log("typeof d :>> ", typeof d); // typeof d :>> boolean const e = undefined; console.log("typeof e :>> ", typeof e); // typeof e :>> undefined const f = Symbol("f"); console.log("typeof f :>> ", typeof f); // typeof f :>> symbol const g = null; console.log("typeof g :>> ", typeof g); // typeof g :>> object const h = () => {}; console.log("typeof h :>> ", typeof h); // typeof h :>> function const i = []; console.log("typeof i :>> ", typeof i); // typeof i :>> object
instanceof用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
数组的forEach和map方法有哪些区别?常用哪些方法去对数组进行增、删、改
- forEach是对数组的每一个元素执行一次给定的函数。
- map是创建一个新数组,该新数组由原数组的每个元素都调用一次提供的函数返回的值。
- pop():删除数组后面的最后一个元素,返回值为被删除的那个元素。
- push():将一个元素或多个元素添加到数组末尾,并返回新的长度。
- shift():删除数组中的第一个元素,并返回被删除元素的值。
- unshift():将一个或多个元素添加到数组的开头,并返回该数组的新长度。
- splice():通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。
- reverse(): 反转数组。
const arr = [1, 2, 3, 4, 5, 6]; arr.forEach(x => { x = x + 1; console.log("x :>> ", x); }); // x :>> 2 // x :>> 3 // x :>> 4 // x :>> 5 // x :>> 6 // x :>> 7 console.log("arr :>> ", arr); // arr :>> [ 1, 2, 3, 4, 5, 6 ] const mapArr = arr.map(x => { x = x * 2; return x; }); console.log("mapArr :>> ", mapArr); // mapArr :>> [ 2, 4, 6, 8, 10, 12 ] console.log("arr :>> ", arr); // arr :>> [ 1, 2, 3, 4, 5, 6 ] const popArr = arr.pop(); console.log("popArr :>> ", popArr); // popArr :>> 6 console.log("arr :>> ", arr); // arr :>> [ 1, 2, 3, 4, 5 ] const pushArr = arr.push("a"); console.log("pushArr :>> ", pushArr); // pushArr :>> 6 console.log("arr :>> ", arr); // arr :>> [ 1, 2, 3, 4, 5, 'a' ] const shiftArr = arr.shift(); console.log("shiftArr :>> ", shiftArr); // shiftArr :>> 1 console.log("arr :>> ", arr); // arr :>> [ 2, 3, 4, 5, 'a' ] const unshiftArr = arr.unshift("b", "c"); console.log("unshiftArr :>> ", unshiftArr); // unshiftArr :>> 7 console.log("arr :>> ", arr); // arr :>> ['b', 'c', 2,3,4,5,'a'] const spliceArr = arr.splice(2, 4, "d", "e"); console.log("spliceArr :>> ", spliceArr); // spliceArr :>> [ 2, 3, 4, 5 ] console.log("arr :>> ", arr); // arr :>> [ 'b', 'c', 'd', 'e', 'a' ] const reverseArr = arr.reverse(); console.log("reverseArr :>> ", reverseArr); // reverseArr :>> [ 'a', 'e', 'd', 'c', 'b' ] console.log("arr :>> ", arr); // arr :>> [ 'a', 'e', 'd', 'c', 'b' ] console.log("reverseArr === arr :>> ", reverseArr === arr); // reverseArr === arr :>> true
闭包和作用域
闭包是作用域应用的特殊场景。 js中常见的作用域包括全局作用域、函数作用域、块级作用域。要知道js中自由变量的查找是在函数定义的地方,向上级作用域查找,不是在执行的地方。 常见的闭包使用有两种场景:一种是函数作为参数被传递;一种是函数作为返回值被返回。
// 函数作为返回值 function create() { let a = 100; return function () { console.log(a); }; } const fn = create(); const a = 200; fn(); // 100 // 函数作为参数被传递 function print(fb) { const b = 200; fb(); } const b = 100; function fb() { console.log(b); } print(fb); // 100
实现一个类似关键字new功能的函数
在js中new关键字主要做了:首先创建一个空对象,这个对象会作为执行new构造函数之后返回的对象实例,将创建的空对象原型(__proto__
)指向构造函数的prototype属性,同时将这个空对象赋值给构造函数内部的this,并执行构造函数逻辑,根据构造函数的执行逻辑,返回初始创建的对象或构造函数的显式返回值。
function newFn(...args) { const constructor = args.shift(); const obj = Object.create(constructor.prototype); const result = constructor.apply(obj, args); return typeof result === "object" && result !== null ? result : obj; } function Person(name) { this.name = name; } const p = newFn(Person, "Jerome"); console.log("p.name :>> ", p.name); // p.name :>> Jerome
如何实现继承(原型和原型链)
使用class语法,用extends进行继承,或直接改变对象的__proto__指向。
class Car { constructor(brand) { this.brand = brand; } showBrand() { console.log("the brand of car :>> ", this.brand); } } class ElectricCar extends Car { constructor(brand, duration) { super(brand); this.duration = duration; } showDuration() { console.log(`duration of this ${this.brand} ElectricCar :>> `, this.duration); } } ElectricCar.prototype.showOriginator = function (originator) { console.log(`originator of this ElectricCar :>> `, originator); }; const tesla = new ElectricCar("tesla", "600km"); tesla.showBrand(); // the brand of car :>> tesla tesla.showDuration(); // duration of this tesla ElectricCar :>> 600km console.log("tesla instanceof Car :>> ", tesla instanceof Car); // tesla instanceof Car :>> true console.log("tesla instanceof ElectricCar :>> ", tesla instanceof ElectricCar); // tesla instanceof ElectricCar :>> true console.log("tesla.__proto__ :>> ", tesla.__proto__); // tesla.__proto__ :>> Car {} console.log("ElectricCar.prototype === tesla.__proto__ :>> ", ElectricCar.prototype === tesla.__proto__); // ElectricCar.prototype === tesla.__proto__ :>> true tesla.showOriginator("Mask"); // originator of this ElectricCar :>> Mask const bydCar = { brand: "比亚迪", duration: "666km", }; bydCar.__proto__ = ElectricCar.prototype; bydCar.showBrand(); //the brand of car :>> 比亚迪 bydCar.showDuration(); // duration of this 比亚迪 ElectricCar :>> 666km
箭头函数和普通函数有什么区别
箭头函数不会创建自身的this,只会从上一级继承this,箭头函数的this在定义的时候就已经确认了,之后不会改变。同时箭头函数无法作为构造函数使用,没有自身的prototype,也没有arguments。
this.id = "global"; console.log("this.id :>> ", this.id); // this.id :>> global function normalFun() { return this.id; } const arrowFun = () => { return this.id; }; const newNormal = new normalFun(); console.log("newNormal :>> ", newNormal); // newNormal :>> normalFun {} try { const newArrow = new arrowFun(); } catch (error) { console.log("error :>> ", error); // error :>> TypeError: arrowFun is not a constructor } console.log("normalFun :>> ", normalFun()); // normalFun :>> undefined console.log("arrowFun() :>> ", arrowFun()); // arrowFun() :>> global const obj = { id: "obj", normalFun, arrowFun, }; const normalFunBindObj = normalFun.bind(obj); const arrowFunBindObj = arrowFun.bind(obj); console.log("normalFun.call(obj) :>> ", normalFun.call(obj)); // normalFun.call(obj) :>> obj console.log("normalFunBindObj() :>> ", normalFunBindObj()); // normalFunBindObj() :>> obj console.log("arrowFun.call(obj) :>> :>> ", arrowFun.call(obj)); // arrowFun.call(obj) :>> :>> global console.log("arrowFunBindObj() :>> ", arrowFunBindObj()); // arrowFunBindObj() :>> global console.log("obj.normalFun() :>> ", obj.normalFun()); // obj.normalFun() :>> obj console.log("obj.arrowFun() :>> ", obj.arrowFun()); // obj.arrowFun() :>> global
迭代器(iterator)接口和生成器(generator)函数的关系
任意一个对象实现了遵守迭代器协议的[Symbol.iterator]方法,那么该对象就可以调用[Symbol.iterator]返回一个遍历器对象。生成器函数就是遍历器生成函数,故可以把generator赋值给对象的[Symbol.iterator]属性,从而使该对象具有迭代器接口。
class ClassRoom { constructor(address, name, students) { this.address = address; this.name = name; this.students = students; } entry(student) { this.students.push(student); } *[Symbol.iterator]() { yield* this.students; } // [Symbol.iterator]() { // let index = 0; // return { // next: () => { // if (index < this.students.length) { // return { done: false, value: this.students[index++] }; // } else { // return { done: true, value: undefined }; // } // }, // return: () => { // console.log("iterator has early termination"); // return { done: true, value: undefined }; // }, // }; // } } const classOne = new ClassRoom("7-101", "teach-one-room", ["rose", "jack", "lily", "james"]); for (const stu of classOne) { console.log("stu :>> ", stu); // stu :>> rose // stu :>> jack // stu :>> lily // stu :>> james // if (stu === "lily") return; }
浏览器的事件循环机制
首先要知道一件事,JavaScript是单线程的(指的是js引擎在执行代码的时候只有一个主线程,每次只能干一件事),同时还是非阻塞运行的(执行异步任务的时候,会先挂起相应任务,待异步返回结果再执行回调),这就要知道其事件的循环机制才能正确理解js代码的执行顺序。
在js代码执行时,会将对象存在堆(heap)中,在栈(stack)中存放一些基础类型变量和对象的指针。在执行方法时,会根据当前方法的执行上下文,来进行一个执行。对于普通函数就是正常的入栈出栈即可,涉及到异步任务的时候,js执行会将对应的任务放到事件队列中(微任务队列、宏任务队列)。
- 常见微任务:queueMicrotask、Promise、MutationObserve等。
- 常见宏任务:ajax、setTimeout、setInterval、script(js整体代码)、IO操作、UI交互、postMessage等。
故事件循环可以理解为是一个桥梁,连接着应用程序的js和系统调用之间的通道。其过程为:
- 执行一个宏任务(一般为一段script),若没有可选的宏任务,就直接处理微任务。
- 执行中遇到微任务,就将其添加到微任务的任务队列中。
- 执行中遇到宏任务,就将其提交到宏任务队列中。
- 执行完当前执行的宏任务后,去查询当前有无需要执行的微任务,有就执行
- 检查渲染,若需要渲染,浏览器执行渲染任务
- 渲染完毕后,Js线程会去执行下一个宏任务。。。(如此循环)
console.log("script start"); const promiseA = new Promise((resolve, reject) => { console.log("init promiseA"); resolve("promiseA"); }); const promiseB = new Promise((resolve, reject) => { console.log("init promiseB"); resolve("promiseB"); }); setTimeout(() => { console.log("setTimeout run"); promiseB.then(res => { console.log("promiseB res :>> ", res); }); console.log("setTimeout end"); }, 500); promiseA.then(res => { console.log("promiseA res :>> ", res); }); queueMicrotask(() => { console.log("queue Microtask run"); }); console.log("script end"); // script start // init promiseA // init promiseB // script end // promiseA res :>> promiseA // queue Microtask run // setTimeout run // setTimeout end // promiseB res :>> promiseB
TypeScript
type和interface的区别
interface可以重复声明,type不行,继承方式不一样,type使用交叉类型方式,interface使用extends实现。在对象扩展的情况下,使用接口继承要比交叉类型的性能更好。建议使用interface来描述对象对外暴露的借口,使用type将一组类型重命名(或对类型进行复杂编程)。
interface iMan { name: string; age: number; } // 接口可以进行声明合并 interface iMan { hobby: string; } type tMan = { name: string; age: number; }; // type不能重复定义 // type tMan = {} // 继承方式不同,接口继承使用extends interface iManPlus extends iMan { height: string; } // type继承使用&,又称交叉类型 type tManPlus = { height: string } & tMan; const aMan: iManPlus = { name: "aa", age: 15, height: "175cm", hobby: "eat", }; const bMan: tManPlus = { name: "bb", age: 15, height: "150cm", };
any、unkonwn、never
any和unkonwn在TS类型中属于最顶层的Top Type,即所有的类型都是它俩的子类型。而never则相反,它作为Bottom Type是所有类型的子类型。
常见的工具类型
- Partial:满足部分属性(一个都没满足也可)即可
- Required:所有属性都需要
- Readonly: 包装后的所有属性只读
- Pick: 选取部分属性
- Omit: 去除部分属性
- Extract: 交集
- Exclude: 差集
关于Vue
虚拟DOM
采用虚拟DOM的更新技术在性能这块,理论上是不可能比原生Js操作DOM高的。不过在大部分情况下,开发者很难写出绝对优化的命令式代码。所以虚拟DOM就是用来解决这一问题,让开发者系的代码在性能上得到保障,甚至无限接近命令式代码的性能。 通常情况下,纯Js层面的操作远比DOM操作快。虚拟DOM就是用Js来模拟出DOM结构,通过diff算法来计算出最小的变更,通过对应的渲染器,来渲染到页面上。
同时虚拟DOM也为跨平台开发提供了极大的便利,开发者写的同一套代码(有些需要针对不同平台做区分),通过不同的渲染规则,就可以生成不同平台的代码。
在vue中会通过渲染器来将虚拟DOM转换为对应平台的真实DOM。如renderer(vnode, container),该方法会根据vnode描述的信息(如tag、props、children)来创建DOM元素,根据规则为对应的元素添加属性和事件,处理vnode下的children。
vue3的变化(改进)
响应式方面
vue3的响应式是基于Proxy来实现的,利用代理来拦截对象的基本操作,配合Refelect.*方法来完成响应式的操作。
书写方面
提供了setup的方式,配合组合式API,可以建立组合逻辑、创建响应式数据、创建通用函数、注册生命周期钩子等。
diff算法方面:
- 在vue2中使用的是双端diff算法:是一种同时比较新旧两组节点的两个端点的算法(比头、比尾、头尾比、尾头比)。一般情况下,先找出变更后的头部,再对剩下的进行双端diff。
- 在vue3中使用的是快速diff算法:它借鉴了文本diff算法的预处理思路,先处理新旧两组节点中相同的前置节点和后置节点。当前置节点和后置节点全部处理完毕后,如果无法通过简单的挂载新节点或者卸载已经不存在的节点来更新,则需要根据节点间的索引关系,构造出一个最长递增子序列。最长递增子序列所指向的节点即为不需要移动的节点。
编译上的优化
- vue3新增了PatchFlags来标记节点类型(动态节点收集与补丁标志),会在一个Block维度下的vnode下收集到对应的dynamicChildren(动态节点),在执行更新时,忽略vnode的children,去直接找到动态节点数组进行更新,这是一种高效率的靶向更新。
- vue3提供了静态提升方式来优化重复渲染静态节点的问题,结合静态提升,还对静态节点进行预字符串化,减少了虚拟节点的性能开销,降低了内存占用。
- vue3会将内联事件进行缓存,每次渲染函数重新执行时会优先取缓存里的事件
关于vue3双向绑定的实现
vue3实现双向绑定的核心是Proxy(代理的使用),它会对需要响应式处理的对象进行一层代理,对象的所有操作(get、set等)都会被Prxoy代理到。在vue中,所有响应式对象相关的副作用函数会使用weakMap来存储。当执行对应的操作时,会去执行操作中所收集到的副作用函数。
// WeakMap常用于存储只有当key所引用的对象存在时(没有被回收)才有价值的消息,十分贴合双向绑定场景 const bucket = new WeakMap(); // 存储副作用函数 let activeEffect; // 用一个全局变量处理被注册的函数 const tempObj = {}; // 临时对象,用于操作 const data = { text: "hello world" }; // 响应数据源 // 用于清除依赖 function cleanup(effectFn) { for (let i = 0; i < effectFn.deps.length; i++) { const deps = effectFn.deps[i]; deps.delete(effectFn); } effectFn.deps.length = 0; } // 处理依赖函数 function effect(fn) { const effectFn = () => { cleanup(effectFn); activeEffect = effectFn; fn(); }; effectFn.deps = []; effectFn(); } // 在get时拦截函数调用track函数追踪变化 function track(target, key) { if (!activeEffect) return; // let depsMap = bucket.get(target); if (!depsMap) { bucket.set(target, (depsMap = new Map())); } let deps = depsMap.get(key); if (!deps) { depsMap.set(key, (deps = new Set())); } deps.add(activeEffect); activeEffect.deps.push(deps); } // 在set拦截函数内调用trigger来触发变化 function trigger(target, key) { const depsMap = bucket.get(target); if (!depsMap) return; const effects = depsMap.get(key); const effectsToRun = new Set(effects); effectsToRun.forEach(effectFn => effectFn()); // effects && effects.forEach(fn => fn()); } const obj = new Proxy(data, { // 拦截读取操作 get(target, key) { if (!activeEffect) return; // console.log("get -> key", key); track(target, key); return target[key]; }, // 拦截设置操作 set(target, key, newValue) { console.log("set -> key: newValue", key, newValue); target[key] = newValue; trigger(target, key); }, }); effect(() => { tempObj.text = obj.text; console.log("tempObj.text :>> ", tempObj.text); }); setTimeout(() => { obj.text = "hi vue3"; }, 1000);
vue3中的ref、toRef、toRefs
- ref:接收一个内部值,生成对应的响应式数据,该内部值挂载在ref对象的value属性上;该对象可以用于模版和reactive。使用ref是为了解决值类型在setup、computed、合成函数等情况下的响应式丢失问题。
- toRef:为响应式对象(reactive)的一个属性创建对应的ref,且该方式创建的ref与源属性保持同步。
- toRefs:将响应式对象转换成普通对象,对象的每个属性都是对应的ref,两者间保持同步。使用toRefs进行对象解构。
function ref(val) { const wrapper = {value: val} Object.defineProperty(wrapper, '__v_isRef', {value: true}) return reactive(wrapper) } function toRef(obj, key) { const wrapper = { get value() { return obj[key] }, set value(val) { obj[key] = val } } Object.defineProperty(wrapper, '__v_isRef', {value: true}) return wrapper } function toRefs(obj) { const ret = {} for (const key in obj) { ret[key] = toRef(obj, key) } return ret } // 自动脱ref function proxyRefs(target) { return new Proxy(target, { get(target, key, receiver) { const value = Reflect.get(target, key, receiver) return value.__v_isRef ? value.value : value }, set(target, key, newValue, receiver) { const value = target[key] if(value.__v_isRef) { value.value = newValue return true } return Reflect.set(target, key, newValue, receiver) } }) }
computed和watch的区别
使用场景:computed适用于一个数据受多个数据影响使用;watch适合一个数据影响多个数据使用。
区别:computed属性默认会走缓存,只有依赖数据发生变化,才会重新计算,不支持异步,有异步导致数据发生变化时,无法做出相应改变;watch不依赖缓存,一旦数据发生变化就直接触发响应操作,支持异步。
vue-router的路由守卫
- 全局前置守卫
router.beforeEach((to, from, next) => { // to: 即将进入的目标 // from:当前导航正要离开的路由 return false // 返回false用于取消导航 return {name: 'Login'} // 返回到对应name的页面 next({name: 'Login'}) // 进入到对应的页面 next() // 放行 })
- 全局解析守卫:类似beforeEach
router.beforeResolve(to => { if(to.meta.canCopy) { return false // 也可取消导航 } })
- 全局后置钩子
router.afterEach((to, from) => { logInfo(to.fullPath) })
- 导航错误钩子,导航发生错误调用
router.onError(error => { logError(error) })
- 路由独享守卫,beforeEnter可以传入单个函数,也可传入多个函数。
function dealParams(to) { // ... } function dealPermission(to) { // ... } const routes = [ { path: '/home', component: Home, beforeEnter: (to, from) => { return false // 取消导航 }, // beforeEnter: [dealParams, dealPermission] } ]
组件内的守卫
const Home = { template: `...`, beforeRouteEnter(to, from) { // 此时组件实例还未被创建,不能获取this }, beforeRouteUpdate(to, from) { // 当前路由改变,但是组件被复用的时候调用,此时组件已挂载好 }, beforeRouteLeave(to, from) { // 导航离开渲染组件的对应路由时调用 } }
composition Api对比 option Api的优势
- 更好的代码组织
- 更好的逻辑复用
- 更好的类型推导
2023前端面试题总结(二):https://developer.aliyun.com/article/1415619