面经

196 阅读56分钟

快手

1、人员的权限如何控制?

在Vue应用中实现权限控制、动态路由、和请求拦截器可以结合使用Vue Router和axios等工具来完成。以下是一些常见的方法:

  1. 权限控制

    • 在应用中实现权限控制通常需要使用路由守卫(Navigation Guards)。你可以使用Vue Router的导航守卫来检查用户是否有权限访问特定的路由。例如,你可以在路由守卫中检查用户的角色或权限,然后决定是否允许导航到某个路由。
    • 在路由定义中,你可以添加meta字段,用于存储路由相关的权限信息,然后在路由守卫中检查这些信息。
    const router = new VueRouter({
      routes: [
        {
          path: '/admin',
          component: Admin,
          meta: { requiresAuth: true } // 该路由需要登录权限
        },
        // ...
      ]
    });
    
    • 在导航守卫中检查权限:
    router.beforeEach((to, from, next) => {
      if (to.meta.requiresAuth && !userHasPermission()) {
        next('/login'); // 重定向到登录页面
      } else {
        next(); // 继续导航
      }
    });
    
  2. 动态路由

    • 动态路由是在运行时根据数据来创建路由。你可以使用Vue Router的addRoutes方法来动态添加路由。这通常用于根据用户角色或权限动态生成路由。
    const dynamicRoutes = generateDynamicRoutes(); // 生成动态路由配置
    router.addRoutes(dynamicRoutes); // 添加动态路由
    
  3. 请求拦截器

    • 请求拦截器通常与HTTP请求库(如axios)一起使用。你可以使用axios的interceptors来在发送请求前进行拦截,以实现各种功能,如添加认证信息、处理错误等。
    axios.interceptors.request.use(
      (config) => {
        // 在请求发送前进行处理,如添加认证信息
        config.headers.Authorization = 'Bearer ' + getToken();
        return config;
      },
      (error) => {
        // 处理请求错误
        return Promise.reject(error);
      }
    );
    

2、首屏加载提升速度,做了哪些优化

在回答这个问题之前 首屏 -- > 当进入该网页并且发送网络请求获取到资源以后 网页从没有内容到内容第一次渲染的时间 (当然是可以没有内容的)称为 首屏

答:我认为优化首屏速度可以从网络和加载资源两个方面去 优化

网络

当我们网络请求较多时 , 浏览器会增加首屏渲染速度 ,如果大量的数据请求慢这样会导致页面渲染也随之较慢,减少网络请求

  • 减少网络请求

例如 : 使用图片懒加载形式 , 只发送用户视口可见的数据请求 这样可以大大增加 请求速度 从而进一步提高渲染速度。

  • 使用缓存

与后端配合 进行 资源缓存 ,如长期不变的 logo 或者 定量更新的资源可以采用缓存的形式 来处理,也可以优化首屏渲染时间

  • 使用CDN进行加速

对资源进行托管,保证服务器的响应速度

加载资源方面

  • 路由懒加载 使用 路由懒加载的形式,进行资源处理,当我们需要用到该资源时再进行资源加载,可以将大量的加载时间空出来留给首屏渲染。
  • script 标签资源异步加载

当一些 不影响系统环境资源的加载时 我们可以考虑使用 async 和 defer 加载

  • webpack splitChunks 代码分割

利用webpack 代码分割 进行优化,可以将 js进行分片 ,首次加载文件体积大量减少,以及资源异步加载

  • css 压缩 js压缩 html压缩 image压缩 gzip压缩

    css 可以通过 minicssExtracPlugin 进行 css压缩

    js 可以通过 TerserWebpackPlugin | uglifyJsPlugin 进行资源压缩

    html 可以通过 HtmlMinimizerWebpackPlugin 进行压缩

    图片可以通过 ImageMinimizerWebpackPlugin 进行压缩

  • css 样式书写规范

css 代码 写得好更有利于 浏览器的解析,还有说什么transform3d 能够硬件加速,我不这么认为,当你开启了一个

transform3d 相当于是在网页上多开了一个图层 , 当你的图层达到一定数量的时候,未免性能能好过普通的 transform translate

  • 服务端渲染提高首屏

服务端渲染 ,后台将你需要绘制的页面结构以及数据全局准备好了,然后直接将 资源文件返回给前端,前端只需要渲染即可。

3、浏览器如何进行缓存,协商缓存强缓存

  1. 强缓存

    强缓存是一种浏览器缓存机制,它告诉浏览器在发起请求之前是否应该使用缓存的副本。强缓存的判断是基于响应的Cache-ControlExpires头。

    • Cache-Control头:这是最常用的强缓存控制头,它包含一些指令,如max-age,指定资源在多长时间内可以被缓存。例如,Cache-Control: max-age=3600表示资源可以在浏览器中缓存1小时。
    • Expires头:它是一个日期时间,表示资源的过期时间。浏览器会比较当前时间与Expires头中的时间,如果资源未过期,浏览器将使用缓存。

    如果资源的Cache-Control头中有max-age指令或Expires头,且缓存仍然有效,浏览器会直接使用缓存,而不向服务器发送请求。这降低了对服务器的请求次数,提高了页面加载速度。

  2. 协商缓存

    协商缓存是另一种缓存机制,它用于检查缓存的有效性,以确定是否需要从服务器获取资源的新版本。协商缓存使用的HTTP头包括Last-ModifiedETag

    • Last-Modified头:当浏览器首次请求资源时,服务器会发送一个Last-Modified头,表示资源的最后修改时间。浏览器将该值存储在缓存中。
    • ETag头:这是服务器生成的唯一标识符,用于表示资源的版本。服务器会发送ETag头。

    当浏览器再次请求资源时,它将使用If-Modified-SinceIf-None-Match头将上次的Last-ModifiedETag值发送给服务器。服务器会比较这些值,如果资源没有更改,服务器将返回304 Not Modified状态码,并告诉浏览器继续使用缓存。如果资源已更改,服务器将发送新的资源内容。

    协商缓存允许服务器决定是否更新缓存,而不是依赖浏览器的本地缓存有效性。这提供了更精细的控制,尤其在多个浏览器或代理服务器缓存的情况下。

总结:强缓存和协商缓存是两种不同的缓存控制机制,用于优化网络请求,减少服务器负载和提高性能。强缓存依赖于响应头中的缓存控制指令,而协商缓存使用Last-ModifiedETag头来检查资源的有效性。通常,浏览器会首先尝试强缓存,如果缓存失效,再使用协商缓存来检查资源是否需要更新

4、如何性能优化,并防劣化

1、前端性能优化 24 条建议

2、防止劣化

  1. 持续监测性能

    • 使用性能监测工具和服务,如应用性能管理(APM)工具、实时分析和日志工具,来监视应用的性能指标。
    • 设置警报和阈值,以便在性能问题出现时能够及时采取措施。
  2. 自动化测试

    • 使用自动化测试工具和测试套件来检测性能问题。
    • 集成性能测试到持续集成(CI)管道中,以确保每次代码更改都会经历性能测试。
  3. 代码审查和最佳实践

    • 进行定期的代码审查,以确保代码质量和性能最佳实践。
    • 遵循最佳实践,如减少不必要的DOM操作、使用合适的数据结构、减小网络请求等。
  4. 性能测试环境

    • 创建模拟生产环境的性能测试环境,以模拟真实用户流量和负载。
    • 在测试环境中定期进行性能测试和负载测试,以模拟不同负荷下的应用性能。
  5. 缓存策略

    • 定期审查和更新缓存策略,以确保缓存的有效性和性能。
    • 监控缓存的命中率,确保缓存起到性能提升作用。
  6. 前端监控

    • 使用前端监控工具来实时监视用户在生产环境中的应用性能。
    • 收集前端错误和性能数据,以及用户体验反馈,以便迅速识别和解决性能问题。
  7. 版本控制和回滚计划

    • 使用版本控制系统来跟踪代码更改,并创建回滚计划,以便在性能问题无法解决时能够快速恢复到稳定版本。
  8. 优化第三方依赖

    • 定期审查和更新应用中的第三方库和依赖,以确保它们不会导致性能问题。
    • 最小化不必要的依赖,或者使用CDN来分发它们。
  9. 定期审查架构和设计

    • 定期审查应用的架构和设计,以确保它们仍然满足性能需求。
    • 根据需求调整架构,以适应新的性能要求。
  10. 培训和教育

    • 对开发团队进行培训,使他们了解性能最佳实践和工具。
    • 促使开发团队认识到性能问题的重要性,并鼓励他们参与性能优化和监控。

性能劣化是一个常见的问题,但通过持续的监测、测试和最佳实践,你可以防止性能问题重新浮现,从而提供稳定和高性能的应用。不断学习和改进是保持应用性能的关键

5、Webworker怎么实现的

一文彻底学会使用web worker

6、撸代码:防抖、节流、封装一个请求重试的方法(url, maxRetries, timeout)

防抖

function debounce(func, delay) {
  let timeout;
  return function (...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

节流

function throttle(func, delay) {
  let lastExecTime = 0;
  return function (...args) {
    const currentTime = Date.now();
    if (currentTime - lastExecTime >= delay) {
      func.apply(this, args);
      lastExecTime = currentTime;
    }
  };
}

3、请求重试的方法

async function retryRequest(url, maxRetries, timeout) {
  let retries = 0;

  async function doRequest() {
    try {
      const response = await fetch(url); // 这里使用fetch作为请求方法,你可以替换成你喜欢的HTTP请求方法
      if (response.ok) {
        return response.json(); // 假设需要返回JSON数据
      }
    } catch (error) {
      if (retries < maxRetries) {
        console.warn(`Request failed. Retrying... (${retries + 1}/${maxRetries})`);
        await new Promise(resolve => setTimeout(resolve, timeout));
        retries++;
        return doRequest(); // 递归重试请求
      }
      throw new Error("Max retries exceeded");
    }
  }

  return doRequest();
}

好未来

1、前端从0-1搭建应该注意什么问题,步骤是什么

  1. 明确项目需求: 首先,你需要明确项目的需求,包括功能、目标受众、设计、用户体验等。这将有助于你规划项目。
  2. 选择技术栈: 根据项目需求和你的熟悉程度,选择合适的前端技术栈。通常包括HTML、CSS、JavaScript,以及可能的框架、库和构建工具(如React、Vue、Webpack等)。
  3. 项目结构和架构设计: 设计项目的文件结构和组件架构。确定哪些模块、组件、样式表、图像等将被创建。
  4. 创建基本文件: 创建项目文件和基本文件结构,包括HTML模板、CSS样式表、JavaScript文件。
  5. 初始化版本控制: 使用Git或其他版本控制工具来管理代码。初始化Git仓库并进行首次提交。
  6. 开发界面: 根据设计和需求,创建应用的用户界面。编写HTML、CSS和JavaScript来实现页面结构、样式和交互。
  7. 实现功能: 开发前端功能,包括用户登录、数据加载、表单验证、交互效果等。根据需求逐步实现功能模块。
  8. 测试: 进行单元测试和集成测试,确保代码的质量和功能的正确性。解决潜在的bug和问题。
  9. 优化性能: 进行性能优化,包括文件压缩、懒加载、CDN使用、资源缓存等,以提高页面加载速度和用户体验。
  10. 响应式设计: 确保你的应用在不同设备和屏幕尺寸上都能正常显示,采用响应式设计原则。
  11. 安全性: 考虑安全性问题,包括跨站点脚本攻击(XSS)、跨站点请求伪造(CSRF)等。采取适当的安全措施,如输入验证和跨站点脚本防护。
  12. 浏览器兼容性: 测试你的应用在不同浏览器中的兼容性,确保它在主流浏览器上正常运行。
  13. 性能测试: 使用性能测试工具来评估应用的性能,如Google PageSpeed Insights、Lighthouse等。
  14. 文档: 编写文档,包括代码注释、使用说明和维护文档,以便其他开发人员能够理解和维护你的代码。
  15. 部署: 部署你的前端应用到生产环境,选择合适的托管服务或云平台。确保服务器和域名配置正确。
  16. 监测和维护: 设置监测工具,以便实时监测应用性能和错误。持续维护和改进应用。
  17. 上线发布: 发布应用到生产环境,确保一切正常运行。
  18. 持续优化: 持续关注用户反馈和应用的性能,不断优化和改进应用。

2、小程序有没有没遇到什么诡异的问题

微信小程序开发遇到的各种问题

  1. 网络请求不生效:有时,小程序中的网络请求可能会出现问题,但没有明显的错误信息。这可能是因为跨域请求未配置正确,或者网络请求库的配置存在问题。
  2. 小程序缓存问题:小程序的缓存机制可能会导致某些数据被缓存,而在预期时间内未更新。这可能需要手动刷新缓存或清除缓存。
  3. 微信开发者工具问题:有时,在微信开发者工具中开发和调试小程序时,会出现与实际设备不一致的问题。这可能是由于工具本身的问题或调试模式不一致引起的。
  4. 样式兼容性问题:不同设备和版本的小程序可能存在不同的样式兼容性问题。需要小心测试和适配不同设备。
  5. 第三方插件问题:使用第三方插件时,可能会遇到插件与小程序版本不兼容或者存在bug的情况。需要谨慎选择和测试插件。
  6. 数据绑定问题:在小程序中,数据绑定和双向数据绑定可能会出现奇怪的问题,如数据未正确更新或更新延迟。
  7. 组件问题:自定义组件或小程序内置组件可能存在不明显的问题,可能需要检查组件的配置和事件处理。
  8. 性能问题:小程序性能问题可能会导致应用运行缓慢或崩溃,需要进行性能分析和优化。
  9. 跳转问题:页面跳转和路由管理可能会出现问题,如页面栈溢出或跳转失败。需要仔细检查路由配置和跳转逻辑。
  10. 数据安全问题:小程序中的数据安全问题可能会导致用户信息泄漏或者非法访问。需要进行数据加密和验证。

3、Electron怎么进行通讯的,内存占用过大如何解决

Electron是一个用于构建跨平台桌面应用程序的框架,它允许使用Web技术(HTML、CSS和JavaScript)来创建本地桌面应用。在Electron中,主要的通信方式是使用Electron的主进程和渲染进程之间的IPC(Inter-Process Communication)来进行通讯。同时,对于内存占用过大的问题,你可以采取一些措施来进行优化。

通信方式

  1. 主进程与渲染进程通信

    • 主进程和渲染进程之间的通信可以使用Electron提供的ipcMainipcRenderer模块来实现。这使得你可以在渲染进程中向主进程发送消息,也可以在主进程中向渲染进程发送消息。

    在主进程中:

    const { ipcMain } = require('electron');
    
    ipcMain.on('message-from-renderer', (event, arg) => {
      console.log(arg); // 处理渲染进程发来的消息
      event.sender.send('message-to-renderer', 'Hello from main process');
    });
    

    在渲染进程中:

    const { ipcRenderer } = require('electron');
    
    ipcRenderer.send('message-from-renderer', 'Hello from renderer process');
    
    ipcRenderer.on('message-to-renderer', (event, arg) => {
      console.log(arg); // 处理主进程发来的消息
    });
    
  2. 渲染进程之间通信

    • 如果你的应用中有多个渲染进程,它们之间也可以通过ipcRenderer进行通信。这可以用于共享数据或协调操作。

    渲染进程A:

    javascriptCopy code
    const { ipcRenderer } = require('electron');
    
    ipcRenderer.sendTo('renderer-process-B', 'message-from-renderer-A', 'Hello from renderer A');
    

    渲染进程B:

    const { ipcRenderer } = require('electron');
    
    ipcRenderer.on('message-from-renderer-A', (event, arg) => {
      console.log(arg); // 处理来自渲染进程A的消息
    });
    

解决内存占用过大问题

  1. 资源释放: 在Electron应用中,确保及时释放不再使用的资源,如关闭不需要的窗口、清理不需要的数据、解除事件监听等。这可以减小内存占用。

  2. 使用垃圾回收: Electron使用了Node.js的垃圾回收机制,但你可以手动触发垃圾回收,以帮助释放不再使用的内存。在主进程和渲染进程中,可以使用global.gc()手动触发垃圾回收。

  3. 内存监控和性能分析: 使用工具如Chrome DevTools或Electron提供的性能工具来监控内存使用情况,并查找内存泄漏问题。

  4. 使用Electron 5.0及以上版本: 较新版本的Electron通常包含更多的性能和内存优化。确保使用最新版本的Electron框架。

  5. 优化渲染进程: 将渲染进程中的代码和资源优化,减少不必要的DOM操作和资源加载,以降低内存占用。

  6. 使用Electron-builder配置: 在构建Electron应用时,使用Electron-builder等工具,可以在构建配置中指定一些优化选项,以减小应用体积和内存占用。

  7. 检查第三方模块: 某些第三方模块可能会导致内存占用过大。确保你的应用中使用的第三方模块是经过优化的,并避免不必要的依赖。

  8. 压缩资源: 将JavaScript和CSS文件进行压缩,以减小文件大小和内存占用。

  9. 合理使用WebContents: 对于创建多个WebContents对象的情况,确保只创建必要数量的WebContents,不要过度创建,以减小内存占用。

  10. 定期性能分析: 定期进行性能分析和内存监控,以及修复潜在的内存泄漏问题,确保应用的内存占用得以控制。

4、多个人如何保证代码进行协同开发

  1. 版本控制系统: 使用版本控制系统(如Git)来管理代码。每个开发人员都应该在自己的分支上工作,然后通过合并请求或合并操作来集成他们的更改。

  2. 清晰的开发规范: 建立一致的代码规范,包括缩进、命名规则、注释等。这有助于团队成员编写易于理解和维护的代码。

  3. 协同工具: 使用协同工具,如GitHub、GitLab、Bitbucket等,来跟踪任务、问题和合并请求。这些工具提供了代码审查、问题追踪、项目管理等功能,有助于团队协同工作。

  4. 代码审查: 进行代码审查,确保代码质量。每个更改都应该经过其他团队成员的审核,以发现潜在的问题和提供反馈。

  5. 持续集成: 集成持续集成工具,如Travis CI、Jenkins等,以确保代码的自动构建和测试。每次提交都应该自动运行测试,以捕获问题并防止引入错误。

  6. 分工明确: 划分任务和职责,确保每个团队成员知道他们应该负责的部分。这可以防止冲突和混淆。

  7. 定期的团队会议: 定期召开团队会议,讨论项目的进展、问题和下一步计划。这有助于团队成员保持协作和协调。

  8. 文档和注释: 编写详细的文档和注释,以便其他团队成员可以理解代码的目的和使用方式。文档对于大型项目尤为重要。

  9. 集中式存储: 确保项目文件和资源都集中存储在一个地方,如代码仓库和云存储,以便团队成员能够轻松访问和共享。

  10. 交流与沟通: 保持良好的沟通,及时回复消息,讨论问题并提供反馈。使用即时消息工具、电子邮件和会议来进行有效的沟通。

  11. 冲突解决: 准备好解决冲突的计划。当不同团队成员之间产生分歧时,应该有一种方法来协调和解决问题。

  12. 自动化工作流程: 自动化重复性工作流程,如构建、测试、部署和代码检查,以减少手动工作,提高效率。

  13. 培训和学习: 为团队成员提供机会学习新技术和工具,以确保他们跟上最新的开发趋势。

5、怎么根据产品需求进行技术选型的,应该注意什么

  1. 了解产品需求: 首先,深入了解产品需求。与产品经理、设计师和利益相关者进行充分的讨论和沟通,以确保对需求有清晰的理解。

  2. 功能和性能需求: 根据产品需求确定所需的功能和性能指标。明确哪些功能必须实现,哪些是可选的,以及项目所需的性能要求。

  3. 技术可行性: 评估所选技术栈是否能够满足产品需求。考虑技术的功能、性能、安全性、扩展性和稳定性等方面。

  4. 团队技能: 考虑开发团队的技能和经验。选择团队熟悉的技术,或者提供培训和支持,以确保他们能够成功开发项目。

  5. 生态系统和社区支持: 考虑所选技术栈的生态系统和社区支持。流行的技术通常有更多的第三方库、文档和社区资源可供使用。

  6. 成本和预算: 估算项目的预算,并考虑技术选择对成本的影响。一些技术可能需要更多的许可费用或资源。

  7. 时间表: 确定项目的时间表和交付期限。某些技术栈可能可以更快地开发,而其他技术可能需要更长的时间。

  8. 可维护性和扩展性: 考虑项目的长期可维护性和扩展性。选择易于维护和扩展的技术栈,以便在产品演进时不断改进和添加功能。

  9. 安全性: 评估技术栈的安全性,并确保它能够满足产品的安全需求。特别注意涉及敏感数据的项目。

  10. 兼容性: 考虑所选技术在不同浏览器、操作系统和设备上的兼容性。确保产品能够广泛运行。

  11. 第三方依赖: 考虑项目中可能需要的第三方库和服务。选择那些适用于产品需求的库和服务,并评估其可用性和成本。

  12. 测试和质量保证: 确保所选技术支持适当的测试框架和工具,以确保产品质量。

  13. 许可和法律问题: 检查所选技术的许可协议,确保它们符合项目的法律和合规要求。

  14. 可伸缩性: 如果产品需求中包括处理大量数据或用户,考虑所选技术在应对高负载情况下的可伸缩性。

  15. 备份和灾难恢复: 考虑数据备份和灾难恢复方案,确保数据安全。

字节

1、判断数据类型

typeofinstanceofObject.prototype.toString.call 是 JavaScript 中用于判断数据类型的三种不同方式,它们的原理和用途有一些不同。

  1. typeof 运算符

    • typeof 用于判断一个值的数据类型,返回一个表示数据类型的字符串。
    • 原理:typeof 对基本数据类型(如字符串、数字、布尔、undefined)和函数返回相应的字符串,但对对象(包括数组、函数、日期等)返回 "object"
    • 注意事项:typeof null 返回 "object",这是一个已知的 JavaScript 缺陷。typeof 不能准确区分数组、函数和日期等复杂数据类型。
    javascriptCopy code
    typeof "Hello"; // "string"
    typeof 42;      // "number"
    typeof true;    // "boolean"
    typeof undefined; // "undefined"
    typeof [1, 2, 3]; // "object"
    typeof function() {}; // "function"
    
  2. instanceof 运算符

    • instanceof 用于判断一个对象是否是某个构造函数的实例,返回一个布尔值。
    • 原理:instanceof 基于原型链,检查对象的原型链中是否包含指定构造函数的 prototype 属性。
    • 使用场景:主要用于检查自定义对象的类型,例如,检查是否是数组、自定义类的实例等。
    javascriptCopy code
    const arr = [1, 2, 3];
    arr instanceof Array; // true
    
  3. Object.prototype.toString.call 方法:typeString.slice(8, -1)

    • Object.prototype.toString.call 是一个用于获取对象的内部属性 [[Class]] 的方法,可以用于精确判断对象的类型。
    • 原理:该方法返回一个表示对象类型的字符串,格式为 "[object 数据类型]",其中数据类型可以是 "Object"、"Array"、"Function" 等。
    • 使用场景:通常用于更精确地判断对象的类型,尤其是用于判断对象是否为数组,因为它能够准确区分数组和其他对象。
    javascriptCopy code
    Object.prototype.toString.call("Hello"); // "[object String]"
    Object.prototype.toString.call(42);      // "[object Number]"
    Object.prototype.toString.call(true);    // "[object Boolean]"
    Object.prototype.toString.call([1, 2, 3]); // "[object Array]"
    Object.prototype.toString.call(function() {}); // "[object Function]"
    

总的来说,typeof 适用于基本数据类型和函数的简单检测,但对复杂数据类型不够准确。instanceof 主要用于检查对象是否是特定构造函数的实例,适用于自定义对象。Object.prototype.toString.call 是一种更精确的方式来判断对象的类型,特别适合检测数组等复杂数据类型。

2、怎么判断一个对象是数组、为什么用它判断、它的工作原理是什么

在JavaScript中,要判断一个对象是否为数组,通常可以使用Array.isArray()方法。这种方法是用来检测一个对象是否为数组的最可靠和推荐的方式。例如:

javascriptCopy code
const arr = [1, 2, 3];
const obj = { key: 'value' };

console.log(Array.isArray(arr)); // true
console.log(Array.isArray(obj)); // false

使用Array.isArray()来判断对象是否为数组有以下优点:

  1. 可靠性Array.isArray()是一种非常可靠的方法,因为它专门用于判断对象是否为数组。它不会受到原型链的干扰,不会误判其他对象为数组。
  2. 跨环境兼容性Array.isArray()是ES5规范引入的,因此在现代的JavaScript环境中广泛支持。它可以用于浏览器、Node.js以及其他JavaScript环境。

Array.isArray()的工作原理很简单:它检查对象的[[Class]]内部属性,如果该属性的值是"Array",则返回true,否则返回false。这种方法不受原型链的干扰,因为只检查[[Class]]属性。

除了Array.isArray(),还有其他方法来检测对象是否为数组,如使用instanceof操作符或检查对象的constructor属性。然而,这些方法都有潜在的问题,可能会受到原型链的干扰或不适用于跨环境的情况。因此,推荐使用Array.isArray()来判断对象是否为数组。

3、说一下this指向、构造函数里的指向、内部是怎么实现的、箭头函数的this指向

  1. 全局上下文中

    • 在全局上下文中(不在任何函数内部),this 指向全局对象,通常是浏览器环境中的 window 对象。
  2. 函数中

    • 在函数中,this 的指向取决于函数是如何调用的。
    • 如果函数是作为普通函数调用的,this 指向全局对象。
    • 如果函数是作为对象的方法调用的,this 指向调用该方法的对象。
    • 如果函数是作为构造函数(使用 new 关键字)调用的,this 指向新创建的对象。
    • 如果函数是使用 callapplybind 方法调用的,this 指向传递给这些方法的对象。
  3. 箭头函数中

    • 箭头函数没有自己的 this,它继承了父级作用域中的 this 值。
    • 这意味着箭头函数的 this 指向与外部函数或上下文中的 this 相同。

以下是一些示例,以帮助理解 this 的不同情况:

// 全局上下文中
console.log(this === window); // 在浏览器环境中为 true

// 函数中
function normalFunction() {
  console.log(this === window); // true,因为是作为普通函数调用
}

const obj = {
  method: function() {
    console.log(this === obj); // true,作为对象的方法调用
  }
};

const constructorFunction = function() {
  this.isConstructor = true;
};
const instance = new constructorFunction();
console.log(instance.isConstructor); // true,作为构造函数调用

const person = {
  name: 'John',
  greet: function() {
    console.log(`Hello, ${this.name}!`);
  }
};
person.greet(); // "Hello, John!"

// 箭头函数中
const arrowFunction = () => {
  console.log(this === window); // true,继承外部函数的this
};
arrowFunction();

总之,this 的指向在不同的上下文和调用方式下有不同的行为,需要根据具体情况来理解和使用。箭头函数的 this 是其最大的特点之一,它能够继承外部作用域中的 this 值,不会发生指向的变化。

4、Promise的几种状态、更改了状态后还可以改变吗?

  1. Pending(等待) :Promise 对象初始的状态,表示异步操作尚未完成,仍在进行中。这是 Promise 对象创建后的默认状态。
  2. Fulfilled(已完成) :Promise 对象的状态变为 Fulfilled 表示异步操作成功完成。当 Promise 进入这个状态时,它会触发 then 方法中的成功回调函数。
  3. Rejected(已拒绝) :Promise 对象的状态变为 Rejected 表示异步操作失败。当 Promise 进入这个状态时,它会触发 then 方法中的失败回调函数。

一旦 Promise 对象的状态从 Pending 变为 Fulfilled 或 Rejected,它就不会再次改变状态。换句话说,Promise 的状态一旦确定,就不可逆转。这是 Promise 的一个重要特点,确保了异步操作的结果只能被获取一次,避免了竞态条件和混乱的状态管理。

5、Promise可以链式调用吗?怎么调用?为什么可以?

Promise 支持链式调用,这是 Promise 的一个重要特性之一,使得异步操作可以更清晰地表达和组织。链式调用是通过 .then() 方法实现的,它返回一个新的 Promise 对象,可以在其上继续调用 .then() 或其他 Promise 方法。

Promise 的链式调用的优势在于它可以处理异步操作的顺序执行,避免了回调地狱(Callback Hell),使代码更加清晰和可读。通过链式调用,您可以在每个 .then() 中指定成功和失败的处理逻辑,并按顺序依赖于前一个 Promise 的结果。

6、手写promise.all,讲一下逻辑

function myPromiseAll(promises) {
  return new Promise((resolve, reject) => {
    const results = new Array(promises.length); // 用于存储每个 Promise 的结果
    let completed = 0; // 已完成的 Promise 数量

    // 检查是否所有 Promise 都已完成,如果是则触发外部 Promise 的解决
    const checkCompletion = () => {
      if (completed === promises.length) {
        resolve(results);
      }
    };

    // 遍历传入的 Promise 数组
    promises.forEach((promise, index) => {
      // 对每个 Promise 添加成功处理函数
      promise
        .then((value) => {
          results[index] = value; // 存储成功的结果
          completed++; // 增加已完成的 Promise 数量
          checkCompletion(); // 检查是否所有 Promise 都已完成
        })
        .catch(reject); // 如果有一个 Promise 失败,则拒绝外部 Promise
    });
  });
}

7、手写深拷贝,讲一下逻辑

function deepClone(obj) {
  // 如果传入的对象是基本类型(null、undefined、数字、字符串、布尔等)或者是 null,则直接返回它本身
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 如果传入的对象是数组,创建一个新数组,对数组中的每个元素递归调用 deepClone
  if (Array.isArray(obj)) {
    const cloneArr = [];
    for (let i = 0; i < obj.length; i++) {
      cloneArr[i] = deepClone(obj[i]);
    }
    return cloneArr;
  }

  // 如果传入的对象是普通对象,创建一个新对象,遍历原对象的属性,并递归调用 deepClone
  if (typeof obj === 'object') {
    const cloneObj = {};
    for (const key in obj) {
      // 使用 hasOwnProperty 检查属性是否是对象自身的属性,而不是继承自原型链的属性
      if (obj.hasOwnProperty(key)) {
        cloneObj[key] = deepClone(obj[key]);
      }
    }
    return cloneObj;
  }
}

8、Vue中为什么data是一个函数

组件中的 data 写成一个函数,数据以函数返回值形式定义。这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果。

9、Vue2和Vue3有什么区别

  • 源码组织方式变化:使用 TS 重写

  • 支持 Composition API:基于函数的API,更加灵活组织组件逻辑(vue2用的是options api)

  • 响应式系统提升:Vue3中响应式数据原理改成proxy,可监听动态新增删除属性,以及数组变化

  • 编译优化:vue2通过标记静态根节点优化diff,Vue3 标记和提升所有静态根节点,diff的时候只需要对比动态节点内容

  • 打包体积优化:移除了一些不常用的api(inline-template、filter)

  • 生命周期的变化:使用setup代替了之前的beforeCreate和created

  • Vue3 的 template 模板支持多个根标签

  • Vuex状态管理:创建实例的方式改变,Vue2为new Store , Vue3为createStore

  • Route 获取页面实例与路由信息:vue2通过this获取router实例,vue3通过使用 getCurrentInstance/ userRoute和userRouter方法获取当前组件实例

  • Props 的使用变化:vue2 通过 this 获取 props 里面的内容,vue3 直接通过 props

  • 父子组件传值:vue3 在向父组件传回数据时,如使用的自定义名称,如 backData,则需要在 emits 中定义一下

中科天玑

1、双向绑定原理

Vue响应式的原理就是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:

Observe(被劫持的数据对象) Compile(vue的编译器) Wather(订阅者) Dep(用于收集Watcher订阅者们)

  1. 需要给Observe的数据对象进行递归遍历,包括子属性对象的属性,都加上settergetter这样的属性,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。

  2. Compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

  3. Watcher订阅者是ObserverCompile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(Dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice() 通知时,能调用自身的update() 方法,并触发Compile中绑定的回调,则功成身退。

  4. MVVM作为数据绑定的入口,整合ObserverCompileWatcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起ObserverCompile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

2、怎么劫持、怎么监听、怎么拿数据去改变视图,怎么收集这些东西、有一个类叫什么

  1. 数据劫持:Vue 2 通过 Object.defineProperty 来劫持数据对象的属性。当一个 Vue 实例被创建时,它会将 data 选项中的属性转化为 getter 和 setter,这使得 Vue 能够追踪属性的读写操作。
  2. 数据监听:Vue 实例在创建时,会通过 Observer 类创建一个与 data 对象对应的响应式对象。这个响应式对象中包含了每个属性的 getter 和 setter,用于监听属性的变化。当属性被访问或修改时,Vue 会自动触发 getter 和 setter,使 Vue 能够知道数据何时被访问和修改。
  3. 数据改变视图:一旦 Vue 知道数据被修改,它会自动更新视图。这是通过虚拟 DOM 和模板编译实现的。Vue 将虚拟 DOM 与旧的虚拟 DOM 进行比较,找出差异,然后只更新需要更新的部分,以提高性能。
  4. 收集依赖和触发更新:Vue 在 getter 中会收集依赖,即将依赖的 Watcher 对象添加到依赖列表中。当属性被修改时,setter 会触发更新,将变化通知依赖的 Watcher 对象,以便更新视图。
  5. Watcher 和 Dep:Vue 中还有两个核心类,分别是 WatcherDepWatcher 对象表示一个依赖,它会在数据变化时执行回调函数,通常用于更新视图。Dep 对象是一个依赖管理器,用于管理多个 Watcher 对象和属性之间的关系。每个属性都有一个关联的 Dep 对象,当属性的 getter 被访问时,会将当前 Watcher 添加到 Dep 中,当属性被修改时,Dep 会通知所有依赖的 Watcher 对象执行回调函数。

3、虚拟dom的作用是什么

  1. 性能优化:虚拟 DOM 减少了直接操作真实 DOM 的性能开销,通过差异更新提高了性能。
  2. 跨平台兼容性:虚拟 DOM 具有跨浏览器和平台的可移植性,无需编写兼容性代码。
  3. 提高开发效率:虚拟 DOM 简化了前端开发,提供了高级抽象,提高了开发效率。
  4. 自动化更新:虚拟 DOM 可自动检测数据变化并更新视图,无需手动处理 DOM 操作和数据同步。

不使用真实 DOM 的原因包括性能问题、复杂性、兼容性问题和调试困难。虚拟 DOM 提供了更高效、简化的前端开发方式,使前端开发更容易维护、跨平台兼容,并提高性能。

4、Ast和虚拟dom有什么关系

  • AST 和虚拟 DOM 在某种程度上都用于描述页面的结构。AST 描述了代码的结构,而虚拟 DOM 描述了页面的结构。
  • 一些前端框架和库,如 React,使用虚拟 DOM 来渲染页面。在这些情况下,虚拟 DOM 可能会使用 AST 来构建虚拟 DOM 树,从而实现视图和状态的映射。

尽管 AST 和虚拟 DOM 有一些相似之处,但它们的主要用途和上下文不同。AST 主要用于静态分析和代码处理,而虚拟 DOM 主要用于优化页面渲染性能。虚拟 DOM 在前端框架中广泛使用,以减少 DOM 操作的次数,而 AST 主要用于工具和编译器,用于代码分析和转换。

5、$nexTick的作用,什么时候用、为什么可以这样、底层怎么实现的、看过这款源码吗

其实一句话就可以把$nextTick这个东西讲明白:就是你放在$nextTick当中的操作不会立即执行,而是等数据更新、DOM更新完成之后再执行,这样我们拿到的肯定就是最新的了。

Vue的响应式并不是只数据发生变化之后,DOM就立刻发生变化,而是按照一定的策略进行DOM的更新。

DOM更新有两种选择,一个是在本次事件循环的最后进行一次DOM更新,另一种是把DOM更新放在下一轮的事件循环当中。Vue优先选择第一种,只有当环境不支持的时候才触发第二种机制。

虽然性能上提高了很多,但这个时候问题就出现了。我已经把数据改掉了,但是它的更新异步的,而我在获取的时候,它还没有来得及改,这个时候就需要用到nextTick

原理:

Vue 的 nextTick 其本质是对 JavaScript 执行原理 EventLoop 的一种应用。

  • Vue2刚开始的时候, $nextTick是宏任务(setTimeout),但是宏任务的性能太差。

  • 后来改成了微任务Mutation Observer,但是还是有一些问题:

    • 速度太快了,在一些特殊场景下,DOM还没更新就去获取了
    • 兼容性不好,很多浏览器不支持
  • 后来又更新成了微宏并行阶段:先判断是否支持Mutation Observer,如果支持就使用,否则使用宏任务

  • Vue2.5版本之后,修复了微任务的那些问题,目前最新的$nextTick采用的是纯微任务。

由于Vue的DOM操作是异步的,所以,在上面的情况中,就要将DOM2获取数据的操作写在$nextTick中。

16.png

所以,在以下情况下,会用到nextTick:

  • 在数据变化后执行的某个操作,而这个操作需要使用随数据变化而变化的DOM结构的时候,这个操作就需要方法在nextTick()的回调函数中。
  • 在vue生命周期中,如果在created()钩子进行DOM操作,也一定要放在nextTick()的回调函数中。

因为在created()钩子函数中,页面的DOM还未渲染,这时候也没办法操作DOM,所以,此时如果想要操作DOM,必须将操作的代码放在nextTick()的回调函数中。

6、Keeplive用过吗?它是怎么实现的

keep-alive 组件是 vue 的内置组件,用于缓存内部组件实例。这样做的目的在于,keep-alive 内部的组件切回时,不用重新创建组件实例,而直接使用缓存中的实例,一方面能够避免创建组件带来的开销,另一方面可以保留组件的状态。

keep-alive 具有 includeexclude 属性,通过它们可以控制哪些组件进入缓存。另外它还提供了 max 属性,通过它可以设置最大缓存数,当缓存的实例超过该数时,vue 会移除最久没有使用的组件缓存。

受keep-alive的影响,其内部所有嵌套的组件都具有两个生命周期钩子函数,分别是 activateddeactivated,它们分别在组件激活和失活时触发。第一次 activated 触发是在 mounted 之后

在具体的实现上,keep-alive 在内部维护了一个 key 数组和一个缓存对象

// keep-alive 内部的声明周期函数
created () {
    this.cache = Object.create(null)
    this.keys = []
}

key 数组记录目前缓存的组件 key 值,如果组件没有指定 key 值,则会为其自动生成一个唯一的 key 值

cache 对象以 key 值为键,vnode 为值,用于缓存组件对应的虚拟 DOM

在 keep-alive 的渲染函数中,其基本逻辑是判断当前渲染的 vnode 是否有对应的缓存,如果有,从缓存中读取到对应的组件实例;如果没有则将其缓存。

当缓存数量超过 max 数值时,keep-alive 会移除掉 key 数组的第一个元素。

7、说下原型原型链

  • 每个对象都有一个 __proto__ 属性,该属性指向自己的原型对象
  • 每个构造函数都有一个 prototype 属性,该属性指向实例对象的原型对象
  • 原型对象里的 constructor 指向构造函数本身 image-20210812161401493

每个对象都有自己的原型对象,而原型对象本身,也有自己的原型对象,从而形成了一条原型链条。

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

8、场景题:找原型链

字节

1、算法题:按照期望的调用方式实现Typewriter类,其中包括startstopreset方法

2、算法题:合并整数,返回下角标

3、闭包的是什么、用途是什么、哪里用了闭包、优缺点

闭包产生的本质

当前环境中存在指向父级作用域的引用

什么是闭包

闭包是一种特殊的对象,它由两部分组成:执行上下文(代号 A),以及在该执行上下文中创建的函数 (代号 B),当 B 执行时,如果访问了 A 中变量对象的值,那么闭包就会产生,且在 Chrome 中使用这个执行上下文 A 的函数名代指闭包。

一般如何产生闭包

  • 返回函数
  • 函数当做参数传递

闭包的应用场景

  • 柯里化 bind
  • 模块

4、Map可能会导致内存泄漏,讲一下为什么

Map 对象的键和值是强引用,这意味着只要 Map 对象存在并引用着某个对象,该对象就不会被垃圾回收。这可以在某些情况下导致内存泄漏,特别是在以下情况下:

  1. 长期持有引用的对象:如果你向 Map 中添加对象,然后不再需要这些对象,但忘记从 Map 中删除它们,那么这些对象将继续存在于内存中,不会被垃圾回收。

    const myMap = new Map();
    const someObject = { data: 'some data' };
    myMap.set('key', someObject);
    // 忘记从 Map 中删除 someObject
    
  2. 循环引用:如果 Map 中的对象之间存在循环引用,这将阻止垃圾回收器正确回收这些对象,因为它们相互引用并且仍然在 Map 中被引用。

    const obj1 = { data: 'object 1' };
    const obj2 = { data: 'object 2' };
    const myMap = new Map();
    myMap.set('key1', obj1);
    myMap.set('key2', obj2);
    obj1.circularRef = obj2;
    obj2.circularRef = obj1;
    // 循环引用,可能导致内存泄漏
    

为避免这种情况下的内存泄漏,你应该定期检查 Map 中的对象,并在不再需要它们时从 Map 中删除它们,或者确保避免循环引用。如果对象不再被引用,并且没有其他引用它的变量,垃圾回收器将最终回收这些对象,释放内存。这是内存管理的一部分,需要特别注意,以确保应用程序不会因未释放的内存而导致性能问题或内存泄漏。

5、Webpack 5 中的 Module Federation

一文通透讲解webpack5 module federation

6、var、const、let

  • var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升。

  • let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。

  • const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升,不可以重复声明。

7、工程化中,webpack常用的loader、plugin

webpack

8、V8引擎机制

v8是一个接受js代码,编译代码然后执行的c++程序,编译后的代码可以执行在多种操作系统上,比如谷歌浏览器的js引擎是v8,node.js的运行环境是v8引擎,electron的底层引擎也是v8

主要负责以下工作: 编译和执行js代码,处理调用栈,内存的分配和垃圾回收

js源代码通过parser(解析器),解析成抽象语法树AST,lgnition(基准解释器)将AST揭示成bytecode(字节码),然后AST就被清除掉了,为了释放内存。然后解释器执行bytecode字节码

在代码的运行中,解释器收集了很多可优化信息,比如变量的类型,那些函数执行频率高,这些信息发送给新的优化编译器(TruboFan),会编译出新的机器代码,会做出如下优化

1、函数只声明未调用,不会解析生成ast

2、函数只被调用一次,bytecode智慧被解释执行

3、函数被调用多次,可能会被标记为热点函数,可能会被编译成机器代码

4、优化后的机器代码也可能会被反编译成bytecode,比如变量类型,一开始是数字,然后被标记为热点函数,就回默认为数组类型,这时候传入字符串就会出现反编译成bytecode

360

1、伪类和伪元素

伪类(Pseudo-class)

  1. 将特殊的效果添加到特定选择器上。它是已有元素上添加类别的,不会产生新的元素。

  2. 伪类以冒号(:)开头,如 :hover:active:visited

    • :hover:选择鼠标悬停在元素上的状态。
    • :first-child:选择作为其父元素的第一个子元素的元素。
    • :nth-child(n):选择第 n 个子元素。

伪元素(Pseudo-element)

  1. 伪元素:在内容元素的前后插入额外的元素或样式,但是这些元素实际上并不在文档中生成。它们只在外部显示可见,但不会在文档的源代码中找到它们,因此,称为“伪”元素

  2. 伪元素以双冒号(::)开头,如 ::before::after

    • ::before:在元素内容之前插入内容。
    • ::after:在元素内容之后插入内容。
    • ::first-line:选择元素的第一行文本。

区别:

  • 伪类选择的是元素的状态或属性,而伪元素选择的是元素的特定部分。
  • 伪类以单冒号开头,伪元素以双冒号开头。
  • 伪元素通常用于创建额外的内容,而伪类用于选择元素的状态或位置。

2、说说两种盒模型以及区别盒模型都是由四个部分组成的,分别是margin、border、padding和content。

标准盒模型和IE盒模型的区别在于设置width和height时,所对应的范围不同:

  • 标准盒模型的width和height属性的范围只包含了content,
  • IE盒模型的width和height属性的范围包含了border、padding和content。

可以通过修改元素的box-sizing属性来改变元素的盒模型:

  • box-sizing: content-box表示标准盒模型(默认值)
  • box-sizing: border-box表示IE盒模型(怪异盒模型)

3、介绍下bfc及其应用

BFC: 块格式化上下文(Block Formatting Context,BFC)是Web页面的可视化CSS渲染的一部分,是布局过程中生成块级盒子的区域,也是浮动元素与其他元素的交互限定区域。

通俗来讲:BFC是一个独立的布局环境,可以理解为一个容器,在这个容器中按照一定规则进行物品摆放,并且不会影响其它环境中的物品。如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。

创建BFC的条件:

  • 根元素:body;
  • 元素设置浮动:float 除 none 以外的值;
  • 元素设置绝对定位:position (absolute、fixed);
  • display 值为:inline-block、table-cell、table-caption、flex等;
  • overflow 值为:hidden、auto、scroll;

BFC的特点:

  • 垂直方向上,自上而下排列,和文档流的排列方式一致。
  • 在BFC中上下相邻的两个容器的margin会重叠
  • 计算BFC的高度时,需要计算浮动元素的高度
  • BFC区域不会与浮动的容器发生重叠
  • BFC是独立的容器,容器内部元素不会影响外部元素
  • 每个元素的左margin值和容器的左border相接触

BFC的作用:

  • 解决margin的重叠问题:由于BFC是一个独立的区域,内部的元素和外部的元素互不影响,将两个元素变为两个BFC,就解决了margin重叠的问题。
  • 解决高度塌陷的问题:在对子元素设置浮动后,父元素会发生高度塌陷,也就是父元素的高度变为0。解决这个问题,只需要把父元素变成一个BFC。常用的办法是给父元素设置overflow:hidden
  • 创建自适应两栏布局:可以用来创建自适应两栏布局:左边的宽度固定,右边的宽度自适应。左侧设置float:left,右侧设置overflow: hidden。这样右边就触发了BFC,BFC的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠,实现了自适应两栏布局。

4、介绍下粘性布局(sticky

position 中的 sticky 值是 CSS3 新增的,设置了 sticky 值后,在屏幕范围(viewport)时该元素的位置并不受到定位影响(设置是top、left等属性无效),当该元素的位置将要移出偏移范围时,定位又会变成fixed,根据设置的left、top等属性成固定位置的效果。

sticky 属性值有以下几个特点:

  • 该元素并不脱离文档流,仍然保留元素原本在文档流中的位置。
  • 当元素在容器中被滚动超过指定的偏移值时,元素在容器内固定在指定位置。亦即如果你设置了top: 50px,那么在sticky元素到达距离相对定位的元素顶部50px的位置时固定,不再向上移动。
  • 元素固定的相对偏移是相对于离它最近的具有滚动框的祖先元素,如果祖先元素都不可以滚动,那么是相对于viewport来计算元素的偏移量

5、link和@import的区别

两者都是外部引用CSS的方式,它们的区别如下:

  • link是XHTML标签,除了加载CSS外,还可以定义RSS等其他事务;@import属于CSS范畴,只能加载CSS。
  • link引用CSS时,在页面载入时同时加载;@import需要页面网页完全载入以后加载。
  • link是XHTML标签,无兼容问题;@import是在CSS2.1提出的,低版本的浏览器不支持。
  • link支持使用Javascript控制DOM去改变样式;而@import不支持。

6、说一下对this的理解,箭头函数的this,箭头函数有没有prototype,有没有arguments

  • 在函数体中,非显式或隐式地简单调用函数时,在严格模式下,函数内的 this 会被绑定到 undefined 上,在非严格模式下则会被绑定到全局对象 window/global 上。

  • 一般使用 new 方法调用构造函数时,构造函数内的 this 会被绑定到新创建的对象上。

  • 一般通过 call/apply/bind 方法显式调用函数时,函数体内的 this 会被绑定到指定参数的对象上。

  • 一般通过上下文对象调用函数时,函数体内的 this 会被绑定到该对象上。

  • 在箭头函数中,this 的指向是由外层(函数或全局)作用域来决定的。

箭头函数(Arrow Functions)在语法上有一些特殊之处:

  1. 没有 prototype 属性

    • 箭头函数没有自己的 prototype 属性。它们不可以被用作构造函数,因此不能用于创建对象实例。
    • 箭头函数本质上是匿名函数表达式,没有原型对象。
  2. 没有 arguments 对象

    • 箭头函数也没有自己的 arguments 对象。它们继承自外围函数的 arguments 对象。
    • 这意味着在箭头函数内部访问 arguments 时,实际上访问的是外围函数的 arguments

7、requestAnimationFrame在页面中的执行时机

requestAnimationFrame 是用于执行动画和其他高性能操作的浏览器API,它在浏览器的重绘周期中执行回调函数。它的执行时机如下:

  1. 在下一帧重绘前执行

    • 当你调用 requestAnimationFrame(callback) 时,浏览器会在下一帧的重绘之前执行回调函数 callback
    • 这意味着它在浏览器的渲染周期中执行,通常以每秒60次的速度执行(取决于浏览器和设备的性能)。
  2. 与屏幕刷新同步

    • requestAnimationFrame 会与浏览器的屏幕刷新同步,以确保动画和渲染的平滑性。
    • 这有助于避免页面闪烁、撕裂和性能问题。
  3. 自动暂停和恢复

    • 如果页面被切换到非激活状态(例如,切换到另一个标签或最小化窗口),requestAnimationFrame 会自动暂停,以节省资源。
    • 当页面重新激活时,它会自动恢复。
  4. 循环执行

    • 你可以使用 requestAnimationFrame 来创建动画循环,不断更新和渲染画面。
    • 在回调函数中再次调用 requestAnimationFrame 可以实现递归执行,创建动画循环。
function animate() {
  // 执行动画逻辑

  requestAnimationFrame(animate); // 循环调用,创建动画循环
}

animate(); // 启动动画

总之,requestAnimationFrame 提供了一种在适当的时机执行高性能操作的方式,以确保良好的动画效果和用户体验。

8、new一个对象之后发生了啥

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

new 关键字会进行如下的操作:
步骤 1:创建一个空的简单 JavaScript 对象,即 { } ;
步骤 2:链接该对象到另一个对象(即设置该对象的原型对象);
步骤 3:将步骤 1 新创建的对象作为 this 的上下文;
步骤 4:如果该函数没有返回对象,则返回 this

9、继承有哪些,都有缺点

JS 的继承随着语言的发展,从最早的对象冒充到现在的圣杯模式,涌现出了很多不同的继承方式。每一种新的继承方式都是对前一种继承方式不足的一种补充。

  1. 原型链继承
  • 重点:让新实例的原型等于父类的实例。

  • 特点:实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!)

  • 缺点:

    • 1、新实例无法向父类构造函数传参。
    • 2、继承单一。
    • 3、所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
  1. 借用构造函数继承
  • 重点:用 call( )  和 apply( )  将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))

  • 特点:

    • 1、只继承了父类构造函数的属性,没有继承父类原型的属性。
    • 2、解决了原型链继承缺点1、2、3。
    • 3、可以继承多个构造函数属性(call多个)。
    • 4、在子实例中可向父实例传参。
  • 缺点:

    • 1、只能继承父类构造函数的属性
    • 2、无法实现构造函数的复用。(每次用每次都要重新调用)
    • 3、每个新实例都有父类构造函数的副本,臃肿。
  1. 组合模式
  • 重点:结合了两种模式的优点,传参和复用
  • 特点:
    • 1、可以继承父类原型上的属性,可以传参,可复用。
    • 2、每个新实例引入的构造函数属性是私有的。
  • 缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。
  1. 寄生组合式继承
  • 重点:修复了组合继承的问题
Child.prototype = Object.create(Parent.prototype);

实际上创建了一个新的对象,该对象的原型是 Parent.prototype。这个新的对象成为子类(Child)的原型,从而实现了原型链继承,子类可以访问父类的方法和属性。

通过这种方式,子类(Child)继承了父类(Parent)的原型,同时避免了调用父构造函数两次,提高了性能。这个新的对象充当了中间桥梁,使继承关系得以建立。

10、写一个倒计时器

<!DOCTYPE html>
<html>
<head>
  <title>倒计时示例</title>
</head>
<body>
  <div id="countdown"></div>
  <input type="number" id="seconds" placeholder="输入秒数">
  <button onclick="startCountdown()">开始倒计时</button>
  <script>
    function startCountdown() {
      var secondsInput = document.getElementById('seconds');
      var countdownDiv = document.getElementById('countdown');

      var totalSeconds = parseInt(secondsInput.value);
      if (isNaN(totalSeconds) || totalSeconds <= 0) {
        alert('请输入有效的秒数。');
        return;
      }

      var countdown = setInterval(function() {
        if (totalSeconds <= 0) {
          clearInterval(countdown);
          countdownDiv.innerHTML = '倒计时结束';
        } else {
          var days = Math.floor(totalSeconds / 86400);
          var hours = Math.floor((totalSeconds % 86400) / 3600);
          var minutes = Math.floor((totalSeconds % 3600) / 60);
          var seconds = totalSeconds % 60;

          countdownDiv.innerHTML = '剩余时间:' + days + '天 ' + hours + '小时 ' + minutes + '分钟 ' + seconds + '秒';
          totalSeconds--;
        }
      }, 1000);
    }
  </script>
</body>
</html>

11、怎么看react的fiber

  • react16之前我们是递归渲染 vdom 的,然后 diff 下来做 patch 的渲染: 先把 vdom 转 fiber,也就是 reconcile 的过程,因为 fiber 是链表,就可以打断,用 schedule 来空闲时调度(requestIdleCallback)就行,最后全部转完之后,再一次性 render,这个过程叫做 commit。

  • 这样,之前只有 vdom 的 render 和 patch,现在却变成了 vdom 转 fiber 的 reconcile,空闲调度 reconcile 的 schedule,最后把 fiber 渲染的 commit 三个阶段。

  • 意义就在于这个可打断上。因为递归渲染 vdom 可能耗时很多,JS 计算量大了会阻塞渲染,而 fiber 是可打断的,就不会阻塞渲染,而且还会在这个过程中把需要用到的 dom 创建好,做好 diff 来确定是增是删还是改。dom 有了,增删改也知道了咋做了,一次性 commit 不就很快了么。

这就是 fiber 架构的意义

  • fiber 是 React16 引入的架构变动,界面通过 vdom 描述,但是不是直接手写 vdom,而是 jsx 编译产生的 render function 之后以后生成的。这样就可以加上 state、props 和一些动态逻辑,动态产生 vdom。

  • vdom 生成之后不再是直接渲染,而是先转成 fiber,这个 vdom 转 fiber 的过程叫做 reconcile。

  • fiber 是一个链表结构,可以打断,这样就可以通过 requestIdleCallback 来空闲调度 reconcile,这样不断的循环,直到处理完所有的 vdom 转 fiber 的 reconcile,就开始 commit,也就是更新到 dom。reconcile 的过程会提前创建好 dom,还会标记出增删改,那么 commit 阶段就很快了。

  • 从之前递归渲染时做 diff 来确定增删改以及创建 dom,提前到了可打断的 reconcile 阶段,让 commit 变得非常快,这就是 fiber 架构的目的和意义。

手写简易版 React 来彻底搞懂 fiber 架构

好未来

1、Map和weakMap

浅析 Map 和 WeakMap 区别以及使用场景

  • Map 的键可以是任意类型,WeakMap 只接受对象作为键(null除外),不接受其他类型的值作为键

  • Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键; WeakMap 的键是弱引用,键所指向的对象可以被垃圾回收,此时键是无效的

  • Map 可以被遍历, WeakMap 不能被遍历

Map使用场景 :

  1. 要添加的键值名和 Object 上的默认键值名冲突,又不想改名时,用 Map
  2. 需要 String 和 Symbol 以外的数据类型做键值时,用 Map
  3. 键值对很多,有需要计算数量时,用 Map
  4. 需要频繁增删键值对时,用 Map

WeakMap使用场景 :

因为 weakMap 不会影响垃圾回收,所以可以用来关联元数据

const m = new Map();
const loginButton = document.querySelector('#login');
m.set(loginButton,{display:true});

当上面代码执行后,登录按钮从DOM树中被删除了,但由于 Map 对节点对象是强引用关系,仍然保存着对按钮的引用,所以会引起内存泄漏

const vm = new WeakMap();
const loginButton = document.querySelector('#login');
vm.set(loginButton,{display:true});

因此可以采用WeakMap当节点删除后,引用计数为0,等待垃圾回收机制回收

2、前端的加载机制,ESmoudle和commonJS

  1. ES Modules (ESM):

    • 静态加载: ESM是静态加载的,这意味着在代码执行之前,JavaScript引擎会分析并确定所有模块的依赖关系。这使得在加载过程中可以在编译时进行优化,提高性能。
    • 导入方式: ESM使用importexport语句来导入和导出模块。导入是静态的,因此导入的模块路径必须是字符串常量,不能是变量。
  2. CommonJS:

    • 动态加载: CommonJS是动态加载的,模块的导入和执行发生在运行时。这意味着模块的依赖可以在代码执行期间动态确定。
    • 导入方式: CommonJS使用require语句来导入模块,模块路径可以是变量或表达式,因为导入是在运行时解析的。

"静态"和"动态"的区别在于模块依赖关系的解析时间。在ESM中,所有依赖关系在编译时就确定了,这使得静态分析和优化成为可能。在CommonJS中,依赖关系在运行时动态解析,因此不能在编译时进行优化。

在浏览器中,ESM是现代JavaScript的标准模块系统,通过<script type="module">标签可以加载ESM模块。CommonJS通常用于Node.js环境,但也可以通过工具(如Webpack或Browserify)在浏览器中使用。

总结,ESM和CommonJS有不同的加载机制和导入方式,ESM是静态加载的,而CommonJS是动态加载的。这些模块系统在不同的环境中使用,具有不同的优点和适用场景。

3、defer和async区别,场景

  1. defer

    • 使用defer属性的脚本会被异步加载,但会按照它们在文档中出现的顺序顺序执行。脚本加载不会阻塞文档的解析,但会在文档解析完成后按照顺序执行,通常在DOMContentLoaded事件之前。
    • 多个脚本使用defer属性时,它们会按照它们在文档中的顺序执行,不会并行加载和执行。
  2. async

    • 使用async属性的脚本会被异步加载,并且在加载完成后立即执行。它们不会按照它们在文档中的顺序执行,而是在下载完成后立即执行。
    • 多个脚本使用async属性时,它们可能以任意顺序执行,因此如果它们之间有依赖关系,可能会导致问题。 总结:
  • 使用defer属性的脚本会按照它们在文档中的顺序顺序执行,但不会阻塞文档的解析。
  • 使用async属性的脚本会按照加载完成的顺序执行,不会按照它们在文档中的顺序。
  • 通常情况下,如果脚本之间没有依赖关系,并且不需要按顺序执行,可以考虑使用async来提高性能。如果脚本之间有依赖关系,使用defer更合适,因为它保证了执行顺序。

4、浏览器缓存

  1. 强缓存(Strong Caching):

    • 强缓存是根据HTTP响应头中的Cache-ControlExpires字段来控制的,通常用于指示浏览器在本地缓存中保存响应的时间。
    • 如果资源的响应具有有效的强缓存,浏览器将不再向服务器发送请求,而是从本地缓存中获取资源,从而减少服务器负载和加速页面加载速度。

    一些常见的强缓存响应头字段:

    • Cache-Control: 用于指定缓存策略,可以包括值如max-ageno-cache
    • Expires: 指定资源的过期日期时间,过期后需要重新请求资源。
  2. 协商缓存(Conditional Caching):

    • 协商缓存是根据HTTP响应头中的Last-ModifiedETag字段来控制的,通常用于验证缓存是否仍然有效。
    • 当浏览器发起请求时,服务器会比较请求中的If-Modified-Since(对应Last-Modified)和If-None-Match(对应ETag)字段与资源的最后修改时间或唯一标识。如果资源没有发生变化,服务器会返回304 Not Modified响应,告诉浏览器可以继续使用缓存。

    一些常见的协商缓存响应头字段:

    • Last-Modified: 指定资源的最后修改时间。
    • ETag: 指定资源的唯一标识。

强缓存和协商缓存通常结合使用,先检查强缓存是否有效,如果无效,则进行协商缓存的验证。这有助于在最大程度上减少对服务器的请求,提高网页加载性能。合理配置缓存策略可以根据资源的特性和需求来实现更好的性能和用户体验。

5、webpack打包流程,原理

  • 1.初始化参数:从配置文件读取与合并参数,得出最终的参数
  • 2.开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,开始执行编译
  • 3.确定入口:根据配置中的 entry 找出所有的入口文件
  • 4.编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 5.完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系
  • 6.输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会
  • 7。输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

6、热更新原理

Webpack 的热更新又称热替换(Hot Module Replacement),缩写为 HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。

HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax 请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp 请求获取该chunk的增量更新。

后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin 来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loadervue-loader 都是借助这些 API 实现 HMR。

7、性能优化

前端性能优化 24 条建议

8、图片类的优化、webp有些机型有问题、怎么办

在使用 WebP 图像时,首先可以检测浏览器是否支持 WebP 格式。如果浏览器支持,就使用 WebP 格式,否则回退到传统的图像格式,如JPEG或PNG。你可以使用以下JavaScript代码来检测支持情况:

9、V8缓存机制

v8是一个接受js代码,编译代码然后执行的c++程序,编译后的代码可以执行在多种操作系统上,比如谷歌浏览器的js引擎是v8,node.js的运行环境是v8引擎,electron的底层引擎也是v8

主要负责以下工作: 编译和执行js代码,处理调用栈,内存的分配和垃圾回收

js源代码通过parser(解析器),解析成抽象语法树AST,lgnition(基准解释器)将AST揭示成bytecode(字节码),然后AST就被清除掉了,为了释放内存。然后解释器执行bytecode字节码

在代码的运行中,解释器收集了很多可优化信息,比如变量的类型,那些函数执行频率高,这些信息发送给新的优化编译器(TruboFan),会编译出新的机器代码,会做出如下优化

1、函数只声明未调用,不会解析生成ast

2、函数只被调用一次,bytecode智慧被解释执行

3、函数被调用多次,可能会被标记为热点函数,可能会被编译成机器代码

4、优化后的机器代码也可能会被反编译成bytecode,比如变量类型,一开始是数字,然后被标记为热点函数,就回默认为数组类型,这时候传入字符串就会出现反编译成bytecode

10、TPC协议和UDP协议区别、应用场景,TCP怎么做到可靠的

TCP协议

  1. 可靠性:TCP是一种可靠的协议。它通过使用确认、序号、重传和拥塞控制等机制来确保数据的可靠传输。如果数据包在传输过程中丢失或损坏,TCP将尝试重新传输数据,直到接收方确认收到正确的数据。
  2. 连接性:TCP是面向连接的协议,它要求在通信之前建立连接,并在通信结束后关闭连接。这意味着数据的传输是双向的,通信双方都可以发送和接收数据。
  3. 有序性:TCP保证数据包的有序性,数据包将按照发送的顺序被接收。
  4. 流控制:TCP支持流控制,可以控制发送方的发送速率,以避免过多数据导致接收方不堪重负。

UDP协议

  1. 可靠性:UDP是一种不可靠的协议。它不提供确认、重传或拥塞控制,因此数据包在传输中可能丢失或乱序。
  2. 连接性:UDP是面向无连接的协议,不需要在通信之前建立连接,也不需要关闭连接。通信是单向的,只能由一个端发送数据。
  3. 无序性:UDP不保证数据包的有序性,数据包可以以任意顺序被接收。
  4. 流控制:UDP不支持流控制,发送方无法控制发送速率。

应用场景

  • TCP适用于需要可靠数据传输和有序性的应用场景,如网页浏览、文件传输、电子邮件等,以确保数据的完整性和正确性。TCP还用于实时音频/视频通信,因为丢失音频/视频数据将导致质量下降。
  • UDP适用于对数据传输速度要求较高,且可以容忍一些数据丢失的应用场景,如实时音频/视频流、在线游戏、DNS查询等。

TCP如何实现可靠性

TCP实现可靠性的关键机制包括:

  1. 确认和重传:TCP接收方会发送确认(ACK)以确认已收到的数据,如果发送方未收到确认,会尝试重新发送数据。
  2. 序号:TCP用序号来标识每个数据包的顺序,确保数据按正确的顺序传输。
  3. 拥塞控制:TCP使用拥塞窗口控制发送速率,以避免网络拥塞和数据丢失。
  4. 流控制:TCP支持流控制,接收方可以通知发送方减慢发送速度,以防止接收方被淹没。

这些机制共同确保了TCP的可靠性,尽管增加了一些开销和延迟,但适用于大多数需要可靠数据传输的应用场景。

11、vue的底层原理,双向绑定

2023高频前端面试题合集之Vue(上篇)

12、从浏览器输入URL到页面过程

参考答案:

  • 浏览器自动补全协议、端口
  • 浏览器自动完成url编码
  • 浏览器根据url地址查找本地缓存,根据缓存规则看是否命中缓存,若命中缓存则直接使用缓存,不再发出请求
  • 通过DNS解析找到服务器的IP地址
  • 浏览器向服务器发出建立TCP连接的申请,完成三次握手后,连接通道建立
  • 若使用了HTTPS协议,则还会进行SSL握手,建立加密信道。使用SSL握手时,会确定是否使用HTTP2
  • 浏览器决定要附带哪些cookie到请求头中
  • 浏览器自动设置好请求头、协议版本、cookie,发出GET请求
  • 服务器处理请求,进入后端处理流程。完成处理后,服务器响应一个HTTP报文给浏览器。
  • 浏览器根据使用的协议版本,以及Connection字段的约定,决定是否要保留TCP连接。
  • 浏览器根据响应状态码决定如何处理这一次响应
  • 浏览器根据响应头中的Content-Type字段识别响应类型,如果是text/html,则对响应体的内容进行HTML解析,否则做其他处理
  • 浏览器根据响应头的其他内容完成缓存、cookie的设置
  • 浏览器开始从上到下解析HTML,若遇到外部资源链接,则进一步请求资源
  • 解析过程中生成DOM树、CSSOM树,然后一边生成,一边把二者合并为渲染树(rendering tree),随后对渲染树中的每个节点计算位置和大小(reflow),最后把每个节点利用GPU绘制到屏幕(repaint)
  • 在解析过程中还会触发一系列的事件,当DOM树完成后会触发DOMContentLoaded事件,当所有资源加载完毕后会触发load事件