likes
comments
collection
share

北京七年前端大专学历,面经系列更新之——找工作竟如此坎坷¿?近N个月面试复盘(二)(附总结答案),进来坐坐学习下?持续更新中

作者站长头像
站长
· 阅读数 5

新人求关注,求内推,也欢迎浏览我的第一篇面经。交流讨论请去第一篇面经文末有入口

  自从上次发了篇面经,也获得了很多掘友们的鼓励,我也没那么低落了,在此向他们表示感谢。


我的历程

  7 月中旬,从某著名程序员在线学习平台领了大礼包毕业后,一直没有马上找工作的想法。

  休息了段时间,也做了很多事情,但人毕竟还是要工作的嘛,还年轻,没到退休呢。就国庆节前后开始准备刷题,看基础,学习,投简历,目前到发文的今天2023年12月1日我也算找了有快两个月了。

  算上之前面了两家加上这次面经的四家,一共才面了六家。面试机会不多,招聘软件上已读不回或者不读的更多一点,最多的还是外包,目前行情外包最多给20K

  外包的 HR,我觉着态度比非外包的要好一些。但是外包也各有各的坑吧,大伙们注意分辨,还有五险一金大概率按最低基数的最低标准交。真要去外包的同胞们,我总结了个需要问清楚的问题,不全面,可以做个参考。

  薪资最高可以给到多少?试用期是否不打折?   五险一金按什么基数交?百分比多少?   是长期项目还是短期项目?项目结束是裁员还是其他?   甲方带队还是项目组进场?   工作时间是几点到几点?是否经常加班?是否996呢?

  还有一部分不知道跟 HR 谈待遇的时候该问些啥,之前在大佬那看到的再加我总结的一些.

  具体的工资(也就是合同上签订的工资),不要杂七杂八什么绩效加起来的那种,如果有年终奖,最好写在合同里   五险一金缴纳的比例,还有基数(一[da]部分公司不按你实际工资基数来交五险一金,这个需要问清楚)   加班是否有加班工资或者调休   是否是 996,我个人很不推荐 996 的公司   加薪升职的情况   其他各种福利,比如餐补、房补、交通补、节假日福利、另外的保险等等

注意:如果加班多的,大小周的一定要考虑好,这种的就得算时薪了。


PS:建了个微信讨论群,如果你也最近在面试,或者有些心态崩了,或者有什么想探讨的可以加微信群,入口在文末。

PS2:最近考取了业余无线电操作证,沉迷业余无线电,有对业余无线电感兴趣的可以加我微信

PS3:我微信也在文末,有想加的可以加我。

PS4:如果二维码过期或者人满,就加我微信,拉你,二维码在文末。

PS5:最后可以在文末加我微信拉你进群,也可以顺便关注下我 N 年前注册的公众号

另:求内推机会,给个孩子个机会吧

求内推机会,给个机会吧,我打招呼刷岗位都没空刷八股文了。


灵X(现场面,11 月 16 日)

现场面,没有录音,记不太清楚都问啥了

一面

1. Local storage 和 Session storage 的区别,以及跨页面之后是否可以共享

localStoragesessionStorage 都是Web Storage API提供的两种客户端存储数据的方式。它们之间有一些重要的区别:

区别:

  1. 数据生命周期:
  • localStorage:数据永久存储,除非用户清除浏览器缓存或通过JavaScript代码删除。
  • sessionStorage:数据仅在当前会话期间存储。关闭浏览器标签页或窗口后,数据将被清除。
  1. 作用域:
  • localStorage:数据在相同域名下的不同页面和标签页之间共享。
  • sessionStorage:数据仅在同一标签页或窗口中共享,不同标签页或窗口之间的数据不共享。
  1. 大小限制:
  • localStorage:存储容量通常比sessionStorage更大(通常为5MB),但具体大小也取决于浏览器。
  • sessionStorage:存储容量通常较小(通常为5MB),同样具体大小取决于浏览器。

是否可以跨页面共享:

  • **localStorage**** 跨页面共享:** 是的,localStorage 的数据在同一域名下的不同页面和标签页之间共享。这使得它成为在应用程序中持久存储数据的一种有效方式。
  • **sessionStorage**** 跨页面共享:** 不是的,sessionStorage 的数据仅在同一标签页或窗口中共享。如果在一个标签页中设置了sessionStorage的值,其他标签页无法直接访问这些值。

总体而言,选择使用localStorage还是sessionStorage取决于你的需求。如果需要在不同会话期间共享数据,使用localStorage;如果只需要在当前会话中共享数据,使用sessionStorage

2. 封装一个自定义表单模块,有什么难点还有需要注意的点

封装自定义表单模块是一个常见但也有一些难点需要注意的任务。以下是一些建议以及可能涉及的一些难点:

2.1. 难点和注意点:

  1. 表单元素的多样性:
  • 表单可能包含各种不同类型的输入元素(文本框、复选框、下拉框等),需要考虑如何统一管理和处理这些不同类型的元素。
  1. 表单校验:
  • 实现表单的校验逻辑是一个挑战。需要考虑如何定义校验规则、在用户输入时进行实时校验、以及在提交时进行整体校验。
  1. 状态管理:
  • 管理表单的状态,例如表单是否已被修改、是否处于提交中等。需要确保表单的状态能够被正确地同步和更新。
  1. 表单数据管理:
  • 如何统一管理和获取表单数据,以便于提交或其他操作。考虑使用响应式数据或者状态管理库来管理表单数据。
  1. 动态表单项:
  • 如果表单项是动态生成的,例如根据后端接口动态生成表单,需要考虑如何处理这种动态性。
  1. 自定义样式:
  • 提供灵活的样式定制选项,使用户能够根据自己的设计需求调整表单样式。
  1. 国际化:
  • 如果应用需要国际化支持,需要考虑如何处理表单中的文本、提示信息等的国际化。
  1. 性能优化:
  • 对于大型表单,需要考虑性能优化,例如懒加载、懒校验等策略,以确保用户体验。
  1. 事件处理:
  • 如何处理用户与表单交互的各种事件,例如输入、点击、提交等。需要确保事件处理的一致性和可扩展性。

2.2. 建议:

  1. 组件化设计:
  • 将表单模块拆分为独立的组件,每个组件负责管理特定类型的表单元素或处理特定的功能,以提高代码的可维护性和可复用性。
  1. 清晰的 API 设计:
  • 提供清晰、简单的 API 接口,使用户能够方便地使用表单模块。良好的API设计有助于提高模块的易用性。
  1. 文档和示例:
  • 提供详细的文档和示例,帮助用户理解如何正确使用和配置表单模块。
  1. 测试:
  • 编写充分的单元测试和集成测试,确保表单模块的稳定性和可靠性。
  1. 版本管理:
  • 对于开源的表单模块,需要考虑版本管理,及时发布新版本以修复bug、添加新功能或适应新的Vue版本。
  1. 社区支持:
  • 如果是一个开源项目,建议建立一个社区,接收用户反馈、解答问题,以及共同推动表单模块的发展。

在封装自定义表单模块时,不同项目的需求可能会有所不同,因此需要灵活地调整和优化设计。仔细考虑上述难点和建议,有助于构建出更加强大、易用和可维护的表单模块。

3. 如果让你重构一个前端项目,很老了,里边有 jQuery,Vue2,Vue3,以及一些 jsp、.net 这种未前后端分离的项目,你感觉难点在哪?

我回答,难点在于老项目里业务的梳理,业务才是最主要的,也是最难搞的,老业务具体的表现,以及想要的效果,肯定是最难梳理了,再加上年久,文档什么的肯定也遗失了。

技术方面是其次了,因为重构,肯定全推翻重写一遍,或者增量处理,切香肠一点点逐步替换。

回答完老哥似乎不满意,问我技术上,我说我都一点也不了解这个项目,我能出啥具体的方案啊,我只能整体的探讨一下。

老哥又说了,你不了解就问啊,学会自主提问会让面试官有很好的感受,我说,哦~,学习了,学习了,那咱们是个什么项目啊?

老哥说,这就对了,问才对,行,下一个问题吧!

4. 剩余的想不起来了,其实问了好多问题,忘了耶。最后我提问的时候,才知道是想招前端负责人

结果

  说是下周 23 号给我结果,刚开始招聘,还得招招人比对比对。

  我 23 号问,人家老哥说崴脚了,这周招聘暂停了,暂时给不了结果,你要是着急就接其他 offer 或者下周再给消息,我就说您还是养伤要紧吧,早日康复。

  今天已经是下周了,我再问问,八成也是凉了。

  以后现场面也不能忘了录音。

聚X外包(现场,11 月 16 号)

一面

1. 其实就记着问了一些 Vue 框架的使用以及 Vue2、3 的区别,还有 VueX 的使用(背 api 的那种),其他想不起来了

结果:没过

妙X科技(线上,11 月 23 号)

一面(线上,主要聊简历里的项目)

1. 自我介绍

2. 离职原因

  裁员,公司收支不平衡。因为 21 年度融资后,开始扩张, 22 年中开始就收支不平衡,每个月亏钱。

  从 22 年开始就启动了多波次的裁员,我部门上一波是在 22 年的 年底,离我这次间隔了半年多,研发部的规模被裁到其实已经很小了,这次呢,也是趁这个机会,选择了离开。

3. 主要问项目情况

3.1. 项目一

  1. 简历里第一个项目的历程,问我是不是一开始就是负责人,有没有好几个人一起协同开发的时候,我说我来了已经上线了,我是负责维护迭代项目以及新需求的开发。
  2. 看你简历老项目升级了 vite,具体的情况。我就回答了为什么有必要升级 vite,以及升级的必要性还有生产打包还是 webpack。
  3. 拆分了作业批改系统,子项目如何跟主项目进行基础数据交互。

3.2. 项目二

   我简历里第二个写的是浏览器插件。

  问我是不是你独立完成的,为什么开发浏览器插件,是你们业务线需要,还是你学习之后,产品觉着可以,才采纳了你做这个的意见呢?

  是不是你自己主动提的,是不是需要频繁维护这个插件项目。

3.3. Q: Hybrid 开发情况,是怎么嵌套进去的。

  A: webview 跳链接。

3.4. Q: Vue3,做的项目不多是吧,2 和 3 的差异。源码了解多吗。

3.5. Q: 上一家公司看你还是做维护啊,是不是闲暇时间多啊,闲暇时候会去学习东西扩展知识面吗?

3.6. Q: 最近看的技术书籍你能说出他的名字吗?举例子,印象深刻的,让你打开自己见识见解的观点啊一些技术栈博客,举一些例子,可以说 vue 相关或不相关。

3.7. Q: 有没有看到一些点,比方说自己工作没有遇到,别人分享出来后感觉非常有益,我举个例子啊,比如说性能优化,SEO 啊平常没用的但是也助力我们成为技术专家的这种。举一些例子这种核心知识点,最近看到的或者了解到的。

3.8. Q: Vue 对 SEO 有用的成型的框架你知道吗,有没有尝试过。

  A: Nuxt,无

3.9. Q: 能够提升首屏加载的方案

  A: 在Vue中,为了提升首屏加载性能,可以采用一些优化方案。以下是一些建议:

  1. 代码拆分 (Code Splitting):
  • 使用Webpack等构建工具的代码拆分功能,将应用分割为多个小块(chunk)。这样,只有在需要时才加载相应的块,减少初始加载时间。
  1. 懒加载 (Lazy Loading):
  • 对于不是首屏必须的组件或路由,可以使用懒加载,将它们按需加载。Vue的路由懒加载可以通过动态导入组件实现。
  1. 按需导入第三方库:
  • 如果使用了一些较大的第三方库,可以考虑按需导入(Tree-shaking)。例如,使用import { someFunction } from 'some-library';,而不是直接导入整个库。
  1. 图片优化:
  • 对于图片资源,使用适当的格式和大小,避免不必要的高分辨率图像。可以使用工具如ImageMin来压缩图片。
  1. 服务端渲染 (SSR):
  • 使用Vue的服务端渲染(SSR)可以加速首屏加载。SSR可以在服务器端渲染初始HTML,并在客户端激活Vue,提供更快的首屏加载时间。
  1. Vue-Router的预加载 (Preload):
  • 对于Vue-Router,可以使用<router-link>prefetch属性来预加载一些异步组件,以提前加载相关资源。
  1. 使用CDN:
  • 将一些常用的第三方库,如Vue、Vue Router等,通过CDN(内容分发网络)加载,减少服务器负担,提高加载速度。
  1. SSG (静态站点生成):
  • 对于不经常变化的内容,可以使用静态站点生成(SSG)的方式,将Vue应用预渲染成静态HTML文件。这可以提高加载速度并降低服务器压力。
  1. 缓存策略:
  • 合理使用浏览器缓存策略,确保不经常变化的资源被缓存,从而减少请求时间。
  1. 组件优化:
  • 在组件中使用keep-alive来缓存组件状态,避免重新渲染。优化组件的mounted生命周期钩子,确保只有必要的操作在首次渲染时执行。

综合使用以上策略,可以有效提升Vue应用的首屏加载性能。具体的优化策略应根据项目的实际情况和需求进行调整。

3.10. Q: 在上上家公司干了些什么,都是一些小程序还有后台产品?

3.11. Q: 团队最多带多少人

  A: 4 人

3.12. Q: 会去经常代码审查,代码规范检查,进度督促等?有没有辅助工具帮助做规范、代码审查之类的。

3.13. Q: 项目的时间排期是你自己定,还是按组员说的来

  A: 这个要综合看待,每个人的开发能力效率还有技术熟练度都不同,需要结合他们自身情况以及项目情况来做综合考量。

3.14. Q: 是否会参与产品验收,还是只交给产品和测试。

3.15. Q: 对还原设计稿的把控,比如做完第一个版本,你的 UI 预期跟设计能达到多少的契合度。

  A: 95 %以上,就说上家公司吧,我出完之后,UI 找我修改是最少的,除非是她看哪儿不顺眼了,想改改,其他都符合设计预期。设计稿完美还原。

3.16. Q: 有写一些总结文档吗,是为了找工作写的吗?

3.17. Q: 7 月份离职,是吗?

  A: 谈了谈离职之后做的事情,思考职业规划啊,办之前没有机会办的事儿啊之类的,增驾 D 本,考取业余无线电操作证书

结果

  可能是嫌我上一家公司里做的项目不行?  这家公司我 10 月底投的简历,快 11 月底了才让我面试。  说是又启动了第二波招人,刚开始招,一面先过项目,等人都过一遍,觉着我合适了再跟我联系。  最后是前两天我问的才给我消息。  说是没有通过,不懂,黑人问号脸,不知道我错在哪里。

美X外包(线上,11 月 23 号)

一面(主要还是围绕简历以及基础知识来聊的)

1. 自我介绍

2. Q:为什么要开发浏览器插件,他有什么作用?

  A:为主站导入流量,方便用户学习,也是一个工具

3. Q:怎么在浏览器插件里拿到用户信息的?

  A: 直接跳转账户中心登录,登录后自然会留下 cookie,之后可以拿到登录态,等等等

4. Q:怎样区分插件来的流量以及主站自身流量,是否有做自己的埋点系统

A:使用第三方埋点系统

5. Q:上家公司目前前端多少人

6. Q:有做过 Vue3 项目是吧

7. Q:老项目拆分后,使用不同 Vue 版本的子项目该如何共存,以及与主项目共存

8. Q:箭头函数和普通函数有什么区别

JavaScript中,箭头函数(Arrow Function)和普通函数(Regular Function)有一些重要的区别。以下是它们之间的主要区别:

  1. 语法形式:
  • 箭头函数: 使用箭头(=>)来定义函数,通常有简洁的语法形式。箭头函数没有自己的 thisargumentssupernew.target,它们继承这些值来自执行上下文。
const arrowFunction = (param1, param2) => expression;
  • 普通函数: 使用关键字 function 来定义函数,可以是函数声明或函数表达式。
function regularFunction(param1, param2) {
  // 函数体
}
  1. this 的值:
  • 箭头函数: 箭头函数没有自己的 this,它继承自包含它的最近的非箭头函数父作用域的 this 值。
  • 普通函数: 函数的 this 值在运行时动态确定,取决于函数如何被调用。在全局作用域中,this 指向全局对象(在浏览器中通常是 window)。在对象方法中,this 指向调用方法的对象。
  1. arguments 对象:
  • 箭头函数: 没有自己的 arguments 对象,但可以使用剩余参数(rest parameters)来达到相似的效果。
  • 普通函数: 有自己的 arguments 对象,它是一个类数组对象,包含传递给函数的所有参数。
  1. 构造函数:
  • 箭头函数: 不能用作构造函数,不能通过 new 关键字调用,否则会抛出错误。
  • 普通函数: 可以用作构造函数,通过 new 关键字调用时会创建一个新对象,并将其绑定到函数的 this 上。
  1. 绑定(Binding):
  • 箭头函数: 不会创建自己的执行上下文,不能通过 call()apply()bind() 改变其 this
  • 普通函数: this 可以通过 call()apply()bind() 方法进行显式绑定。

总体而言,箭头函数通常更适用于简单的、无需使用 thisarguments 的情况,而普通函数则更灵活,可以适应更多的场景,包括构造函数和需要动态绑定 this 的情况。选择使用哪种类型的函数取决于特定的需求和语境。

9. 求 obj.getKey()

const obj = {
    key: 'test',
    getKey: function() {
        console.log(this.key)
    }
}

obj.getKey() // test

在这个例子中,obj.getKey() 是通过对象 obj 调用的,因此 this 将指向调用它的对象,即 obj。所以,console.log(this.key) 将打印 'test'

所以,obj.getKey() 的打印结果将是 'test'

10. 求 fn()

const obj = {
    key: 'test',
    getKey: function() {
        console.log(this.key)
    }
}

const fn = obj.getKey

fn() // undefined

在这个例子中,fn是从 obj.getKey 中提取出来的函数,并且它被独立地调用。由于 JavaScript 中函数的执行上下文与调用方式相关,此时 fn 被作为全局函数调用,因此 this 将指向全局对象(在浏览器环境中通常是 window)。

由于 fn 的执行上下文不再与 obj 对象相关,因此在 fn 函数内部的 this.key 将无法访问到 obj 对象的 key 属性。这将导致在调用 fn()this.keyundefined

所以,console.log(fn()) 的打印结果将是 undefined

11. 接上一题,如果想让 fn()结果打印 test应该怎么做

要确保 console.log(fn()) 打印结果为 'test',你可以在创建 fn 时使用 bind 方法,显式绑定 obj 作为执行上下文。这样,无论在哪里调用 fn,它的 this 将始终指向 obj

const obj = {
  key: 'test',
  getKey: function() {
    console.log(this.key);
  }
};

const fn = obj.getKey.bind(obj);

console.log(fn()); // 打印结果 'test'

使用 bind(obj) 创建了一个新的函数,该函数与 obj.getKey 具有相同的函数体,但在执行时 this 将始终指向 obj。因此,调用 fn() 就会正确地打印 'test'

12. 接上一题,还有其他办法吗

在这个情况下,console.log(fn()) 打印的结果为 undefined,因为在执行 fn() 时,this 的值指向的是全局对象(在浏览器环境中通常是 window)。由于在全局对象中没有名为 key 的属性,因此会输出 undefined

要确保 console.log(fn()) 打印结果为 'test',你可以使用 bind 方法或者箭头函数来显式指定函数执行时的 this 值。以下是两种方式的示例:

  1. 使用 **bind** 方法:
const obj = {
  key: 'test',
  getKey: function() {
    console.log(this.key);
  }
};

const fn = obj.getKey.bind(obj);
console.log(fn()); // 打印结果 'test'
  1. 使用箭头函数:
const obj = {
  key: 'test',
  getKey: function() {
    console.log(this.key);
  }
};

const fn = () => obj.getKey();
console.log(fn()); // 打印结果 'test'

这两种方式都能确保在调用 fn() 时,this 指向 obj,从而打印出正确的结果 'test'

使用 callapply 方法可以在调用函数时显式地指定函数执行时的 this 值。在这个例子中,你可以使用 callapply 来确保 fn() 执行时 this 指向 obj。以下是使用 callapply 的示例:

  1. 使用 **call** 方法:
const obj = {
  key: 'test',
  getKey: function() {
    console.log(this.key);
  }
};

const fn = obj.getKey;
console.log(fn.call(obj)); // 打印结果 'test'
  1. 使用 **apply** 方法:
const obj = {
  key: 'test',
  getKey: function() {
    console.log(this.key);
  }
};

const fn = obj.getKey;
console.log(fn.apply(obj)); // 打印结果 'test'

这两种方法都允许你在调用函数时显式地设置 this 的值。在这里,fn.call(obj)fn.apply(obj) 都会使 this 指向 obj,从而使 console.log(this.key) 打印出 'test'

13. call``apply``bind的区别

callapplybind 是 JavaScript 中用于处理函数执行上下文的方法。它们有一些区别:

  1. 传递参数的方式:
  • **call** 以参数列表的形式传递参数。
func.call(context, arg1, arg2, ...);
  • **apply** 以数组的形式传递参数。
func.apply(context, [arg1, arg2, ...]);
  • **bind** 创建一个新的函数,并以参数列表的形式传递参数,但不会立即执行原函数。
const newFunc = func.bind(context, arg1, arg2, ...);
  1. 立即执行与返回新函数:
  • **call**** 和 **apply**:** 立即执行原函数。
  • **bind** 返回一个新的函数,不会立即执行原函数,而是返回一个新函数,你可以稍后调用。
  1. 返回值:
  • **call**** 和 **apply**:** 返回原函数的执行结果。
const result = func.call(context, arg1, arg2, ...);
  • **bind** 返回一个新的函数。你需要调用这个新函数才能获取结果。
const newFunc = func.bind(context, arg1, arg2, ...);
const result = newFunc();
  1. 使用场景:
  • **call**** 和 **apply**:** 主要用于借用其他对象的方法,或者在一个对象上调用一个函数,同时指定该函数内部的 this 值。
  • **bind** 主要用于创建一个与原函数拥有相同 this 值的新函数,方便稍后调用。
  1. 性能:
  • **call**** 和 **apply**:** 由于立即执行原函数,可能略微更高效,但在大多数情况下性能差异微乎其微。
  • **bind** 创建了一个新的函数,性能可能稍差一些,但同样在大多数情况下不会引起明显的性能问题。

示例:

const obj = { value: 42 };

function getValue(arg) {
  console.log(this.value + arg);
}

getValue.call(obj, 2);      // 输出: 44
getValue.apply(obj, [2]);   // 输出: 44

const boundFunc = getValue.bind(obj, 2);
boundFunc();                // 输出: 44

总体而言,callapplybind 提供了一些灵活性,可以根据具体的使用场景选择适当的方法。

14. testFnA() 打印什么

const a = 'aaaaaa'
function testFn() {
	const a = 'bbbbbb'
	return function () {
		console.log(a)
	}
}


const testFnA = testFn()

testFnA() // bbbbbb

在这个例子中,存在两个变量 a,一个是全局作用域下的 a,值为 'aaaaaa',另一个是在 testFn 函数内部的局部作用域下的 a,值为 'bbbbbb'

testFn 函数内部,返回了一个内部函数,并且内部函数引用了外部函数 testFn 中的变量 a。当执行 testFn() 后,返回的内部函数被赋值给了 testFnA,然后再调用 testFnA()

在调用 testFnA() 时,内部函数访问的变量 a 是通过闭包的方式捕获的,它引用的是外部函数 testFn 中的局部变量 a,而不是全局作用域下的变量 a

因此,console.log(a) 打印的结果是 'bbbbbb',而不是全局作用域下的 'aaaaaa'。这是因为 JavaScript 中函数在执行时会按照词法作用域的规则查找变量,找到最近的定义。

15. 跟第 9题有什么区别

一个对象的取值,一个闭包,闭包不会被垃圾回收机制清理

16. 刚谈到闭包没有被垃圾回收,那怎么手动回收呢

JavaScript 中的垃圾回收是由 JavaScript 引擎自动进行的,开发者通常无需手动干预。垃圾回收器会负责检测不再使用的对象,并释放其占用的内存。

对于闭包,如果闭包中的函数引用了外部作用域的变量,那么这些变量可能会被保留在内存中,即使外部函数执行完毕。这时,垃圾回收器不会回收这些变量,因为闭包仍然在使用它们。

在一般情况下,我们无法手动回收闭包中的变量。垃圾回收是自动进行的,它会检测不再使用的对象,并在适当的时候将其回收。如果确实有需要释放某些资源,可以考虑解除对闭包的引用,以便让垃圾回收器能够回收相关的资源。

以下是一个简单的例子:

function createClosure() {
  const data = 'some data';
  const innerFunction = function() {
    console.log(data);
  };

  return innerFunction;
}

// 创建闭包
const closure = createClosure();

// 执行闭包
closure();

// 现在解除对闭包的引用
closure = null;

// 这时,如果没有其他地方引用闭包,垃圾回收器可能会回收相关资源

在这个例子中,通过将 closure 设置为 null,解除了对闭包的引用。这样,如果没有其他地方引用这个闭包,垃圾回收器可能会在适当的时候回收相关资源。需要注意的是,这种方式并不是手动回收,而是通过解除引用来让垃圾回收器自动进行回收。

17. 节流与防抖是什么,以及他们的区别

节流(throttling)和防抖(debouncing)是两种用于控制函数执行频率的前端优化技术。

  1. 节流(Throttling):
  • 节流是指在一定时间间隔内,无论事件触发多少次,只执行一次相应的函数。它通过设置一个时间间隔,确保在这个时间间隔内只触发一次函数。这有助于减少函数的执行频率,特别在处理一些高频率触发的事件时,例如滚动事件或鼠标移动事件。
function throttle(func, delay) {
  let timer;
  return function() {
    if (!timer) {
      timer = setTimeout(() => {
        func.apply(this, arguments);
        timer = null;
      }, delay);
    }
  };
}
  1. 防抖(Debouncing):
  • 防抖是指在一定时间内,如果事件持续触发,则不执行相应的函数,直到一定时间内没有触发事件为止。防抖主要用于处理一些频繁触发但只需执行一次的操作,例如搜索框输入事件或窗口大小调整事件。
function debounce(func, delay) {
  let timer;
  return function() {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, arguments);
    }, delay);
  };
}

区别:

  • 执行时机:
    • 节流: 在一定时间间隔内,无论触发多少次事件,都只执行一次函数。
    • 防抖: 在一定时间内连续触发事件,只执行一次函数,直到一定时间没有触发事件。
  • 实现方式:
    • 节流: 使用 setTimeout 控制函数执行的频率,确保在一定时间间隔内只执行一次。
    • 防抖: 使用 setTimeout 控制函数的执行,每次触发事件都重新设置定时器,确保在一定时间内不会执行函数。
  • 应用场景:
    • 节流: 适用于高频率触发的事件,如滚动事件、鼠标移动事件。
    • 防抖: 适用于频繁触发但只需执行一次的事件,如搜索框输入事件、窗口大小调整事件。

选择使用节流还是防抖取决于具体的应用场景和需求。

18. 浏览器的宏任务与微任务是什么

在 JavaScript 中,浏览器执行异步任务时,任务分为宏任务(macro task)和微任务(micro task)。这两者的执行顺序和优先级有一定的区别。

  1. 宏任务(Macro Task):
  • 宏任务是由浏览器发起的任务,通常包括整体代码、setTimeout、setInterval、I/O 操作等。宏任务会被添加到宏任务队列中,等待浏览器的执行。
  • 宏任务执行完毕后,会清空微任务队列。

示例宏任务:

setTimeout(() => {
  console.log('This is a macro task.');
}, 0);
  1. 微任务(Micro Task):
  • 微任务是在宏任务执行完毕后立即执行的任务。微任务通常包括 Promise 的回调、MutationObserver 等。
  • 微任务队列有自己的执行时机,在每个宏任务执行完毕后,会检查微任务队列并执行其中的所有任务。

示例微任务:

Promise.resolve().then(() => {
  console.log('This is a micro task.');
});

执行顺序:

  1. 执行整体代码(同步任务)。
  2. 执行所有宏任务,每个宏任务执行完毕后,检查微任务队列并执行其中的所有微任务。
  3. 重复步骤 2 直到宏任务队列和微任务队列都为空。

执行顺序示例:

console.log('Start');

setTimeout(() => {
  console.log('This is a macro task.');
}, 0);

Promise.resolve().then(() => {
  console.log('This is a micro task.');
});

console.log('End');

上述代码输出顺序:

Start
End
This is a micro task.
This is a macro task.

在执行过程中,首先输出同步任务 'Start' 和 'End',然后执行宏任务和微任务。微任务会在宏任务执行完毕后立即执行,因此先输出微任务 'This is a micro task.',再输出宏任务 'This is a macro task.'。

理解宏任务和微任务的执行顺序对于处理异步编程、避免回调地狱等都是非常重要的。

19. 如果一个 Promise进行了两次 then,那 then是一个微任务还是两个微任务

如果一个 Promise 进行了两次 then,它将产生两个微任务。每次调用 then 方法都会创建一个新的 Promise 对象,并返回该 Promise 对象。当 Promise 状态改变时(例如,由 pending 变为 resolved 或 rejected),相应的微任务队列中的任务就会被添加。

示例:

const promise = new Promise((resolve, reject) => {
  resolve('First then');
});

promise.then((result) => {
  console.log(result); // 输出: 'First then'
});

promise.then((result) => {
  console.log(result); // 输出: 'First then'
});

console.log('End');

在这个例子中,promise 被解决为 'First then',并且有两个 then 方法被调用。每个 then 方法都会返回一个新的 Promise 对象,并且它们的回调函数都会被添加到微任务队列中。因此,输出顺序将是 'End',然后两次 'First then'。每个 then 方法都产生了一个微任务。

20. Promise内部原理

Promise 是 JavaScript 中用于处理异步操作的一种机制,它提供了更方便的方式来处理回调函数的问题。下面是 Promise 的简要内部原理:

  1. Promise 状态:
  • Promise 有三种状态:pending(等待中)、fulfilled(已成功)、rejected(已失败)。
  • 初始状态是 pending,表示操作还在进行中。
  1. Promise 解决和拒绝:
  • 当操作成功完成时,Promise 会变为 fulfilled 状态,并保存操作结果。
  • 当操作失败时,Promise 会变为 rejected 状态,并保存错误信息。
  1. then 方法:
  • Promise 提供 then 方法用于处理异步操作的结果。
  • then 方法接受两个参数:onFulfilled(当 Promise 转为 fulfilled 状态时调用)和 onRejected(当 Promise 转为 rejected 状态时调用)。
  1. 微任务队列:
  • Promise 的回调函数是异步执行的,它们会被放入微任务队列中。
  • 在一个宏任务执行完毕后,会检查微任务队列并依次执行其中的任务。
  1. Promise 链:
  • then 方法返回一个新的 Promise,可以通过链式调用 then 方法。
  • 这样的链式调用形成了 Promise 链,每个 then 方法都有机会处理上一个 Promise 的结果。

简化版 Promise 内部实现示例:

class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach(callback => callback(value));
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.value = reason;
        this.onRejectedCallbacks.forEach(callback => callback(reason));
      }
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        const result = onFulfilled(this.value);
        resolve(result);
      } else if (this.state === 'rejected') {
        const result = onRejected(this.value);
        resolve(result);
      } else if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(value => {
          const result = onFulfilled(value);
          resolve(result);
        });

        this.onRejectedCallbacks.push(reason => {
          const result = onRejected(reason);
          resolve(result);
        });
      }
    });
  }
}

// 示例用法
const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('Hello, Promise!');
  }, 1000);
});

promise.then(
  result => console.log(result),
  error => console.error(error)
);

这是一个简化版的 Promise 内部实现,实际的 Promise 还包含了一些额外的特性,例如 catch 方法、finally 方法等。在真正的实现中,还需要处理异步操作、链式调用等更复杂的场景。

21. 接口返回值如何使用 TS 来定义 interface

例:

{
  code: 0,
  data: {
    list: [{ name: "张三", age: 14 }],
    total: 100,
  },
  message: '返回成功',
}

答:在 TypeScript 中,你可以使用接口(interface)来定义这个数据结构。根据你提供的 JSON 结构,可以定义一个包含 codedatamessage 的接口,其中 data 包含一个 list 数组和一个 total 数字。下面是一个可能的 TypeScript 接口定义:

interface ResponseData {
  code: number;
  data: {
    list: {
      name: string;
      age: number;
    }[];
    total: number;
  };
  message: string;
}

// 使用示例
const responseData: ResponseData = {
  code: 0,
  data: {
    list: [{ name: "张三", age: 14 }],
    total: 100,
  },
  message: '返回成功',
};

console.log(responseData.code); // 输出: 0
console.log(responseData.data.list[0].name); // 输出: 张三
console.log(responseData.data.total); // 输出: 100
console.log(responseData.message); // 输出: 返回成功

在上述定义中:

  • **ResponseData**** 是整个响应数据的接口。**
  • **data**** 中包含了一个 **list** 数组,其中每个元素都是一个包含 **name****age** 字段的对象。**
  • **total**** 是一个数字。**
  • **code**** 是一个数字。**
  • **message**** 是一个字符串。**

你可以根据实际需要调整接口定义。上述示例提供了一个基本的结构,你可以根据你的实际数据结构进行扩展或修改。

22. 接上一题,如果 data 里返回数据格式不确定呢?

如果 data 下的数据格式不确定,你可以使用泛型(Generics)来定义一个更灵活的接口。在 TypeScript 中,泛型可以用于创建可重用的、适用于不同数据结构的接口。下面是一个示例:

interface ResponseData<T> {
  code: number;
  data: T;
  message: string;
}

// 使用示例
const responseDataWithList: ResponseData<{ list: { name: string; age: number }[]; total: number }> = {
  code: 0,
  data: {
    list: [{ name: "张三", age: 14 }],
    total: 100,
  },
  message: '返回成功',
};

const responseDataWithString: ResponseData<string> = {
  code: 0,
  data: 'Some dynamic data',
  message: '返回成功',
};

console.log(responseDataWithList.code); // 输出: 0
console.log(responseDataWithList.data.list[0].name); // 输出: 张三
console.log(responseDataWithList.data.total); // 输出: 100
console.log(responseDataWithList.message); // 输出: 返回成功

console.log(responseDataWithString.code); // 输出: 0
console.log(responseDataWithString.data); // 输出: Some dynamic data
console.log(responseDataWithString.message); // 输出: 返回成功

在这个示例中,ResponseData 接口使用了一个泛型 T,它表示 data 的类型。在使用时,你可以通过给泛型传递不同的类型来适应不同的数据结构。

这种方式可以更灵活地适应不同的数据结构,但同时也需要在使用时确保数据的一致性。

23. TS 的泛型是什么

在 TypeScript 中,泛型(Generics)是一种用于创建可复用代码的工具,允许在定义函数、类、接口等时使用占位符,使其可以接受不同类型的参数。泛型在增强代码的灵活性和可重用性方面发挥了重要作用。

23.1. 泛型函数:

function identity<T>(arg: T): T {
  return arg;
}

// 使用方式
let result = identity<string>('Hello, Generics!');
console.log(result); // 输出: 'Hello, Generics!'

在这个例子中,<T> 表示泛型参数,允许 identity 函数接受不同类型的参数,同时保持返回类型与输入类型一致。

23.2. 泛型接口:

interface Pair<T, U> {
  first: T;
  second: U;
}

// 使用方式
let pair: Pair<number, string> = { first: 1, second: 'two' };

这里,Pair 是一个泛型接口,可以用不同的类型实例化。

23.3. 泛型类:

class Box<T> {
  value: T;

  constructor(value: T) {
    this.value = value;
  }
}

// 使用方式
let numberBox = new Box<number>(42);
let stringBox = new Box<string>('Generics');

Box 是一个泛型类,它可以包装不同类型的值。

23.4. 泛型约束:

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

// 使用方式
loggingIdentity('Hello'); // 输出: 5

这个例子中,T 被约束为拥有 length 属性的类型。

泛型在 TypeScript 中提供了一种强大的方式,使代码更加灵活,并能够在类型安全的同时实现可重用的抽象。

24. 如果返回的数据格式是 string 或者 number,用 TS 该如何定义

24.1. 联合类型

如果一个函数返回的数据可能是 stringnumber 类型,可以使用联合类型(Union Types)来定义返回类型。在 TypeScript 中,使用 string | number 表示一个值可以是字符串或数字。

以下是一个示例:

function getStringOrNumber(): string | number {
  // 一些逻辑,可能返回字符串或数字
  const randomNumber = Math.random();
  return randomNumber > 0.5 ? 'Hello' : 42;
}

// 使用方式
const result: string | number = getStringOrNumber();

// 根据实际情况判断返回类型
if (typeof result === 'string') {
  console.log('It is a string:', result.toUpperCase());
} else {
  console.log('It is a number:', result.toFixed(2));
}

在这个例子中,getStringOrNumber 函数的返回类型被定义为 string | number,表示它可以返回字符串或数字。在使用函数返回值时,可以使用 typeof 运算符进行类型判断,确保正确处理不同类型的返回值。

除了使用联合类型之外,还可以使用类型断言(Type Assertion)或函数重载(Function Overloading)等方式来处理返回 stringnumber 类型的情况。

24.2. 类型断言:

function getStringOrNumber(): string | number {
  const randomNumber = Math.random();
  return randomNumber > 0.5 ? 'Hello' : 42 as string | number;
}

// 使用方式
const result: string | number = getStringOrNumber();

在这个例子中,通过 as string | number 进行类型断言,告诉 TypeScript 返回值可以是 stringnumber

24.3. 函数重载:

function getStringOrNumber(): string;
function getStringOrNumber(): number;
function getStringOrNumber(): string | number {
  const randomNumber = Math.random();
  return randomNumber > 0.5 ? 'Hello' : 42;
}

// 使用方式
const result: string | number = getStringOrNumber();

通过函数重载,可以为函数提供多个不同的签名,让 TypeScript 根据实际调用的情况来选择正确的返回类型。

每种方法都有其适用的场景,选择其中一种取决于代码的具体需求和设计。一般而言,使用联合类型是最简单和直观的方式。

25. Vue2、3 的区别

Vue 2.x 和 Vue 3.x 之间有一些显著的区别,其中 Vue 3.x 引入了一些新的特性和优化。以下是一些主要的区别:

  1. 性能优化:
  • Vue 3.x 在性能方面进行了大幅度的优化。它引入了新的响应式系统(Proxy),用于替代 Vue 2.x 中的 Object.defineProperty,提高了响应式数据的效率。
  1. Composition API:
  • Vue 3.x 引入了 Composition API,这是一种新的组织组件代码的方式。相比于 Vue 2.x 的选项式 API,Composition API 更灵活,使得组件的逻辑更容易复用和组合。
  1. Teleport:
  • Vue 3.x 引入了 Teleport,用于在组件的模板中将内容传送(teleport)到另一个地方,使得更容易处理全局弹窗等场景。
  1. Fragments:
  • Vue 3.x 支持 Fragments,允许组件返回多个根节点而无需包裹在额外的标签中。
  1. Custom Directives:
  • Vue 3.x 提供了更灵活的自定义指令 API,使得创建和管理自定义指令变得更加简单。
  1. Tree-Shakable:
  • Vue 3.x 被设计为更容易进行 Tree-Shaking,以减小应用的体积。
  1. 全局 API 重构:
  • Vue 3.x 对全局 API 进行了重构,包括全局 API 的导入方式和一些方法的修改。例如,Vue.component 变为 app.component
  1. 事件修饰符:
  • Vue 3.x 中的事件修饰符的语法进行了一些变化,更符合自然语言。
  1. Suspense 和异步组件:
  • Vue 3.x 引入了 Suspense 和异步组件的新语法,使得处理异步数据更加优雅。
  1. 更好的 TypeScript 支持:
  • Vue 3.x 对 TypeScript 的支持更加完善,包括更好的类型推导和更准确的类型提示。

需要注意的是,虽然 Vue 3.x 引入了许多新特性和改进,但也带来了一些不兼容的变化。在迁移时,开发者需要仔细阅读官方文档和迁移指南,以确保应用能够平滑过渡。

26. Vue2、3 响应式的区别

Vue 2.x 和 Vue 3.x 在响应式系统上有一些显著的区别。Vue 3.x 引入了新的响应式系统,使用 Proxy 替代了 Vue 2.x 中的 Object.defineProperty,带来了性能和功能上的改进。

26.1. Vue 2.x 响应式系统:

  1. 基于 **Object.defineProperty**
  • Vue 2.x 使用 Object.defineProperty 来实现响应式。这个方法会劫持对象的属性访问,当属性发生变化时,触发相应的更新。
  1. 数组的变异方法:
  • 对于数组,Vue 2.x 通过重写数组的变异方法(例如 pushpopsplice 等)来实现响应式。这意味着只有这些方法能触发视图更新,直接通过索引修改数组的元素是不会触发响应式更新的。
  1. 性能问题:
  • Vue 2.x 的响应式系统在大规模数据和频繁更新的情况下可能引起性能问题,因为 Object.defineProperty 的实现相对较为复杂。

26.2. Vue 3.x 响应式系统:

  1. 基于 **Proxy**
  • Vue 3.x 使用了新的 Proxy API 来实现响应式。Proxy 提供了更直观和强大的拦截器,使得对对象的访问和修改更加灵活。
  1. Reflect API:
  • Vue 3.x 使用 Reflect API 来进行一些操作,例如 Reflect.setReflect.get 等。
  1. 不同类型的 reactive:
  • Vue 3.x 提供了 reactive 函数和 ref 函数,分别用于创建对象和创建基本类型的响应式数据。这种方式更加直观和一致。
  1. 数组的处理:
  • 在 Vue 3.x 中,通过 Proxy 可以对数组的所有操作进行拦截,不再需要特殊处理数组的变异方法。直接通过索引修改数组元素也能触发响应式更新。
  1. 性能优化:
  • Vue 3.x 的响应式系统在性能上有较大优势,特别是在大规模数据和频繁更新的场景下。
  1. 逻辑拆分:
  • Vue 3.x 的响应式系统逻辑更加清晰,拆分为模块,使得维护和扩展更容易。

总体而言,Vue 3.x 的响应式系统通过 Proxy 带来了性能上的提升,并使得 API 更加灵活和直观。在使用上,开发者可能会更容易理解和处理响应式数据。

27. 为什么 Vue2 嵌套的深层数据没法响应

在 Vue 2.x 中,嵌套的深层数据如果是在初始时已存在(即在响应式对象被创建之前就存在的属性),Vue 2.x 可能无法做到深层嵌套的响应式。这是因为 Vue 2.x 的响应式系统是基于 Object.defineProperty 实现的,而该方法只会在对象属性被访问时添加 getter 和 setter,因此对于初始时就存在的深层嵌套数据,Vue 2.x 可能无法捕捉到变化。

以下是一个示例:

<template>
  <div>
    <p>{{ deepData.nestedValue }}</p>
    <button @click="updateData">Update Data</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      deepData: {
        nestedValue: 'Initial Value'
      }
    };
  },
  methods: {
    updateData() {
      // 这里的数据更新可能无法触发深层嵌套数据的响应
      this.deepData.nestedValue = 'Updated Value';
    }
  }
};
</script>

在上述示例中,如果 deepData 对象在组件创建时已经存在,那么在 updateData 方法中对 nestedValue 的修改可能无法触发视图的更新,因为初始时该属性已存在,不会被 Object.defineProperty 拦截。

为了解决这个问题,一种常见的做法是使用 Vue.setthis.$set 方法,手动触发深层嵌套数据的响应式。例如:

this.$set(this.deepData, 'nestedValue', 'Updated Value');

这样就能确保深层嵌套的数据也能够被 Vue 2.x 的响应式系统捕捉到变化,从而触发视图更新。在 Vue 3.x 中,由于采用了 Proxy,这个问题已经得到了更好的解决。

28. 为什么 vue3 可以实现对象数据的深度响应

Vue 3.x 在实现深度响应式时,采用了新的响应式系统,其中使用了 Proxy 替代了 Vue 2.x 中的 Object.defineProperty。这一变化带来了一些重要的优势,使得 Vue 3.x 能够更有效地实现对象数据的深度响应。

以下是一些原因:

  1. Proxy 拦截器:
  • Proxy 提供了一组强大的拦截器,允许对对象的访问、修改、删除等操作进行拦截。这意味着 Vue 3.x 可以更灵活地捕获对象上的操作,包括深层嵌套数据的变化。
  1. 递归代理:
  • Vue 3.x 的响应式系统能够递归地对对象的每个属性进行代理。当访问或修改对象的嵌套属性时,系统能够动态地为嵌套属性创建新的 Proxy 实例,从而实现对深层嵌套数据的深度响应。
  1. 更高性能:
  • 使用 Proxy 相对于 Object.defineProperty 在性能上有很大的提升。Proxy 是 JavaScript 引擎提供的原生机制,它的实现更为高效,尤其在处理大规模数据和频繁更新时,性能优势更加明显。
  1. 更直观的 API:
  • Proxy 提供了一个更直观和强大的 API,比 Object.defineProperty 更易于理解和使用。这使得 Vue 3.x 的响应式系统更加灵活,可以更容易地处理复杂的数据结构。
  1. Track 和 Trigger 阶段:
  • Vue 3.x 引入了 Track 和 Trigger 阶段,分别用于跟踪依赖关系和触发更新。这个机制更加精确地追踪了数据的访问和修改,使得 Vue 3.x 能够更准确地知道何时以及如何触发视图的更新。

总的来说,Vue 3.x 利用了 Proxy 的灵活性和性能优势,通过递归代理等机制实现了更为强大和高效的深度响应式系统。这也是 Vue 3.x 在响应式方面相对于 Vue 2.x 的一个显著改进。

29. 请看例子,父组件传参name变化的话,子组件 newName 也会跟着变吗

例:

setup(props) {

    const { name } = props

    const newName = computed(() => `姓名: ${name}`)

    return {
        newName
    }
}

// 父级组件如果传参 name 变更的话,子组件的 newName 会跟着变吗

29.1. Vue3 中

Vue3 内的用法感谢掘友 @土逗99 指正

一般来说在Vue 3中,setup 函数中使用的 props 是响应式的,因此当父级组件传递的 name 发生变更时,newName 会跟着变化。

在您提供的代码示例中,newName 是一个计算属性,它依赖于 props 中的 name

但是,有一个关键的问题:您没有在 setup 函数中正确地解构 props。在您的代码中,您写了 const { name } = props,但这样写会导致解构出的name失去响应式。

如果想解构使用的话可以使用 toRefs 保持响应性const { name } = toRefs(props)

使用的时候name.value

示例:

<!-- 父级组件 -->
<template>
  <ChildComponent :name="parentName" />
  <button @click="changeName">点我改变</button>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  data() {
    return {
      parentName: 'John',
    };
  },
  methods: {
    changeName() {
      this.parentName = 'Doe'; // 改变父级组件的 name
    },
  },
  components: {
    ChildComponent,
  },
};
</script>
<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <p>{{ newName }}</p>
  </div>
</template>

<script>
import { computed, toRefs } from 'vue';

export default {
  props: {
    name: String,
  },
  setup(props) {
    const { name } = toRefs(props);
    const newName = computed(() => `姓名: ${name.value}`);

    return {
      newName,
    };
  },
};
</script>

在这个示例中,当调用 changeName 方法时,父级组件的 parentName 发生变化,这会导致子组件中的 newName 重新计算,最终反映在子组件的视图中。

如果不想这么麻烦的解构,可以直接使用props.name

<!-- 子组件 ChildComponent.vue -->
<template>
  <div>
    <p>{{ newName }}</p>
  </div>
</template>

<script>
import { computed } from 'vue';

export default {
  props: {
    name: String,
  },
  setup(props) {
    const newName = computed(() => `姓名: ${props.name}`);

    return {
      newName,
    };
  },
};
</script>

29.2. Vue2 中

在 Vue 2.x 中,computed 仅仅是计算属性,并不具备响应式更新传入的 props 的能力。当 props 发生变化时,computed 不会自动更新。

如果需要在 Vue 2.x 中实现类似的效果,你可以使用 watch 来手动监听 props 的变化,并在回调中更新相应的计算属性。以下是一个简单的示例:

export default {
  props: {
    name: String,
  },
  data() {
    return {
      // 使用 data 中的属性作为计算属性的缓存
      cachedName: '',
    };
  },
  computed: {
    newName() {
      return `姓名: ${this.cachedName}`;
    },
  },
  watch: {
    name(newName) {
      // 监听 props 的变化,更新计算属性的缓存
      this.cachedName = newName;
    },
  },
};

在这个示例中,我们使用了 watch 来监听 propsname 的变化,并在 watch 的回调中手动更新了计算属性 newName 使用的 cachedName。这样就实现了在父级组件传入的 name 变化时,计算属性 newName 也能跟随变化。

30. 用过在 Vue2 中写 Vue3 的语法吗

在 Vue 2 中使用 Vue 3 的 Composition API 需要借助 @vue/composition-api 插件。这个插件提供了 Vue 3 的 Composition API 在 Vue 2 中的实现。

以下是一个简单的示例:

  1. 安装 @vue/composition-api 插件:
npm install @vue/composition-api
  1. 在你的 Vue 2 项目的入口文件(通常是 main.js)中使用 Composition API:
import Vue from 'vue';
import VueCompositionApi from '@vue/composition-api';
import App from './App.vue';

// 使用插件
Vue.use(VueCompositionApi);

new Vue({
  render: (h) => h(App),
}).$mount('#app');
  1. 在组件中使用 Composition API:
<template>
  <div>
    <p>{{ count.value }}</p>
    <button @click="increment">Increment</button>
  </div>
</template>

<script>
import { ref } from '@vue/composition-api';

export default {
  setup() {
    const count = ref(0);

    const increment = () => {
      count.value++;
    };

    return {
      count,
      increment,
    };
  },
};
</script>

在这个示例中,我们引入了 @vue/composition-api 插件,并在组件的 setup 函数中使用了 Composition API 提供的 ref 函数。这使得我们能够在 Vue 2 中使用类似 Vue 3 Composition API 的语法。

请注意,虽然这样做可以让你在 Vue 2 中使用 Composition API,但并不意味着所有 Vue 3 Composition API 的特性都能在 Vue 2 中完全还原。一些高级特性可能需要更多的调整或不支持。在实际使用中,建议谨慎使用 Composition API 插件,并查阅相应文档以了解可能的限制和适用场景。

31. Vue2 自定义指令是什么

在 Vue 2 中,自定义指令是一种强大的扩展 Vue 的方式,它允许你注册全局或局部的指令,并在 DOM 元素上绑定、更新、解绑时执行相应的逻辑。以下是在 Vue 2 中创建自定义指令的简单示例:

31.1. 全局自定义指令

// main.js 或任何入口文件
import Vue from 'vue';

// 注册全局自定义指令 v-color
Vue.directive('color', {
  // bind 钩子函数在指令绑定到元素时触发
  bind(el, binding) {
    // 设置元素的颜色为指令的值
    el.style.color = binding.value;
  },
});

在上述代码中,我们注册了一个全局自定义指令 v-color,它在绑定时设置元素的文本颜色为指令的值。

31.2. 局部自定义指令

// 在组件中注册局部自定义指令
export default {
  directives: {
    // 注册局部自定义指令 v-font-size
    'font-size': {
      // bind 钩子函数在指令绑定到元素时触发
      bind(el, binding) {
        // 设置元素的字体大小为指令的值
        el.style.fontSize = binding.value + 'px';
      },
    },
  },
};

在上述代码中,我们在组件中注册了一个局部自定义指令 v-font-size,它在绑定时设置元素的字体大小为指令的值。

31.3. 在模板中使用自定义指令

<template>
  <div>
    <!-- 使用全局自定义指令 v-color -->
    <p v-color="'red'">This text is red.</p>
    
    <!-- 使用局部自定义指令 v-font-size -->
    <p v-font-size="20">This text has font size 20px.</p>
  </div>
</template>

在模板中,我们可以使用自定义指令来修改元素的样式或行为。

以上是一个简单的例子,自定义指令还有更多的钩子函数(如 updatecomponentUpdatedunbind 等)和参数,可以根据需求进行定制。详细的文档可以参考 Vue 2 的自定义指令文档

32. Monorepo 应用场景有哪些

Monorepo(单仓库)是指将多个项目(通常是相关联的项目)放置在同一个版本控制仓库中的做法。这种架构有许多潜在的应用场景,以下是一些常见的 Monorepo 应用场景:

  1. 代码共享:
  • Monorepo 允许在不同的项目之间共享代码。这样,你可以创建共享的库、组件、工具函数等,确保它们在不同项目中保持同步,减少重复工作。
  1. 版本一致性:
  • 所有项目共享相同的版本控制仓库,确保它们使用相同的依赖版本。这有助于维持整体系统的版本一致性,减少因为不同项目使用不同版本而导致的问题。
  1. 跨项目重构:
  • 当需要进行大规模的重构或架构调整时,Monorepo 可以更容易地进行代码共享和迁移,因为所有相关代码都在同一个仓库中,可以更方便地协同变更。
  1. 单一构建和发布:
  • Monorepo 允许进行单一构建和发布。你可以使用一个统一的构建系统和发布流程来管理所有的项目,简化了部署和维护流程。
  1. 统一的开发环境:
  • 统一的开发环境可以简化配置,提高开发效率。开发者可以更轻松地切换不同项目,共享配置文件、脚本等。
  1. 依赖管理:
  • Monorepo 简化了依赖管理,你可以通过工具(如 Yarn Workspaces 或 Lerna)来管理共享依赖,确保不同项目使用相同的依赖版本。
  1. 综合项目管理:
  • 在一个仓库中综合管理多个项目,有助于更好地理解和维护系统的整体结构。这对于大型组织或复杂的项目来说尤为有益。
  1. 测试和持续集成:
  • 在 Monorepo 中,可以更容易地设置统一的测试策略和持续集成(CI)管道,确保所有项目都通过相同的测试和构建流程。

需要注意的是,虽然 Monorepo 提供了一些明显的优势,但它也可能引入一些管理上的挑战,如合理的项目结构设计、团队协作和权限控制等。在决定是否使用 Monorepo 时,需要权衡其优势和挑战,并根据具体情况做出决策。

33. yarn 和 pnpm 有什么区别

Yarn、pnpm、以及npm都是 JavaScript 包管理工具,用于管理项目依赖。它们有一些区别,下面是它们之间的一些主要区别:

33.1. 安装依赖的方式:

  • Yarn: Yarn 使用离线缓存(offline cache)来提高安装速度。它会下载所有依赖包到本地缓存,然后从缓存中拷贝到项目中。
  • pnpm: pnpm 采用符号链接(symlink)的方式来安装依赖。它会在全局缓存中维护一个单一版本的每个包,并在项目中创建符号链接,避免了重复的文件拷贝。

33.2. 存储方式:

  • Yarn: Yarn 使用一个全局存储目录,将所有版本的每个包都下载到这个目录中。
  • pnpm: pnpm 使用一个分散的存储模型,每个项目都有自己的 node_modules 文件夹,但不同项目之间可以共享相同的包版本,以及共享硬链接,减少磁盘占用。

33.3. 硬链接:

  • Yarn: Yarn 不使用硬链接,而是通过文件拷贝的方式安装依赖。
  • pnpm: pnpm 使用硬链接,这意味着相同的依赖在磁盘上只存储一份,可以在不同项目之间共享。

33.4. 安装速度:

  • Yarn: Yarn 的安装速度通常较快,特别是当使用离线缓存时。
  • pnpm: pnpm 通过符号链接和硬链接的方式,也能提供较快的安装速度,尤其在多个项目之间共享依赖时。

33.5. 磁盘空间:

  • Yarn: Yarn 使用的是全局存储,可能导致磁盘空间的浪费,因为每个项目都会有一份完整的依赖包。
  • pnpm: pnpm 通过分散存储和硬链接的方式,可以减少磁盘空间的占用。

33.6. 并发安装:

  • Yarn: Yarn 具有并发安装的能力,可以同时安装多个依赖。
  • pnpm: pnpm 同样支持并发安装,这有助于提高安装速度。

总的来说,Yarn、pnpm 和 npm 都有各自的优势和适用场景。选择使用哪一个工具取决于项目的需求、团队的喜好,以及个人的使用习惯。

34. 在 package.json中未定义的包,在 yarn 和 pnpm 中能引用吗

yarn 可以,pnpm 不可以npm/yarn 安装依赖时,存在依赖提升,某个项目使用的依赖,并没有在其 package.json 中声明,也可以直接使用,这种现象称之为 “幽灵依赖”;随着项目迭代,这个依赖不再被其他项目使用,不再被安装,使用幽灵依赖的项目,会因为无法找到依赖而报错。

35. 为什么 pnpm 不可以引用未定义的包

在 pnpm 中,一些设计原则导致它在安装时不会引用未定义的包(undeclared dependencies)。这是 pnpm 与其他包管理工具(如 npm 和 Yarn)的一个区别。

35.1. pnpm 的设计原则:

  1. 显式声明: pnpm 遵循显式声明的原则,即只有在 package.json 中显式声明的依赖才会被安装。未在 package.json 中声明的包不会被引用。
  2. 安装原子性: pnpm 具有原子性的安装特性,即每个包都有独立的 node_modules 目录。这确保了每个包只有它所声明的依赖。
  3. 符号链接: pnpm 使用符号链接(symbolic links)来创建包之间的链接。这减少了磁盘占用和提高了安装速度。但同时,pnpn 的符号链接机制也要求每个包的依赖关系必须是明确定义的。

35.2. 为什么不引用未定义的包:

  • 避免隐式依赖: 显式声明依赖关系可以避免隐式依赖。这样可以确保每个包的依赖是明确的,而不是依赖于其他包的隐式存在。
  • 确保安全: 通过明确声明依赖关系,可以确保安装的包是可预测的。避免了引入未知或未定义的包,减少了项目的安全风险。
  • 更容易维护: 显式声明依赖关系使得项目更容易维护。开发者可以清晰地知道每个包的依赖情况,降低了项目的复杂性。

虽然这些设计原则在某些情况下可能会引起一些不便,但它们有助于确保项目的可维护性、安全性和稳定性。如果你有确实需要引用未定义的包的情况,可能需要重新考虑项目的架构和依赖管理的策略。

36. 如果 pnpm 的 package.json 注册了 A 包,A 包内又引用了 B 包,那我能在项目中直接使用B包吗?如果不能,是因为什么

在 pnpm 中,"幽灵依赖"是指 node_modules 中存在但未在 package.json 中声明的依赖。pnpm 通过其设计原则,特别是显式声明依赖关系的原则,决定不允许使用未在 package.json 中声明的依赖。

以下是 pnpm 不允许使用幽灵依赖的原因:

  1. 显式声明的原则: pnpm 遵循显式声明的原则,只有在 package.json 中显式声明的依赖才会被安装。这种设计有助于确保依赖关系是明确的,降低了项目的复杂性。
  2. 安全性: 通过显式声明依赖关系,可以确保项目使用的依赖是可预测的和已审核的。避免了因为引入未知或未定义的依赖而导致的潜在安全风险。
  3. 维护性: 显式声明依赖关系使得项目更容易维护。开发者可以清晰地知道每个包的依赖情况,降低了项目的复杂性。

虽然 pnpm 在这方面可能会导致一些不便,但这是为了提高项目的可维护性、安全性和稳定性而作出的权衡。如果你有确实需要使用幽灵依赖的情况,可能需要重新考虑项目的架构和依赖管理的策略。

二面(线上,牛客网)

1. 自我介绍

2. 举例子,在前端负责人角色下,具体项目里怎样和同事协作

3. 举例,做了什么项目,框架搭建过程中是怎么样去思考的,怎样通过框架实现业务目标

4. 技术框架怎么选型的,为什么要做这个选型

5. 菲波那切数列,第 N 项的值

原题是力扣LCR 126.斐波那契数下面是计算第 N 项斐波那契数列的值的 JavaScript 示例代码:

function fibonacci(n) {
    if (n <= 1) {
        return n;
    }

    let a = 0;
    let b = 1;

    for (let i = 2; i <= n; i++) {
        const temp = a + b;
        a = b;
        b = temp;
    }

    return b;
}

// 示例
const n = 7; // 你想要计算的项数
const result = fibonacci(n);

console.log(`第 ${n} 项的斐波那契数列的值是: ${result}`);

这段代码中,fibonacci 函数接受一个整数参数 n,并返回斐波那契数列的第 N 项的值。在循环中,我们使用两个变量 ab 来迭代计算下一项的值,直到达到目标项数。这样可以有效地计算斐波那契数列,避免了递归的重复计算。对于上述的迭代方法,时间复杂度为 O(n),其中 n 是目标项数。这是因为我们使用循环迭代计算直到达到目标项数,循环的次数与目标项数线性相关。

空间复杂度为 O(1),这是因为我们只使用了常量级的额外空间,即变量 abtemp 等,与目标项数无关。这种空间复杂度为常量的情况称为空间复杂度是 O(1)。

总体而言,迭代方法是一种效率较高且占用常量级空间的解决方案。

结果:没过

我的面试复盘系列

面试复盘一:

面试复盘二(本篇):

面试复盘三(上):

讨论交流,入口在第一篇面经文末