2023-07-15 前端面试题
- JS 数据类型都有哪些?
- JS 闭包是什么?应用场景?
- Vue2 data 为什么是函数?
- Vue2 生命周期钩子的都有哪些?
- JS 写一个 EventBus?
- JS 写一个快速排序?
- JS 事件循环机制?
- JS MutationObserver?
1. JavaScript 数据类型
在 JavaScript 中,数据类型分为两类:基本数据类型和引用数据类型。
基本数据类型有:
- Undefined
- Null
- Boolean
- Number
- String
- Symbol
- BigInt(ES2020 引入)
引用数据类型有:
- Object
- Array
- Function
2. JavaScript 闭包
闭包是 JavaScript 的一个重要特性,指的是一个函数与其相关的引用环境组合而成的实体。闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中,函数在每次创建时生成闭包。
应用场景:闭包常用于实现 JavaScript 层的私有方法和私有变量,也用于实现数据隐藏和封装。
3. Vue2 中 data 为什么是函数?
这是为了防止多个组件实例之间数据互相影响。每个 Vue 组件实例应该管理和维护自己的数据状态,因此需要一个自己独立的 data 对象。如果 data 是一个对象,则当我们复用组件时,所有实例会共享同一个 data 对象,从而导致一个组件的状态更改可能影响其他所有实例。如果 data 是一个函数,每个实例会返回一个全新的数据副本,实例之间的状态相互独立,互不干扰。
4. Vue2 生命周期钩子
Vue 的生命周期钩子主要包括:
- beforeCreate
- created
- beforeMount
- mounted
- beforeUpdate
- updated
- beforeDestroy
- destroyed
- errorCaptured (2.5.0+ 新增)
- activated (对于 keep-alive 组件)
- deactivated (对于 keep-alive 组件)
5. JavaScript EventBus 实现
函数的实现方式
// 面试题:实现 EventBus 类
// on、emit、off、once
function EventBus() {
this.events = new Map(); // {key => [func, func, ...]}
this.on = function (name, func) {
// 1. 判断name 是否有值
// 2. 如果有那么直接设置值
// 3. 没有的话添加一个值
// 4. key 是 传入的name
// 5. value [func, func]
let arr = this.events.get(name);
if (Array.isArray(arr)) {
arr.push({ func });
} else {
arr = [{ func }];
}
this.events.set(name, arr);
};
this.emit = function (name, ...reset) {
const arr = this.events.get(name);
if (Array.isArray(arr)) {
arr.forEach(({ func, once }) => {
func(...reset);
if (once) {
this.off(name, func);
}
});
}
};
this.off = function (name, func) {
let arr = this.events.get(name);
if (Array.isArray(arr)) {
arr = arr.filter(({ func: item }) => item !== func);
}
this.events.set(name, arr);
};
this.once = function (name, func) {
let arr = this.events.get(name);
if (Array.isArray(arr)) {
arr.push({ once: true, func });
} else {
arr = [{ once: true, func }];
}
this.events.set(name, arr);
};
}
Class 类的实现方式
// 面试题:实现 EventBus 类
// on、emit、off、once
class EventBus {
constructor() {
this.events = new Map(); // {key => [func, func, ...]}
}
on(name, func) {
// 1. 判断name 是否有值
// 2. 如果有那么直接设置值
// 3. 没有的话添加一个值
// 4. key 是 传入的name
// 5. value [func, func]
let arr = this.events.get(name);
if (Array.isArray(arr)) {
arr.push({ func });
} else {
arr = [{ func }];
}
this.events.set(name, arr);
}
emit(name, ...reset) {
const arr = this.events.get(name);
if (Array.isArray(arr)) {
arr.forEach(({ func, once }) => {
func(...reset);
if (once) {
this.off(name, func);
}
});
}
}
off(name, func) {
let arr = this.events.get(name);
if (Array.isArray(arr)) {
arr = arr.filter(({ func: item }) => item !== func);
}
this.events.set(name, arr);
}
once(name, func) {
let arr = this.events.get(name);
if (Array.isArray(arr)) {
arr.push({ once: true, func });
} else {
arr = [{ once: true, func }];
}
this.events.set(name, arr);
}
}
const eventBus = new EventBus();
function sayHello(name) {
console.log(name + " hello!");
}
function add(a, b) {
console.log(a + b);
}
eventBus.on("say-hello", sayHello);
eventBus.on("add", add);
eventBus.emit("say-hello", "Lizhongyi1");
eventBus.off("say-hello", sayHello);
eventBus.emit("say-hello", "Lizhongyi2");
eventBus.emit("add", 1, 2);
eventBus.emit("add", 3, 2);
eventBus.emit("add", 4, 2);
eventBus.emit("add", 9, 2);
6. JavaScript 快速排序
// 快速排序(Quick Sort)是一种使用分治法(Divide and Conquer)策略来实现的排序算法,由C. A. R. Hoare在1960年提出。在平均情况下,排序n个项目需要O(n log n)次比较,在最坏的情况下需要O(n^2)次比较。
// left right swaIndex
function quickStart(arr, left = 0, right = arr.length - 1) {
if (left < right) {
const swaIndex = pivot(arr, left, right);
quickStart(arr, left, swaIndex - 1);
quickStart(arr, swaIndex + 1, right);
}
return arr;
}
// 根据pivot对数组进行划分,并返回pivot的最终位置
function pivot(arr, start = 0, end = arr.length - 1) {
const fistItem = arr[start];
let swaIndex = start;
for (let index = start + 1; index <= end; index++) {
if (fistItem > arr[index]) {
swaIndex++;
// const item1 = arr[index];
// arr[index] = arr[swaIndex];
// arr[swaIndex] = item1;
[arr[swaIndex], arr[index]] = [arr[index], arr[swaIndex]];
}
}
// const item1 = arr[start];
// arr[start] = arr[swaIndex];
// arr[swaIndex] = item1;
[arr[swaIndex], arr[start]] = [arr[start], arr[swaIndex]];
return swaIndex;
}
const input = [5, 2, 1, 4, 6, 3, 7, -1, 4, 2, 2, 3];
const output = quickStart(input);
console.log(output); // [1, 2 , 3, 4, 5, 6, 7]
7. JavaScript 事件循环机制
JavaScript 是单线程的,所以为了防止阻塞主线程,引入了事件循环(Event Loop)机制。事件循环是 JS 实现异步的一种方法,它的运行机制如下:
- 同步任务都在主线程上执行,形成一个执行栈。
- 主线程之外,还存在一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。
- 主线程不断重复上面的第三步。
宏任务(macro-task)和微任务(micro-task)是事件循环的重要概念。宏任务包括:script(全局任务)、setTimeout、setInterval、setImmediate(Node.js 环境)等;微任务包括:Promise、process.nextTick(Node.js 环境)、MutationObserver(html5 新特性)等。
事件循环的顺序,决定了 JavaScript 代码的执行顺序。过程如下:
- 执行一个宏任务,这通常是 script(全局任务),在这个过程中如果遇到微任务,就将它添加到微任务的任务队列中。
- 当前宏任务执行完成后,会查看微任务队列,并将其中的所有微任务一次执行完毕。
- 当所有微任务完成后,有可能需要进行 UI 渲染。
- 然后开始下一个宏任务,执行相应的任务队列。
8. JS MutationObserver
JavaScript 中的 MutationObserver API 用于监视 DOM(文档对象模型)的变化。它设计的目的是观察 DOM 树结构的变化。当 DOM 发生诸如添加或移除元素,或者修改属性等变化时,它会被调用。
这意味着,通过使用 MutationObserver,开发者可以定义当特定部分的 web 页面(即 DOM 树的某个部分)发生改变时需要执行的特定动作或操作。这对于处理动态内容、编写用户脚本,或者与 DOM 元素交互的情况非常有用。
以下是一个简单的 MutationObserver 的使用示例:
// 选择需要观察变动的节点
const targetNode = document.getElementById("some-id");
// 观察器的配置(需要观察什么变动)
const config = { attributes: true, childList: true, subtree: true };
// 当观察到变动时执行的回调函数
const callback = function (mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.type === "childList") {
console.log("A child node has been added or removed.");
} else if (mutation.type === "attributes") {
console.log("The " + mutation.attributeName + " attribute was modified.");
}
}
};
// 创建一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上面的配置开始观察目标节点
observer.observe(targetNode, config);
// 之后,可停止观察
// observer.disconnect();
在这个例子中,我们监视 id 为"some-id"的元素。我们关注其属性、子节点列表以及子树的变动。当我们观察到这些变化时,我们在回调函数中进行了处理。