2022 前端开发实习生面经

321 阅读18分钟

阿里

一面

js中0.1+0.2===0.3的结果是什么?不知道浮点数位数的时候怎么判断和第三个浮点数的和相等?

在JavaScript中,0.1 + 0.2 不等于 0.3。这是因为在计算机中,浮点数的表示是有限的,而不是无限的。由于浮点数采用二进制表示,因此有些十进制分数可能无法精确表示为浮点数,导致计算时出现舍入误差。在这种情况下,0.1 和 0.2 的计算结果可能略微偏离其实际值,因此它们的和可能略微偏离 0.3。

要判断两个浮点数是否相等,通常可以使用一个误差范围来进行比较。例如,可以定义一个极小的误差值,然后判断两个浮点数之间的差是否小于这个误差值。如果差小于误差值,则可以认为这两个浮点数相等。

x = 0.2;
y = 0.3;
z = 0.1;
equal = (Math.abs(x - y + z) < Number.EPSILON);

项目是js写法,全部使用var定义,需要把所有的var转成let?直接替换有什么问题?怎么避免呢?程序跑起来会有风险吗?除了人工替换还有什么办法?

在 JavaScript 中,varlet 有不同的作用域和生命周期。var 定义的变量具有函数级别的作用域,而 letconst 定义的变量具有块级别的作用域。因此,在将所有的 var 替换成 let 时,需要注意可能会改变代码的作用域和生命周期。

直接替换 varlet 通常是可行的,但也需要注意以下几点:

  1. 作用域问题:如果原来的 var 定义在函数内部,替换成 let 后可能会改变其作用域,从而影响程序逻辑。
  2. 生命周期问题:如果原来的 var 定义在循环内部,替换成 let 后可能会改变其生命周期,从而影响程序逻辑。
  3. 全局变量问题:如果原来的 var 定义在全局作用域中,替换成 let 后可能会改变其访问方式,从而影响程序逻辑。

为了避免这些问题,可以使用一些自动化工具进行替换,如Babel或TypeScript等。这些工具可以自动将 var 替换成 letconst,并提供一些附加功能,如类型检查和代码转换等。

const定义的对象可以被修改吗?为什么可以被修改?怎么防止它被修改?

const 声明创建一个值的只读引用。但这并不意味着它所持有的值是不可变的,只是变量标识符不能重新分配。例如,在引用内容是对象的情况下,这意味着可以改变对象的内容。 这个问题的本质是关于JavaScript中的变量赋值机制。使用const定义的变量只是不能被重新赋值,而不是不能修改变量所指向的对象。如果要防止对象被修改,可以使用Object.freeze()方法。

Object.freeze()  方法可以冻结一个对象。一个被冻结的对象再也不能被修改;冻结了一个对象则不能向这个对象添加新的属性,不能删除已有属性,不能修改该对象已有属性的可枚举性、可配置性、可写性,以及不能修改已有属性的值。此外,冻结一个对象后该对象的原型也不能被修改。freeze() 返回和传入的参数相同的对象。

vue3怎么监听数据变化然后响应到视图上的?vue3可以监听到数组的变化吗?vue2可以监听数组的变化吗?

在 Vue 3 中,可以使用 reactive 函数和 watch 函数来监听数据变化并响应到视图上。

reactive 函数用来创建一个响应式数据对象,将原始对象转化为响应式对象,从而使数据变化时能够触发视图更新。当响应式对象中的属性发生变化时,Vue 会自动将变化更新到视图中。

例如:

import { reactive, watch } from 'vue';

const state = reactive({
  count: 0,
});

watch(
  () => state.count,
  (newValue, oldValue) => {
    console.log(`count 值变为 ${newValue}`);
  }
);

state.count++; // count 值变为 1

Vue 3 也可以监听数组的变化,使用方法与监听对象的变化类似,可以使用 reactive 函数或者 ref 函数将数组转换为响应式数据,也可以使用 watch 函数来监听数组变化。

而在 Vue 2 中,可以使用 Object.defineProperty 函数和 watch 函数来监听数据变化并响应到视图上,但是 Vue 2 只能监听对象属性的变化,无法监听数组元素的变化,需要使用特定的方法来实现数组变化的监听,如使用 Vue.set 或者 splice 等方法。 Vue3的响应式系统利用了ES6中的Proxy和Reflect来监听数据的变化,从而实现了高效的响应式更新。在Vue3中,可以通过定义reactive对象来实现数据的响应式监听。当reactive对象中的数据发生变化时,Vue3会自动重新渲染相关的组件,从而实现了响应式更新。Vue3可以监听到数组的变化,并且提供了一些特殊的API来对数组进行操作,例如:push、pop、shift、unshift、splice、sort、reverse等。Vue2也可以监听到数组的变化,但是需要使用特殊的方法来对数组进行操作,例如:$ set、 $delete、 $watch等。

对vue的虚拟DOM怎么理解的?虚拟DOM会有哪些属性?

虚拟DOM是Vue框架的核心概念之一,它是一种轻量级的JavaScript对象,它描述了真实DOM的层次结构、属性和内容,用于提高DOM的操作效率。

当Vue组件的数据发生变化时,Vue框架会重新计算虚拟DOM的变化,然后将虚拟DOM和之前的虚拟DOM进行对比,找出需要更新的部分,然后只更新需要更新的部分到真实的DOM树中,从而提高了DOM操作的效率。

虚拟DOM对象有以下属性:

  • tag:表示元素的标签名。
  • props:表示元素的属性。
  • children:表示元素的子元素,是一个数组。
  • key:用于优化渲染性能的key。

在Vue框架中,每个组件都有一个自己的虚拟DOM对象,组件的数据发生变化时,Vue框架会重新计算组件的虚拟DOM对象,并将新的虚拟DOM对象和旧的虚拟DOM对象进行比较,找出需要更新的部分,然后只更新需要更新的部分到真实的DOM树中,从而提高了DOM操作的效率。

vue的虚拟DOM怎么对比的?

vue中的虚拟dom - 掘金 (juejin.cn)

core/renderer.ts at main · vuejs/core (github.com)

vue2 diff的时间复杂度?vue3 diff的时间复杂度?

Vue.js 2.x版本的虚拟DOM diff算法的时间复杂度是O(n^3),主要原因是每次执行diff操作时,需要递归遍历整个旧虚拟DOM树和新虚拟DOM树,并进行节点比较。树的最小编辑距离需要O(n^3)。 因此,当旧虚拟DOM树和新虚拟DOM树非常大且复杂时,执行diff操作的时间复杂度会非常高,导致应用程序的性能下降。

在 Vue 2 中,虚拟 DOM 的 diff 算法的时间复杂度是 O(n),其中 n 是节点的数量。Vue 2 中的 diff 算法在比较两个节点时,采用的是同层比较的策略,即先比较父节点,如果父节点相同,则比较子节点,如果子节点相同,则比较孙节点,以此类推。

在 Vue 3 中,diff 算法的时间复杂度是 O(n log n),其中 n 是节点的数量。Vue 3 中的 diff 算法采用了新的递归策略,称为“递归优化算法”,该算法通过动态规划和缓存的方式优化了 diff 算法的效率,避免了不必要的计算和遍历,从而大大提高了性能。此外,Vue 3 还使用了静态模板编译和强化的静态分析技术,进一步优化了 diff 算法的性能。

渲染列表的时候为什么要给每项定义一个key?

在 Vue 中,当使用 v-for 渲染列表时,Vue 会尽可能地复用已经存在的元素,以提高性能和减少浏览器的重绘次数。Vue 会根据每个元素的 key 值来判断它是否已经存在于列表中,如果存在,则会将其复用,否则会重新创建一个新的元素。

因此,给每个列表项设置一个唯一的 key 值非常重要,它可以帮助 Vue 更准确地判断哪些元素需要复用,哪些元素需要重新创建。如果不设置 key 值,Vue 会默认使用每个元素的索引作为 key 值,这样可能会导致一些性能问题,例如在列表中插入或删除元素时,会导致整个列表重新渲染,而不是只更新部分元素。

除了提高性能外,给每个列表项设置一个唯一的 key 值还可以避免一些潜在的 bug。例如,如果两个列表项的 key 值相同,但它们的数据不同,可能会导致某些数据错误地被复用,从而出现一些奇怪的 bug。

先进行同层对比还是先查找key?

在 Vue.js 中,当进行列表渲染时,会优先使用 key 属性进行同层级节点的比对,如果新旧节点的 key 值相同,则认为是同一个节点,不需要进行进一步的比对。

如果新旧节点的 key 值不同,则会使用双指针算法(即双端比对算法)进行同层级节点的比对,以尽可能地复用旧节点,并减少操作 DOM 的次数。

因此,在 Vue.js 中,优先进行同层对比,然后再查找 key 值。这也是为什么在列表渲染时,设置合适的 key 值可以提高渲染性能的原因之一。

在没有 key 的情况下,Vue 将使用一种最小化元素移动的算法,并尽可能地就地更新/复用相同类型的元素。如果传了 key,则将根据 key 的变化顺序来重新排列元素,并且将始终移除/销毁 key 已经不存在的元素。

同一个父元素下的子元素必须具有唯一的 key。重复的 key 将会导致渲染异常。

计算属性缓存 和 方法有什么区别?

若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。 相比之下,方法调用总是会在重渲染发生时再次执行函数。 虽然计算属性和方法都可以返回计算出来的值,但它们在使用上有一些区别。

计算属性是在模板中像普通属性一样绑定使用,计算结果会被缓存,只有在相关响应式数据变化时才会重新计算。而方法需要在模板中使用函数调用语法,每次使用时都会重新执行计算,不会缓存结果。

在使用计算属性时,可以像普通属性一样直接使用,更符合模板编写的语义;而方法需要使用函数调用语法,可能会显得繁琐一些。

总的来说,如果一个值是需要经常计算的,并且计算量不大,那么使用计算属性会更合适;如果一个值是需要根据不同条件返回不同值的,并且计算量较大,那么使用方法会更合适。

怎么在第一个promise那里终止链式调用?

保持pending状态或返回一个pending状态的promise。

二面

输入URL到渲染到页面上发生了什么?

B

一面 38min

一般用到的技术栈是什么?React有了解过吗?

项目中用到es6的东西有哪些?

await会阻塞后面的代码执行吗?

在async/await中,当使用await关键字等待一个Promise对象时,它会阻塞后面代码的执行,直到该Promise对象状态变为resolved(已解决)或rejected(已拒绝)。

具体来说,当代码执行到一个包含await的语句时,JS引擎会暂停该函数的执行,并等待Promise对象的状态变化。如果Promise对象状态为resolved,await表达式会返回resolve的值,函数会继续执行。如果Promise对象状态为rejected,await表达式会抛出一个异常,可以通过try-catch来捕获。

需要注意的是,await只能在async函数内部使用,而且在同一个async函数中,await语句之间是按顺序执行的,后面的await语句会等待前面的await语句执行完成后才执行。如果需要并行执行多个异步操作,可以使用Promise.all()等方法来实现。

总的来说,虽然await会阻塞后面的执行,但是它可以让我们更方便地处理异步操作,避免了回调地狱等问题,提高了代码的可读性和可维护性。

let 和 const有用到过吗?

数组的操作方法有哪些?

  1. Array.prototype.at()
  2. Array.prototype.concat()
  3. Array.prototype.copyWithin()
  4. Array.prototype.entries()
  5. Array.prototype.every()
  6. Array.prototype.fill()
  7. Array.prototype.filter()
  8. Array.prototype.find()
  9. Array.prototype.findIndex()
  10. Array.prototype.findLast()
  11. Array.prototype.findLastIndex()
  12. Array.prototype.flat()
  13. Array.prototype.flatMap()
  14. Array.prototype.forEach()
  15. Array.from()
  16. Array.prototype.group()(en-US)实验性
  17. Array.prototype.groupToMap()(en-US)实验性
  18. Array.prototype.includes()
  19. Array.prototype.indexOf()
  20. Array.isArray()
  21. Array.prototype.join()
  22. Array.prototype.keys()
  23. Array.prototype.lastIndexOf()
  24. Array.prototype.map()
  25. Array.of()
  26. Array.prototype.pop()
  27. Array.prototype.push()
  28. Array.prototype.reduce()
  29. Array.prototype.reduceRight()
  30. Array.prototype.reverse()
  31. Array.prototype.shift()
  32. Array.prototype.slice()
  33. Array.prototype.some()
  34. Array.prototype.sort()
  35. Array.prototype.splice()
  36. Array.prototype.toLocaleString()
  37. Array.prototype.toString()
  38. Array.prototype.unshift()
  39. Array.prototype.values()

修改数组中的某一项怎么修改?

  • 可以通过数组索引来修改数组中的某一项
  • 使用数组的一些方法来修改数组中的某一项,例如 splice() 方法、pop()push() 方法等

map和forEach的区别?

map()forEach() 都是 JavaScript 数组的方法,用于遍历数组中的元素。它们的区别在于,map() 方法会返回一个新的数组,而 forEach() 方法则不会返回任何值。

forEach()  方法对数组的每个元素执行一次给定的函数。 map()  方法创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。

删除数组中的某一项?

可以使用 splice() 方法来删除数组中的某一项。该方法会修改原数组,并返回被删除的项。splice() 方法接受两个参数,第一个参数是要删除的元素的下标,第二个参数是要删除的元素的个数。如果只想删除一个元素,可以将第二个参数设置为 1。如果要删除的元素下标是最后一个元素的下标,可以将第二个参数省略,如 splice(arr.length - 1)。此外,splice() 方法也可以用于在数组中插入新的元素,只需要指定要删除的元素个数为 0,然后在第一个参数位置插入新的元素即可。

节流和防抖

使用场景是什么?

节流和防抖都是为了限制函数的执行频率,提高性能。

防抖(debounce)是指在一段时间内多次触发同一个事件,只执行最后一次事件的回调函数。常见的应用场景是用户频繁地输入,为了减少网络请求,只有在用户输入完毕之后再进行请求。防抖的实现方式是在事件触发时设定一个定时器,然后每次触发事件时清除定时器,并重新设定一个新的定时器。如果在定时器到期之前再次触发了事件,就会清除旧的定时器并设置新的定时器,直到事件触发后定时器到期,执行回调函数。

下面是一个简单的防抖实现:

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

// 使用防抖函数包装回调函数,确保回调函数在 500ms 内只会执行一次
const debounceCallback = debounce(() => {
  console.log('debounced callback');
}, 500);

// 触发回调函数 3 次
debounceCallback(); // 不会执行
debounceCallback(); // 不会执行
debounceCallback(); // 500ms 后执行一次

节流(throttle)是指在一段时间内多次触发同一个事件,只执行第一次事件的回调函数。常见的应用场景是滚动事件,为了减少滚动事件的回调函数的执行次数,可以设置一个固定的时间间隔,比如每 500ms 执行一次回调函数。节流的实现方式是设定一个定时器,在定时器到期之前所有的事件都会被忽略,直到定时器到期后才执行一次回调函数。

一个简单的节流实现:

//时间戳版
function throttle(func, wait) {
  let lastTime = 0;

  return function() {
    let now = Date.now();

    if (now - lastTime > wait) {
      func.apply(this, arguments);
      lastTime = now;
    }
  };
}
//定时器版
function throttle(func, wait) {
  let timerId = null;

  return function() {
    if (!timerId) {
      timerId = setTimeout(() => {
        func.apply(this, arguments);
        timerId = null;
      }, wait);
    }
  };
}


// 使用节流函数包装回调函数,确保回调函数每 500ms 执行一次
const throttleCallback = throttle(() => {
  console.log('throttled callback');
}, 500);

// 触发回调函数 3 次
throttleCallback(); // 立即执行
throttleCallback(); // 500ms 后执行
throttleCallback(); // 500ms 后执行

防抖和节流的区别在于它们对待多次事件的方式不同。防抖会忽略中间的多个事件,只执行最后一个事件,而节流会执行第一个事件,并在固定的时间间隔内执行后续的事件。因此,在一些场景下,需要根据实际需求选择防抖或节流。

cookie,localStorage,sessionStorage区别

cookie、localStorage和sessionStorage是Web前端中用来存储数据的三种机制,它们之间的主要区别如下:

  1. 存储大小不同:cookie的存储大小受到浏览器的限制,一般不超过4KB;而localStorage和sessionStorage的存储大小一般为5MB或更大。
  2. 存储位置不同:cookie是存储在客户端浏览器中的,每次请求都会携带cookie发送到服务器端;localStorage和sessionStorage是存储在客户端浏览器中的,不会被发送到服务器端。
  3. 生命周期不同:cookie可以设置一个过期时间,一旦过期就会被浏览器删除;如果同时设置了Expires和Max-Age属性,则Max-Age属性会覆盖Expires属性的设置。另外,如果不设置cookie的过期时间,该cookie会在用户关闭浏览器时自动过期。localStorage的生命周期是永久的,除非用户手动清除;sessionStorage的生命周期与会话相关,当会话结束(例如关闭浏览器)时,数据就会被删除。
  4. API不同:cookie只提供了简单的读写操作;而localStorage和sessionStorage提供了setItem、getItem、removeItem和clear等API,更加方便实用。

根据以上差异,我们可以根据实际需求来选择使用不同的存储机制。如果需要与服务器进行交互并在多个页面间共享数据,可以使用cookie;如果只需要在客户端进行数据存储,并且不需要与服务器进行交互,可以使用localStorage和sessionStorage。而如果需要在会话期间保持数据,但在会话结束后数据就不再需要,可以使用sessionStorage。

怎么让localStorage在两天后过期?

可以通过设置localStorage的过期时间来实现在两天后过期,具体实现方式如下:

  1. 获取当前时间戳和两天后的时间戳:
const now = new Date().getTime();
const twoDaysLater = now + 2 * 24 * 60 * 60 * 1000;
  1. 将过期时间存储到localStorage中:
localStorage.setItem('expirationTime', twoDaysLater.toString());
  1. 在获取localStorage的时候,检查当前时间是否已经超过了过期时间:
const expirationTime = localStorage.getItem('expirationTime');
if (expirationTime && now > parseInt(expirationTime)) {
  localStorage.removeItem('expirationTime');
  localStorage.removeItem('data');
} else {
  const data = localStorage.getItem('data');
  // do something with the data
}

在这个例子中,我们将过期时间存储在了localStorage中的expirationTime键中,如果过期时间已经到了,我们就将这个键和数据一起从localStorage中移除。如果过期时间还没有到,我们就从localStorage中取出数据并进行相应的操作。

需要注意的是,由于localStorage是存储在浏览器端的,因此如果用户清除了浏览器缓存或者更换了浏览器,localStorage中的数据和过期时间都会被清除。

跨域

跨域 - 掘金 (juejin.cn)

前端安全问题

XSS攻击

CSRF

事件循环

闭包

浏览器的回流和重绘

减少回流和重绘

盒模型

统一设置盒模型

position的属性值

static 该关键字指定元素使用正常的布局行为,即元素在文档常规流中当前的布局位置。此时 toprightbottomleft 和 z-index 属性无效。

relative 该关键字下,元素先放置在未添加定位时的位置,再在不改变页面布局的前提下调整元素位置(因此会在此元素未添加定位时所在位置留下空白)。position:relative 对 table-*-group, table-row, table-column, table-cell, table-caption 元素无效。

absolute 元素会被移出正常文档流,并不为元素预留空间,通过指定元素相对于最近的非 static 定位祖先元素的偏移,来确定元素位置。绝对定位的元素可以设置外边距(margins),且不会与其他边距合并。

fixed 元素会被移出正常文档流,并不为元素预留空间,而是通过指定元素相对于屏幕视口(viewport)的位置来指定元素位置。元素的位置在屏幕滚动时不会改变。打印时,元素会出现在的每页的固定位置。fixed 属性会创建新的层叠上下文。当元素祖先的 transformperspectivefilter 或 backdrop-filter 属性非 none 时,容器由视口改为该祖先。

sticky 元素根据正常文档流进行定位,然后相对它的最近滚动祖先(nearest scrolling ancestor)和 containing block(最近块级祖先 nearest block-level ancestor),包括 table-related 元素,基于 toprightbottom 和 left 的值进行偏移。偏移值不会影响任何其他元素的位置。 该值总是创建一个新的层叠上下文(stacking context)。注意,一个 sticky 元素会“固定”在离它最近的一个拥有“滚动机制”的祖先上(当该祖先的 overflow 是 hiddenscrollauto 或 overlay 时),即便这个祖先不是最近的真实可滚动祖先。这有效地抑制了任何“sticky”行为。

设置背景图的大小

background-size 设置背景图片大小。图片可以保有其原有的尺寸,或者拉伸到新的尺寸,或者在保持其原有比例的同时缩放到元素的可用空间的尺寸。

移动端1px边框

flex布局

项目相关

浏览器如何解析CSS选择器

浏览器解析CSS选择器的过程可以分为两个阶段:匹配和应用。

  1. 匹配阶段:浏览器会根据CSS选择器从右到左逐级匹配元素。具体来说,浏览器会先找到所有满足最右侧选择器的元素,然后逐级向左匹配,直到匹配到整个选择器。如果整个选择器都能匹配成功,则将该元素作为选择器的结果返回。
  2. 应用阶段:浏览器将根据选择器的结果,将样式应用到元素上。具体来说,浏览器会根据CSS规则的优先级和继承关系,计算出最终的样式,并将其应用到相应的元素上。

在匹配阶段中,浏览器会根据选择器的类型和特定的匹配规则进行匹配。常见的选择器类型包括标签选择器、类选择器、ID选择器、属性选择器、伪类选择器等。对于每种选择器类型,浏览器会有不同的匹配规则,如对于类选择器,浏览器会通过比较元素的class属性和选择器中的类名来进行匹配。

CSS选择器的性能对于页面的渲染速度和响应性能有着很大的影响。如果选择器过于复杂或者嵌套层数过深,会导致浏览器的性能下降。

高性能选择器

从选择器性能的角度,更少的特定选择器是比更多的要快

CSS选择器优先级

优先级就是分配给指定的 CSS 声明的一个权重,它由 匹配的选择器中的 每一种选择器类型的 数值 决定。 下面列表中,选择器类型的优先级是递增的:

  1. [类型选择器](例如,h1)和伪元素(例如,::before
  2. 类选择器,属性选择器(例如,[type="radio"])和伪类(例如,:hover
  3. [ID 选择器](例如,#example)。

针对给定的元素计算选择器的特殊性,具体如下:

计算选择器中 ID 选择器的数量(= A)。

计算选择器中类选择器、属性选择器和伪类的数量(= B)。

计算选择器中类型选择器和伪元素的数量(= C)。

忽略通配选择器。

通过按顺序比较三个组成部分来比较优先级:值越大优先级越高。

持续更新...