文章

冰岩前端组 2024 年笔试 - 个人解答

需要特别注意,本文内容并非标准答案,均为本人个人认识,仅留作自己学习使用。
本文正在施工中,并非最终版本,只有前半部分题目的解答。

Part 0

Q1

  • rem: root em, 代表根元素的 font-size 大小
  • vw: viewport width, 代表 viewport 宽度的 1%, 即为浏览器中网站可见内容宽度的 1%
  • vh: viewport height, 代表 viewport 高度的 1%, 即为浏览器中网站可见内容高度的 1%

px 为绝对单位,而 rem vw vh 为相对单位,后者在进行响应式布局时可以进行自适应的修正,为开发带来便利。

Q2

HTML 语义化提高了代码的可读性,使代码的作用清晰可见。

HTML5 添加了:

  • <canvas>: 标签定义图形,比如图表和其他图像。该标签基于 JavaScript 的绘图 API.
  • <audio>: 定义音频内容
  • <video>: 定义视频(video 或者 movie)
  • <source>: 定义多媒体资源 <video><audio>
  • <embed>: 定义嵌入的内容,比如插件。
  • <track>: 为诸如 <video><audio> 元素之类的媒介规定外部文本轨道。
  • <datalist>: 定义选项列表。请与 <input> 元素配合使用该元素,来定义 input 可能的值。
  • <keygen>: 规定用于表单的密钥对生成器字段。
  • <output>: 定义不同类型的输出,比如脚本的输出。
  • <article>: 定义页面独立的内容区域。
  • <aside>: 定义页面的侧边栏内容。
  • <bdi>: 允许您设置一段文本,使其脱离其父元素的文本方向设置。
  • <command>: 定义命令按钮,比如单选按钮、复选框或按钮
  • <details>: 用于描述文档或文档某个部分的细节
  • <dialog>: 定义对话框,比如提示框
  • <summary>: 标签包含 <details> 元素的标题
  • <figure>: 规定独立的流内容(图像、图表、照片、代码等等)。
  • <figcaption>: 定义 <figure> 元素的标题
  • <footer>: 定义 <section><document> 的页脚。
  • <header>: 定义了文档的头部区域
  • <mark>: 定义带有记号的文本。
  • <meter>: 定义度量衡。仅用于已知最大和最小值的度量。
  • <nav>: 定义导航链接的部分。
  • <progress>: 定义任何类型的任务的进度。
  • <ruby>: 定义 ruby 注释(中文注音或字符)。
  • <rt>: 定义字符(中文注音或字符)的解释或发音。
  • <rp>: 在 ruby 注释中使用,定义不支持 ruby 元素的浏览器所显示的内容。
  • <section>: 定义文档中的节(section、区段)。
  • <time>: 定义日期或时间。
  • <wbr>: 规定在文本中的何处适合添加换行符。

Refer to: https://www.runoob.com/html/html5-new-element.html

Q3

CSS 伪类用于扩展选择器,用于选择位于某一特殊状态真实存在的元素。

而 CSS 伪元素并非选择某一真实存在的元素,而是相当于在一个虚构出的元素上应用效果。

Q4

text-overflow: ellipsis;

Q5

50px. 相接触的两个元素的外边距会合并,若二者均为正 / 负,则取其中绝对值最大的作为二者间距。

顺带一提,若两个外边距为一正一负,则会将二者相加,将和作为二者间距。

将其更改为 margin-top: 70px; 即可。

Q6

<input type="radio" name="bingyan" />
<div><span> 选项A </span></div>
<input type="radio" name="bingyan" />
<div><span> 选项B </span></div>
<style>
input[type="radio"]:checked+div span {
    color: blue;
}
</style>

添加 name 属性,使两个 radio 互斥。

input[type="radio"] 选择单选项,而 :checked 伪类选择了被选中的 radio input.

最后,+ 选择了紧接在 radio input 之后的 div 内的 span 元素。

Q7

  • margin: auto;
  • 在父元素中使用
    display: flex;
    justify-content: center;
    align-items: center;
    

Q8

<div class="triangle">&#9650;</div>
<style>
.triangle {
    -webkit-text-stroke: 4px black; /* 描边宽度及颜色 */
    color: transparent;
    font-size: 50px; /* 三角形大小 */
}
</style>

利用了三角型的文字,并使用 -webkit-text-stroke 设置了文字描边。

Refer to: Stack Overflow

也可以使用 svg 或 css 的 clip-path 来实现。

Part 1

Q1

基本类型:

  • string
  • number
  • boolean
  • null
  • undefined
  • bigint
  • symbol

对象类型:

  • Object
  • Array
  • Function
  • Date
  • RegExp
  • Error

Q2

  • null 是 JS 基本类型之一,虽然 typeof null == 'object',但是它真的是一个基本类型。它用于指代对象的值为空,或许可以算是一个空指针?
  • undefined 也是 JS 基本类型之一,代表变量还未赋值或对象的属性不存在。
  • NaN (Not a Number),当一个数字无法被表示时,会返回 NaN。值得注意的是,NaN 与任何值都不相等,包括自身。

Q3

for in 用于遍历对象的可枚举字符串属性。

for of 用于遍历可迭代对象的值序列,就像 forEach 那样。

Q4

JS 的事件循环包含一个或多个 task queue 以及一个 microtask queue :

  1. 从 task queue 中取出并执行一个 task,这个 task 被常常称为一个宏任务。
  2. 宏任务执行完成后,检查 microtask queue 内是否有任务,若有,则依次执行至 queue 为空。
  3. 重复上述过程。

Q5

button
#button:hover {
    border: 3px solid rgb(111, 219, 53); /* 边框颜色 */
document.getElementById("button").addEventListener("click", () => {
  alert("button clicked 🎉");
});

Q6

下两行的空格代表换行:

  1. 5 5 5 5 5
  2. 0 1 2 3 4

1 中使用了 var 声明,而 2 使用了 let 声明。

在 for 循环中使用 var 声明的变量会被提升到函数作用域的顶部,而 let 声明的变量则只是语句的局部变量,在每次循环中都会被重新创建。也就是说,这相当于 var i 是在 for 外部声明的,而 let i 是 for 内部的一个局部变量。

因此,在 for 循环结束后,setTimeout 开始依次执行时,它们引用的 var i 是同一个变量,因此输出的值均为 5,而 let i 则是在每次循环中重新创建的,因此输出的值为 0 1 2 3 4。

Q7

undefined
5
1
6
20
200

根据 JS 的作用域链,访问变量时会先在当前作用域查找,若找不到则会向上查找直至全局作用域。

L1 中定义了全局变量 var a = 1,此时全局作用域中 a = 1.

在 function fn 中,又定义了 var a,使得 fn 函数作用域中也有了 a 变量。

L4 输出变量 a 的值,由于 L4 在 fn 作用域中,而此时 a 仍未初始化,因此输出 undefined

L5 将 a 的值赋为 5,此时 fn 作用域中的 a = 5。

L6 在函数 fn 中,因此输出 fn 作用域中 a 的值,输出 5

L7 使 fn 作用域中 a 自增,此时 fn 作用域中 a = 6。

L8 为 var a,但是由于 var 定义会被提升至作用域顶部,因此此行无效果。

L9 调用 fn3 函数,而 fn3 位于全局作用域,因此输出全局作用域中的 a,即输出 1

同时,fn3 中还将 a 的值赋为 200,此时全局作用域中 a = 200。

L10 调用 fn2 函数,而 fn2 位于 fn 函数作用域中,因此输出fn 作用域中的 a,即输出 6

同时,fn2 中还将 a 的值赋为 20,使 fn 作用域中 a = 20。

L11 输出变量 a 的值,由于 L11 在 fn 作用域中,因此输出 20

最后在 L25 中,输出全局作用域中的 a,即输出 200

Q8

1 == true // true
1 + 1 == true // false
'' == false // true
0 == '' // true
0 == '0' // true
0 === '0' // false
['a'] === 'a' // false
['a'] == 'a' // true
[] == [] // false
[] == true // false
![] == '' // true
[] == ![] // true

Q9

<body> 指 document.body 的值,在控制台中显示的内容可能因浏览器而异。

<body>
Promise, <body>
setTimeout, <body>
setTimeout(() => {
    console.log("setTimeout, ", document.body);
});
Promise.resolve().then(() => {
    console.log("Promise, ", document.body);
});
console.log(document.body);

setTimeout 在 task queue 中创建了一个新的 task,而 Promise 的 then 方法则会创建一个 microtask。

根据事件循环,此时先执行当前的 task,即 console.log(document.body);,输出 <body>

输出完成后,task 执行完毕,开始依次执行 microtask queue 中的任务,输出 Promise, <body>

最后,task queue 中的 setTimeout 任务执行,输出 setTimeout, <body>

Q10

function compareVersion(a, b) {
    if (a === b) return 0;
    let splittedA = a.split(/[\.-]/);
    let splittedB = b.split(/[\.-]/);
    for (let i = 0; i < 3; i++) {
        let a = parseInt(splittedA[i]);
        let b = parseInt(splittedB[i]);
        if (a < b) return -1;
        if (a > b) return 1;
    }
    if (splittedA.length > 3 && splittedB.length === 3) return -1;
    if (splittedA.length === 3 && splittedB.length > 3) return 1;
    if (splittedA[3] > splittedB[3]) return 1;
    if (splittedA[3] < splittedB[3]) return -1;
    if (splittedA[4] > splittedB[4]) return 1;
    if (splittedA[4] < splittedB[4]) return -1;
    return 0;
}

Q11

function _myget(object, path, defaultValue) {
    if (typeof path === 'string') {
        path = path.replaceAll(/\[(.*?)\]/g, '.$1')
    }
    let paths = typeof path === 'string' ? path.split('.') : path;
    for (let next of paths) {
        if (object[next] === undefined) return defaultValue;
        object = object[next];
    }
    return object;
}

Part 3

Q1

useRef 用于储存一个不需要渲染的值,改变其值不会引起组件的重新渲染。

而 useState 用于储存一个状态值,改变其值会引起组件的重新渲染。

useRef 可用于操作 DOM 元素,例如为元素添加事件监听器等。

而 useState 用于储存组件的状态,例如用户输入的值等。

Q2

使用 Context,或使用状态管理库,如 redux 等。

Q3

使用 useMemo hook 将计算结果缓存,避免重复计算和渲染。

Q4

const newPersonList = personList; 并未创建新的数组,而是将 newPersonList 指向了 personList 的引用,此时,newPersonList 和 personList 指向同一个对象。

根据 React 文档

如果你提供的新值与当前 state 相同(由 Object.is 比较确定),React 将 跳过重新渲染该组件及其子组件。

此时若调用 setState(newPersonList),由于 newPersonList 和 personList 指向同一个对象,因此 React 认为两者相同,不会触发重新渲染。

可改为:const newPersonList = [...personList];,这样会创建一个新的数组,避免了此问题。

或使用深拷贝,const newPersonList = JSON.parse(JSON.stringify(personList));

也可以使用 immer 进行更便捷的修改。

Q5

根据 React 文档

set 函数仅更新下一次渲染的状态变量。如果在调用 set 函数后读取状态变量,则仍会得到在调用之前显示在屏幕上的旧值。

React 会批量处理状态更新。

因此,三次连续调用 setNumber 时,读取的 number 变量实际上是同一个值,而在三次调用全部结束后,React 才会处理状态更新并触发重新渲染,因此只会使 number +1.

  • setNumber(number + 3);
  • 三次 setNumber(n => n + 1);

Q6

bowl
bowll

首先,使用 useState 将 bowl 的值设置为 'bowl'。

在第一个 useEffect 中,将 bowl 的值设置为了 'bowll',但是由于 useEffect 中的状态更新并不会立即执行的,

因此在第一个 useEffect 中设置的值并不会立即生效,其后的 console.log(bowl) 输出的是 'bowl'。

之后,React 会重新渲染组件,此时 useEffect 中的状态更新生效,

因此重新渲染后再次触发 console.log(bowl),其输出的是 'bowll'。

// WIP

License:  CC BY-NC 4.0