base北京 微软 前端实习面经
投递渠道: 牛客网友内推
时间线
- 2024-02-21 一面
- 2024-02-26 二面
- 2024-02-27 三面
一面
- 自我介绍与实习经历
- 学业相关
1. 算法:反转链表
下面是使用 JavaScript 实现反转链表的代码:
class ListNode {
constructor(val = 0, next = null) {
this.val = val;
this.next = next;
}
}
function reverseList(head) {
let prev = null;
let current = head;
while (current) {
const nextNode = current.next;
current.next = prev;
prev = current;
current = nextNode;
}
return prev;
}
这段代码定义了一个 ListNode 类来表示链表节点,然后实现了 reverseList 函数来反转链表。在 reverseList 函数中,我们使用了三个指针 prev、current 和 nextNode,并使用循环来遍历链表,反转节点的指针关系。最后返回的 prev 就是反转后的链表的头节点。
使用示例:
// 创建一个链表:1 -> 2 -> 3 -> 4 -> 5
const node5 = new ListNode(5);
const node4 = new ListNode(4, node5);
const node3 = new ListNode(3, node4);
const node2 = new ListNode(2, node3);
const node1 = new ListNode(1, node2);
// 反转链表
const newHead = reverseList(node1);
// 输出反转后的链表
let current = newHead;
while (current) {
console.log(current.val);
current = current.next;
}
这段 JavaScript 代码与之前的 Python 代码逻辑相同,只是语法上有些差异。
3. 算法:求一个数开方后的整数部分
你可以使用 JavaScript 中的 Math.sqrt() 方法来求一个数的开方,并取其整数部分。然后使用 Math.floor() 方法将结果向下取整,得到整数部分。下面是一个示例:
function squareRootIntegerPart(num) {
return Math.floor(Math.sqrt(num));
}
// 示例用法
const number = 16;
const result = squareRootIntegerPart(number);
console.log(result); // 输出:4
在这个示例中,我们定义了一个 squareRootIntegerPart 函数来计算一个数的开方后的整数部分。然后我们将 16 作为示例输入,并将结果打印出来,得到的结果是 4,即 16 的开方的整数部分是 4。
二面
- 实习工作内容, 如何完善前端开发规范的
1. for...in 与 for...of 的区别
for...in 和 for...of 都是 JavaScript 中用来遍历数据结构(比如数组、对象、字符串等)的循环语句,但它们之间有一些重要的区别:
-
for...in循环:- 用于遍历对象的可枚举属性,或者数组/类数组对象的索引。
- 不仅遍历自身的可枚举属性,还会遍历继承的属性。
- 示例:
const obj = {a: 1, b: 2, c: 3}; for (let key in obj) { console.log(key, obj[key]); } // 输出: // a 1 // b 2 // c 3
-
for...of循环:- 用于遍历可迭代对象(iterable),例如数组、字符串、Map、Set 等。
- 不会遍历对象的属性,只会遍历对象本身的元素。
- 示例:
const arr = [1, 2, 3]; for (let item of arr) { console.log(item); } // 输出: // 1 // 2 // 3
综上所述,for...in 主要用于遍历对象的属性,而 for...of 主要用于遍历可迭代对象的元素。在实际应用中,根据需求选择合适的循环语句来遍历数据结构。
3. async/await 的理解
async/await 是 JavaScript 中用于处理异步操作的一种语法糖,它基于 Promise,并且让异步代码看起来更像同步代码,使得异步操作更加容易理解和管理。
以下是对 async/await 的一些基本理解:
-
async函数:async函数是一个特殊的函数,它在函数前面加上async关键字来定义。async函数内部可以使用await关键字来等待 Promise 对象的状态改变,即等待异步操作的结果。
-
await表达式:await关键字只能在async函数内部使用。await右侧通常是一个返回 Promise 对象的表达式,可以是一个异步函数调用、Promise 对象或者任何符合 Promise 标准的对象。await表达式会暂停async函数的执行,直到await右侧的 Promise 对象状态变为 resolved(成功)或者 rejected(失败)。
-
错误处理:
- 在
async/await中,可以使用try...catch语句来捕获异步操作中的错误。 - 如果
await右侧的 Promise 对象状态变为 rejected,那么await表达式会抛出一个异常,可以通过try...catch捕获该异常并进行处理。
- 在
-
返回值:
async函数总是返回一个 Promise 对象,无论函数内部有没有异步操作。- 如果
async函数显式地使用return返回一个值,那么该值会被 Promise 对象 resolve。
-
与 Promise 的区别:
- 相较于原始的 Promise 链式调用,
async/await更加简洁、易读,尤其是对于多个异步操作的连续执行。 async/await更贴近同步编程的写法,减少了回调地狱(callback hell)的问题。
- 相较于原始的 Promise 链式调用,
下面是一个简单的示例,演示了如何使用 async/await 处理异步操作:
function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function asyncCall() {
console.log('calling');
const result = await resolveAfter2Seconds();
console.log(result); // 等待2秒后输出:resolved
}
asyncCall();
在这个示例中,asyncCall 函数中使用了 await 关键字等待 resolveAfter2Seconds 函数的 Promise 对象结果。当 resolveAfter2Seconds 返回的 Promise 对象状态变为 resolved 时,await 表达式会返回其结果,并继续执行后续代码。
- 是否了解 AST ? 是否了解 ESLint 的原理
我了解抽象语法树(Abstract Syntax Tree,AST)和 ESLint 的原理。
-
抽象语法树(AST):
- 抽象语法树是源代码的抽象语法结构的树状表现形式。它由节点构成,每个节点表示源代码中的一种结构,比如语句、表达式、变量声明等。
- AST 可以被用来分析、转换和理解代码。在编译器、解析器、静态分析工具等领域都有广泛的应用。
-
ESLint 的原理:
- ESLint 是一个用于静态代码分析的工具,它可以帮助开发者发现和修复代码中的问题,规范代码风格,并且提供了可扩展的插件系统。
- ESLint 的工作原理主要是基于 AST。它将源代码解析成 AST,然后使用预定义的规则对 AST 进行遍历和检查,发现代码中的问题并给出相应的警告或错误信息。
- ESLint 的规则可以自定义,也可以使用社区提供的现成规则集。用户可以根据项目的需求选择合适的规则,也可以根据团队的编码规范制定自定义规则。
- ESLint 可以集成到代码编辑器、构建工具等开发环境中,实现实时检查和反馈,帮助开发者在开发过程中保持代码质量和一致性。
总的来说,AST 是 ESLint 实现静态代码分析的基础,而 ESLint 则是在 AST 的基础上实现了具体的代码规范检查功能。
看代码说结果:
- 具名导入与默认导出
// utils.js
const sum = (a, b) => a + b;
export default sum;
export function minus(a, b) {
return a - b;
}
// index.js
import * as sum from "./utils";
// 如何调用相关函数
// sum.default(1, 2)
// sum.minus(1, 2)
在这段代码中,utils.js 文件中定义了一个默认导出 sum 和一个具名导出 minus。在 index.js 文件中,使用 import * as sum 导入了 utils.js 文件中的所有导出,并将它们作为 sum 对象的属性。因此,你可以按照以下方式调用相关函数:
// 调用默认导出的函数
sum.default(1, 2);
// 调用具名导出的函数
sum.minus(1, 2);
在调用默认导出的函数时,需要使用 sum.default() 语法。而调用具名导出的函数时,则直接使用 sum.minus() 语法。
- var / let / const 的变量提升
在 JavaScript 中,var、let 和 const 三种声明变量的方式在变量提升方面存在一些差异。
-
var变量提升:- 使用
var声明的变量会存在变量提升,即变量声明会被提升到所在作用域的顶部,但初始化的赋值不会提升。 - 这意味着,在变量声明之前访问变量会得到
undefined,而不会抛出错误。 - 示例:
console.log(foo); // 输出:undefined var foo = 10;
- 使用
-
let和const变量提升:- 使用
let和const声明的变量也会存在变量提升,但是在访问变量之前会处于 "暂时性死区"(Temporal Dead Zone,TDZ)。 - TDZ 是指在当前作用域内,变量虽然已经被声明,但由于尚未初始化赋值,因此无法访问该变量。
- 在 TDZ 内访问变量会导致 ReferenceError 错误。
- 示例:
console.log(foo); // ReferenceError: Cannot access 'foo' before initialization let foo = 10;
- 使用
总的来说,var 在变量提升时会将变量声明提升到作用域顶部,而 let 和 const 在变量提升时会进入 TDZ,无法访问,直到变量被赋值初始化之后才能正常访问。
let 与 const 都存在变量提升, 提升到块级作用域顶部, 但同时也存在暂时性死区的概念
- 给函数的属性赋值是否会报错
function fn() {}
fn.name = "fn";
不会报错。在 JavaScript 中,函数是一种特殊的对象,因此可以像普通对象一样给函数添加属性。在你的示例中,给函数 fn 添加了一个 name 属性,赋值为字符串 "fn"。这种做法是合法的,并且不会报错。
这样的用法在一些场景下很有用,比如给函数添加一些自定义的元数据,或者在调试时标识函数。
算法题:给定一个数字, 你可以将该数字中的任意一种数字替换为另一种数字, 例如你可以将该数中的所有 0 替换为 1. 你只能将一种数字替换为另一种数字, 请问通过该替换操作能得到的最大值与最小值之差为多少?
要找到通过替换操作能得到的最大值与最小值之差,可以通过以下步骤实现:
- 将数字转换为字符串,以便于逐位处理。
- 找到当前数字中的最大位数和最小位数。
- 对于最大值,将最小位数替换为最大值,对于最小值,将最大位数替换为最小值。
- 计算替换后的数字的差值。
下面是一个示例 JavaScript 代码:
function findDifference(number, replaceFrom, replaceTo) {
// 将数字转换为字符串
let numberStr = number.toString();
// 初始化最大值和最小值
let maxNumber = Number(numberStr.replace(new RegExp(replaceFrom, 'g'), replaceTo));
let minNumber = Number(numberStr.replace(new RegExp(replaceTo, 'g'), replaceFrom));
// 计算差值
let difference = maxNumber - minNumber;
return difference;
}
// 示例用法
const number = 328515;
const replaceFrom = '3';
const replaceTo = '9';
const difference = findDifference(number, replaceFrom, replaceTo);
console.log("最大值与最小值之差为:" + difference);
在这个示例中,我们将数字 328515 中的所有 3 替换为 9,然后计算替换后的最大值和最小值之差。
三面
- 自我介绍与实习经历
- 是否了解过性能优化, 有哪些方式
- 原生实现 tooltip 要实现一个原生的 tooltip,你可以使用 HTML、CSS 和 JavaScript 来创建一个简单的提示框。下面是一个基本的示例:
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tooltip Example</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="tooltip">
Hover over me
<span class="tooltiptext">Tooltip text</span>
</div>
<script src="script.js"></script>
</body>
</html>
CSS(styles.css):
.tooltip {
position: relative;
display: inline-block;
border-bottom: 1px dotted black; /* 下划线样式 */
}
.tooltip .tooltiptext {
visibility: hidden;
width: 120px;
background-color: black;
color: white;
text-align: center;
border-radius: 6px;
padding: 5px 0;
/* 位置 */
position: absolute;
z-index: 1;
bottom: 100%;
left: 50%;
margin-left: -60px; /* 让提示框居中 */
}
.tooltip:hover .tooltiptext {
visibility: visible;
}
JavaScript(script.js):
// 如果需要动态更改提示框文本,可以使用以下代码
// const tooltipText = document.querySelector('.tooltiptext');
// tooltipText.textContent = 'New tooltip text';
这个示例中,当鼠标悬停在 "Hover over me" 文本上时,会显示一个提示框,并在提示框中显示 "Tooltip text" 文本。你可以根据需要自定义提示框的样式和行为。
3. CSS 如何绘制一个三角形
你可以使用 CSS 的 border 属性来绘制一个简单的三角形。下面是一种常见的方法:
HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Triangle Example</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="triangle"></div>
</body>
</html>
CSS(styles.css):
.triangle {
width: 0;
height: 0;
border-left: 50px solid transparent;
border-right: 50px solid transparent;
border-bottom: 100px solid red; /* 修改颜色和大小 */
}
在这个示例中,我们创建了一个 .triangle 类,使用 border 属性设置了三个边框。通过调整 border-width、border-color 和 border-style 可以控制三角形的大小、颜色和形状。
5. 算法:手写堆排序
堆排序(Heap Sort)是一种高效的排序算法,它利用了堆的数据结构来实现排序过程。堆是一种特殊的二叉树,它满足堆的性质:父节点的值总是大于等于(最大堆)或小于等于(最小堆)其子节点的值。
堆排序的基本思想是先将待排序的序列构建成一个堆,然后不断地将堆顶元素(最大元素或最小元素)与堆的最后一个元素交换,并调整堆,使得剩余元素仍然满足堆的性质,最终完成排序。
下面是堆排序的 JavaScript 实现:
function heapSort(arr) {
// 构建最大堆
buildMaxHeap(arr);
// 不断取出堆顶元素(最大元素),并调整堆
for (let i = arr.length - 1; i > 0; i--) {
// 将堆顶元素(最大元素)与堆的最后一个元素交换
[arr[0], arr[i]] = [arr[i], arr[0]];
// 调整堆,使得剩余元素仍然满足堆的性质
heapify(arr, 0, i);
}
}
// 构建最大堆
function buildMaxHeap(arr) {
// 从最后一个非叶子节点开始,依次向前调整堆
for (let i = Math.floor(arr.length / 2) - 1; i >= 0; i--) {
heapify(arr, i, arr.length);
}
}
// 调整堆
function heapify(arr, index, size) {
let largest = index; // 当前节点、左子节点和右子节点中的最大值索引
const left = 2 * index + 1; // 左子节点索引
const right = 2 * index + 2; // 右子节点索引
// 找到当前节点、左子节点和右子节点中的最大值索引
if (left < size && arr[left] > arr[largest]) {
largest = left;
}
if (right < size && arr[right] > arr[largest]) {
largest = right;
}
// 如果最大值不是当前节点,则交换它们,并递归调整堆
if (largest !== index) {
[arr[index], arr[largest]] = [arr[largest], arr[index]];
heapify(arr, largest, size);
}
}
// 示例用法
const arr = [4, 10, 3, 5, 1];
heapSort(arr);
console.log(arr); // 输出:[1, 3, 4, 5, 10]
在这个实现中,heapSort 函数用于执行堆排序,buildMaxHeap 函数用于构建最大堆,heapify 函数用于调整堆。通过这三个函数协作,可以实现对数组的原地排序。
7. 算法:lc 168. Excel 表列名称
好的,下面是使用 JavaScript 实现的解答:
function convertToTitle(n) {
let result = '';
while (n > 0) {
n--;
result = String.fromCharCode(n % 26 + 65) + result;
n = Math.floor(n / 26);
}
return result;
}
// 测试代码
console.log(convertToTitle(1)); // 输出:'A'
console.log(convertToTitle(28)); // 输出:'AB'
console.log(convertToTitle(701)); // 输出:'ZY'
这段代码的思路与 Python 实现相同。在 JavaScript 中,我们使用 String.fromCharCode 方法将 ASCII 码转换为字符,并且 ASCII 码中 A 对应的是 65,而不是 97(即 'A' 的 ASCII 码是 65)。
作者:zbwer
链接:www.nowcoder.com/discuss/595…
来源:牛客网