• 2023前端面试题总结


     给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

    地址:web前端面试题库

    Html5和CSS3

    常见的水平垂直居中实现方案
    • 最简单的方案当然是flex布局
    1. .father {
    2. display: flex;
    3. justify-content: center;
    4. align-items: center;
    5. }
    6. .son {
    7. ...
    8. }
    • 绝对定位配合margin:auto,的实现方案
    1. .father {
    2. position: relative;
    3. }
    4. .son {
    5. position: absolute;
    6. top: 0;
    7. left: 0;
    8. bottom: 0;
    9. right: 0;
    10. margin: auto;
    11. }
    • 绝对定位配合transform实现
    1. .father {
    2. position: relative;
    3. }
    4. .son {
    5. position: absolute;
    6. top: 50%;
    7. left: 50%;
    8. transform: translate(-50%, -50%);
    9. }
    BFC问题

    BFC:块格式上下文,是一块独立的渲染区域,内部元素不会影响外部的元素。

    flex:1; 是哪些属性的缩写,对应的属性代表什么含义

    flex: 1;在浏览器中查看分别是flex-grow(设置了对应元素的增长系数)、flex-shrink(指定了对应元素的收缩规则,只有在所有元素的默认宽度之和大于容器宽度时才会触发)、flex-basis(指定了对应元素在主轴上的大小)

    image.png

    隐藏元素的属性有哪些
    • display: none;
    • visibility: hidden;
    • opacity: 0;

    Js相关

    Js的基础类型,typeof和instanceof的区别

    基础类型有:boolean、string、number、bigint、undefined、symbol、null。

    typeof能识别所有的值类型,识别函数,能区分是否是引用类型。

    1. const a = "str";
    2. console.log("typeof a :>> ", typeof a); // typeof a :>> string
    3. const b = 999;
    4. console.log("typeof b :>> ", typeof b); // typeof b :>> number
    5. const c = BigInt(9007199254740991);
    6. console.log("typeof c :>> ", typeof c); // typeof c :>> bigint
    7. const d = false;
    8. console.log("typeof d :>> ", typeof d); // typeof d :>> boolean
    9. const e = undefined;
    10. console.log("typeof e :>> ", typeof e); // typeof e :>> undefined
    11. const f = Symbol("f");
    12. console.log("typeof f :>> ", typeof f); // typeof f :>> symbol
    13. const g = null;
    14. console.log("typeof g :>> ", typeof g); // typeof g :>> object
    15. const h = () => {};
    16. console.log("typeof h :>> ", typeof h); // typeof h :>> function
    17. const i = [];
    18. console.log("typeof i :>> ", typeof i); // typeof i :>> object

    instanceof用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

    数组的forEach和map方法有哪些区别?常用哪些方法去对数组进行增、删、改
    • forEach是对数组的每一个元素执行一次给定的函数。
    • map是创建一个新数组,该新数组由原数组的每个元素都调用一次提供的函数返回的值。
    • pop():删除数组后面的最后一个元素,返回值为被删除的那个元素。
    • push():将一个元素或多个元素添加到数组末尾,并返回新的长度。
    • shift():删除数组中的第一个元素,并返回被删除元素的值。
    • unshift():将一个或多个元素添加到数组的开头,并返回该数组的新长度
    • splice():通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。
    • reverse(): 反转数组。
    1. const arr = [1, 2, 3, 4, 5, 6];
    2. arr.forEach(x => {
    3. x = x + 1;
    4. console.log("x :>> ", x);
    5. });
    6. // x :>> 2
    7. // x :>> 3
    8. // x :>> 4
    9. // x :>> 5
    10. // x :>> 6
    11. // x :>> 7
    12. console.log("arr :>> ", arr); // arr :>> [ 1, 2, 3, 4, 5, 6 ]
    13. const mapArr = arr.map(x => {
    14. x = x * 2;
    15. return x;
    16. });
    17. console.log("mapArr :>> ", mapArr); // mapArr :>> [ 2, 4, 6, 8, 10, 12 ]
    18. console.log("arr :>> ", arr); // arr :>> [ 1, 2, 3, 4, 5, 6 ]
    19. const popArr = arr.pop();
    20. console.log("popArr :>> ", popArr); // popArr :>> 6
    21. console.log("arr :>> ", arr); // arr :>> [ 1, 2, 3, 4, 5 ]
    22. const pushArr = arr.push("a");
    23. console.log("pushArr :>> ", pushArr); // pushArr :>> 6
    24. console.log("arr :>> ", arr); // arr :>> [ 1, 2, 3, 4, 5, 'a' ]
    25. const shiftArr = arr.shift();
    26. console.log("shiftArr :>> ", shiftArr); // shiftArr :>> 1
    27. console.log("arr :>> ", arr); // arr :>> [ 2, 3, 4, 5, 'a' ]
    28. const unshiftArr = arr.unshift("b", "c");
    29. console.log("unshiftArr :>> ", unshiftArr); // unshiftArr :>> 7
    30. console.log("arr :>> ", arr); // arr :>> ['b', 'c', 2,3,4,5,'a']
    31. const spliceArr = arr.splice(2, 4, "d", "e");
    32. console.log("spliceArr :>> ", spliceArr); // spliceArr :>> [ 2, 3, 4, 5 ]
    33. console.log("arr :>> ", arr); // arr :>> [ 'b', 'c', 'd', 'e', 'a' ]
    34. const reverseArr = arr.reverse();
    35. console.log("reverseArr :>> ", reverseArr); // reverseArr :>> [ 'a', 'e', 'd', 'c', 'b' ]
    36. console.log("arr :>> ", arr); // arr :>> [ 'a', 'e', 'd', 'c', 'b' ]
    37. console.log("reverseArr === arr :>> ", reverseArr === arr); // reverseArr === arr :>> true
    闭包和作用域

    闭包是作用域应用的特殊场景。 js中常见的作用域包括全局作用域、函数作用域、块级作用域。要知道js中自由变量的查找是在函数定义的地方,向上级作用域查找,不是在执行的地方。 常见的闭包使用有两种场景:一种是函数作为参数被传递;一种是函数作为返回值被返回。

    1. // 函数作为返回值
    2. function create() {
    3. let a = 100;
    4. return function () {
    5. console.log(a);
    6. };
    7. }
    8. const fn = create();
    9. const a = 200;
    10. fn(); // 100
    11. // 函数作为参数被传递
    12. function print(fb) {
    13. const b = 200;
    14. fb();
    15. }
    16. const b = 100;
    17. function fb() {
    18. console.log(b);
    19. }
    20. print(fb); // 100
    实现一个类似关键字new功能的函数

    在js中new关键字主要做了:首先创建一个空对象,这个对象会作为执行new构造函数之后返回的对象实例,将创建的空对象原型(__proto__)指向构造函数的prototype属性,同时将这个空对象赋值给构造函数内部的this,并执行构造函数逻辑,根据构造函数的执行逻辑,返回初始创建的对象或构造函数的显式返回值。

    1. function newFn(...args) {
    2. const constructor = args.shift();
    3. const obj = Object.create(constructor.prototype);
    4. const result = constructor.apply(obj, args);
    5. return typeof result === "object" && result !== null ? result : obj;
    6. }
    7. function Person(name) {
    8. this.name = name;
    9. }
    10. const p = newFn(Person, "Jerome");
    11. console.log("p.name :>> ", p.name); // p.name :>> Jerome
    如何实现继承(原型和原型链)

    使用class语法,用extends进行继承,或直接改变对象的__proto__指向。

    1. class Car {
    2. constructor(brand) {
    3. this.brand = brand;
    4. }
    5. showBrand() {
    6. console.log("the brand of car :>> ", this.brand);
    7. }
    8. }
    9. class ElectricCar extends Car {
    10. constructor(brand, duration) {
    11. super(brand);
    12. this.duration = duration;
    13. }
    14. showDuration() {
    15. console.log(`duration of this ${this.brand} ElectricCar :>> `, this.duration);
    16. }
    17. }
    18. ElectricCar.prototype.showOriginator = function (originator) {
    19. console.log(`originator of this ElectricCar :>> `, originator);
    20. };
    21. const tesla = new ElectricCar("tesla", "600km");
    22. tesla.showBrand(); // the brand of car :>> tesla
    23. tesla.showDuration(); // duration of this tesla ElectricCar :>> 600km
    24. console.log("tesla instanceof Car :>> ", tesla instanceof Car); // tesla instanceof Car :>> true
    25. console.log("tesla instanceof ElectricCar :>> ", tesla instanceof ElectricCar); // tesla instanceof ElectricCar :>> true
    26. console.log("tesla.__proto__ :>> ", tesla.__proto__); // tesla.__proto__ :>> Car {}
    27. console.log("ElectricCar.prototype === tesla.__proto__ :>> ", ElectricCar.prototype === tesla.__proto__); // ElectricCar.prototype === tesla.__proto__ :>> true
    28. tesla.showOriginator("Mask"); // originator of this ElectricCar :>> Mask
    29. const bydCar = {
    30. brand: "比亚迪",
    31. duration: "666km",
    32. };
    33. bydCar.__proto__ = ElectricCar.prototype;
    34. bydCar.showBrand(); //the brand of car :>> 比亚迪
    35. bydCar.showDuration(); // duration of this 比亚迪 ElectricCar :>> 666km
    箭头函数和普通函数有什么区别

    箭头函数不会创建自身的this,只会从上一级继承this,箭头函数的this在定义的时候就已经确认了,之后不会改变。同时箭头函数无法作为构造函数使用,没有自身的prototype,也没有arguments。

    1. this.id = "global";
    2. console.log("this.id :>> ", this.id); // this.id :>> global
    3. function normalFun() {
    4. return this.id;
    5. }
    6. const arrowFun = () => {
    7. return this.id;
    8. };
    9. const newNormal = new normalFun();
    10. console.log("newNormal :>> ", newNormal); // newNormal :>> normalFun {}
    11. try {
    12. const newArrow = new arrowFun();
    13. } catch (error) {
    14. console.log("error :>> ", error); // error :>> TypeError: arrowFun is not a constructor
    15. }
    16. console.log("normalFun :>> ", normalFun()); // normalFun :>> undefined
    17. console.log("arrowFun() :>> ", arrowFun()); // arrowFun() :>> global
    18. const obj = {
    19. id: "obj",
    20. normalFun,
    21. arrowFun,
    22. };
    23. const normalFunBindObj = normalFun.bind(obj);
    24. const arrowFunBindObj = arrowFun.bind(obj);
    25. console.log("normalFun.call(obj) :>> ", normalFun.call(obj)); // normalFun.call(obj) :>> obj
    26. console.log("normalFunBindObj() :>> ", normalFunBindObj()); // normalFunBindObj() :>> obj
    27. console.log("arrowFun.call(obj) :>> :>> ", arrowFun.call(obj)); // arrowFun.call(obj) :>> :>> global
    28. console.log("arrowFunBindObj() :>> ", arrowFunBindObj()); // arrowFunBindObj() :>> global
    29. console.log("obj.normalFun() :>> ", obj.normalFun()); // obj.normalFun() :>> obj
    30. console.log("obj.arrowFun() :>> ", obj.arrowFun()); // obj.arrowFun() :>> global
    迭代器(iterator)接口和生成器(generator)函数的关系

    任意一个对象实现了遵守迭代器协议的[Symbol.iterator]方法,那么该对象就可以调用[Symbol.iterator]返回一个遍历器对象。生成器函数就是遍历器生成函数,故可以把generator赋值给对象的[Symbol.iterator]属性,从而使该对象具有迭代器接口。

    1. class ClassRoom {
    2. constructor(address, name, students) {
    3. this.address = address;
    4. this.name = name;
    5. this.students = students;
    6. }
    7. entry(student) {
    8. this.students.push(student);
    9. }
    10. *[Symbol.iterator]() {
    11. yield* this.students;
    12. }
    13. // [Symbol.iterator]() {
    14. // let index = 0;
    15. // return {
    16. // next: () => {
    17. // if (index < this.students.length) {
    18. // return { done: false, value: this.students[index++] };
    19. // } else {
    20. // return { done: true, value: undefined };
    21. // }
    22. // },
    23. // return: () => {
    24. // console.log("iterator has early termination");
    25. // return { done: true, value: undefined };
    26. // },
    27. // };
    28. // }
    29. }
    30. const classOne = new ClassRoom("7-101", "teach-one-room", ["rose", "jack", "lily", "james"]);
    31. for (const stu of classOne) {
    32. console.log("stu :>> ", stu);
    33. // stu :>> rose
    34. // stu :>> jack
    35. // stu :>> lily
    36. // stu :>> james
    37. // if (stu === "lily") return;
    38. }
    浏览器的事件循环机制

    首先要知道一件事,JavaScript是单线程的(指的是js引擎在执行代码的时候只有一个主线程,每次只能干一件事),同时还是非阻塞运行的(执行异步任务的时候,会先挂起相应任务,待异步返回结果再执行回调),这就要知道其事件的循环机制才能正确理解js代码的执行顺序。

    在js代码执行时,会将对象存在堆(heap)中,在栈(stack)中存放一些基础类型变量和对象的指针。在执行方法时,会根据当前方法的执行上下文,来进行一个执行。对于普通函数就是正常的入栈出栈即可,涉及到异步任务的时候,js执行会将对应的任务放到事件队列中(微任务队列、宏任务队列)。

    • 常见微任务:queueMicrotask、Promise、MutationObserve等。
    • 常见宏任务:ajax、setTimeout、setInterval、script(js整体代码)、IO操作、UI交互、postMessage等。

    故事件循环可以理解为是一个桥梁,连接着应用程序的js和系统调用之间的通道。其过程为:

    1. 执行一个宏任务(一般为一段script),若没有可选的宏任务,就直接处理微任务。
    2. 执行中遇到微任务,就将其添加到微任务的任务队列中。
    3. 执行中遇到宏任务,就将其提交到宏任务队列中。
    4. 执行完当前执行的宏任务后,去查询当前有无需要执行的微任务,有就执行
    5. 检查渲染,若需要渲染,浏览器执行渲染任务
    6. 渲染完毕后,Js线程会去执行下一个宏任务。。。(如此循环)
    1. console.log("script start");
    2. const promiseA = new Promise((resolve, reject) => {
    3. console.log("init promiseA");
    4. resolve("promiseA");
    5. });
    6. const promiseB = new Promise((resolve, reject) => {
    7. console.log("init promiseB");
    8. resolve("promiseB");
    9. });
    10. setTimeout(() => {
    11. console.log("setTimeout run");
    12. promiseB.then(res => {
    13. console.log("promiseB res :>> ", res);
    14. });
    15. console.log("setTimeout end");
    16. }, 500);
    17. promiseA.then(res => {
    18. console.log("promiseA res :>> ", res);
    19. });
    20. queueMicrotask(() => {
    21. console.log("queue Microtask run");
    22. });
    23. console.log("script end");
    24. // script start
    25. // init promiseA
    26. // init promiseB
    27. // script end
    28. // promiseA res :>> promiseA
    29. // queue Microtask run
    30. // setTimeout run
    31. // setTimeout end
    32. // promiseB res :>> promiseB

    TypeScript

    type和interface的区别

    interface可以重复声明,type不行,继承方式不一样,type使用交叉类型方式,interface使用extends实现。在对象扩展的情况下,使用接口继承要比交叉类型的性能更好。建议使用interface来描述对象对外暴露的借口,使用type将一组类型重命名(或对类型进行复杂编程)。

    1. interface iMan {
    2. name: string;
    3. age: number;
    4. }
    5. // 接口可以进行声明合并
    6. interface iMan {
    7. hobby: string;
    8. }
    9. type tMan = {
    10. name: string;
    11. age: number;
    12. };
    13. // type不能重复定义
    14. // type tMan = {}
    15. // 继承方式不同,接口继承使用extends
    16. interface iManPlus extends iMan {
    17. height: string;
    18. }
    19. // type继承使用&,又称交叉类型
    20. type tManPlus = { height: string } & tMan;
    21. const aMan: iManPlus = {
    22. name: "aa",
    23. age: 15,
    24. height: "175cm",
    25. hobby: "eat",
    26. };
    27. const bMan: tManPlus = {
    28. name: "bb",
    29. age: 15,
    30. height: "150cm",
    31. };
    any、unkonwn、never

    any和unkonwn在TS类型中属于最顶层的Top Type,即所有的类型都是它俩的子类型。而never则相反,它作为Bottom Type是所有类型的子类型。

    常见的工具类型
    • Partial:满足部分属性(一个都没满足也可)即可
    • Required:所有属性都需要
    • Readonly: 包装后的所有属性只读

      image.png

    • Pick: 选取部分属性
    • Omit: 去除部分属性

      image.png

    • Extract: 交集
    • Exclude: 差集

      image.png

    关于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来存储。当执行对应的操作时,会去执行操作中所收集到的副作用函数。

    1. // WeakMap常用于存储只有当key所引用的对象存在时(没有被回收)才有价值的消息,十分贴合双向绑定场景
    2. const bucket = new WeakMap(); // 存储副作用函数
    3. let activeEffect; // 用一个全局变量处理被注册的函数
    4. const tempObj = {}; // 临时对象,用于操作
    5. const data = { text: "hello world" }; // 响应数据源
    6. // 用于清除依赖
    7. function cleanup(effectFn) {
    8. for (let i = 0; i < effectFn.deps.length; i++) {
    9. const deps = effectFn.deps[i];
    10. deps.delete(effectFn);
    11. }
    12. effectFn.deps.length = 0;
    13. }
    14. // 处理依赖函数
    15. function effect(fn) {
    16. const effectFn = () => {
    17. cleanup(effectFn);
    18. activeEffect = effectFn;
    19. fn();
    20. };
    21. effectFn.deps = [];
    22. effectFn();
    23. }
    24. // 在get时拦截函数调用track函数追踪变化
    25. function track(target, key) {
    26. if (!activeEffect) return; //
    27. let depsMap = bucket.get(target);
    28. if (!depsMap) {
    29. bucket.set(target, (depsMap = new Map()));
    30. }
    31. let deps = depsMap.get(key);
    32. if (!deps) {
    33. depsMap.set(key, (deps = new Set()));
    34. }
    35. deps.add(activeEffect);
    36. activeEffect.deps.push(deps);
    37. }
    38. // 在set拦截函数内调用trigger来触发变化
    39. function trigger(target, key) {
    40. const depsMap = bucket.get(target);
    41. if (!depsMap) return;
    42. const effects = depsMap.get(key);
    43. const effectsToRun = new Set(effects);
    44. effectsToRun.forEach(effectFn => effectFn());
    45. // effects && effects.forEach(fn => fn());
    46. }
    47. const obj = new Proxy(data, {
    48. // 拦截读取操作
    49. get(target, key) {
    50. if (!activeEffect) return; //
    51. console.log("get -> key", key);
    52. track(target, key);
    53. return target[key];
    54. },
    55. // 拦截设置操作
    56. set(target, key, newValue) {
    57. console.log("set -> key: newValue", key, newValue);
    58. target[key] = newValue;
    59. trigger(target, key);
    60. },
    61. });
    62. effect(() => {
    63. tempObj.text = obj.text;
    64. console.log("tempObj.text :>> ", tempObj.text);
    65. });
    66. setTimeout(() => {
    67. obj.text = "hi vue3";
    68. }, 1000);
    vue3中的ref、toRef、toRefs
    • ref:接收一个内部值,生成对应的响应式数据,该内部值挂载在ref对象的value属性上;该对象可以用于模版和reactive。使用ref是为了解决值类型在setup、computed、合成函数等情况下的响应式丢失问题。
    • toRef:为响应式对象(reactive)的一个属性创建对应的ref,且该方式创建的ref与源属性保持同步。
    • toRefs:将响应式对象转换成普通对象,对象的每个属性都是对应的ref,两者间保持同步。使用toRefs进行对象解构。
    1. function ref(val) {
    2. const wrapper = {value: val}
    3. Object.defineProperty(wrapper, '__v_isRef', {value: true})
    4. return reactive(wrapper)
    5. }
    6. function toRef(obj, key) {
    7. const wrapper = {
    8. get value() {
    9. return obj[key]
    10. },
    11. set value(val) {
    12. obj[key] = val
    13. }
    14. }
    15. Object.defineProperty(wrapper, '__v_isRef', {value: true})
    16. return wrapper
    17. }
    18. function toRefs(obj) {
    19. const ret = {}
    20. for (const key in obj) {
    21. ret[key] = toRef(obj, key)
    22. }
    23. return ret
    24. }
    25. // 自动脱ref
    26. function proxyRefs(target) {
    27. return new Proxy(target, {
    28. get(target, key, receiver) {
    29. const value = Reflect.get(target, key, receiver)
    30. return value.__v_isRef ? value.value : value
    31. },
    32. set(target, key, newValue, receiver) {
    33. const value = target[key]
    34. if(value.__v_isRef) {
    35. value.value = newValue
    36. return true
    37. }
    38. return Reflect.set(target, key, newValue, receiver)
    39. }
    40. })
    41. }
    computed和watch的区别

    使用场景:computed适用于一个数据受多个数据影响使用;watch适合一个数据影响多个数据使用。

    区别:computed属性默认会走缓存,只有依赖数据发生变化,才会重新计算,不支持异步,有异步导致数据发生变化时,无法做出相应改变;watch不依赖缓存,一旦数据发生变化就直接触发响应操作,支持异步。

    vue-router的路由守卫
    • 全局前置守卫
    1. router.beforeEach((to, from, next) => {
    2. // to: 即将进入的目标
    3. // from:当前导航正要离开的路由
    4. return false // 返回false用于取消导航
    5. return {name: 'Login'} // 返回到对应name的页面
    6. next({name: 'Login'}) // 进入到对应的页面
    7. next() // 放行
    8. })
    • 全局解析守卫:类似beforeEach
    1. router.beforeResolve(to => {
    2. if(to.meta.canCopy) {
    3. return false // 也可取消导航
    4. }
    5. })
    • 全局后置钩子
    1. router.afterEach((to, from) => {
    2. logInfo(to.fullPath)
    3. })
    • 导航错误钩子,导航发生错误调用
    1. router.onError(error => {
    2. logError(error)
    3. })
    • 路由独享守卫,beforeEnter可以传入单个函数,也可传入多个函数。
    1. function dealParams(to) {
    2. // ...
    3. }
    4. function dealPermission(to) {
    5. // ...
    6. }
    7. const routes = [
    8. {
    9. path: '/home',
    10. component: Home,
    11. beforeEnter: (to, from) => {
    12. return false // 取消导航
    13. },
    14. // beforeEnter: [dealParams, dealPermission]
    15. }
    16. ]

    组件内的守卫

    1. const Home = {
    2. template: `...`,
    3. beforeRouteEnter(to, from) {
    4. // 此时组件实例还未被创建,不能获取this
    5. },
    6. beforeRouteUpdate(to, from) {
    7. // 当前路由改变,但是组件被复用的时候调用,此时组件已挂载好
    8. },
    9. beforeRouteLeave(to, from) {
    10. // 导航离开渲染组件的对应路由时调用
    11. }
    12. }
    composition Api对比 option Api的优势
    • 更好的代码组织
    • 更好的逻辑复用
    • 更好的类型推导

    浏览器相关

    跨域问题

    由于浏览器同源策略(浏览器安全功能,它会阻止一个域与另一个域的内容进行交互,能有效防止XSS、CSRF攻击)的限制,非同源的请求会被限制。

    解决跨域问题的方法:

    • 配置nginx反向代理
    • 使用jsonp方式(script方式)
    • 使用图片
    • 设置CORS(跨域资源共享)
    • 利用iframe实现
    • WebSocket
    浏览器的存储有哪些及它们间的区别
    • cookie
    • session storage
    • local storage
    • indexedDB:用于客户端存储大量的结构化数据(文件/二进制大型对象(blobs))。该API使用索引实现对数据的高性能搜索。
    • cache storage:用于对Cache对象的存储。
    说说浏览器渲染页面的过程

    首先输入一个网址,浏览器会向服务器发起DNS请求,得到对应的IP地址(会被缓存一段时间,后续访问就不用再去向服务器查询)。之后会进行TCP三次握手与服务器建立连接,连接建立后,浏览器会代表用户发送一个初始的GET请求,通常是请求一个HTML文件。服务器收到对应请求后 ,会根据相关的响应头和HTML内容进行回复。

    一旦浏览器拿到了数据,就会开始解析信息,这个过程中,浏览器会根据HTML文件去构建DOM树,当遇到一些阻塞资源时(如同步加载的script标签)会去加载阻塞资源而停止当前DOM树构建(所以能够异步的或延迟加载的就尽量异步或延迟,同时页面的脚本还是越少越好)。在构建DOM树时,浏览器的主线程被占据着,不过浏览器的预加载扫描器会去请求高优先级的资源(如css、js、字体),预加载扫描器很好的优化了阻塞问题。接下来浏览器会处理CSS生成CSSDOM树,将CSS规则转换为可以理解和使用的样式映射,这个过程非常快(通常小于一次DNS查询所需时间)。有了DOM树和CSSDOM树,浏览器会将其组合生成一个Render树,计算样式或渲染树会从DOM的根节点开始构建,遍历每一个可见节点(将相关样式匹配到每一个可见节点,并根据CSS级联去的每个节点的计算样式)。接下来开始布局,该过程(依旧是从根节点开始)会确定所有节点的宽高和位置,最后通过渲染器将其在页面上绘制。绘制完成了,并不代表交互也都生效了,因为主线程可能还无法抽出时间去处理滚动、触摸等交互,要等到js加载完成,同时主线程空闲了整个页面才是正常可用的状态。

    screenshot_02.png

    工具链相关题目

    对webpack的理解

    webpack是一个前端打包器,帮助开发者将js模块(各种类型的模块化规范)打包成一个或多个js脚本。webpack的工作过程可以分为依赖解析过程和代码打包过程,首先执行对应的build命令,webpack首先分析入口文件,会递归解析AST获取对应依赖,得到一个依赖图。然后为每一个模块添加包裹函数(webpack的模块化),从入口文件为起点,递归执行模块,进行拼接IIFE(立即调用函数表达式:保证了模块变量不会影响全局作用域),产出对应的bundle。

    webpack中plugin和loader分别做什么?它们之间的执行顺序?
    • loader:用于将不同类型的文件转换成webpack可以识别的文件(webpack只认识js和json)。
    • plugin:存在于webpack整个生命周期中,是一种基于事件机制工作的模式,可以在webpck打包过程对某些节点做某些定制化处理。同时plugin可以对loader解析过程中做一些处理,协同处理文件。
    • 执行顺序:两者不存在明显的先后顺序,不过webpack在初始化处理时,会优先识别到plugin中的内容。
    webpack常见的优化方案
    • 基于esm的tree shaking
    • 对balel设置缓存,缩小babel-loader的处理范围,及精准指定要处理的目录。
    • 压缩资源(mini-css-extract-plugin,compression-webpack-plugin)
    • 配置资源的按需引入(第三方组件库)
    • 配置splitChunks来进行按需加载(根据)
    • 设置CDN优化
    1. rules: [
    2. {
    3. test: /\.m?js$/,
    4. exclude: /node_modules/
    5. include: path.resolve(__dirname, 'src'),
    6. use: {
    7. loader: 'babel-loader?cacheDirectory'
    8. }
    9. },
    10. ]
    关于babel的理解

    babel是一个工具链,主要用于将ES2015+代码转换为当前和旧浏览器或环境中向后兼容的Js版本。这句话比较官方,其实babel就是一个语法转换工具链,它会将我们书写的代码(vue或react)通过相关的解析(对应的Preset),主要是词法解析和语法解析,通过babel-parser转换成对应的AST树,再对得到的抽象语法树根据相关的规则配置,转换成最终需要的目标平台识别的AST树,再得到目标代码。

    在日程的Webpack使用主要有三个插件:babel-loader、babel-core、babel-preset-env。 babel本质上会运行babel-loader一个函数,在运行时会匹配到对应的文件,根据babel.config.js(.balelrc)的配置(这里会配置相关的babel-preset-env,它会告诉babel用什么规则去进行代码转换)去将代码进行一个解析和转换(转换依靠的是babel-core),最终得到目标平台的代码。

    vite和webpak的区别

    vite在开环境时基于ESBuild打包,相比webpack的编译方式,大大提高了项目的启动和热更新速度。

    关于React

    说说看类组件的生命周期,函数组件使用哪些hook来代替的哪些生命周期
    • 类组件生命周期
    1. 初始化阶段,类组件会执行constructor(其只会在初始化阶段执行一次,使用super(props)确保props传递成功,同时做一些初始化操作,如声明state,绑定this等)。接下来,如果存在getDerivedStateFromProps就执行getDerivedStateFromProps(该函数传入两个参数(nextProps,prevState),其作用是:代替componentWillMount和componentWillReceiveProps;在组件初始化或更新时,将props映射到state;其返回值会与state合并,可作为shouldComponentUpdate的第二个参数newState,用于判断是否需要渲染),不存在的话componentWillMount(由于存在隐匿风险已经废弃,不建议使用)将会被执行,到此mountClassComponent函数咨询完成,之后会执行render(创建React.element元素的过程)渲染函数,形成children,接下来React会调用reconcileChildren方法深度调和children。react调和完所有的fiber节点,就会进入到commit阶段,然后会执行componentDidMount(其执行时机和componentDidUpdate一样,只是一个是初始化阶段,一个是更新阶段,此时DOM已经挂载,可以进行DOM操作,同时可以向服务端请求数据,渲染视图)。
    1. constructor ->
    2. getDerivedStateFromProps ->
    3. componentWillMount ->
    4. render ->
    5. componentDidMount
    1. 更新阶段,类组件会判断是否存在getDerivedStateFromProps,不存在会执行componentWillReceiveProps,存在就执行getDerivedStateFromProps(返回的值用于合成新的state)。之后执行shouldComponentUpdate(用于性能优化),传入新的props、state、context,根据其返回值来决定是否执行render函数。接下来执行componentWillUpdate,到这里updateClassInstance方法执行完毕。接下来进入render函数,得到最新的React Element元素,然后继续调和子节点。 之后进入commit阶段,会执行getSnapshotBeforeUpdate(会返回一个DOM修改前的快照,作为传递给compontDidUpdate的第三个参数,该参数不限于DOM的信息,可以时DOM计算出的产物),然后会执行compontDidUpdate(此时dom已经修改完成,可以进行dom操作;不能再这个函数里执行setState操作,否则会导致无限循环)。这就是一个完整的更新。
    1. componentWillReciveProps(props改变)/getDrivedStateFromProp ->
    2. shouldComponentUpdate ->
    3. componentWillUpdate ->
    4. render ->
    5. getSnapshotBeforeUpdate ->
    6. componentDidUpdate
    1. 销毁阶段,类组件会先执行componentWillUnmount(清除一些定时器、事件监听器)
    • 函数组件的生命周期替代方案

    useEffect:其第一个参数cb,返回的destory作为下一次cb执行之前调用,用于清楚上一次cb产生的副作用;第二个参数是依赖项,为一个数组,依赖改变,执行上一次cb返回的destory,和执行新的effect的cb。 useEffect的执行,React采用的异步调用的逻辑,对于每一个effect的cb,React会将其放入到事件队列中,等主线程完成,DOM更新,js执行完毕,视图绘制完成,才执行,故,effect的回调不会阻塞浏览器的视图绘制。

    1. useEffect(() => {
    2. return destory
    3. }, dep)

    useLayoutEffect:不同于useEffect的是,其采用了同步执行,它是在DOM更新前,浏览器绘制之前执行,适合在这个时候修改DOM,这样浏览器只会绘制一次。如果将修改DOM操作放在useEffect中,会导致浏览器的重绘和回流。故useLayoutEffect的cb会阻塞浏览器绘制。

    1. useLayoutEffect(() => {
    2. // deal Dom
    3. }, dep)
    对于Fiber架构理解

    Fiber出现在React16版本,在15及以前的版本,React更新DOM都是使用递归的方式进行遍历,每次更新都会从应用根部递归执行,且一旦开始,无法中断,这样层级越来越深,结构复杂度高的项目就会出现明显的卡顿。fiber架构出现就是为了解决这个问题,fiber是在React中最小粒度的执行单元,可以将fiber理解为是React的虚拟DOM。在React中,更新fiber的过程叫做调和,每一个fiber都可以作为一个执行单元进行处理,同时每个fiber都有一个优先级lane(16版本是expirationTime)来判断是否还有空间或时间来执行更新,如果没有时间更新,就会把主动权交给浏览器去做一些渲染(如动画、重排、重绘等),用户就不会感觉到卡顿。然后,当浏览器空闲了(requestIdleCallback),就通过scheduler(调度器)将执行恢复到执行单元上,这样本质上是中断了渲染,不过题改了用户的体验。React实现的fiber模式是一个具有链表和指针的异步模型。

    fiber作为react创建的element和真实DOM之间的桥梁,每一次更新的触发会在React element发起,经过fiber的调和,然后更新到真实DOM上。fiber上标识了各种不同类型的element,同时记录了对应和当前fiber有关的其他fiber信息(return指向父级、child指向子级、sibling指向兄弟)。

    在React应用中,应用首次构建时,会创建一个fiberRoot作为整个React应用的根基。然后当ReactDOM.render渲染出来时,会创建一个rootFiber对象(一个Ract应用可以用多个rootFiber,但只能有一个fiberRoot),当一次挂载完成时,fiberRoot的current属性会指向对应rootFiber。挂载完成后,会进入正式渲染阶段,在这个阶段必须知道一个workInProgerss树(它是正在内存在构建的Fiber树,在一次更新中,所有的更新都发生在workInProgeress树上,更新完成后,将变成current树用于渲染视图),当前的current树(rootFiber)的alternate会作为workInProgerss,同时会用alternate将workInProgress与current树进行关联(该关联只有在初始化第一次创建alternate时进行)。

    1. currentFiber.alternate = workInProgressFiber
    2. workInProgressFiber.alternate = currentFiber

    关联之后,会在心间的alternate上,完成整个fiber树的遍历。最后workInProgerss会作为最新的渲染树,来称为fiberRoot指向的current Fiber树。

    之后更新的时候依旧会重新创建一颗workInProgerss树,复用current上面的alternate,由于初始化的rootfiber有alternate,对于剩余的字节点,React都会创建一份,进行相同的关联。待渲染完毕之后,workInProgerss树再次变成current树。

    项目相关题

    关于模块化

    首先模块化的目的是将程序划分为一个个小的结构。在这些结构中编写自己的逻辑代码,有自己的作用域,不会影响到其他的结构。同时这些结构可以将自己希望暴露的函数、变量、对象等导出给其他结构使用,也可通过某种方式,将另外结构中的函数、变量、对象等导入使用。

    微前端

    随着项目的开发,会出现一个前端项目模块巨多的情况,不利于开发和维护。微前端就能帮助我们解决这个问题,帮我们实现了前端复杂项目的解耦,同时能做到跨团队和跨部门协同开发。 对于微前端,它与技术栈无关(主框架不限制介入应用的技术栈,微应用具有完全的自主权),各个微应用间仓库独立,每个微应用之间状态隔离,运行时状态不共享。 常见的微前端实现方案:

    • 基于iframe的完全隔离,iframe是浏览器自带的功能,使用简单,隔离完美,不过它无法保持路由状态,页面一刷新状态就丢失,同时iframe中的状态无法突破对应的应用,同时整个应用是全量加载,速度慢。
    • 基于single-spa路由劫持的方案。qiankun就是基于这种方案实现的,通过对single-spa做一层封装,根据执行环境的修改,来解析微应用的资源,实现了JS沙箱、样式隔离等特性。
    • 借鉴WebComponent思想的micro-app,通过CustomElement结合自定义的ShadowDom,将微前端封装成一个类Web Component组件。
    前端低代码的认识

    低代码平台一般提供一个可视化的编辑页面,供知晓低代码开发规则的人员进行编程,是一种声明式编程。 常见的低代码工作流程如图:

    image.png

    低代码的好处:

    • 门槛低,所见即所得,上手容易
    • 基于现成组件库开发,开发速度快

    低代码的缺点:

    • 灵活性差,只适合某些特定领域
    • 调试困难,对使用者来说是个黑盒
    • 对运行环境有一定要求,兼容性不好,低代码开发的兼容性完全取决于低代码平台的支持

     给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

    地址:web前端面试题库

    前端权限设计思路

    项目中,尤其是管理后台必不可少的一个环节就是权限设计。通常一个系统下的不同用户会对应不同的角色,不同角色会对应不同的组织。在进入到管理里后台的时候会去请求对应的权限接口,这个接口里有和后台约定好的权限标识内容,如果权限管理不是很复杂,可以将当前用户的所有权限标识一次性返回,前端进行一个持久化存储,之后根据规则处理即可。如果是个极为复杂的权限管理,甚至存在不同操作导致同一用户对应后续流程权限变化的情况,这里就建议用户首次登录管理后台时,获取的是最高一层权限,即可以看到的页面权限,之后在用户每次做了不同操作,切换页面的时候,根据约定好的规则,在页面路由切换的时候去请求下一个页面对应的权限(可以精确到每个交互动作),这样能更加精确的管理权限。

    taro是如何将react代码转换成对应的小程序代码或其他平台代码

    平时使用React JSX进行开发时,要知道React将其核心功能分成了三部分:React Core(负责处理核心API、与终端平台和渲染解耦,提供了createElement、createClass、Component、Children等方法)、React Renderer(渲染器,定义了React Tree如何构建以接轨不同平台,有React-dom、React-Natvie等)、React Reconciler(调和器,负责diff算法,接驳patch行为。为渲染器提供基础计算能力,主要有16版本之前的Stack Reconciler和16及其之后的Fiber Reconciler)。React团队将Reconciler作为一个单独的包发布,任何平台的渲染器函数只要在HostConfig(宿主配置)内置基本方法,就可以构造自己的渲染逻辑。有了react-reconciler的支持。Taro团队就是提供了taro-react(实现了HostConfig)包来连接react-reconciler和taro-runtime。开发者写的React代码,Taro通过CLI将代码进行webpack打包,taro实现了一套完整的DOM和BOM API在各个平台的适配,打包完之后,就可以将程序渲染到对应的平台上。 核心就在于对输入的源代码的语法分析,语法树构建,随后对语法树进行转换操作再解析生成目标代码的过程。

    token可以放在cookie里吗?

    当被问这个问题的时候,第一时间要想到安全问题。通常回答不可以,因为存在CSRF(跨站请求伪造)风险,攻击者可以冒用Cookie中的信息来发送恶意请求。解决CSRF问题,可以设置同源检测(Origin和Referer认证),也可以设置Samesite为Strict。最好嘛,就是不把token放在cookie里咯。

    前端埋点的实现,说说看思路

    对于埋点方案:一般分为手动埋点(侵入性强,和业务强关联,用于需要精确搜集并分析数据,不过该方式耗时耗力,且容易出现误差,后续要调整,成本较高)、可视化埋点(提供一个可视化的埋点控制台,只能在可视化平台已支持的页面进行埋点)、无埋点(就是全埋点,监控页面发生的一切行为,优点是前端只需要处理一次埋点脚本,不过数据量过大会产生大量的脏数据,需要后端进行数据清洗)。

    埋点通常传采用img方式来上传,首先所有浏览器都支持Image对象,并且记录的过程很少出错,同时不存在跨域问题,请求Image也不会阻塞页面的渲染。建议使用1*1像素的GIF,其体积小。

    现在的浏览器如果支持Navigator.sendBeacon(url, data)方法,优先使用该方法来实现,它的主要作用就是用于统计数据发送到web服务器。当然如果不支持的话就继续使用图片的方式来上传数据。

    说说封装组件的思路

    要考虑组件的灵活性、易用性、复用性。 常见的封装思路是,对于视图层面,如相似度高的视图,进行一个封装,提供部分参数方便使用者修改。对于业务复用度较高的,提取出业务组件。

    性能优化题

    什么情况下会重绘和回流,常见的改善方案

    浏览器请求到对应页面资源的时候,会将HTML解析成DOM,把CSS解析成CSSDOM,然后将DOM和CSSDOM合并就产生了Render Tree。在有了渲染树之后,浏览器会根据流式布局模型来计算它们在页面上的大小和位置,最后将节点绘制在页面上。

    那么当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变,浏览器就会重新渲染页面,这个就是浏览器的回流。常见的回流操作有:页面的首次渲染、浏览器窗口尺寸改变、部分元素尺寸或位置变化、添加或删除可见的DOM、激活伪类、查询某些属性或调用方法(各种宽高的获取,滚动方法的执行等)。

    当页面中的元素样式的改变不影响它在文档流的位置时(如color、background-color等),浏览器对应元素的样式,这个就是重绘。

    可见:回流必将导致重绘,重绘不一定会引起回流。回流比重绘的代价更高

    常见改善方案:

    • 在进行频繁操作的时候,使用防抖和节流来控制调用频率。
    • 避免频繁操作DOM,可以利用DocumentFragment,来进行对应的DOM操作,将最后的结果添加到文档中。
    • 灵活使用display: none属性,操作结束后将其显示出来,因为display的属性为none的元素上进行的DOM操作不会引发回流和重绘。
    • 获取各种会引起重绘/回流的属性,尽量将其缓存起来,不要频繁的去获取。
    • 对复杂动画采用绝对定位,使其脱离文档流,否则它会频繁的引起父元素及其后续元素的回流。
    一次请求大量数据怎么优化,数据多导致渲染慢怎么优化

    个人觉得这就是个伪命题,首先后端就不该一次把大量数据返回前端,但是会这么问,那么我们作为面试的就老老实实回答呗。

    首先大量数据的接收,那么肯定是用异步的方式进行接收,对数据进行一个分片处理,可以拆分成一个个的小单元数据,通过自定义的属性进行关联。这样数据分片完成。接下来渲染的话,由于是大量数据,如果是长列表的话,这里就可以使用虚拟列表(当前页面需要渲染的数据拿到进行渲染,然后对前面一段范围及后面一段范围,监听对应的滚动数据来切换需要渲染的数据,这样始终要渲染的就是三部分)。当然还有别的渲染情况,比如echarts图标大量点位数据优化等。

    手写题

    模拟链表结构

    主要思路就是要时刻清楚对应Node的next和prev的指向,并利用while循环去做对应的增删改查操作。

    image.png

    1. class Node {
    2. constructor(data) {
    3. this.data = data; // 节点数据
    4. this.next = null; // 指向下一个节点
    5. this.prev = null; // 指向前一个节点
    6. }
    7. }
    8. class LinkedList {
    9. constructor() {
    10. this.head = null; // 链表头
    11. this.tail = null; // 链表尾
    12. }
    13. // 在链表尾部添加新节点
    14. add(item) {
    15. let node = new Node(item);
    16. if (!this.head) {
    17. this.head = node;
    18. this.tail = node;
    19. } else {
    20. node.prev = this.tail;
    21. this.tail.next = node;
    22. this.tail = node;
    23. }
    24. }
    25. // 链表指定位置添加新节点
    26. addAt(index, item) {
    27. let current = this.head;
    28. let counter = 1;
    29. let node = new Node(item);
    30. if (index === 0) {
    31. this.head.prev = node;
    32. node.next = this.head;
    33. this.head = node;
    34. } else {
    35. while (current) {
    36. current = current.next;
    37. if (counter === index) {
    38. node.prev = current.prev;
    39. current.prev.next = node;
    40. node.next = current;
    41. current.prev = node;
    42. }
    43. counter++;
    44. }
    45. }
    46. }
    47. remove(item) {
    48. let current = this.head;
    49. while (current) {
    50. if (current.data === item) {
    51. if (current == this.head && current == this.tail) {
    52. this.head = null;
    53. this.tail = null;
    54. } else if (current == this.head) {
    55. this.head = this.head.next;
    56. this.head.prev = null;
    57. } else if (current == this.tail) {
    58. this.tail = this.tail.prev;
    59. this.tail.next = null;
    60. } else {
    61. current.prev.next = current.next;
    62. current.next.prev = current.prev;
    63. }
    64. }
    65. current = current.next;
    66. }
    67. }
    68. removeAt(index) {
    69. let current = this.head;
    70. let counter = 1;
    71. if (index === 0) {
    72. this.head = this.head.next;
    73. this.head.prev = null;
    74. } else {
    75. while (current) {
    76. current = current.next;
    77. if (current == this.tail) {
    78. this.tail = this.tail.prev;
    79. this.tail.next = null;
    80. } else if (counter === index) {
    81. current.prev.next = current.next;
    82. current.next.prev = current.prev;
    83. break;
    84. }
    85. counter++;
    86. }
    87. }
    88. }
    89. reverse() {
    90. let current = this.head;
    91. let prev = null;
    92. while (current) {
    93. let next = current.next;
    94. current.next = prev;
    95. current.prev = next;
    96. prev = current;
    97. current = next;
    98. }
    99. this.tail = this.head;
    100. this.head = prev;
    101. }
    102. swap(index1, index2) {
    103. if (index1 > index2) {
    104. return this.swap(index2, index1);
    105. }
    106. let current = this.head;
    107. let counter = 0;
    108. let firstNode;
    109. while (current !== null) {
    110. if (counter === index1) {
    111. firstNode = current;
    112. } else if (counter === index2) {
    113. let temp = current.data;
    114. current.data = firstNode.data;
    115. firstNode.data = temp;
    116. }
    117. current = current.next;
    118. counter++;
    119. }
    120. return true;
    121. }
    122. traverse(fn) {
    123. let current = this.head;
    124. while (current !== null) {
    125. fn(current);
    126. current = current.next;
    127. }
    128. return true;
    129. }
    130. find(item) {
    131. let current = this.head;
    132. let counter = 0;
    133. while (current) {
    134. if (current.data == item) {
    135. return counter;
    136. }
    137. current = current.next;
    138. counter++;
    139. }
    140. return false;
    141. }
    142. isEmpty() {
    143. return this.length() < 1;
    144. }
    145. length() {
    146. let current = this.head;
    147. let counter = 0;
    148. while (current !== null) {
    149. counter++;
    150. current = current.next;
    151. }
    152. return counter;
    153. }
    154. }
    手写一个深拷贝
    1. // 手写一个深拷贝
    2. function deepCloneextends Array | any>(obj: T): T {
    3. if (typeof obj !== "object" || obj === null) return obj;
    4. const result: T = obj instanceof Array ? ([] as T) : ({} as T);
    5. for (const key in obj) {
    6. if (obj.hasOwnProperty(key)) {
    7. result[key] = deepClone(obj[key]);
    8. }
    9. }
    10. return result;
    11. }
    12. const obj = {
    13. a: 1,
    14. b: {
    15. bb: "hh",
    16. },
    17. c() {
    18. console.log("cc");
    19. },
    20. };
    21. const cloneObj = deepClone(obj);
    22. obj.a = 999;
    23. console.log("cloneObj :>> ", cloneObj);
    24. console.log("obj :>> ", obj);
    25. // cloneObj :>> { a: 1, b: { bb: 'hh' }, c: [Function: c] }
    26. // obj :>> { a: 999, b: { bb: 'hh' }, c: [Function: c] }
    27. const arr: Array = [1, 2, 3, "6"];
    28. const copyArr = deepClone(arr);
    29. arr[3] = 4;
    30. console.log("arr | copyArr :>> ", arr, copyArr); // arr | copyArr :>> [ 1, 2, 3, 4 ] [ 1, 2, 3, '6' ]
    手写Promise
    1. const PROMISE_STATUS_PENDING = "pending";
    2. const PROMISE_STATUS_FULFILLED = "fulfilled";
    3. const PROMISE_STATUS_REJECTED = "rejected";
    4. // help fun
    5. function execFunctionWithCatchError(execFun, value, resolve, reject) {
    6. try {
    7. const result = execFun(value);
    8. resolve(result);
    9. } catch (error) {
    10. reject(error);
    11. }
    12. }
    13. class MyPromise {
    14. constructor(executor) {
    15. this.status = PROMISE_STATUS_PENDING; // 记录promise状态
    16. this.value = undefined; // resolve返回值
    17. this.reason = undefined; // reject返回值
    18. this.onFulfilledFns = []; // 存放成功回调
    19. this.onRejectedFns = []; // 存放失败回调
    20. const resolve = value => {
    21. if (this.status === PROMISE_STATUS_PENDING) {
    22. queueMicrotask(() => {
    23. if (this.status !== PROMISE_STATUS_PENDING) return;
    24. this.status = PROMISE_STATUS_FULFILLED;
    25. this.value = value;
    26. this.onFulfilledFns.forEach(fn => {
    27. fn(this.value);
    28. });
    29. });
    30. }
    31. };
    32. const reject = reason => {
    33. if (this.status === PROMISE_STATUS_PENDING) {
    34. queueMicrotask(() => {
    35. if (this.status !== PROMISE_STATUS_PENDING) return;
    36. this.status = PROMISE_STATUS_REJECTED;
    37. this.reason = reason;
    38. this.onRejectedFns.forEach(fn => {
    39. fn(this.reason);
    40. });
    41. });
    42. }
    43. };
    44. try {
    45. executor(resolve, reject);
    46. } catch (error) {
    47. reject(error);
    48. }
    49. }
    50. then(onFulfilled, onRejected) {
    51. onFulfilled =
    52. onFulfilled ||
    53. (value => {
    54. return value;
    55. });
    56. onRejected =
    57. onRejected ||
    58. (err => {
    59. throw err;
    60. });
    61. return new MyPromise((resolve, reject) => {
    62. // 1、 when operate then, status have confirmed
    63. if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
    64. execFunctionWithCatchError(onFulfilled, this.value, resolve, reject);
    65. }
    66. if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
    67. execFunctionWithCatchError(onRejected, this.reason, resolve, reject);
    68. }
    69. if (this.status === PROMISE_STATUS_PENDING) {
    70. // this.onFulfilledFns.push(onFulfilled);
    71. if (onFulfilled) {
    72. this.onFulfilledFns.push(() => {
    73. execFunctionWithCatchError(onFulfilled, this.value, resolve, reject);
    74. });
    75. }
    76. // this.onRejectedFns.push(onRejected);
    77. if (onRejected) {
    78. this.onRejectedFns.push(() => {
    79. execFunctionWithCatchError(onRejected, this.reason, resolve, reject);
    80. });
    81. }
    82. }
    83. });
    84. }
    85. catch(onRejected) {
    86. return this.then(undefined, onRejected);
    87. }
    88. finally(onFinally) {
    89. this.then(
    90. () => {
    91. onFinally();
    92. },
    93. () => {
    94. onFinally();
    95. }
    96. );
    97. }
    98. static resolve(value) {
    99. return new MyPromise(resolve => resolve(value));
    100. }
    101. static reject(reason) {
    102. return new MyPromise((resolve, reject) => reject(reason));
    103. }
    104. static all(promises) {
    105. return new MyPromise((resolve, reject) => {
    106. const values = [];
    107. promises.forEach(promise => {
    108. promise.then(
    109. res => {
    110. values.push(res);
    111. if (values.length === promises.length) {
    112. resolve(values);
    113. }
    114. },
    115. err => {
    116. reject(err);
    117. }
    118. );
    119. });
    120. });
    121. }
    122. static allSettled(promises) {
    123. return new MyPromise(resolve => {
    124. const results = [];
    125. promises.forEach(promise => {
    126. promise.then(
    127. res => {
    128. results.push({ status: PROMISE_STATUS_FULFILLED, value: res });
    129. if (results.length === promises.length) {
    130. resolve(results);
    131. }
    132. },
    133. err => {
    134. results.push({ status: PROMISE_STATUS_REJECTED, value: err });
    135. if (results.length === promises.length) {
    136. resolve(results);
    137. }
    138. }
    139. );
    140. });
    141. });
    142. }
    143. static race(promises) {
    144. return new MyPromise((resolve, reject) => {
    145. promises.forEach(promise => {
    146. promise.then(
    147. res => {
    148. resolve(res);
    149. },
    150. err => {
    151. reject(err);
    152. }
    153. );
    154. });
    155. });
    156. }
    157. static any(promises) {
    158. return new MyPromise((resolve, reject) => {
    159. const reasons = [];
    160. promises.forEach(promise => {
    161. promise.then(
    162. res => {
    163. resolve(res);
    164. },
    165. err => {
    166. reasons.push(err);
    167. if (reasons.length === promise.length) {
    168. // reject(new AggreagateError(reasons));
    169. reject(reasons);
    170. }
    171. }
    172. );
    173. });
    174. });
    175. }
    176. }
    177. const p1 = new MyPromise((resolve, reject) => {
    178. setTimeout(() => {
    179. console.log("--- 1 ---");
    180. resolve(111);
    181. });
    182. }).then(res => {
    183. console.log("p1 res :>> ", res);
    184. });
    185. const p2 = new MyPromise((resolve, reject) => {
    186. console.log("--- 2 ---");
    187. resolve(222);
    188. });
    189. const p3 = new MyPromise((resolve, reject) => {
    190. console.log("--- 3 ---");
    191. resolve(333);
    192. });
    193. const p4 = new MyPromise((resolve, reject) => {
    194. console.log("--- 4 ---");
    195. reject(444);
    196. });
    197. MyPromise.all([p2, p3]).then(res => {
    198. console.log("p2&p3 res :>> ", res);
    199. });
    200. MyPromise.all([p2, p4])
    201. .then(res => {
    202. console.log("p2&p4 res :>> ", res);
    203. })
    204. .catch(err => {
    205. console.log("err :>> ", err);
    206. });
    207. // --- 2 ---
    208. // --- 3 ---
    209. // --- 4 ---
    210. // p2&p3 res :>> [ 222, 333 ]
    211. // err :>> 444
    212. // --- 1 ---
    213. // p1 res :>> 111
    手写防抖和节流函数
    1. function debounce(fn: Function, delay: number) {
    2. let timer: any = null;
    3. return function () {
    4. if (timer) {
    5. clearTimeout(timer);
    6. }
    7. timer = setTimeout(() => {
    8. fn.apply(this, arguments);
    9. timer = null;
    10. }, delay);
    11. };
    12. }
    13. function throttle(fn: Function, delay: number) {
    14. let timer: any = null;
    15. return function () {
    16. if (timer) return;
    17. timer = setTimeout(() => {
    18. fn.apply(this, arguments);
    19. timer = null;
    20. }, delay);
    21. };
    22. }
    手写快速排序
    1. function quickSort(arr: number[], startIndex = 0): number[] {
    2. if (arr.length <= 1) return arr;
    3. const right: number[] = [],
    4. left: number[] = [],
    5. startNum = arr.splice(startIndex, 1)[0];
    6. for (let i = 0; i < arr.length; i++) {
    7. if (arr[i] < startNum) {
    8. left.push(arr[i]);
    9. } else {
    10. right.push(arr[i]);
    11. }
    12. }
    13. return [...quickSort(left), startNum, ...quickSort(right)];
    14. }
    输入为两个一维数组,将这两个数组合并,去重,不要求排序,返回一维数组
    1. function dealArr(arr1: any[], arr2: any[]): any[] {
    2. return Array.from(new Set([...arr1.flat(), ...arr2.flat()]));
    3. }
    4. const arr1 = ["a", 1, 2, 3, ["b", "c", 5, 6]];
    5. const arr2 = [1, 2, 4, "d", ["e", "f", "5", 6, 7]];
    6. console.log("dealArr(arr1, arr2 ); :>> ", dealArr(arr1, arr2)); // dealArr(arr1, arr2 ); :>> [ 'a', 1, 2, 3,'b', 'c', 5,6, 4, 'd', 'e', 'f','5', 7]
    编写函数convert(money) ,传入金额,将金额转换为千分位表示法。ex:-87654.3 => -87,654.3

    思路:判断是否是负数,判断是否有小数点,将整数部分进行处理。

    1. function convert(money: number): string {
    2. let result: string[] = []; // 用于存放整数部分
    3. let negativeFlag: string = ""; // 是否要负号
    4. let tail: string = ""; // 用于存放小数点后面部分
    5. let arr: string[] = [...String(money)];
    6. // 判断是否是负数
    7. if (arr[0] === "-") {
    8. negativeFlag = "-";
    9. arr.shift();
    10. }
    11. // 判断是否存在小数点
    12. const dotIndex: number = arr.indexOf(".");
    13. if (dotIndex !== -1) {
    14. tail = arr.splice(dotIndex, arr.length - dotIndex).join("");
    15. }
    16. // 处理整数部分加上千分位
    17. const reverseArray: string[] = arr.reverse();
    18. for (let i = 0; i < reverseArray.length; i++) {
    19. if ((i + 1) % 3 === 0 && i + 1 < reverseArray.length) {
    20. result[i] = "," + reverseArray[i];
    21. } else {
    22. result[i] = reverseArray[i];
    23. }
    24. }
    25. return negativeFlag + result.reverse().join("") + tail;
    26. }

    总结

    一个渣渣前端在面试过程中遇到的题目😝。

     给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

    地址:web前端面试题库

  • 相关阅读:
    “它经济”盛行,宠物食品行业如何做好口碑营销
    vue一些知识补充
    MySQL5.7主从复制教程
    R语言Sys.Date函数获取当前日期、获取指定日期自1970年1月1日以来经过的天数(Julian Date)
    使用element-plus 完成密码再次验证(修改密码)
    根文件系统
    【产线故障】线上接口请求过慢如何排查?
    造轮子之消息实时推送
    Alkyne-PEG-CHO,炔基PEG醛基
    Python 算法高级篇:贪心算法的原理与应用
  • 原文地址:https://blog.csdn.net/weixin_42981560/article/details/133933022