2025最新出炉--前端面试题三

1,050 阅读18分钟

问题:说一下平时项目中遇到的一个难点亮点。

在我的上一个项目中,我们遇到了一个难点,即如何优化大型 SPA(单页应用程序)的首屏加载时间。为了解决这个问题,我实现了一个亮点功能:利用 Web Workers 进行代码分割和预加载,同时结合懒加载技术,将首屏需要的资源优先加载。这样不仅显著提升了首屏加载速度,还优化了用户体验。

问题:两个页面跨域名如何读取 localStorage 中的数据,除了 postMessage 还有吗?

除了使用 postMessage,还可以通过 CORS(跨源资源共享)来实现跨域资源共享。具体来说,可以在服务端设置 Access-Control-Allow-Origin 头,允许特定的外部域名访问 localStorage。此外,还可以使用 JSONP(只支持 GET 请求)或者 Websocket 进行跨域通信。

问题:服务端语言平时了解过哪些,nodejs, php, java 之类的?

我了解的服务端语言包括 Node.js、PHP 和 Java。在 Node.js 中,我熟悉 Express 和 Koa 框架;在 PHP 中,我有使用 Laravel 和 Symfony 的经验;在 Java 方面,我了解 Spring Boot 框架,并能够使用它进行基本的 Web 开发。

问题:vue 里面从设置变量到页面更新,中间主要的流程都有哪些?


在 Vue 中,从设置变量到页面更新的主要流程包括:

  1. 数据劫持:通过 Object.defineProperty()或 Proxy 对数据进行劫持。

  2. 依赖收集:当读取数据时,Dep 订阅中心会记录所有依赖于该数据的 Watcher。

  3. 触发更新:当数据发生变化时,通知 Dep 订阅中心,然后 Dep 通知所有 Watcher 进行更新。

  4. 派发更新:Watcher 接收到通知后,调用其 update 方法,进而触发虚拟 DOM 的重新渲染。

  5. diff 算法:比较新旧虚拟 DOM,计算出最小的更新步骤。

  6. 页面渲染:根据 diff 结果,进行 DOM 操作,完成页面更新。

问题:能详细的说一下 Dep 订阅中心的机制吗?

Dep 订阅中心是 Vue 中实现响应式系统的关键部分。它的机制如下:

  • 每个响应式数据都有一个对应的 Dep 实例。

  • 当数据被读取时,会触发 getter 函数,此时 Dep 实例会记录当前的 Watcher。

  • 当数据被修改时,会触发 setter 函数,此时 Dep 实例会通知所有订阅了该数据的 Watcher。

  • Watcher 在接收到通知后,会执行其回调函数,从而触发视图更新。

问题:vue 是如何对比新旧节点,然后实现页面更新的?

Vue 通过 diff 算法对比新旧虚拟 DOM 节点。对比过程如下:

  1. 首先对比新旧节点的标签名,如果不同,则直接替换。

  2. 如果标签名相同,则对比节点的属性,更新变化的部分。

  3. 对比子节点,Vue 采用双端对比算法,提高对比效率,减少 DOM 操作。

  4. 根据对比结果,进行 DOM 的添加、删除或修改操作,实现页面更新。

问题:diff 算法中除了 key 属性之外,还有别的增加对比效率的东西吗?

是的,除了 key 属性,Vue 的 diff 算法还采用了以下优化措施:

  • 虚拟 DOM:通过虚拟 DOM 减少直接操作真实 DOM 的次数。

  • 双端对比:在对比子节点时,从新旧节点的两端开始对比,减少不必要的遍历。

  • 列表优化:对于列表节点,Vue 会尽可能复用已有节点,减少 DOM 创建和销毁的次数。

问题:如果说让你来提升一下 diff 算法的对比效率,你有什么想法和思路吗?

如果要提升 diff 算法的对比效率,我有以下几个思路:

  • 引入智能预判:通过分析用户行为和页面结构,预判哪些部分可能会发生变化,提前进行优化。

  • 批量更新:对于多个数据变化,可以合并成一次更新操作,减少重复的 diff 过程。

  • 组件级别 diff:对于静态组件,可以跳过 diff 过程,直接复用。

问题:vuex 的数据的响应式是如何处理的,那你有什么思路吗?

Vuex 的数据响应式处理是通过将 state 数据变为响应式数据,然后通过 Vue 的响应式系统来实现的。我的思路是:

  • 使用 Vue 的响应式系统,确保 state 的变化能够触发视图更新。

  • 对于大型应用,可以采用模块化管理 state,减少不必要的响应式依赖,提高性能。

  • 使用 computed 属性来获取 store 中的数据,确保数据变化时能够自动更新视图。

问题:vue-router 的 hash 和 history 有什么区别?

vue-router 的 hash 模式和 history 模式主要有以下区别:

hash 模式:

  • URL 中的#符号是 hash 模式的特点,例如http://www.example.com/#/user

  • hash 模式背后的原理是onhashchange事件,当 URL 的片段标识符(即 hash 部分)发生变化时,会触发该事件。

  • hash 模式不需要后端配置,因为 hash 的改变不会触发浏览器发送请求,所以不会导致页面重新加载。

  • hash 模式兼容性较好,可以支持低版本的浏览器。

history 模式:

  • history 模式没有#符号,URL 看起来更像是标准 URL,例如http://www.example.com/user

  • history 模式使用 HTML5 的history.pushStatehistory.replaceState方法来改变 URL,同时不会重新加载页面。

  • history 模式需要后端配置支持。因为当用户刷新页面或者直接访问某个路由时,浏览器会向服务器发送请求。如果服务器没有配置相应的路由处理,就会返回 404 错误。

  • history 模式在 SEO 方面更有优势,因为搜索引擎会忽略带有#的 URL。

总的来说,hash 模式更简单,兼容性好,但 URL 不够美观;而 history 模式 URL 更标准,对 SEO 友好,但需要后端支持。在实际项目中,根据需求和后端配置情况选择合适的模式。

问题:router-view 是如何定位到将要发生改变并渲染的组件呢?

router-view 是 Vue Router 提供的一个内置组件,它的工作原理主要基于 Vue 的组件系统和响应式系统。以下是定位和渲染组件的过程:

  • 当用户导航到一个新的路由时,Vue Router 会通过监听 URL 的变化来触发路由的更新。

  • 路由配置中定义了路径与组件的映射关系。当路由变化时,Vue Router 会根据当前路径找到对应的路由记录。

  • router-view 组件会接收一个名为 name 的 prop,如果没有指定,它会渲染默认的组件。Vue Router 会根据当前路由记录中定义的组件来决定渲染哪个组件。

  • router-view 通过查看当前路由的组件定义,将其作为子组件进行渲染。这个过程涉及到 Vue 的虚拟 DOM 和组件生命周期钩子。

  • 如果路由有参数或者查询字符串的变化,Vue Router 会确保组件正确地接收到这些参数,并触发组件的更新。

说一下 js 的基本数据类型和引用类型, 二者有什么区别

JavaScript 中有两种数据类型:基本数据类型(Primitive types)和引用数据类型(Reference types)。

基本数据类型包括:

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Symbol(ES6 新增)

引用数据类型通常指的是对象,包括:

  • Object
  • Array
  • Function
  • Date
  • RegExp

区别:

  • 存储方式:基本数据类型是按值存储,引用数据类型是按引用存储。基本数据类型的值直接存储在变量中,而引用数据类型存储的是指向对象内存地址的引用。

  • 复制:基本数据类型复制的是值本身,而引用数据类型复制的是引用地址,这意味着如果修改了引用类型的一个实例,另一个实例也会受到影响。

  • 参数传递:基本数据类型作为函数参数传递时,传递的是值的副本;引用数据类型传递的是引用地址,因此函数内部对参数的修改可能会影响到原始对象。

问题:拷贝 js 的数据结构有哪些方式, 如何实现一个深拷贝

拷贝 JavaScript 数据结构的方式主要有两种:浅拷贝和深拷贝。

浅拷贝的方式包括:

  • 使用 Object.assign 方法

  • 使用扩展运算符(...)

  • Array.prototype.slice 和 Array.prototype.concat

深拷贝的实现方式:

  • 使用 JSON.parse(JSON.stringify(obj)),但这种方法不能复制函数、undefined、循环引用等。
  • 使用递归来实现一个自定义的深拷贝函数:
function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return null;
  if (typeof obj !== 'object') return obj;
  if (hash.has(obj)) return hash.get(obj); // 处理循环引用
  let cloneObj = Array.isArray(obj) ? [] : {};
  hash.set(obj, cloneObj);
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}

问题: 对象和函数之间有什么具体的区别吗, 二者之间有什么关联

对象和函数在 JavaScript 中有着密切的关系,但它们有以下几个具体的区别:

区别:

对象是键值对的集合,可以包含函数、基本数据类型和其他对象。 函数是一段可执行的代码块,可以被调用执行,并且可以返回值。

关联:

函数实际上是对象的一种特殊形式,它们继承自 Function.prototype,因此函数也是对象。 函数可以存储在对象的属性中,作为对象的方法被调用。 函数可以创建对象,例如通过构造函数或者工厂函数。

问题:能说一下 js 里面关于原型和原型链的概念吗?

在 JavaScript 中,原型(Prototype)和原型链(Prototype Chain)是面向对象编程中非常重要的概念。

原型(Prototype):

每个 JavaScript 函数都有一个原型属性(prototype),这个属性是一个对象,它包含了可以由该函数创建的所有实例共享的属性和方法。

当创建一个函数时,该函数的 prototype 属性会自动获得一个 constructor 属性,指向函数自身。

通过原型,可以实现属性和方法的继承。

原型链(Prototype Chain):

当访问一个对象的属性或方法时,如果这个对象本身没有这个属性或方法,解释器会沿着原型链向上查找,直到找到为止。

原型链是由原型对象构成的,每个对象都有一个原型对象,原型对象也可能有它自己的原型,这样一直延伸到 Object.prototype,它是原型链的顶端。

问题:如何实现一个 es5 的原型链继承

在 ES5 中,可以通过设置构造函数的原型来实现原型链继承。以下是一个基本的实现方式:


function Parent() {
  this.parentProperty = true;
}
Parent.prototype.getParentProperty = function() {
  return this.parentProperty;
};
function Child() {
  this.childProperty = false;
}
// 继承Parent
Child.prototype = new Parent();
// 修复构造函数指向
Child.prototype.constructor = Child;
Child.prototype.getChildProperty = function() {
  return this.childProperty;
};

在这个例子中,Child 的原型被设置为 Parent 的一个实例,这样 Child 的实例就可以访问 Parent 原型上的方法。

问题:如果是想继承父类的实例属性和实例方法该如何实现

要继承父类的实例属性和实例方法,可以在子类的构造函数中调用父类的构造函数,并使用 call 或 apply 方法来改变 this 的指向:

function Parent(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
  console.log(this.name);
};
function Child(name, age) {
  Parent.call(this, name); // 继承实例属性
  this.age = age;
}
// 继承原型方法
Child.prototype = new Parent();
Child.prototype.constructor = Child;
Child.prototype.sayAge = function() {
  console.log(this.age);
};

在这个例子中,Child 通过 Parent.call(this, name)继承了 Parent 的实例属性,并通过原型链继承了 Parent 的原型方法。

问题:es6 的 class 在继承的时候为什么要调用 super 方法, 用来做什么的

在 ES6 中,class 语法糖提供了更简洁的继承方式。super 关键字用于调用父类的构造函数,它有以下作用:

当在子类的构造函数中使用 super 时,它实际上是在调用父类的构造函数。

super 的目的是为了初始化父类的构造函数,确保父类的实例属性能够在子类实例上正确设置。

如果不调用 super,子类就无法正确地继承父类的实例属性和方法。 以下是一个使用 class 和 super 的例子:

class Parent {
  constructor(name) {
    this.name = name;
  }
}
class Child extends Parent {
  constructor(name, age) {
    super(name); // 调用父类的构造函数
    this.age = age;
  }
}

在这个例子中,Child 类通过 super(name)调用了 Parent 类的构造函数,这样 Child 的实例就继承了 Parent 的实例属性 name。

问题:小程序和 H5 什么区别吗, 小程序的渲染原理和交互通信是如何做的 (渲染引擎, 交互引擎)

小程序与 H5 的主要区别在于运行环境、API、组件、性能等方面。 区别:

运行环境: 小程序运行在特定的平台(如微信、支付宝)提供的环境中,而 H5 运行在浏览器的环境中。

API: 小程序可以直接调用平台提供的原生 API,如支付、位置等,而 H5 通常需要依赖第三方库或 API。

组件: 小程序有一套自己的组件库,而 H5 使用 HTML、CSS 和 JavaScript 构建界面。

性能: 小程序通常性能更好,因为它可以直接调用原生组件和 API。

渲染原理:

小程序的渲染引擎是基于 Web 技术构建的,但做了优化和定制。它使用双线程模型,其中主线程负责逻辑处理,渲染线程负责 UI 渲染。

小程序的 UI 渲染是通过虚拟 DOM 来完成的,当数据发生变化时,会生成新的虚拟 DOM,并与旧的虚拟 DOM 进行比较,然后进行必要的 DOM 更新。

交互通信:

小程序的交互通信主要通过事件系统完成。开发者可以通过事件绑定来处理用户的交互行为,如点击、滑动等。

小程序还提供了全局的 App 和页面的 Page 对象,它们可以用来处理全局状态和页面状态,以及进行页面间的通信。

问题:vue3 平时有用到过吗, vue3 主要的升级点和改动的地方能说一说吗

是的,我在平时的工作中已经使用过 Vue 3。

主要升级点:

Composition API: 提供了一种更灵活的代码组织方式,解决了 Vue 2 中组件逻辑复用和代码组织的问题。

性能提升: 通过引入 Proxy 实现响应式系统,提高了数据更新的性能。

类型支持: 更好的 TypeScript 集成,提供了更好的类型支持。

组合式 API: 新增了 setup 函数,作为组件的入口点,用于组合逻辑。

全局 API 和内部组件的更改: 全局 API 需要通过 createApp 来创建应用实例,内部组件也有所调整。

问题:vue3 的 compositionAPI 的初衷是什么, 相较于 vue2 的 optionsAPI 有什么优点

Vue 3 的 Composition API 的初衷是为了解决 Vue 2 中 Options API 在处理复杂组件时遇到的问题。

优点:

逻辑复用和抽象: Composition API 允许开发者将相关的逻辑抽象成一个可复用的函数,便于在多个组件之间共享。

类型支持: Composition API 更好地支持 TypeScript,使得类型推断和类型检查更加容易。

更好的代码组织: 使用 Composition API,可以将同一功能的代码放在一起,而不是分散在不同的选项中,使得代码更易于阅读和维护。

更灵活的代码结构: Composition API 提供了更多的灵活性,允许开发者按照自己的需求组织代码结构。

问题:babel 是怎么通过 webpack 把一些浏览器不支持的语法进行转换的

Babel 是一个 JavaScript 编译器,它可以将使用最新 JavaScript 特性的代码转换为广泛兼容的版本。以下是 Babel 通过 Webpack 转换代码的过程:

解析(Parsing): Babel 首先使用解析器(如 Babylon)将源代码解析成抽象语法树(AST)。

转换(Transformation): Babel 遍历 AST,使用插件和预设(如@babel/preset-env)将新的语法转换为旧的语法。例如,将箭头函数转换为普通函数。

生成(Code Generation): 最后,Babel 将转换后的 AST 生成新的 JavaScript 代码。

Webpack 集成: 在 Webpack 配置中,通常会使用 babel-loader 来集成 Babel。babel-loader 会在 Webpack 构建过程中调用 Babel,将源代码转换为兼容的代码。

配置: 开发者需要配置.babelrc 文件或 Babel 部分在 webpack.config.js 中,指定使用的插件和预设,以及目标环境(如浏览器版本)。

通过这个过程,Babel 能够确保即使在不支持最新特性的旧版浏览器中,代码也能正常运行。

以下是我作为应聘者的详细回答:

问题:平时项目中用的什么 css 预处理器, 还是其他 postcss 之类的配置?

在平时项目中,我主要使用 Sass(SCSS 语法)作为 CSS 预处理器。Sass 提供了变量、嵌套、混合(Mixins)、函数等功能,这些特性让 CSS 的编写更加高效和模块化。此外,我还经常使用 PostCSS 来增强 CSS 的功能,比如通过 autoprefixer 插件自动添加浏览器前缀,以及使用 postcss-preset-env 来使用未来的 CSS 特性。

问题:如果让你实现一键换肤的功能, 你会如何实现, 除了 css 变量你还有其他方案吗?

实现一键换肤功能,除了使用 CSS 变量,我还可以考虑以下方案: CSS 变量: 通过切换:root 中的 CSS 变量来实现主题的更换。

:root {
  --primary-color: #3498db;
  --secondary-color: #2ecc71;
}

类名切换: 为不同的主题定义不同的类名,通过 JavaScript 动态切换类名来实现换肤。

function changeTheme(themeName) {
  document.body.className = themeName;
}

切换样式表: 为每个主题创建一个单独的 CSS 文件,通过 JavaScript 动态加载不同的样式表。

function switchStyleSheet(sheet) {
  document.getElementById('theme-style').href = sheet;
}

问题:为什么 css 变量可以在运行时做更新呢, css 变量他会带来什么问题吗?

CSS 变量可以在运行时更新,是因为它们是浏览器原生支持的,并且是动态的。当通过 JavaScript 修改了 CSS 变量的值时,所有使用该变量的 CSS 属性都会自动更新。 CSS 变量可能带来的问题包括:

兼容性: 不是所有浏览器都支持 CSS 变量,尽管现代浏览器大多已支持。

性能: 过度使用 CSS 变量可能会影响页面的性能,尤其是在大量动态更新变量时。

可读性: 如果变量名不够明确,可能会降低 CSS 代码的可读性。

问题:你总结一下你做前端八年了, 有哪些擅长的点和欠缺的点

擅长的点:

熟练掌握 HTML5、CSS3、JavaScript(ES6+)及相关框架和库,如 React、Vue、Angular。

对前端工程化有深入理解,熟练使用 Webpack、Gulp 等构建工具。

有丰富的跨浏览器兼容性和性能优化经验。

对响应式设计和移动端开发有深入研究和实践经验。

良好的代码组织和架构能力,能够编写可维护和可扩展的代码。

欠缺的点:

在服务器端编程和数据库管理方面经验不足,需要进一步提升 Node.js 和相关后端技术。

对于新兴的前端技术(如 WebAssembly、量子计算在前端的应用等)了解不够深入,需要持续学习和实践。

在大型团队协作和国际项目沟通方面,还有提升空间,特别是在多文化背景下的沟通技巧。

注意这种问题属于开放性问题, 能问到这类问题时证明你的已经距离 offer 不远了, 但是也要注意回答的技巧

  1. 优点突出:

确定三到四个你最强的技能或经验点,这些应该是与职位高度相关的。

准备具体的例子来支持你的优点,比如项目成果、团队领导的经历、技术难题的解决等。

  1. 缺点适度:

选择一到两个相对较轻的缺点,这些缺点不应该是对应聘职位至关重要的技能。

确保这些缺点是可以改进的,并且你已经有了改善的计划或行动。

不要花费太多时间在缺点上,而是将重点放在你如何克服这些缺点上