1. 1. 手写promise
    1. 1.1. 基础架子
    2. 1.2. test.js
    3. 1.3. terminal
  2. 2. 前端路由
    1. 2.1. 问题关键点
    2. 2.2. 路由是什么
    3. 2.3. 前端路由的实现方式和实现原理
    4. 2.4. 前端路由和服务端路由的区别
    5. 2.5. 前端路由的不足
  3. 3. webpack
    1. 3.1. 问题1:请解释webpack的核心概念及其作用
    2. 3.2. 问题2:Webpack 5 相比 Webpack 4 有哪些重大改进?请举例说明
    3. 3.3. 问题3:请解释loader的执行顺序和编写自定义loader
    4. 3.4. 问题4:请解释webpack插件机制并编写一个简单插件
    5. 3.5. 问题5:如何优化webpack构建性能?请结合具体npm包说明
    6. 3.6. 问题6:请解释Tree Shaking原理及如何确保其有效性
    7. 3.7. 问题7:请解释以下概念并说明其应用场景
  4. 4. 判断是否是平衡二叉树
    1. 4.1. 前序遍历
    2. 4.2. 后序遍历
  5. 5. new操作符的模拟实现
  6. 6. 防抖和节流
    1. 6.1. 防抖:搜索/窗口大小变化
    2. 6.2. 节流:滚动事件/按钮防止重复点击/鼠标移动和拖拽
  7. 7. 反转链表
  8. 8. 将列表还原为树状结构
    1. 8.1. 递归
  9. 9. 二叉搜索树的第k大节点
    1. 9.1. 递归
  10. 10. 找的数组中重复的数字
    1. 10.1. 哈希表
    2. 10.2. 交换
  11. 11. 前端面试:BFC(块级格式化上下文)全面解析
    1. 11.1. 1. 核心概念:什么是BFC?
    2. 11.2. 2. 如何触发/创建BFC?
    3. 11.3. 3. BFC的布局规则/特性
    4. 11.4. 4. BFC的常见应用场景
    5. 11.5. 场景一:解决外边距重叠(Margin Collapse)
      1. 11.5.1. 场景二:清除浮动,防止父元素高度塌陷
      2. 11.5.2. 场景三:阻止元素被浮动元素覆盖(实现两栏自适应布局)
    6. 11.6. 5. 面试官可能追问的问题
    7. 11.7. Q: overflow: hidden 创建 BFC 有什么缺点?
    8. 11.8. Q: BFC 和 IFC(内联格式化上下文)有什么区别?
    9. 11.9. Q: BFC 和 Flex/Grid 布局有什么关系?
    10. 11.10. 6. 总结回答思路
  12. 12. 跨域

2025年|剑指前端offer

手写promise

基础架子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153

function Promise(executor){
this.state = "pending";
this.onFulfilledCB = [];
this.onRejectedCB = [];

const self = this;

function resolve(value){
setTimeout(function(){
if(self.state === "pending") {
self.state = "fulfilled";
self.data = value;
for(let i=0; i<self.onFulfilledCB.length;i++){
self.onFulfilledCB[i](value)
}
}
});
}

function reject(reason){
setTimeout(function(){
if(self.state === "pending"){
self.state = "rejected";
self.data = reason;
for(let i=0; i<self.onRejectedCB.length;i++){
self.onRejectedCB[i](reason);
}
}
});
}

try{
executor(resolve, reject)
}catch(reason){
reject(reason)
}
}

Promise.prototype.then = function(onFulfilled, onRejected){
const self = this;
let promise2;
return promise2 = new Promise(function(resolve, reject){
if(self.state === "fulfilled"){
setTimeout(function(){
if(typeof onFulfilled === "function"){
try{
const x = onFulfilled(self.data);
promiseResolutionProcedure(promise2, x, resolve, reject);
}catch(e){
reject(e);
}
} else {
resolve(promise1Value)
}
});
}
else if(self.state === "rejected") {
setTimeout(function(){
if(typeof onRejected === "function"){
try{
const x = onRejected(self.data);
promiseResolutionProcedure(promise2, x, resolve, reject)
} catch(e){
reject(e)
}
} else {
reject(self.data)
}
})
} else if(self.state === "pending"){
self.onFulfilledCB.push(function(promise1Value){
if(typeof onFulfilled === "function"){
try{
const x = onFulfilled(self.data)
promiseResolutionProcedure(promise2, x, resolve, reject);
}catch(e){
reject(e)
}
} else {
resolve(promise1Value)
}
});
self.onRejectedCB.push(function(promise1Reason){
if(typeof onRejected === "function"){
try{
const x = onRejected(self.data)
promiseResolutionProcedure(promise2, x, resolve, reject)
}catch(e){
reject(e)
}
}
else {
reject(promise1Reason)
}
})
}
});
}

function promiseResolutionProcedure(promise2, x, resolve, reject){
if(promise2 === x){
return reject(new TypeError("chaining cycle detected for promise"))
}

if(x instanceof Promise){
if(x.state === "pending"){
x.then(function(value){
promiseResolutionProcedure(promise2, value, resolve, reject)
}, reject)
}
else if(x.state === "fulfilled"){
resolve(x.data);
}
else if(x.state === "rejected"){
reject(x.data)
}
return
}
if(x && (typeof x === "object" || typeof x === "function")){
let isCalled = false;
try{
let then = x.then;
if(typeof then === "function"){
then.call(
x,
function resolvePromise(y){
if(isCalled) return;
isCalled = true;
return promiseResolutionProcedure(promise2, y, resolve, reject)
},
function rejectPromise(r){
if(isCalled) return;
isCalled = true;
return reject(r)
}
);
}
else{
resolve(x);
}
}catch(e){
if(isCalled) return;
isCalled = true;
reject(e)
}
}
else {
resolve(x)
}
}

module.exports = Promise;

test.js

1
2
3
4
5
6
7
8
9
10
11
12
const Promise = require('./promise.js')
Promise.deferred = function(){
const obj = {};
obj.promise = new Promise(function(resolve, reject){
obj.resolve = resolve;
obj.reject = reject;
})
return obj;

}

module.exports = Promise;

terminal

1
$ npx promises-aplus-tests test.js

前端路由

问题关键点

路由,hash路由,History路由,无刷新,SPA

路由是什么

  • 路由(Routing)是指根据不同的URL地址(路径)来显示不同的内容或页面的机制。
  • 服务端路由(Server Routing):URL发送到服务器,由服务器决定返回哪个页面或数据。
  • 前端路由(Client-side Routing):在浏览器端,根据URL的变化动态切换页面内容,而无需重新加载整个页面。

路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。对于Web开发来说,路由的实质是URL到对应的处理程序的映射。Web路由既可以由服务端,也可以由前端实现。其中前端路由根据实现方式的不同,可以分为Hash路由和History路由。
前端路由对于服务端路由来说,最显著的特点是页面可以在无刷新的情况下进行页面的切换。基于前端路由的这一特点,诞生了一种无刷新的单页应用开发模式SPA。SPA通过前端路由避免了页面的切换打断用户体验,让Web应用的体验更接近一个桌面应用程序。

前端路由的实现方式和实现原理

  • 前端路由主要通过监听URL的变化 来控制页面内容的渲染,常见实现方式有两种:
    方法一:Hash模式
  • URL中带#(例如:https://music.163.com/?from=infinity#/song?id=5308028)
  • 浏览器不会向服务器发送#后面的内容
  • 通过监听window.onhashchange来捕捉URL变化
    1
    2
    3
    4
    5
    6
    7
      foo://example.com:8042/over/there?name=ferret#nose
    \_/ \______________/\_________/ \_________/ \__/
    | | | | |
    scheme authority path query fragment
    | _____________________|__
    / \ / \
    urn:example:animal:ferret:nose
    fragment 本质是用来标识次级资源,fragment有以下特点:
  • 修改fragment的内容不会触发网页重载。
  • 修改fragment的内容会改变浏览器的历史记录。
  • 修改fragment的内容会触发浏览器的onhashchange事件。
  • 基于fragment的以上特点,可实现基于Hash的前端路由。
    原理:
  • 用户点击链接 → 改变hash
  • hashchange事件触发
  • 根据hash渲染对应的页面内容
  • 优点:兼容性好,不会刷新页面, 无需服务端配置
  • 缺点:URL不够美观,无法完全利用浏览器历史API,服务端无法获取hash部分内容,可能和锚点功能冲突,SEO不友好。
    M1
    1
    2
    3
    4
    window.addEventListener('hashchange', () => {
    const route = location.hash.slice(1); // 去掉 #
    render(route);
    });
    M2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    /**
    * 解析 hash
    * @param hash
    * @returns
    */
    function parseHash(hash) {
    // 去除 # 号
    hash = hash.replace(/^#/, "");

    // 简单解析示例
    const parsed = hash.split("?");
    //如果 hash 中没有 ?,search 会是 undefined。
    // 返回 hash 的 path 和 query
    return {
    pathname: parsed[0],
    search: parsed[1],
    };

    //如果URL hash是#page?foo=1&bar=2
    //const parsedHashRes = parseHash("#page?foo=1&bar=2");
    //console.log(parsedHashRes);
    // { pathname: "page", search: "foo=1&bar=2" }

    //const params = new URLSearchParams(parsedHashRes.search);
    //console.log(params.get("foo")) // "1"
    //console.log(params.get("bar")) // "2"
    /**
    for (const [key, value] of params) {
    console.log(key, value);
    }
    **/
    //foo 1
    //bar 2
    }

    /**
    * 监听 hash 变化
    * @returns
    */
    function onHashChange() {
    // 解析 hash
    const { pathname, search } = parseHash(location.hash);

    // 切换页面内容
    switch (pathname) {
    case "/home":
    document.body.innerHTML = `Hello ${search}`;
    return;
    default:
    return;
    }
    }

    window.addEventListener("hashchange", onHashChange);
    方法二:History 模式(HTML5 History API)
  • URL 看起来像正常路径(例如:http://example.com/home)
  • 利用 history.pushState() 和 history.replaceState() 修改浏览器历史记录
  • history.pushState:将给定的 Data 添加到当前标签页的历史记录栈中。
  • history.replaceState:将给定的 Data 更新到历史记录栈中最新的一条记录中。
  • 监听 popstate 事件来检测浏览器前进/后退
    原理:
  • 用户点击链接 → 调用 pushState 改变 URL
  • 渲染对应页面内容
  • 用户按浏览器回退 → 触发 popstate 事件 → 渲染对应页面
  • 优点:前端监控友好,SEO相对Hash路由友好,服务端可获取完整的链接和参数。
  • 缺点:兼容性稍弱。,刷新页面会请求服务器,服务器需要配置重定向到入口页(各 path 均指向同一个 HTML如 index.html)
    M1
    1
    2
    3
    4
    5
    6
    7
    8
    window.addEventListener('popstate', () => {
    render(location.pathname);
    });

    function navigate(path) {
    history.pushState({}, '', path);
    render(path);
    }
    M2
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    function onHistoryChange() {
    const {pathname, search} = location;
    switch(){
    case "/home":
    document.body.innnerHTML = `Hello ${search.replace(/^\?/,"")}`
    return;
    default:
    document.body.innerHTML = "hello world";
    return;
    }
    }

    function pushState(target) {
    history.pushState(null, "", target);
    onHistoryChange()
    }

    setTimeout(() => {
    pushState("/home?name=HZSDD")
    },3000)

    setTimeout(() => {
    history.back()
    },6000)

    window.addEventListener("popstate", onHistoryChange)

    前端路由和服务端路由的区别

    特性 前端路由 服务端路由
    页面刷新 页面不刷新,只渲染局部 每次请求 URL 都刷新页面
    URL 变化 浏览器地址栏变化,但请求不发送到服务器 URL 变化时,浏览器向服务器发请求
    响应速度 快,局部渲染 慢,需要重新加载整个页面
    SEO 不太友好(除非使用 SSR) 好,搜索引擎可直接抓取
    技术实现 JS + hash/history API 服务器端路由框架(Express、Django、Rails 等)
    ### 前端路由的优势
  • 体验更流畅:页面不刷新,用户操作即时响应
  • 减少服务器压力:不需要每次请求都重新渲染整个页面
  • 灵活控制页面渲染:可以按需加载组件或模块,提高性能
  • 便于构建单页应用(SPA):结合 Vue、React、Angular 等框架使用

    前端路由的不足

  • 首次加载慢:单页应用需要一次性加载 JS 包
  • SEO不友好:搜索引擎抓取困难,需要 SSR 或 prerender 解决
  • 浏览器刷新问题:History 模式下刷新会向服务器发请求,服务器需配置重定向
  • JS 依赖:如果用户禁用 JS,页面可能无法正常访问

参考资料:

  1. 剑指前端 Offer-前端路由实现
  2. chatgpt 4
  3. Hash嵌套路由解析 -> *Prac.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    function parseHash(hash) {
    hash = hash.replace(/^#/, "");
    const [pathname, search] = hash.split("?");
    const query = {};

    if (search) {
    const params = new URLSearchParams(search);

    for (const [key, value] of params) {
    const keys = key.split(/[\[\]]+/).filter(Boolean);
    let parsedValue;

    // 改进的类型转换逻辑
    if (value === "") {
    parsedValue = "";
    } else if (!isNaN(value) && value.trim() !== "" && value.trim() !== "NaN") {
    // 避免将 "NaN" 转换为数字
    const num = Number(value);
    parsedValue = String(num) === value.trim() ? num : value;
    } else if (value.toLowerCase() === "true") {
    parsedValue = true;
    } else if (value.toLowerCase() === "false") {
    parsedValue = false;
    } else if (value === "null") {
    parsedValue = null;
    } else if (value === "undefined") {
    parsedValue = undefined;
    } else {
    parsedValue = value;
    }

    let current = query;
    for (let i = 0; i < keys.length; i++) {
    const k = keys[i];

    // 处理数组语法:tags[] 或 tags[0]
    const isArrayKey = k === "" || (!isNaN(k) && k !== "");

    if (i === keys.length - 1) {
    // 最终赋值
    if (isArrayKey) {
    // 如果是数组语法,确保父级是数组
    const parentKey = keys[i - 1];
    if (i > 0 && current[parentKey] && !Array.isArray(current[parentKey])) {
    current[parentKey] = [current[parentKey]];
    }
    if (Array.isArray(current)) {
    current.push(parsedValue);
    } else {
    current[k] = parsedValue;
    }
    } else if (current[k] !== undefined) {
    // 处理重复键名
    if (!Array.isArray(current[k])) {
    current[k] = [current[k]];
    }
    current[k].push(parsedValue);
    } else {
    current[k] = parsedValue;
    }
    } else {
    // 遍历嵌套对象
    if (!current[k] || typeof current[k] !== "object") {
    current[k] = isArrayKey ? [] : {};
    }
    current = current[k];
    }
    }
    }
    }

    return { pathname, query };
    }

    // 增强的测试用例
    const testCases = [
    "#page?foo=1&bar=2&bar=3&tags[]=a&tags[]=b&empty&flag=true&count=0&user[name]=Alice&user[age]=20",
    "#home?ids[0]=1&ids[1]=2&filter[name]=John&filter[active]=true",
    "#test?single=hello&number=42&bool=false&nullval=null",
    "#empty",
    "#path?arr=1&arr=2&obj[a]=1&obj[b]=2"
    ];

    testCases.forEach(test => {
    console.log(test);
    console.log(parseHash(test));
    console.log("---");
    });

    webpack

问题1:请解释webpack的核心概念及其作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
// 期待回答包含:
// - Entry: 打包入口起点
// 单入口:entry: './src/index.js'
// 多入口:entry: { app: './src/app.js', admin: './src/admin.js' }
entry: {
app: './src.app.js',
admin: './src/admin.js'
}
// - Output: 输出配置,指定打包后文件的存放位置和命名规则
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].[contenthash].js',
clean: true //webpack5特有,代替clean-webpack-plugin插件
}
// - Loader: 处理非JS文件转换,将不同类型的文件转换为webpack能够处理的模块
// loader的执行顺序是从右到左,从下到上。
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
// - Plugin: 执行更广泛的任务(打包优化、资源管理等)
// 作用:自动生成 HTML 文件,并自动注入打包后的 JS/CSS 资源
const htmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: '.src/index.html',
minify: true //打包优化,压缩html
})
]
}
//将 CSS 从 JS 中提取为独立文件,支持 CSS 代码分割
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})
],
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
}
}
//压缩优化
module.exports = {
optimization: {
minimizer: [
new TerserPlugin(), //js压缩
new CssMinimizerPlugin() //css压缩
]
}

}
//代码分割
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
},
common: {
test: /[\\/]src[\\/]common[\\/]/,
name: 'common',
chunks: 'all',
minSize: 0, //10000 -》 10kb
minChunks: 2 //被两个以上的chunks引用才提取
}
},
},
},
}
//bundle分析
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin()
]
}
//静态资源管理
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
plugins: [
new CopyWebpackPlugins({
patterns: [
{
from: 'public',
to: 'assets'
}
]
})
]
}
//环境变量注入
cosnt webpack = require('webpack');
module.exports = {
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production')//文本替换
})
]
}
// - Mode: 开发/生产模式
module.exports = {
mode: 'development',
}
// - Module: 模块系统
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
// - Chunk: 代码块,模块的集合,webpack 内部的中间构建产物
module.exports = {
optimization: {
slitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all'
}
}
}
}
}

// - Bundle: 最终输出文件
module.exports = {
output: {
filename: '[name].bundle.js',
chunkFilename: '[name].chunk.js',
path: path.resolve(__dirname, 'dist')
}
}

问题2:Webpack 5 相比 Webpack 4 有哪些重大改进?请举例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 期待回答:
// 1. 模块联邦 (Module Federation)
// 2. 持久化缓存改进
// 3. Tree Shaking 增强
// 4. 资源模块 (Asset Modules)
// 5. 更好的Tree Shaking
// 6. 移除Node.js polyfills

// 编码题:演示Webpack 5的模块联邦配置
// micro-frontend-app/webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'app',
remotes: {
mfeApp: 'mfeApp@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true },
'react-dom': { singleton: true }
}
})
]
};

问题3:请解释loader的执行顺序和编写自定义loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 问题:以下loader的执行顺序是什么?
module: {
rules: [
{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}
]
}

// 答案:从右到左,从下到上执行
// sass-loader → css-loader → style-loader

// 编码题:编写一个简单的自定义loader
// my-loader.js
module.exports = function(source) {
// 实现简单的文本替换
return source.replace(/console\.log\(.*\);/g, '');
};

// 使用自定义loader
module: {
rules: [
{
test: /\.js$/,
use: path.resolve(__dirname, 'my-loader.js')
}
]
}

问题4:请解释webpack插件机制并编写一个简单插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 插件编写示例
class MyCleanPlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync('MyCleanPlugin', (compilation, callback) => {
// 清理构建前删除旧文件
const fs = require('fs');
const outputPath = compiler.options.output.path;

if (fs.existsSync(outputPath)) {
fs.rmSync(outputPath, { recursive: true });
}

console.log('清理完成');
callback();
});
}
}

// 使用插件
plugins: [
new MyCleanPlugin()
]

问题5:如何优化webpack构建性能?请结合具体npm包说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const TerserPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');

module.exports = {
optimization: {
// Webpack 4 vs Webpack 5 差异点
minimize: true,
minimizer: [
// Webpack 5 推荐使用TerserPlugin
new TerserPlugin({
parallel: true, // 开启多进程
terserOptions: {
compress: {
drop_console: true, // 生产环境移除console
}
}
}),
new CssMinimizerPlugin(),
],
// 代码分割策略
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10,
reuseExistingChunk: true,
},
common: {
name: 'common',
minChunks: 2,
priority: 5,
reuseExistingChunk: true,
}
}
}
},
plugins: [
new SpeedMeasurePlugin(), // 构建速度分析
new BundleAnalyzerPlugin({ // 包体积分析
analyzerMode: 'static',
openAnalyzer: false
})
],
// Webpack 5 持久化缓存
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename]
}
}
};

问题6:请解释Tree Shaking原理及如何确保其有效性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// package.json 配置
{
"sideEffects": false,
// 或指定有副作用的文件
"sideEffects": [
"*.css",
"*.scss"
]
}

// webpack.config.js 配置
module.exports = {
mode: 'production',
optimization: {
usedExports: true, // 标记使用到的导出
minimize: true, // 压缩时删除未使用代码
sideEffects: true // 识别package.json中的sideEffects
}
};

// 示例:确保ES6模块语法
// utils.js - 可以Tree Shaking
export const util1 = () => console.log('util1');
export const util2 = () => console.log('util2');

// main.js - 只使用util1
import { util1 } from './utils'; // 只有util1被打包

=

问题7:请解释以下概念并说明其应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
// Module Federation: 微前端架构,多个独立构建的应用共享代码
// Host(宿主):消费远程模块的应用
// Remote(远程):暴露模块给其他应用使用的应用
// Shared(共享):在应用间共享的依赖
// 场景1:微前端架构 - 多个团队独立开发
// app1 (产品团队) 暴露产品相关模块
new ModuleFederationPlugin({
name: 'app1',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/components/ProductList',
'./ProductDetail': './src/components/ProductDetail'
},
shared: {
react: { singleton: true, eager: true },
'react-dom': { singleton: true, eager: true }
}
});

// app2 (订单团队) 消费产品模块
new ModuleFederationPlugin({
name: 'app2',
remotes: {
app1: 'app1@http://localhost:3001/remoteEntry.js'
}
});

// 在app2中使用远程组件
const ProductList = React.lazy(() => import('app1/ProductList'));
// Code Splitting: 代码分割,按需加载
// 1. 动态导入 - 路由级别分割
const Home = lazy(() => import(/* webpackChunkName: "home" */ './pages/Home'));
const About = lazy(() => import(/* webpackChunkName: "about" */ './pages/About'));
const Contact = lazy(() => import(/* webpackChunkName: "contact" */ './pages/Contact'));

// 2. 组件级别分割 - 大型组件按需加载
const HeavyComponent = lazy(() =>
import(/* webpackChunkName: "heavy-component" */ './components/HeavyComponent')
);

// 3. 第三方库分割 - webpack配置
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// React相关库
react: {
test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
name: 'react-vendor',
priority: 40
},
// 工具库
utils: {
test: /[\\/]node_modules[\\/](lodash|moment|axios)[\\/]/,
name: 'utils-vendor',
priority: 30
},
// 公共模块
common: {
minChunks: 2,
name: 'common',
priority: 20,
reuseExistingChunk: true
}
}
}
}
// Hot Module Replacement: 热模块替换,开发时保持状态
// 在应用程序运行过程中,替换、添加或删除模块,而无需完全重新加载页面。
// webpack.config.js - 开发环境配置
module.exports = {
devServer: {
hot: true, // 开启HMR
liveReload: false, // 禁用全页面刷新
},
// 针对不同文件类型的HMR处理
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader', // 支持CSS HMR
'css-loader'
]
},
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
plugins: [
// React组件热更新
isDevelopment && 'react-refresh/babel'
].filter(Boolean)
}
}
}
]
}
};

// 在代码中处理HMR
if (module.hot) {
// 接受当前模块的热更新
module.hot.accept();

// 接受特定依赖的热更新
module.hot.accept('./dep.js', () => {
// 依赖更新后的回调
console.log('依赖已更新');
});

// React组件热更新
module.hot.accept('./App.js', () => {
const NextApp = require('./App.js').default;
ReactDOM.render(<NextApp />, document.getElementById('root'));
});
}
// Scope Hoisting: 作用域提升,减少闭包,优化包体积
// webpack.config.js
module.exports = {
optimization: {
concatenateModules: true, // 开启Scope Hoisting
usedExports: true, // 标记未使用代码
sideEffects: true // 识别副作用
}
};

// package.json - 帮助webpack识别无副作用的模块
{
"sideEffects": false,
// 或指定有副作用的文件
"sideEffects": [
"*.css",
"*.scss",
"./src/polyfill.js"
]
}
// webpack的构建流程是怎样的?
/**
1. 初始化阶段

2. 编译阶段

3. 模块构建阶段

4. 完成编译阶段

5. 输出阶段
**/
// 如何实现按需加载和预加载?

// 如何处理webpack打包时的内存溢出?

// 如何自定义webpack的解析(resolve)规则?
const path = require('path');

module.exports = {
resolve: {
// 1. 路径别名 - 简化导入路径
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@assets': path.resolve(__dirname, 'src/assets'),
'@styles': path.resolve(__dirname, 'src/styles'),
// 针对特定包的别名
'react': path.resolve(__dirname, 'node_modules/react'),
'react-dom': path.resolve(__dirname, 'node_modules/react-dom')
},

// 2. 扩展名解析顺序
extensions: [
'.tsx', '.ts', // TypeScript
'.jsx', '.js', // JavaScript
'.mjs', '.cjs', // ES模块和CommonJS
'.json', // JSON文件
'.css', '.scss', '.sass' // 样式文件
],

// 3. 模块解析目录
modules: [
path.resolve(__dirname, 'src'), // 优先从src目录解析
path.resolve(__dirname, 'node_modules'), // 然后是node_modules
'node_modules' // 最后是全局node_modules
],

// 4. package.json主字段解析顺序
mainFields: [
'browser', // 浏览器特定版本
'module', // ES6模块版本
'main' // CommonJS版本
],

// 5. 包的主文件名称
mainFiles: ['index', 'main'],

// 6. 不跟随符号链接(提升构建性能)
symlinks: false,

// 7. 缓存解析结果(提升构建性能)
cache: {
type: 'filesystem',
cacheDirectory: path.resolve(__dirname, '.resolve-cache')
}
},

// 8. 条件性解析配置
resolveLoader: {
modules: [
'node_modules',
path.resolve(__dirname, 'loaders') // 自定义loader目录
]
}
};

// 使用示例
// 配置前
import Button from '../../../components/Button';
import utils from '../../../../utils/helpers';

// 配置后
import Button from '@components/Button';
import utils from '@utils/helpers';

判断是否是平衡二叉树

前序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

const isBalanced = function(root){
if(root === null){
return true;
}else {
return Math.abs(height(root.left) -height(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right)
}
}

const height = function(root){
if(root === null){
return 0
} else {
return Math.max(height(root.left), height(root.right)) + 1
}
}

后序遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let isBalanced = function(root){
return height(root) != -1
}

let height = function(root){
if(root == null){
return 0
}

const left = height(root.left)
const right = height(root.right)

if(left === -1 || right === -1 || Math.abs(left-right)>1){
return -1
}

return Math.max(left, right)

}

new操作符的模拟实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

function fakeNew() {
let obj = Object.create(null);
let Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype;
let ret = Constructor.apply(obj, arguments);
return typeof ret === "object" && rest !== null ? ret : obj;
}

function Group(name, member){
this.name = name;
this.member = member;
}

let group = fakeNew(Group, 'xx', 1)

防抖和节流

防抖:搜索/窗口大小变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

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

function debounce1(func, delay){
let timer = null;
return function(){
if(timer){
clearTimeout(timer);
timer = null
}
let self = this;
let args = arguments

timer = setTimeout(() => {
func.apply(self, args)
timer = null
}, delay)
}
}

节流:滚动事件/按钮防止重复点击/鼠标移动和拖拽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

function throttle(func, delay){
let last = 0;
return function(...args){
const now = Date.now();
if(now-last >= delay){
func.apply(this, args);
last = now;
}
}
}

function throttle1(func, delay, options = {}) {
let last = 0;
let timer = null;
const { leading = true, trailing = true } = options;

return function(...args) {
const now = Date.now();
const context = this;

// 如果是第一次且不需要立即执行
if (!last && !leading) {
last = now;
}

const remainWaitTime = delay - (now - last);

if (remainWaitTime <= 0) {
// 清除之前的定时器
if (timer) {
clearTimeout(timer);
timer = null;
}
last = now;
func.apply(context, args);
} else if (!timer && trailing) {
// 设置定时器确保最后一次执行
timer = setTimeout(() => {
last = leading ? Date.now() : 0;
timer = null;
func.apply(context, args);
}, remainWaitTime);
}
};
}

// 1. 默认行为:首尾都执行
throttle1(func, 300); // 等同于 {leading: true, trailing: true}

// 2. 只执行首部(立即执行)
throttle1(func, 300, {leading: true, trailing: false});

// 3. 只执行尾部(延迟执行)
throttle1(func, 300, {leading: false, trailing: true});

// 4. 都不执行(无意义,函数永远不会执行)
throttle1(func, 300, {leading: false, trailing: false});

反转链表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 迭代法
function reverseList(head){
if(!head || !head.next) return head
let prev = null, curr = head;
while(curr){
// 用于临时存储 curr 后继节点
let next = curr.next;
// 反转 curr 的后继指针
curr.next = prev
// 变更prev、curr
// 待反转节点指向下一个节点
prev = curr;
curr = next
}
head = prev
return head
}

将列表还原为树状结构

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

function listToTree(list, pid = null, {idName = "id", pidName = "pid", childName = "children"} = {}){
return list.reduce(() => {
if(item[pidNmae] === pid){
const children = listToTree(list, item[idName]);
if(children.length > 0){
item[childName] = children;
}
return [...root, item];
}
return root;
}, [])
}

function listToTree1(list, rootId = null, {idName = "id", pidName = "pid", childName = "children"} = {}){
const record = {};
const root = [];

list.forEach(item => {
record[item[idName]] = item;
item[childName] = []
})

list.forEach((item) => {
if(item[pidName] === rootId){
root.push(item)
}else{
record[item[pidName]][childName].push(item)
}
});
return root;
}


function listToTree2(list, rootId = null, {idName="id", pidName="pid", childName="children"} = {}){
const record = {};
const root = [];

list.forEach((item) => {
const id = item[idName]
const parentId = item[pidName]

record[id] = !record[id] ? item : {...item, ...record[id]}
const treeItem = record[id]
if(parentId === rootId){
root.push(treeItem)
}else{
if(!record[parentId]){
record[parentId] = {}
}

if(!record[parentId][childName]){
record[parentId][childName] = []
}

record[parentId][childName].push(treeItem)
}
})

return root;
}

function listToTree3(list, rootId=null, {idName="id", pidName="pid", childName="children"}={}){
const record = {};
const root = [];
list.forEach((item) => {
const newItem = Object.assign({}, item);
const id = newItem[idName]
const parentId = newItem[pidName]

newItem[childName] = record[id]?record[id]:(record[id]=[])
if(parentId === rootId){
root.push(newItem)
}else{
if(!record[parentId]){
record[parentId] = []
}
record[parentId].push(newItem)
}
})
return root;
}

二叉搜索树的第k大节点

递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const kthLargest = () => {
let res = null;
const dfs = (root) => {
if(!root) return;
dfs(root.right);
if(k===0) return;
if(--k===0){
res=root.val
}
dfs(root.left);
}
dfs(root);
return res;
}

const kthLargest1 = function(root, k){
if(!root) return 0;
const stack = [];
while(stack.length || root){
while(root){
stack.push(root);
root = root.right;
}

const cur = stack.pop();
k--;
if(k===0) return cur.val;
root = cur.left;
}
return 0
}

找的数组中重复的数字

哈希表

1
2
3
4
5
6
7
8
9
10
11
12
const findRepeatNumber = function(nums){
let map = new Map();
let i =0;
while(i<nums.length){
if(map.has(nums[i])){
return map.get(nums[i]);
}

map.set(nums[i], nums[i]);
i++;
}
}

交换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

const findRepeatNumber = function(nums) {
let i = 0;
while(i<nums.length){
if(nums[i] === i){
i++;
continue;
}

if(nums[nums[i]] === nums[i]){
return nums[i]
}

[nums[nums[i]], nums[i]] = [nums[i], nums[nnums[i]]]
}

return -1;
}

前端面试:BFC(块级格式化上下文)全面解析

1. 核心概念:什么是BFC?

标准回答:
BFC(Block Formatting Context)是 CSS 渲染过程中的一个独立区域。它决定了块级盒如何布局,并且与这个区域的外部毫不相干。简单来说,一个创建了 BFC 的元素,就像一个被隔离出来的独立容器,里面的子元素不会在布局上影响到外面的元素。
通俗解释:
“可以把它理解成 Web 页面里的一个’结界’。在这个’结界’内部,元素按照自己的规则进行排列,不受外部影响,同时内部的元素也不会跑出去影响外面的布局。”

2. 如何触发/创建BFC?

一个元素在满足以下任一条件时即可创建 BFC:

  • 根元素<html>) - 整个页面就是一个最大的BFC
  • 浮动元素 - float 的值不是 none
  • 绝对定位元素 - position 的值是 absolutefixed
  • display 为特定值 - inline-blocktable-celltable-captionflexinline-flexgridinline-grid
  • overflow 值不是 visible - 如 overflow: hiddenscrollauto(最常用且副作用较小的方式之一)
  • contain 值为 layoutcontentpaint 的元素
  • display: flow-root现代推荐方式,专门用于创建BFC,无副作用)

    面试技巧: 说完以上列表后,可以补充:”在实际开发中,我最常用的是 overflow: hiddendisplay: flow-root,因为 flow-root 是专门为创建BFC而设计的,没有任何副作用。”

    3. BFC的布局规则/特性

    一个BFC区域内的布局遵循以下规则:
  1. 垂直排列 - 内部的块级盒会在垂直方向上一个接一个地放置
  2. 外边距重叠 - 垂直方向上的距离由 margin 决定。属于同一个BFC的两个相邻块级盒的 margin 会发生重叠
  3. 接触包含块边界 - 每个元素的左外边距与包含块的左边界相接触(对于从左往右的格式化),即使存在浮动也是如此
  4. 不与浮动重叠 - BFC的区域不会与浮动元素重叠
  5. 独立容器 - BFC是一个独立的容器,容器里面的子元素不会影响到外面的元素,反之亦然
  6. 包含浮动元素 - 计算BFC的高度时,浮动元素也参与计算

    4. BFC的常见应用场景

    场景一:解决外边距重叠(Margin Collapse)

问题代码:

1
2
3
4
5
6
7
<style>
.box { width: 100px; height: 100px; background: lightblue; margin: 50px; }
</style>
<body>
<div class="box"></div>
<div class="box"></div>
</body>

两个 .box 之间的垂直距离是 50px,而不是 100px,因为它们的 margin 发生了重叠。
BFC解决方案:

1
2
3
4
5
6
7
8
9
10
<style>
.box { width: 100px; height: 100px; background: lightblue; margin: 50px; }
.bfc { overflow: hidden; /* 触发BFC */ }
</style>
<body>
<div class="box"></div>
<div class="bfc">
<div class="box"></div>
</div>
</body>

现在两个 .box 之间的垂直距离就是 100px,因为它们的 margin 处于不同的BFC中,不会重叠。

场景二:清除浮动,防止父元素高度塌陷

问题代码:

1
2
3
4
5
6
7
<style>
.parent { border: 5px solid red; }
.child { float: left; width: 100px; height: 100px; background: blue; }
</style>
<div class="parent">
<div class="child"></div>
</div>

由于 .child 浮动脱离了文档流,.parent 的高度会塌陷为0。
BFC解决方案:

1
2
3
4
<style>
.parent { border: 5px solid red; overflow: hidden; /* 触发BFC */ }
.child { float: left; width: 100px; height: 100px; background: blue; }
</style>

根据BFC规则:计算BFC的高度时,浮动元素也参与计算。所以父元素 .parent 的高度被撑开了。

场景三:阻止元素被浮动元素覆盖(实现两栏自适应布局)

问题代码:

1
2
3
4
5
6
7
8
<style>
.aside { float: left; width: 100px; height: 150px; background: lightgreen; }
.main { height: 200px; background: lightblue; }
</style>
<body>
<div class="aside">侧边栏</div>
<div class="main">主内容区</div>
</body>

主内容区 .main 会被浮动元素 .aside 覆盖一部分。
BFC解决方案:

1
2
3
4
<style>
.aside { float: left; width: 100px; height: 150px; background: lightgreen; }
.main { height: 200px; background: lightblue; overflow: hidden; /* 触发BFC */ }
</style>

根据BFC规则:BFC的区域不会与浮动元素重叠。所以 .main 会紧贴着 .aside 的右边框,形成两栏自适应布局。

5. 面试官可能追问的问题

Q: overflow: hidden 创建 BFC 有什么缺点?

A: 可能会意外地剪裁掉定位在容器之外的内容(如工具提示)。所以 display: flow-root 是更安全的选择。

Q: BFC 和 IFC(内联格式化上下文)有什么区别?

A: BFC 是针对块级元素的布局环境,规则是垂直排列;IFC 是针对内联元素的,规则是水平排列。

Q: BFC 和 Flex/Grid 布局有什么关系?

A: Flex容器和Grid容器本身会为它们的子项创建新的格式化上下文(FFC/GFC),这些上下文同样具有隔离性,可以解决类似BFC的问题(如margin重叠,清除浮动)。

6. 总结回答思路

当被问到BFC时,建议按照以下逻辑链回答:

  1. 下定义 - BFC是一个独立的渲染区域,内外互不影响
  2. 讲创建 - 列举多种触发BFC的方式,指出常用和推荐的
  3. 说规则 - 阐述BFC的核心布局规则
  4. 举例子 - 结合代码说明BFC如何解决实际开发中的三大经典问题

面试题来源

跨域

跨域问题,主要是因为浏览器同源策略,同源策略限制了脚本从 another-domain.com 访问 another-domain.com/api/data。
解决方案:JSONP,CORS,反向代理,页面通信postMessage。

跨域与监控
前端项目在统计前端报错监控时会遇到上报的内容只有 Script Error 的问题。这个问题也是由同源策略引起。在 <script> 标签上添加 crossorigin="anonymous" 并且返回的 JS 文件响应头加上 Access-Control-Allow-Origin: * 即可捕捉到完整的错误堆栈。