Vue 3 + Vite 项目部署后报错排查实录

背景

最近将一个 Vue 3 + Vite + Element Plus + Leafer 的项目部署到生产环境后,遇到了一系列 ReferenceError: Cannot access 'X' before initialization 的错误。本文记录了完整的排查过程和解决方案。

错误现象

第一次报错

1
2
3
4
Uncaught ReferenceError: Cannot access 'It' before initialization 
at Ut (components-B06wb0qP.js:14:12875)
at At (components-B06wb0qP.js:14:12799)
at element-plus-CJsS0rX2.js:1:35632

第二次报错(修复后)

1
2
3
4
Uncaught ReferenceError: Cannot access 'It' before initialization 
at Ut (components-CPioHCuz.js:14:12875)
at At (components-CPioHCuz.js:14:12799)
at element-plus-HfL6FSd0.js:1:35632

第三次报错(继续修复后)

1
2
Uncaught (in promise) ReferenceError: Cannot access 't' before initialization 
at leafer-plugins-DgZw1Q1x.js:1:782

问题分析

这类错误通常表示在代码执行时访问了尚未初始化的变量,常见原因包括:

  1. 循环依赖 - 模块 A 依赖模块 B,模块 B 又依赖模块 A
  2. 代码分割导致的加载顺序问题 - 不同 chunk 之间的依赖关系不正确
  3. SSR/构建时访问浏览器 API - 在服务端渲染或构建时访问了 window 等浏览器对象
  4. 异步组件和 Suspense 的组合问题 - 异步加载的组件在初始化时依赖未就绪

排查过程

第一步:检查 window 对象访问

首先怀疑是组件中直接访问了 window.innerWidth 来判断移动端,这在 SSR 或构建时会导致错误。

问题代码:

1
2
// 多个组件中都存在
const isMobile = computed(() => window.innerWidth <= 768);

修复方案:

1
2
3
4
const isMobile = ref(false);
onMounted(() => {
isMobile.value = window.innerWidth <= 768;
});

涉及组件:

  • Sidebar.vue
  • IconToolbar.vue
  • Element.vue
  • Fill.vue

第二步:检查 Element Plus 加载方式

发现 main.ts 中 Element Plus 是异步加载的:

1
2
3
4
5
6
7
8
9
10
// 问题代码
const app = createApp(App);
app.use(pinia);
app.mount("#app");

(async () => {
const ElementPlus = (await import("element-plus")).default;
const zhCn = (await import("element-plus/es/locale/lang/zh-cn")).default;
app.use(ElementPlus, { locale: zhCn });
})();

这会导致组件在渲染时 Element Plus 可能还未加载完成。

修复方案:

1
2
3
4
5
6
7
import ElementPlus from "element-plus";
import zhCn from "element-plus/es/locale/lang/zh-cn";

const app = createApp(App);
app.use(pinia);
app.use(ElementPlus, { locale: zhCn });
app.mount("#app");

第三步:移除 Suspense 组件

项目中大量使用了 Suspense 包裹异步组件,但 Suspense 与 Element Plus 的骨架屏组件 (el-skeleton) 组合使用时,在打包后可能出现初始化顺序问题。

移除的 Suspense:

  • App.vue 中的 LeaferCanvasStylesDialog
  • Sidebar.vue 中的 el-segmentedIconToolbar 和动态组件

同时将所有异步组件改为同步导入:

1
2
3
4
5
// 之前
const IconToolbar = defineAsyncComponent(() => import("./IconToolbar.vue"));

// 之后
import IconToolbar from "./IconToolbar.vue";

第四步:调整代码分割策略

Vite 配置中的 manualChunks 将代码分割成了多个 chunk,但分割方式导致了依赖问题:

原配置(有问题):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
manualChunks: (id) => {
// Leafer 核心单独打包
if (id.includes("leafer-ui") || id.includes("leafer-editor")) {
return "leafer-core";
}
// Leafer 插件单独打包 - 这会导致循环依赖!
if (id.includes("@leafer-in/")) {
return "leafer-plugins";
}
// Vue 组件单独打包 - 这会导致组件和 Element Plus 分离!
if (id.includes("/src/components/")) {
return "components";
}
}

修复方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
manualChunks: (id) => {
// Leafer 核心和插件打包在一起,避免循环依赖
if (id.includes("leafer-ui") || id.includes("leafer-editor") ||
id.includes("leafer-draw") || id.includes("@leafer-in/") ||
id.includes("leafer-x-")) {
return "leafer";
}
// Element Plus 及其图标单独打包
if (id.includes("element-plus")) {
return "element-plus";
}
// 其他工具库
if (id.includes("exifr") || id.includes("pinia")) {
return "utils";
}
}

关键改动:

  1. leafer-coreleafer-plugins 合并为 leafer
  2. 移除了 Vue 组件的单独打包,让组件和它们的依赖打包在一起

总结

根本原因

  1. 代码分割不当 - 将相互依赖的模块分割到不同 chunk,导致加载顺序问题
  2. 异步加载时机 - Element Plus 异步加载导致组件初始化时依赖未就绪
  3. Suspense 副作用 - Suspense 与异步组件、Element Plus 骨架屏的组合在打包后出现问题

最佳实践

  1. 避免在计算属性中直接访问浏览器 API

    1
    2
    3
    4
    5
    6
    7
    8
    // ❌ 错误
    const isMobile = computed(() => window.innerWidth <= 768);

    // ✅ 正确
    const isMobile = ref(false);
    onMounted(() => {
    isMobile.value = window.innerWidth <= 768;
    });
  2. 核心库同步加载
    UI 框架等核心依赖应该在应用启动时就同步加载完成,避免运行时依赖缺失。

  3. 谨慎使用代码分割

    • 相互依赖的模块应该打包在一起
    • 避免将组件和它们的 UI 框架依赖分割到不同 chunk
    • 使用 manualChunks 时要考虑模块间的依赖关系
  4. Suspense 使用注意

    • 生产环境中谨慎使用 Suspense 包裹异步组件
    • 避免在 Suspense 中使用第三方 UI 组件作为 fallback

最终配置

vite.config.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: (id) => {
// Leafer 相关全部打包在一起
if (id.includes("leafer")) {
return "leafer";
}
// Element Plus 单独打包
if (id.includes("element-plus")) {
return "element-plus";
}
},
},
},
},
});

main.ts:

1
2
3
4
5
6
7
import { createApp } from "vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";

const app = createApp(App);
app.use(ElementPlus);
app.mount("#app");

结语

这类初始化错误在开发环境往往不会出现,因为开发时模块是按需加载的,而生产环境打包后的代码分割和加载顺序可能导致问题。遇到此类错误时,建议:

  1. 检查浏览器 API 的访问时机
  2. 检查代码分割配置
  3. 检查异步组件和依赖的加载顺序
  4. 使用 source map 定位压缩后的代码位置

希望本文对遇到类似问题的开发者有所帮助!

Autojs实现折叠面板

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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
"ui";

// ========== 禁止自动布局,全手动 ==========

/**
* 创建 Element Plus 风格折叠面板
*/
function createCollapse(title, options) {
options = options || {};
var ctx = activity;

var COLOR_WHITE = android.graphics.Color.parseColor("#FFFFFF");
var COLOR_BORDER = android.graphics.Color.parseColor(options.borderColor || "#DCDFE6");
var COLOR_HEADER_BG = android.graphics.Color.parseColor(options.headerBg || "#FAFAFA");
var COLOR_TITLE = android.graphics.Color.parseColor("#303133");
var COLOR_ARROW = android.graphics.Color.parseColor("#C0C4CC");

// 主容器
var container = new android.widget.LinearLayout(ctx);
container.setOrientation(android.widget.LinearLayout.VERTICAL);
container.setPadding(0, 0, 0, 0);
container.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
));

var containerBg = new android.graphics.drawable.GradientDrawable();
containerBg.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
containerBg.setCornerRadius(8);
containerBg.setColor(COLOR_WHITE);
containerBg.setStroke(1, COLOR_BORDER);
container.setBackground(containerBg);

// 标题栏 FrameLayout
var headerFrame = new android.widget.FrameLayout(ctx);
headerFrame.setClickable(true);
headerFrame.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
));

var headerBg = new android.graphics.drawable.GradientDrawable();
headerBg.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
headerBg.setColor(COLOR_HEADER_BG);
headerFrame.setBackground(headerBg);

// 标题 LinearLayout
var headerLinear = new android.widget.LinearLayout(ctx);
headerLinear.setOrientation(android.widget.LinearLayout.HORIZONTAL);
headerLinear.setGravity(android.view.Gravity.CENTER_VERTICAL);
headerLinear.setPadding(32, 24, 24, 24);
headerLinear.setLayoutParams(new android.widget.FrameLayout.LayoutParams(
android.widget.FrameLayout.LayoutParams.MATCH_PARENT,
android.widget.FrameLayout.LayoutParams.WRAP_CONTENT
));

// 标题文字
var titleText = new android.widget.TextView(ctx);
titleText.setText(title);
titleText.setTextSize(15);
titleText.setTextColor(COLOR_TITLE);
titleText.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
0, android.widget.LinearLayout.LayoutParams.WRAP_CONTENT, 1.0
));
headerLinear.addView(titleText);

// 箭头
var arrow = new android.widget.TextView(ctx);
arrow.setText("▼");
arrow.setTextSize(12);
arrow.setTextColor(COLOR_ARROW);
arrow.setPadding(16, 0, 8, 0);
headerLinear.addView(arrow);

headerFrame.addView(headerLinear);
container.addView(headerFrame);

// 内容区
var bodyWrapper = new android.widget.LinearLayout(ctx);
bodyWrapper.setOrientation(android.widget.LinearLayout.VERTICAL);
bodyWrapper.setVisibility(android.view.View.GONE);
bodyWrapper.setPadding(32, 24, 32, 32);
bodyWrapper.setBackgroundColor(COLOR_WHITE);
bodyWrapper.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
));
container.addView(bodyWrapper);

// 状态管理
var collapsed = options.collapsed !== false;

function updateState(c) {
collapsed = !!c;
bodyWrapper.setVisibility(collapsed ? android.view.View.GONE : android.view.View.VISIBLE);
arrow.setRotation(collapsed ? 0 : 180);

var bg = headerFrame.getBackground();
if (bg instanceof android.graphics.drawable.GradientDrawable) {
if (collapsed) {
bg.setCornerRadii([8, 8, 8, 8, 8, 8, 8, 8]);
} else {
bg.setCornerRadii([8, 8, 8, 8, 0, 0, 0, 0]);
}
}

if (options.onChange) options.onChange(collapsed);
}

updateState(collapsed);

headerFrame.setOnClickListener(function() {
updateState(!collapsed);
});

return {
view: container,
body: bodyWrapper,
isCollapsed: function() { return collapsed; },
toggle: function() { updateState(!collapsed); },
expand: function() { updateState(false); },
collapse: function() { updateState(true); }
};
}

// ========== 创建界面 ==========
var root = new android.widget.LinearLayout(activity);
root.setOrientation(android.widget.LinearLayout.VERTICAL);
root.setPadding(32, 32, 32, 32);
root.setBackgroundColor(android.graphics.Color.parseColor("#F5F7FA"));
root.setLayoutParams(new android.view.ViewGroup.LayoutParams(
android.view.ViewGroup.LayoutParams.MATCH_PARENT,
android.view.ViewGroup.LayoutParams.MATCH_PARENT
));

// 面板1
var panel1 = createCollapse("基础设置", { collapsed: false });
var t1 = new android.widget.TextView(activity);
t1.setText("这里是基础设置的内容");
t1.setTextSize(14);
t1.setTextColor(android.graphics.Color.parseColor("#606266"));
panel1.body.addView(t1);

var b1 = new android.widget.Button(activity);
b1.setText("按钮1");
b1.setTextSize(13);
var b1lp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
b1lp.topMargin = 16;
b1.setLayoutParams(b1lp);
b1.setBackgroundColor(android.graphics.Color.parseColor("#409EFF"));
b1.setTextColor(android.graphics.Color.parseColor("#FFFFFF"));
b1.setOnClickListener(function() { toast("点击了面板1按钮"); });
panel1.body.addView(b1);
root.addView(panel1.view);

// 间距
var space1 = new android.widget.Space(activity);
space1.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 24
));
root.addView(space1);

// 面板2
var panel2 = createCollapse("高级配置", {
collapsed: true,
onChange: function(c) { toast("面板2: " + (c ? "折叠" : "展开")); }
});
var t2 = new android.widget.TextView(activity);
t2.setText("CPU: 4核\n内存: 8GB");
t2.setTextSize(13);
t2.setTextColor(android.graphics.Color.parseColor("#909399"));
panel2.body.addView(t2);
root.addView(panel2.view);

var space2 = new android.widget.Space(activity);
space2.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 24
));
root.addView(space2);

// 面板3
var panel3 = createCollapse("关于");
var t3 = new android.widget.TextView(activity);
t3.setText("Element Plus 风格折叠面板\n版本 1.0.0");
t3.setTextSize(13);
t3.setTextColor(android.graphics.Color.parseColor("#909399"));
panel3.body.addView(t3);
root.addView(panel3.view);

var space3 = new android.widget.Space(activity);
space3.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT, 40
));
root.addView(space3);

// 按钮行
var btnRow = new android.widget.LinearLayout(activity);
btnRow.setOrientation(android.widget.LinearLayout.HORIZONTAL);
btnRow.setGravity(android.view.Gravity.CENTER);
btnRow.setLayoutParams(new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.MATCH_PARENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
));

var expandAll = new android.widget.Button(activity);
expandAll.setText("全部展开");
expandAll.setTextSize(14);
expandAll.setBackgroundColor(android.graphics.Color.parseColor("#409EFF"));
expandAll.setTextColor(android.graphics.Color.parseColor("#FFFFFF"));
expandAll.setOnClickListener(function() {
panel1.expand();
panel2.expand();
panel3.expand();
});
btnRow.addView(expandAll);

var collapseAll = new android.widget.Button(activity);
collapseAll.setText("全部折叠");
collapseAll.setTextSize(14);
var calp = new android.widget.LinearLayout.LayoutParams(
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT,
android.widget.LinearLayout.LayoutParams.WRAP_CONTENT
);
calp.leftMargin = 24;
collapseAll.setLayoutParams(calp);
collapseAll.setOnClickListener(function() {
panel1.collapse();
panel2.collapse();
panel3.collapse();
});
btnRow.addView(collapseAll);

root.addView(btnRow);

// ========== 设置根布局(关键!)==========
ui.setContentView(root);

Autojs实现Element Plus风格开关

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
"ui";

function createElSwitch(checked, onChange) {
var ctx = activity;

var TRACK_WIDTH = 150;
var TRACK_HEIGHT = 80;
var THUMB_DIAM = 76;
var THUMB_MARGIN = (TRACK_HEIGHT - THUMB_DIAM) / 2;
var thumbLeftClosed = THUMB_MARGIN;
var thumbLeftOpened = TRACK_WIDTH - THUMB_MARGIN - THUMB_DIAM;

var container = new android.widget.FrameLayout(ctx);
container.setLayoutParams(
new android.view.ViewGroup.LayoutParams(TRACK_WIDTH, TRACK_HEIGHT)
);
container.setClickable(true);

// ---- 轨道 ----
var track = new android.view.View(ctx);
track.setLayoutParams(new android.widget.FrameLayout.LayoutParams(TRACK_WIDTH, TRACK_HEIGHT));
var trackBg = new android.graphics.drawable.GradientDrawable();
trackBg.setShape(android.graphics.drawable.GradientDrawable.RECTANGLE);
trackBg.setCornerRadius(TRACK_HEIGHT / 2);
if (checked) {
trackBg.setColor(android.graphics.Color.parseColor("#409EFF"));
} else {
trackBg.setColor(android.graphics.Color.parseColor("#DCDFE6"));
}
track.setBackground(trackBg);
container.addView(track);

// ---- 滑块 ----
var thumb = new android.view.View(ctx);
var thumbParams = new android.widget.FrameLayout.LayoutParams(THUMB_DIAM, THUMB_DIAM);
thumbParams.gravity = android.view.Gravity.CENTER_VERTICAL;
if (checked) {
thumbParams.leftMargin = thumbLeftOpened;
} else {
thumbParams.leftMargin = thumbLeftClosed;
}
thumb.setLayoutParams(thumbParams);
var thumbBg = new android.graphics.drawable.GradientDrawable();
thumbBg.setShape(android.graphics.drawable.GradientDrawable.OVAL);
thumbBg.setColor(android.graphics.Color.parseColor("#FFFFFF"));
// 细边框模拟阴影
thumbBg.setStroke(1, android.graphics.Color.parseColor("#33000000"));
// 轻微 elevation(Android 5.0+)
if (android.os.Build.VERSION.SDK_INT >= 21) {
thumb.setElevation(2);
}
thumb.setBackground(thumbBg);
container.addView(thumb);

// ---- 状态逻辑 ----
var isChecked = !!checked;

function updateSwitch(on) {
isChecked = !!on;
var td = track.getBackground();
if (td instanceof android.graphics.drawable.GradientDrawable) {
td.setColor(android.graphics.Color.parseColor(isChecked ? "#409EFF" : "#DCDFE6"));
}
var lp = thumb.getLayoutParams();
lp.leftMargin = isChecked ? thumbLeftOpened : thumbLeftClosed;
thumb.setLayoutParams(lp);
}

container.setOnClickListener(function(v) {
var newState = !isChecked;
updateSwitch(newState);
if (onChange) onChange(newState);
});

container.isSwitchOn = function() { return isChecked; };
container.setSwitchOn = function(on) { updateSwitch(on); };

return container;
}

// ---------- 布局示例 ----------
ui.layout(
<vertical padding="16" gravity="center">
<linear gravity="center_vertical">
<text text="消息通知" textSize="16sp" textColor="#303133" layout_weight="1" />
<frame id="switchContainer" w="auto" h="auto" />
</linear>
</vertical>
);

var mySwitch = createElSwitch(false, function(checked) {
toast("状态:" + (checked ? "开启" : "关闭"));
});
ui.switchContainer.addView(mySwitch);

Vue2 面试题 200 题速记版

Vue2 面试题 200 题速记版

一、基础认知(1-15)

1. 什么是 Vue2?

  • Vue2 是渐进式前端框架,核心能力是响应式数据绑定和组件化开发。
  • 渐进式的意思是可以只用核心库,也可以逐步加路由、Vuex、工程化。

2. Vue2 的核心特性有哪些?

  • 响应式:数据变,视图自动更新
  • 组件化:页面拆成多个可复用模块
  • 指令系统:如 v-if、v-for、v-model
  • 虚拟 DOM:先比对 JS 层结构,再更新真实 DOM
  • 单向数据流:父传子靠 props,子改父靠事件
  • 生态完善:Vue Router、Vuex

3. Vue2 的优点是什么?

  • 上手快
  • 组件化开发效率高
  • 数据驱动视图
  • 虚拟 DOM 降低直接操作 DOM 成本
  • 生态成熟
  • 适合中后台和传统前端项目快速开发

4. MVC 和 MVVM 区别是什么?

  • MVC:Model、View、Controller
  • MVVM:Model、View、ViewModel
  • Vue 属于 MVVM,ViewModel 就是 Vue 实例。
  • Vue 实例负责把数据和视图关联起来。

5. Vue2 为什么适合做 SPA?

  • 组件化 + 路由切换 + 状态管理,适合构建单页应用。
  • SPA 只在首次加载时请求主页面,后续主要靠前端路由切换。

6. SPA 的优缺点是什么?

  • 优点:页面切换快、交互流畅、前后端分离
  • 缺点:首屏慢、SEO 差、首包大
  • 所以 SPA 常配合懒加载、缓存、骨架屏优化体验。

7. Vue 实例从创建到销毁经历哪些阶段?

  • 创建
  • 挂载
  • 更新
  • 销毁
  • 这是理解生命周期钩子的主线。

8. Vue 生命周期有哪些?

  • beforeCreate
  • created
  • beforeMount
  • mounted
  • beforeUpdate
  • updated
  • beforeDestroy
  • destroyed
  • keep-alive 组件还有 activated、deactivated

9. Vue2 和 jQuery 思路区别?

  • jQuery 是手动操作 DOM
  • Vue2 是数据驱动视图
  • jQuery 更关注“怎么改 DOM”,Vue 更关注“数据变成什么”。

10. Vue2 和 React 的主要区别?

  • Vue 偏模板语法
  • React 偏 JS 表达 UI
  • Vue2 响应式基于 defineProperty,React 主要依赖 setState 触发更新
  • Vue 学习门槛通常更低,React 生态更偏函数式思维。

11. 什么是渐进式框架?

  • 可以只用核心库,也可以逐步接入路由、状态管理、工程化。
  • 不强制一开始就上全套。

12. Vue2 适合哪些项目?

  • 中后台
  • 管理系统
  • 中小型 SPA
  • 老项目维护
  • 尤其适合已有 Vue2 存量项目。

13. Vue2 的缺点有哪些?

  • 对新增属性和数组下标修改支持不直接
  • 大型项目类型能力弱于现代方案
  • 对老版本生态依赖较多
  • 对复杂逻辑复用,mixin 可维护性一般。

14. 什么是声明式渲染?

  • 只描述页面应该长什么样,具体 DOM 更新交给框架处理。
  • 比如只写模板和数据,不自己手动增删 DOM。

15. 什么是数据驱动?

  • 数据变,视图自动变,开发者少直接操作 DOM。
  • 重点是“以数据为中心”,不是“以 DOM 为中心”。

二、生命周期(16-30)

16. created 和 mounted 区别?

  • created:数据已初始化,DOM 还没挂载
  • mounted:DOM 已挂载,可操作真实 DOM
  • 所以请求一般放 created,操作 DOM 一般放 mounted。

17. 一般在哪个生命周期发请求?

  • 常见在 created
  • 需要操作 DOM 再请求时用 mounted
  • 两者都能发请求,只是常见习惯不同。

18. 哪个生命周期适合操作 DOM?

  • mounted
  • 更新后拿最新 DOM 用 this.$nextTick
  • 因为 mounted 之后真实节点已经生成。

19. 组件销毁前适合做什么?

  • 清除定时器
  • 解绑事件
  • 销毁第三方实例
  • 取消订阅
  • 核心目的是防内存泄漏。

20. beforeUpdate 和 updated 区别?

  • beforeUpdate:数据变了,DOM 还没更新
  • updated:DOM 已完成更新
  • 如果要拿更新后的 DOM,通常在 updated 或 nextTick 里处理。

21. beforeCreate 能拿到 data 吗?

  • 不能,data、methods、watch 都还没初始化。
  • 这个阶段主要是做实例初始化前的准备。

22. created 能操作 DOM 吗?

  • 不适合,模板还没挂载到页面。
  • 此时能拿到数据,拿不到最终渲染后的 DOM。

23. beforeMount 有什么用?

  • 挂载前最后一步,此时模板已编译,但还没替换页面真实 DOM。
  • 实战里用得不多,面试常问概念。

24. beforeDestroy 和 destroyed 区别?

  • beforeDestroy:实例还可用
  • destroyed:实例相关绑定基本已移除
  • 清理副作用通常放 beforeDestroy 更稳妥。

25. keep-alive 相关生命周期有哪些?

  • activated
  • deactivated
  • 只有被 keep-alive 缓存的组件才会触发。

26. activated 什么时候触发?

  • 被 keep-alive 缓存的组件再次激活时触发。
  • 常用于页面回来后刷新数据。

27. deactivated 什么时候触发?

  • 被 keep-alive 缓存的组件切走时触发。
  • 常用于暂停轮询、暂停定时器等。

28. 父子组件生命周期执行顺序?

  • 挂载时一般是父 beforeCreate/created -> 父 beforeMount -> 子 beforeCreate/created/beforeMount/mounted -> 父 mounted。
  • 面试常考“父先创建,子先挂载,最后父挂载完成”。

29. 更新阶段常见问题是什么?

  • 不要在 updated 里继续改会触发更新的数据,容易死循环。
  • updated 更适合做依赖 DOM 的收尾逻辑。

30. 销毁组件的方式有哪些?

  • 条件渲染让组件卸载
  • 路由切换
  • this.$destroy(较少手动用)
  • 实战里主要靠条件渲染和路由切换。

三、响应式原理(31-55)

31. Vue2 响应式原理是什么?

  • 通过 Object.defineProperty 劫持对象属性的 getter/setter。
  • 读取时收集依赖,修改时通知 watcher 更新。
  • 简单说就是“读的时候记住谁用过,改的时候通知它们刷新”。

32. Vue2 为什么不用 Proxy?

  • Vue2 诞生时要兼容较老浏览器,Proxy 兼容性差。
  • Vue3 才全面转向 Proxy。

33. Vue2 怎么监听数组变化?

  • 重写 push、pop、shift、unshift、splice、sort、reverse 等方法。
  • 因为 defineProperty 不能直接拦截这些数组变异行为。

34. 为什么对象新增属性不是响应式?

  • 因为 defineProperty 只能劫持已存在属性,后加属性没被劫持。
  • 所以后加属性默认不会自动更新视图。

35. 怎么让新增属性变成响应式?

  • 用 this.$set(obj, key, value) 或 Vue.set。
  • 本质是手动补上响应式处理。

36. 为什么通过下标改数组不一定更新?

  • 直接 arr[index] = value 无法被 Vue2 侦测。
  • 应用 splice 或 this.$set(arr, index, value)。
  • 因为 Vue2 主要拦截的是变异方法,不是数组索引赋值。

37. data 为什么必须是函数?

  • 组件复用时如果 data 是对象,多个实例会共享同一份数据。
  • 写成函数可保证每次返回独立对象。
  • 根实例 new Vue 里的 data 可以是对象,组件里必须是函数。

38. 什么是依赖收集?

  • 渲染时读取了哪些响应式数据,就把对应 watcher 收集起来,后续数据变更再触发更新。
  • 谁在模板里用到了这个值,谁就是它的依赖。

39. watcher、dep、observer 分别是什么?

  • observer:把数据变成响应式
  • dep:依赖收集中心
  • watcher:观察者,负责更新视图或执行回调
  • 三者配合完成“收集依赖 + 通知更新”。

40. 什么是 Observer?

  • 用来遍历对象/数组并把它们转成响应式数据的模块。
  • 可以理解成响应式改造器。

41. 什么是 Dep?

  • 每个响应式属性对应一个依赖管理器,负责收集和通知 watcher。
  • 类似一个订阅列表。

42. 什么是 Watcher?

  • 用于订阅数据变化,数据变更后执行更新函数。
  • 常见有渲染 watcher、用户 watcher、计算属性 watcher。

43. 渲染 watcher 是什么?

  • 组件渲染对应的 watcher,负责视图重新渲染。
  • 页面更新本质上就是它在工作。

44. 用户 watcher 是什么?

  • 开发者通过 watch 选项或 this.$watch 创建的 watcher。
  • 主要用于监听变化后做副作用逻辑。

45. 计算属性 watcher 是什么?

  • computed 内部也基于 watcher,默认带懒执行和缓存。
  • 只有依赖变化且被访问时才重新计算。

46. 响应式更新为什么不是同步立刻更新 DOM?

  • Vue 会把同一轮修改合并,异步批量更新更省性能。
  • 否则多次 set 会导致多次无意义渲染。

47. Vue2 能监听对象删除属性吗?

  • 不能直接监听普通 delete。
  • 需要用 Vue.delete 或 this.$delete。
  • 因为删除动作本身不会触发现有 setter。

48. Vue.set 原理大致是什么?

  • 新增属性后手动做响应式处理,再通知依赖更新。
  • 所以它不仅是赋值,还补了通知过程。

49. 数组方法为什么能触发更新?

  • 因为 Vue2 重写了变异方法,方法执行后会手动通知更新。
  • 比如 splice 后,Vue 知道数组内容发生了变化。

50. 为什么修改 length 可能不更新?

  • length 变化不是被重写数组方法触发,Vue2 无法很好侦测。
  • 所以不建议直接改 length 控制视图。

51. defineProperty 的缺点是什么?

  • 不能直接监听新增/删除属性
  • 不能直接监听数组下标和 length 变化
  • 需要递归遍历对象,初始化成本较高
  • 这也是 Vue3 改用 Proxy 的原因之一。

52. Vue2 响应式是深层的吗?

  • 初始化时会递归处理已有嵌套对象,所以已有层级通常是深层响应式。
  • 但前提是这些层级在初始化时就存在。

53. 为什么大对象初始化会变慢?

  • 因为 Vue2 要递归遍历属性并逐个做 defineProperty 劫持。
  • 数据层级越深、字段越多,初始化越重。

54. this.$watch 有什么用?

  • 手动监听某个数据变化,适合动态注册监听逻辑。
  • 比配置项 watch 更灵活。

55. this.$watch 返回什么?

  • 返回一个取消监听的函数。
  • 调用后就不再继续监听该数据。

四、模板与指令(56-80)

56. v-if 和 v-show 区别?

  • v-if:控制是否渲染,切换时销毁/重建
  • v-show:始终渲染,只切换 display
  • 频繁切换用 v-show
  • 很少切换、初始化成本高的逻辑可以考虑 v-if。

57. v-for 为什么必须加 key?

  • 让 diff 更准确地识别节点身份,减少错误复用,提高更新效率。
  • 没有 key 时,Vue 只能尽量就地复用节点。

58. key 为什么不建议用 index?

  • 列表增删后 index 会变化,可能导致复用错位、状态错乱。
  • 尤其表单项、动画列表更容易出问题。

59. v-model 原理是什么?

  • 本质是 value + input 事件。
  • 输入框值变化后触发事件更新数据。
  • 也就是语法糖,不是魔法。

60. v-html 有什么风险?

  • 有 XSS 风险,不能直接渲染不可信 HTML。
  • 只适合渲染可信内容。

61. 常见 Vue 指令有哪些?

  • v-bind:动态绑定属性
  • v-on:绑定事件
  • v-model:双向绑定表单
  • v-if:条件渲染
  • v-show:条件显示
  • v-for:列表渲染
  • v-text:设置纯文本
  • v-html:插入 HTML
  • v-once:只渲染一次
  • v-pre:跳过编译

62. v-bind 的作用?

  • 动态绑定 HTML 属性、class、style、组件 props。
  • 简写是 :。

63. v-on 的作用?

  • 绑定事件监听器。
  • 简写是 @。

64. v-once 的作用?

  • 只渲染一次,后续数据变化不再更新。
  • 适合纯静态内容优化。

65. v-pre 的作用?

  • 跳过该节点编译,加快编译速度。
  • 常用于大段静态内容。

66. v-cloak 的作用?

  • 配合 CSS 解决模板编译前插值闪烁问题。
  • 本质是编译完成前先隐藏页面相关区域。

67. v-text 和插值表达式区别?

  • v-text 会替换整个文本内容;插值更常用、更灵活。
  • 插值写法是 {{}}。

68. 为什么 v-if 和 v-for 不建议写一起?

  • 不建议写一起,因为列表遍历和条件判断耦合在一处,可读性和性能都一般。
  • 更推荐先用 computed 过滤数据,再循环渲染。
  • 面试里不要死记某个优先级版本细节,核心结论是“拆开写更好”。

69. template 标签有什么用?

  • 作为逻辑包裹层,不会渲染成真实 DOM。
  • 常配合 v-if、v-for 使用。

70. slot 是什么?

  • 插槽,用于父组件向子组件指定内容分发位置。
  • 可以理解成组件预留的“内容坑位”。

71. 具名插槽是什么?

  • 给多个插槽起名字,父组件可定向传入内容。
  • 解决一个组件有多个插槽位置的问题。

72. 作用域插槽是什么?

  • 子组件把数据传给父组件插槽内容使用。
  • 父组件决定渲染方式,子组件提供数据。

73. 为什么说作用域插槽可复用性高?

  • 结构由子组件控制,内容渲染由父组件决定,灵活度更高。
  • 常用于表格、列表、下拉框等组件封装。

74. 动态 class 怎么写?

  • 可以写对象、数组或表达式。
  • 对象适合条件类名,数组适合多个类组合。

75. 动态 style 怎么写?

  • 可以绑定对象或对象数组。
  • 对象 key 通常是 CSS 属性名。

76. 事件修饰符有哪些?

  • .stop:阻止冒泡
  • .prevent:阻止默认行为
  • .capture:用捕获阶段监听
  • .self:只在事件源是自己时触发
  • .once:只触发一次
  • .passive:告诉浏览器不会阻止默认行为,常用于滚动优化

77. 按键修饰符有哪些?

  • enter、tab、delete、esc、space、up、down、left、right 等。
  • 用来限制只有按下指定键时才触发事件。

78. 表单修饰符有哪些?

  • .lazy:默认 v-model 多数表单会在 input 事件时同步;加上它后改成 change 时再同步,常见于失焦或确认后更新
  • .number:把输入值尽量转成数字,避免得到字符串
  • .trim:自动去掉输入首尾空格
  • 这题面试很爱问具体含义,尤其是 .lazy。

79. 什么是自定义指令?

  • 开发者自定义 DOM 行为复用逻辑的方式。
  • 适合封装聚焦、拖拽、权限控制等直接操作 DOM 的场景。

80. 自定义指令常见钩子有哪些?

  • bind:指令第一次绑定到元素时
  • inserted:元素插入父节点时
  • update:组件更新时
  • componentUpdated:组件及子组件更新后
  • unbind:指令解绑时

五、computed / watch / methods(81-95)

81. computed 和 watch 区别?

  • computed:用于派生新值,有缓存
  • watch:用于监听数据变化后执行副作用,可写异步逻辑
  • 记忆方式:算结果用 computed,做事情用 watch。

82. computed 为什么有缓存?

  • 依赖不变时直接返回上次结果,不重新执行 getter。
  • 所以模板里多次使用同一计算属性不会重复算很多次。

83. watch 能监听对象内部变化吗?

  • 能,但要配 deep: true。
  • 因为默认只监听引用变化,不深挖内部层级。

84. watch 的 immediate 有什么用?

  • 组件初始化时先执行一次回调。
  • 相当于“先执行一次,再继续监听后续变化”。

85. methods 和 computed 区别?

  • methods:每次调用都执行
  • computed:依赖不变直接走缓存
  • 模板里高频重复计算更适合 computed。

86. computed 能写 set 吗?

  • 能,写成 getter/setter 形式即可。
  • 这样既能读也能写,常见于 v-model 包装。

87. 什么场景用 computed?

  • 模板中需要根据多个响应式数据推导新值。
  • 比如全名拼接、列表过滤结果、状态文案。

88. 什么场景用 watch?

  • 数据变化后发请求、做本地存储、处理异步逻辑。
  • 它更像“监听后执行动作”。

89. watch 为什么适合异步?

  • 因为它关注的是变化后的副作用,不要求必须同步返回值。
  • 所以请求接口、延时处理都更适合放这里。

90. computed 为什么不适合异步?

  • 计算属性本质用于同步返回计算结果,异步会破坏依赖追踪与使用体验。
  • 面试答法:computed 要的是“值”,watch 要的是“过程”。

91. 多个数据组合计算该用什么?

  • 优先用 computed。
  • 因为它语义更对,也有缓存。

92. 监听路由变化怎么做?

  • watch $route 或使用路由守卫。
  • 简单监听用 watch,页面进出控制常用守卫。

93. watch 写函数和对象形式区别?

  • 对象形式可配置 handler、deep、immediate。
  • 所以对象形式功能更全。

94. 什么是侦听属性?

  • 就是 watch,用来观察某个数据的变化。
  • 变化后通常执行副作用,而不是返回值。

95. 计算属性依赖没变会重新执行吗?

  • 不会,会直接读缓存结果。
  • 这是 computed 相比 methods 的关键优势。

六、异步更新与 nextTick(96-105)

96. Vue2 为什么是异步更新?

  • 同一轮事件循环内多次改数据会合并更新,减少重复渲染,提高性能。
  • 这叫批量异步更新策略。

97. nextTick 是什么?

  • 在下次 DOM 更新完成后执行回调。
  • 常用于“改完数据,立刻拿最新 DOM”。

98. nextTick 的使用场景?

  • 改完数据后立刻拿最新 DOM
  • 处理滚动、聚焦、尺寸计算
  • 总之是依赖“更新后 DOM”的场景。

99. nextTick 原理是什么?

  • 优先用微任务,如 Promise、MutationObserver;降级再用宏任务。
  • 核心目的是把回调放到 DOM 更新后再执行。

100. 数据改了为什么拿到的 DOM 还是旧的?

  • 因为 DOM 更新是异步批量执行的,需要等 nextTick。
  • 改数据成功不等于 DOM 立刻同步完成。

101. Vue 的异步队列是什么?

  • Vue 会把同一轮数据变更对应的 watcher 放进队列,去重后统一刷新。
  • 这样能避免重复更新同一个组件。

102. watcher 为什么要去重?

  • 避免同一 watcher 重复入队,多次无效渲染。
  • 一句话:防重复刷新。

103. nextTick 一定是微任务吗?

  • 优先微任务,不行再降级到宏任务。
  • 所以不能绝对说它永远是微任务。

104. this.$nextTick 和 Vue.nextTick 区别?

  • 本质类似,this.$nextTick 更常在组件实例里使用。
  • 写在组件里通常直接用 this.$nextTick。

105. 什么情况下不需要 nextTick?

  • 不依赖更新后 DOM,只关心数据本身时不需要。
  • 比如只是继续处理 JS 数据,不读界面节点。

七、组件通信(106-120)

106. 组件通信方式有哪些?

  • 父传子:props
  • 子传父:$emit
  • 父拿子:ref
  • 跨层级:provide/inject
  • 任意组件:EventBus
  • 全局状态:Vuex
  • 面试通常先答这 6 类就够了。

107. 父子组件传值怎么做?

  • 父传子用 props
  • 子传父用 $emit
  • 这是最基本、最规范的通信方式。

108. 兄弟组件通信怎么做?

  • 通过共同父组件中转
  • 或 EventBus
  • 复杂项目一般用 Vuex
  • 推荐优先选更清晰、可维护的方式。

109. 为什么不推荐直接改 props?

  • props 是单向下行绑定,子组件直接改会破坏数据流。
  • Vue 也会给出警告。

110. .sync 语法糖是什么?

  • 本质是父传值 + 子触发 update:xxx 事件。
  • 只是把这套写法简化了。

111. ref 有什么用?

  • 获取组件实例或 DOM 节点。
  • 常用于调用子组件方法或拿原生节点。

112. $children 和 ref 区别?

  • ref 更明确可控,$children 顺序不稳定且不推荐依赖。
  • 实战优先用 ref。

113. provide/inject 适合什么场景?

  • 跨多层组件传递依赖或配置,避免层层 props。
  • 常用于组件库或祖孙组件通信。

114. EventBus 原理是什么?

  • 通过统一事件中心做发布订阅。
  • 一个组件 emit,另一个组件 on 接收。

115. EventBus 有什么问题?

  • 事件来源不清晰、难维护、容易忘记解绑。
  • 项目一大就容易混乱。

116. 父组件如何调用子组件方法?

  • 通过 ref 获取子组件实例再调用。
  • 比如 this.$refs.child.xxx()。

117. 子组件如何通知父组件?

  • this.$emit(‘事件名’, 参数)
  • 父组件监听这个自定义事件。

118. 非父子组件通信推荐什么?

  • 简单场景 EventBus,复杂共享状态更推荐 Vuex。
  • 面试答法要体现“场景区分”。

119. props 校验怎么做?

  • 用 type、required、default、validator 做约束。
  • 这样组件接口更清晰,也更不容易误用。

120. props 为什么默认工厂函数返回对象?

  • 避免多个组件实例共享同一引用类型默认值。
  • 跟 data 写成函数是同一类思路。

八、组件化与复用(121-130)

121. 组件是什么?

  • 可复用、可维护、独立封装的 UI 单元。
  • 本质是把页面拆成更小的职责模块。

122. 组件化有什么好处?

  • 提高复用性
  • 降低耦合
  • 方便维护和协作
  • 也是大型前端项目的基础。

123. Vue 组件由哪几部分组成?

  • template
  • script
  • style
  • 分别对应结构、逻辑、样式。

124. 单文件组件是什么?

  • 把模板、逻辑、样式写在一个 .vue 文件中。
  • 这是 Vue 项目最常见的组件组织形式。

125. mixin 是什么?

  • 多组件复用逻辑的一种机制,会把配置合并到组件里。
  • 常用于复用 data、methods、生命周期等。

126. mixin 的缺点是什么?

  • 来源不直观、命名冲突、依赖关系隐式。
  • 所以复杂项目里维护成本会变高。

127. extends 和 mixin 区别?

  • extends 更像单继承扩展,mixin 是混入多个配置。
  • mixin 更灵活,但冲突概率也更高。

128. keep-alive 有什么用?

  • 缓存组件实例,切换回来不重新创建。
  • 常用于列表页返回保留滚动和表单状态。

129. keep-alive 常用属性有哪些?

  • include:只缓存指定组件
  • exclude:排除指定组件
  • max:限制最大缓存数量
  • 控制缓存范围,避免无限缓存。

130. 异步组件是什么?

  • 按需加载组件,减少首屏包体积。
  • 只有用到时才去加载对应组件代码。

九、虚拟 DOM 与 diff(131-140)

131. 什么是虚拟 DOM?

  • 用 JS 对象描述真实 DOM 结构。
  • 它不是页面节点本身,而是页面结构的抽象表示。

132. 为什么要有虚拟 DOM?

  • 方便用 JS 描述 UI,结合 diff 减少直接操作真实 DOM 的成本。
  • 它更像框架更新视图的中间层。

133. diff 算法的作用是什么?

  • 比较新旧虚拟 DOM,找出最小更新范围。
  • 核心目标是少改真实 DOM。

134. Vue2 的 diff 是全量比较吗?

  • 不是,通常是同层比较,不做跨层级暴力比较。
  • 这样复杂度更可控。

135. 为什么 key 能优化 diff?

  • 帮助快速识别节点身份,减少错误复用和无效移动。
  • 没 key 或 key 不稳定时,列表更新更容易出错。

136. patch 是什么?

  • 把虚拟 DOM 的差异应用到真实 DOM 的过程。
  • diff 找差异,patch 负责真正更新。

137. Vue2 更新列表时为什么要避免频繁重排?

  • 真实 DOM 重排重绘成本高,diff 目标就是减少这种成本。
  • 所以列表优化本质是在减少真实节点操作。

138. 虚拟 DOM 一定比真实 DOM 快吗?

  • 不一定。简单场景直接操作 DOM 可能更快,但框架整体维护成本更低。
  • 面试不要绝对化回答“虚拟 DOM 一定更快”。

139. 为什么说模板最终会变成 render?

  • 模板会先编译成 render 函数,再生成虚拟 DOM。
  • 所以模板语法本质上最终都会变成 JS 渲染逻辑。

140. render 函数的作用是什么?

  • 用 JS 描述组件应该渲染成什么样。
  • 它返回的是虚拟 DOM 结构。

十、路由(141-155)

141. Vue Router 的两种模式是什么?

  • hash
  • history
  • 区别核心在 URL 形式和服务端支持要求。

142. hash 和 history 区别?

  • hash 带 #,不依赖服务端配置
  • history URL 更美观,但刷新需要服务端兜底到 index.html
  • 所以 history 更好看,但部署要求更高。

143. route 和 router 区别?

  • $route:当前路由信息对象
  • $router:路由实例,负责跳转等操作
  • 记忆:一个是“当前信息”,一个是“操作工具”。

144. 路由传参有哪些方式?

  • params
  • query
  • 动态路由 + params
  • 实战中 query 更常见,params 更适合路径语义。

145. 导航守卫有哪些?

  • 全局守卫:beforeEach、beforeResolve、afterEach
  • 路由独享守卫
  • 组件内守卫
  • 常用于权限、登录、离开确认。

146. beforeEach 常用来做什么?

  • 登录校验、权限控制、埋点、页面标题处理。
  • 它是最常用的全局前置守卫。

147. params 和 query 区别?

  • params 更像路径参数
  • query 更像 URL 查询参数
  • query 通常会直接显示在 ? 后面。

148. 编程式导航怎么写?

  • this.$router.push、replace、go。
  • 就是用 JS 主动控制路由跳转。

149. push 和 replace 区别?

  • push 会新增历史记录
  • replace 会替换当前记录
  • 所以 replace 后点返回可能回不到当前页。

150. 路由懒加载怎么做?

  • 组件写成动态 import。
  • 本质是代码分包,访问到该路由再加载。

151. 什么是嵌套路由?

  • 在父路由下继续配置 children 渲染子页面。
  • 常用于布局页 + 内容区结构。

152. 什么是动态路由?

  • 路由路径中带变量参数,如 /user/:id。
  • 适合详情页、用户页这类路径模式固定但参数变化的场景。

153. 组件内路由守卫有哪些?

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave
  • 分别对应进入、复用更新、离开。

154. beforeRouteLeave 常见用途?

  • 页面离开前确认、表单未保存提示。
  • 很典型的拦截离开场景。

155. history 模式刷新 404 怎么办?

  • 服务端统一回退到 index.html。
  • 因为前端路由地址需要交还给前端应用处理。

十一、Vuex(156-170)

156. Vuex 是什么?

  • Vue 官方状态管理库,用于集中管理共享状态。
  • 适合多个组件依赖同一份状态的场景。

157. Vuex 的核心概念有哪些?

  • state:状态
  • getters:派生状态
  • mutations:同步修改状态
  • actions:异步操作
  • modules:模块化拆分
  • 这五个是面试必答核心。

158. mutations 和 actions 区别?

  • mutations:必须同步,直接改 state
  • actions:可异步,最终通过 commit 调 mutations
  • 记忆:mutation 改值,action 处理流程。

159. 为什么 mutations 必须同步?

  • 方便 devtools 追踪状态变化,保证变更可预测。
  • 如果异步,调试时难以定位状态何时变化。

160. getters 有什么用?

  • 对 state 做派生计算,类似 store 的 computed。
  • 不直接存新状态,而是基于已有状态算结果。

161. commit 和 dispatch 区别?

  • commit 触发 mutations
  • dispatch 触发 actions
  • 一个偏同步提交,一个偏流程调度。

162. modules 有什么用?

  • 把大型 store 按业务拆分,降低维护成本。
  • 避免所有状态都堆在一个文件里。

163. 命名空间 namespaced 有什么用?

  • 避免模块间方法名、getter 名冲突。
  • 大项目里基本都会用到。

164. Vuex 持久化怎么做?

  • 常配合 localStorage/sessionStorage 或插件做持久化。
  • 因为 Vuex 默认只存在内存里。

165. 页面刷新后 Vuex 数据为什么没了?

  • Vuex 默认是内存状态,刷新页面会重新初始化。
  • 所以需要持久化方案补上。

166. 什么时候不需要 Vuex?

  • 组件层级浅、共享状态少时没必要上全局状态管理。
  • 否则反而增加复杂度。

167. Vuex 和 EventBus 区别?

  • Vuex 更规范、可追踪;EventBus 更轻但不易维护。
  • 项目规模越大,越能体现 Vuex 的优势。

168. mapState、mapGetters 有什么用?

  • 快速把 store 中状态和 getter 映射到组件计算属性。
  • 减少重复样板代码。

169. mapMutations、mapActions 有什么用?

  • 快速把 mutations、actions 映射为组件方法。
  • 让组件里调用 store 更简洁。

170. 严格模式 strict 有什么作用?

  • 监控是否在 mutation 之外修改 state。
  • 开发阶段排查问题很有用,生产一般不常开。

十二、性能优化(171-180)

171. Vue2 常见性能优化手段有哪些?

  • 路由懒加载
  • 组件按需加载
  • v-if/v-show 合理使用
  • keep-alive
  • 防抖节流
  • 长列表优化
  • 核心思路就是减少首屏体积和减少无效渲染。

172. 为什么不要滥用 deep watch?

  • 深度遍历成本高,数据大时性能差。
  • 只有确实需要监听深层变化时再开。

173. 为什么不要在模板里写复杂表达式?

  • 可读性差,重复执行成本更高。
  • 复杂逻辑更适合放 computed 或 methods。

174. 长列表怎么优化?

  • 分页
  • 虚拟列表
  • 避免无意义重渲染
  • 本质是减少同时存在的 DOM 数量。

175. 为什么 key 要稳定?

  • 稳定 key 能减少错误复用和不必要更新。
  • 不稳定 key 会让 diff 判断失真。

176. computed 为什么常能优化性能?

  • 有缓存,避免模板中重复计算。
  • 同样依赖不变时不会反复执行。

177. 事件销毁不及时会怎样?

  • 内存泄漏、重复触发、页面越来越卡。
  • 常见于 window 事件、总线事件、定时器没清理。

178. keep-alive 是不是越多越好?

  • 不是,缓存过多会增加内存占用。
  • 只缓存真正需要保留状态的页面。

179. 异步组件为什么能优化首屏?

  • 把非首屏组件拆出去,减少首包体积。
  • 首次只加载当前必要资源。

180. 图片很多时怎么优化?

  • 懒加载、压缩、合理尺寸、CDN。
  • 图片问题本质是请求量和资源体积问题。

十三、工程化与实践(181-190)

181. Vue CLI 是什么?

  • Vue 官方脚手架,用于快速初始化 Vue2 工程。
  • 帮你生成基础目录、配置和开发环境。

182. .vue 文件怎么被浏览器识别?

  • 先经过构建工具和 vue-loader 编译,再打包成浏览器可运行代码。
  • 浏览器本身并不认识 .vue 文件。

183. scoped 原理是什么?

  • 给当前组件元素和样式加特定属性选择器,限制样式作用范围。
  • 常见表现是元素上多一个 data-v-xxx。

184. scoped 一定能完全隔离样式吗?

  • 不能,子组件根元素、深度选择器等场景仍需注意。
  • 所以 scoped 不是绝对隔离。

185. 深度选择器怎么写?

  • 常见写法有 ::v-deep、/deep/、>>>,取决于构建环境。
  • 用于穿透 scoped 影响子组件内部样式。

186. CSS Module 和 scoped 区别?

  • scoped 是样式作用域限制,CSS Module 是类名局部化。
  • 两者目标类似,但实现思路不同。

187. 环境变量有什么用?

  • 区分开发、测试、生产环境配置。
  • 比如接口地址、开关配置通常都靠它区分。

188. 为什么前端要做按需加载?

  • 降低首屏资源体积,提升加载速度。
  • 不常用代码没必要一开始就全下到浏览器。

189. 什么是 source map?

  • 把打包后代码映射回源码,方便调试。
  • 这样报错时更容易定位到原始代码位置。

190. 生产环境为什么常关掉 source map?

  • 减少源码暴露和构建体积压力。
  • 也能降低被逆向分析的风险。

十四、常见场景题(191-200)

191. 登录权限一般怎么做?

  • 路由守卫拦截 + token 校验 + 角色权限控制。
  • 常见流程是登录后存 token,进路由前判断权限。

192. 页面刷新后如何保留登录态?

  • token 存 localStorage/cookie,初始化时再恢复用户信息。
  • 只存 Vuex 不够,因为刷新会丢。

193. 列表页切详情再返回怎么保留状态?

  • keep-alive、路由缓存、状态持久化。
  • 常见要保留滚动位置、筛选条件、分页状态。

194. 表单重复提交怎么处理?

  • 按钮禁用、loading、防抖节流。
  • 核心是限制重复触发提交逻辑。

195. 请求取消怎么做?

  • 常见用 axios 取消机制或组件销毁时中断请求。
  • 避免页面已销毁但请求回来还更新数据。

196. 如何统一处理接口错误?

  • 在请求拦截器/响应拦截器统一处理。
  • 这样错误提示、登录过期处理都能收口。

197. 如何封装公共组件?

  • 明确输入输出:props、events、slots,保证低耦合和可复用。
  • 组件接口越清晰,复用越稳定。

198. 如何做页面级缓存?

  • keep-alive + 路由元信息 + 缓存名单控制。
  • 不是所有页面都缓存,要按场景控制。

199. 如何避免内存泄漏?

  • 销毁定时器、解绑监听、清理订阅、释放第三方实例。
  • 面试答这题一定要提“副作用清理”。

200. 中后台项目 Vue2 面试最常问什么?

  • 生命周期
  • 响应式原理
  • computed/watch
  • nextTick
  • 组件通信
  • 路由守卫
  • Vuex
  • 这些基本就是 Vue2 面试主线。

高频补充

  • 核心主线:响应式 -> 模板编译 -> 虚拟 DOM -> diff -> 组件更新
  • 高频对比:computed/watch、v-if/v-show、hash/history、route/router、action/mutation
  • 高频陷阱:对象新增属性、数组下标修改、key 用 index、props 直接修改、updated 死循环
  • 高频场景:nextTick、$set、keep-alive、权限路由、状态持久化、长列表优化

JavaScript 面试常见八股文 200 题速记版

JavaScript 面试常见八股文 200 题速记版

一、基础类型与类型判断(1-20)

1. JS 有哪些基本类型?

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

2. 引用类型有哪些?

  • object
  • array
  • function
  • date
  • regexp 等本质都属于对象。

3. typeof 能判断什么?

  • 基本类型大多可以判断。
  • 但 null 会返回 object。

4. 为什么 typeof null 是 object?

  • 这是历史遗留问题。

5. 怎么准确判断数组?

  • Array.isArray()。

6. instanceof 原理是什么?

  • 顺着原型链找构造函数的 prototype。

7. Object.prototype.toString 有什么用?

  • 更准确判断数据类型。

8. NaN 是什么?

  • 表示不是一个有效数字。
  • 它和自己都不相等。

9. isNaN 和 Number.isNaN 区别?

  • Number.isNaN 更严格。

10. == 和 === 区别?

  • == 会隐式类型转换
  • === 不会

11. null 和 undefined 区别?

  • null 表示空对象
  • undefined 表示未定义

12. symbol 有什么用?

  • 创建唯一值,常用于避免属性名冲突。

13. bigint 有什么用?

  • 表示超大整数。

14. Number 精度问题为什么会发生?

  • JS 使用 IEEE 754 双精度浮点数。

15. 0.1 + 0.2 为什么不等于 0.3?

  • 二进制浮点数精度误差。

16. 怎么判断一个值是不是对象?

  • 一般先排除 null,再看 typeof 是否是 object/function。

17. 基本类型和引用类型区别?

  • 一个按值存储
  • 一个按引用地址存储

18. const 定义的对象能改吗?

  • 能改对象内部属性。
  • 不能改变量绑定地址。

19. wrapper object 是什么?

  • 基本类型的包装对象,如 new String。

20. 为什么不推荐 new String/Boolean/Number?

  • 容易产生类型判断和隐式转换问题。

二、变量、作用域与闭包(21-45)

21. var、let、const 区别?

  • var 有变量提升
  • let/const 有暂时性死区
  • const 不可重新赋值

22. 什么是变量提升?

  • 声明在编译阶段被提前处理。

23. 什么是暂时性死区?

  • let/const 声明前不能访问。

24. 块级作用域是什么?

  • 大括号内形成独立作用域。

25. 函数作用域是什么?

  • 变量只在函数内部有效。

26. 什么是作用域链?

  • 当前作用域找不到变量时向上层作用域查找。

27. 什么是闭包?

  • 函数访问并记住其词法作用域中的变量。

28. 闭包的本质是什么?

  • 函数 + 被保留的外部变量环境。

29. 闭包有什么用?

  • 封装私有变量
  • 缓存数据
  • 延长变量生命周期

30. 闭包有什么缺点?

  • 变量可能长期不释放,导致内存占用增加。

31. for 循环里 var 为什么会有问题?

  • 共用同一个变量。

32. for 循环里 let 为什么正常?

  • 每次循环都有独立块级作用域。

33. 什么是词法作用域?

  • 作用域由代码书写位置决定,不由调用位置决定。

34. 函数声明和函数表达式区别?

  • 函数声明会整体提升。
  • 函数表达式不会完整提升。

35. IIFE 是什么?

  • 立即执行函数表达式。

36. 为什么以前常用 IIFE?

  • 创建独立作用域,避免变量污染。

37. this 和作用域是一回事吗?

  • 不是。
  • this 是调用时决定,作用域是定义时决定。

38. 闭包会内存泄漏吗?

  • 不一定。
  • 只有无意义长期引用才可能造成问题。

39. 什么是自由变量?

  • 当前作用域没有定义,去上层作用域找的变量。

40. JS 有几种作用域?

  • 全局作用域
  • 函数作用域
  • 块级作用域

41. eval 为什么不推荐?

  • 性能差
  • 不安全
  • 难优化

42. with 为什么不推荐?

  • 作用域不明确,影响性能和可读性。

43. const 一定不可变吗?

  • 不是。
  • 只是绑定不可变。

44. 什么是模块作用域?

  • ES Module 文件自身形成独立作用域。

45. ES Module 为什么天然避免变量污染?

  • 每个模块都是独立作用域。

三、原型与继承(46-70)

46. 什么是原型?

  • 对象共享属性和方法的基础机制。

47. 什么是 prototype?

  • 函数身上的原型对象属性。

48. 什么是 proto

  • 对象内部指向原型的链接。

49. prototype 和 proto 区别?

  • prototype 在函数上
  • proto 在对象上

50. 什么是原型链?

  • 对象查找属性时沿着原型向上查找的链路。

51. Object.create 有什么用?

  • 以指定对象为原型创建新对象。

52. new 的过程是什么?

  • 创建对象
  • 连接原型
  • 绑定 this
  • 执行构造函数
  • 返回对象

53. instanceof 原理是什么?

  • 看左边对象原型链上是否出现右边构造函数 prototype。

54. 什么是继承?

  • 子对象复用父对象能力的机制。

55. JS 常见继承方式有哪些?

  • 原型链继承
  • 构造函数继承
  • 组合继承
  • 寄生组合继承
  • class extends

56. 为什么原型链继承有缺点?

  • 引用类型属性会被实例共享。

57. 构造函数继承优缺点?

  • 能避免共享引用属性
  • 但不能复用原型方法

58. 组合继承为什么常见?

  • 兼顾实例属性和原型方法复用。

59. 寄生组合继承为什么更优?

  • 减少一次父构造函数的多余调用。

60. class 本质上是什么?

  • class 是基于原型继承的语法糖。

61. extends 做了什么?

  • 建立原型链继承关系。

62. super 有什么用?

  • 调父类构造函数或父类方法。

63. 静态方法怎么定义?

  • 用 static。

64. 静态方法能被实例调用吗?

  • 不能。
  • 只能类本身调用。

65. 为什么说函数也是对象?

  • 因为函数可以有属性,也能被当作对象处理。

66. Function 和 Object 谁更顶层?

  • 两者关系较绕,面试主要答清原型链关系即可。

67. 所有对象都有原型吗?

  • 大多数有。
  • Object.create(null) 创建的对象没有原型。

68. 没有原型的对象有什么特点?

  • 不能继承 Object.prototype 上的方法。

69. in 和 hasOwnProperty 区别?

  • in 会查原型链
  • hasOwnProperty 只看自身

70. Object.hasOwn 有什么好处?

  • 更安全直接判断自身属性。

四、this、call、apply、bind(71-90)

71. this 指向由什么决定?

  • 调用方式决定。

72. 默认绑定是什么?

  • 普通函数直接调用时,非严格模式指向全局对象。

73. 隐式绑定是什么?

  • 对象调用方法时,this 指向该对象。

74. 显式绑定是什么?

  • 通过 call/apply/bind 指定 this。

75. new 绑定是什么?

  • new 调用时,this 指向新创建对象。

76. 箭头函数有 this 吗?

  • 没有自己的 this。
  • 继承外层 this。

77. 为什么箭头函数不能当构造函数?

  • 没有 prototype,也没有自己的 this。

78. call 和 apply 区别?

  • 参数传递方式不同。
  • call 逐个传,apply 数组传。

79. bind 和 call/apply 区别?

  • bind 不立即执行,返回新函数。

80. bind 返回函数能 new 吗?

  • 能。
  • new 优先级高于 bind 绑定的 this。

81. 事件回调里 this 指向谁?

  • 普通函数常指向触发事件的元素或调用环境。

82. 定时器里的 this 指向谁?

  • 普通函数通常指向全局对象或宿主环境。

83. 严格模式下普通函数 this 是什么?

  • undefined。

84. 对象方法赋值给变量再调用,this 为什么变了?

  • 调用位置变了,丢失隐式绑定。

85. 手写 call 核心思路是什么?

  • 把函数临时挂到对象上执行。

86. 手写 apply 核心思路是什么?

  • 和 call 一样,只是参数是数组。

87. 手写 bind 核心思路是什么?

  • 返回一个新函数,内部 apply 原函数并处理 new 场景。

88. 箭头函数适合做对象方法吗?

  • 一般不适合依赖 this 的对象方法。

89. 为什么 Vue methods 一般不用箭头函数?

  • 因为会影响 this 指向。

90. this 面试主线是什么?

  • 调用位置
  • 绑定规则
  • 箭头函数
  • call/apply/bind

五、执行上下文与事件循环(91-115)

91. 什么是执行上下文?

  • 代码执行时的运行环境信息。

92. 执行上下文包括什么?

  • 变量环境
  • 词法环境
  • this 绑定

93. 调用栈是什么?

  • 函数执行的栈结构。

94. 什么是事件循环?

  • JS 处理同步任务、异步任务的调度机制。

95. 宏任务有哪些?

  • script
  • setTimeout
  • setInterval
  • I/O
  • UI 渲染

96. 微任务有哪些?

  • Promise.then
  • MutationObserver
  • queueMicrotask

97. 宏任务和微任务执行顺序?

  • 先同步
  • 再清空微任务
  • 再执行下一个宏任务

98. Promise.then 为什么比 setTimeout 先执行?

  • because then 是微任务,setTimeout 是宏任务。

99. async/await 本质是什么?

  • Promise 的语法糖。

100. await 后面的代码什么时候执行?

  • 当前同步代码后,放入微任务队列继续执行。

101. setTimeout(0) 真的是 0ms 吗?

  • 不是。
  • 只是尽快进入宏任务队列。

102. requestAnimationFrame 有什么特点?

  • 在浏览器下一次重绘前执行。

103. queueMicrotask 有什么用?

  • 显式创建微任务。

104. 渲染发生在什么时候?

  • 一般在宏任务和微任务清空后的合适时机进行。

105. 为什么长任务会卡页面?

  • 因为主线程被占住,无法处理渲染和交互。

106. JS 是单线程吗?

  • 主线程执行 JS 是单线程。
  • 但浏览器有其他线程配合。

107. Web Worker 能做什么?

  • 开子线程处理计算任务。

108. Web Worker 能操作 DOM 吗?

  • 不能。

109. 什么是任务队列?

  • 存放待执行异步任务回调的队列。

110. 浏览器为什么要分微任务和宏任务?

  • 为了更细粒度控制异步执行优先级。

111. Promise 构造函数是同步还是异步?

  • executor 同步执行。
  • then 回调异步执行。

112. async 函数返回什么?

  • 返回 Promise。

113. await 一定会阻塞主线程吗?

  • 不会像同步阻塞那样卡死主线程。
  • 它只是让后续逻辑异步继续。

114. 什么是饥饿问题?

  • 微任务过多可能让宏任务和渲染迟迟得不到执行。

115. 事件循环面试高频点有哪些?

  • Promise
  • async/await
  • setTimeout
  • 微任务宏任务顺序

六、Promise 与异步(116-140)

116. Promise 是什么?

  • 解决异步回调地狱的对象。

117. Promise 有几种状态?

  • pending
  • fulfilled
  • rejected

118. Promise 状态可以反复变吗?

  • 不能。
  • 一旦确定就不可再变。

119. then 返回什么?

  • 新 Promise。

120. catch 是什么?

  • then(null, onRejected) 的语法糖。

121. finally 有什么用?

  • 无论成功失败都会执行。

122. Promise.resolve 有什么用?

  • 把值包装成 Promise。

123. Promise.reject 有什么用?

  • 返回一个 rejected Promise。

124. Promise.all 有什么特点?

  • 全成功才成功
  • 一个失败就失败

125. Promise.race 有什么特点?

  • 谁先有结果就返回谁。

126. Promise.allSettled 有什么特点?

  • 等全部结果结束,不管成功失败。

127. Promise.any 有什么特点?

  • 只要一个成功就成功。
  • 全失败才失败。

128. async/await 优点是什么?

  • 写法更像同步,代码更清晰。

129. async/await 缺点是什么?

  • 串行 await 可能影响并发性能。

130. 怎么让多个请求并发?

  • Promise.all。

131. 回调地狱是什么?

  • 多层回调嵌套导致代码难维护。

132. Promise 为什么能链式调用?

  • then 会返回新 Promise。

133. then 里 return 普通值会怎样?

  • 会变成成功态 Promise 的值。

134. then 里抛错会怎样?

  • 会走后续 rejected 流程。

135. Promise 为什么比回调好?

  • 状态清晰
  • 链式调用
  • 错误处理集中

136. 手写 Promise 核心点有哪些?

  • 状态管理
  • then 链式调用
  • 异步执行回调
  • 错误捕获

137. Promise 和 async/await 怎么选?

  • 常规流程用 async/await
  • 并发组合常配 Promise API

138. await 后面可以跟普通值吗?

  • 可以。
  • 会自动包成 Promise.resolve。

139. Promise 可以取消吗?

  • 原生不能直接取消。
  • 常通过外部标记或中断机制实现。

140. 异步面试主线是什么?

  • Promise
  • async/await
  • 事件循环
  • 并发控制

七、数组、字符串与常用 API(141-160)

141. map 和 forEach 区别?

  • map 返回新数组
  • forEach 没有返回结果

142. filter 是什么?

  • 过滤出满足条件的新数组。

143. reduce 是什么?

  • 把数组累计成一个结果。

144. some 和 every 区别?

  • some 一个满足就 true
  • every 全满足才 true

145. find 和 filter 区别?

  • find 找第一个元素
  • filter 找全部匹配元素

146. includes 和 indexOf 区别?

  • includes 返回布尔值
  • indexOf 返回索引

147. slice 和 splice 区别?

  • slice 不改原数组
  • splice 会改原数组

148. concat 会改原数组吗?

  • 不会。

149. sort 默认有什么坑?

  • 默认按字符串排序。

150. flat 有什么用?

  • 数组扁平化。

151. join 有什么用?

  • 数组转字符串。

152. split 有什么用?

  • 字符串转数组。

153. trim 有什么用?

  • 去掉字符串首尾空格。

154. startsWith/endsWith 有什么用?

  • 判断字符串开头/结尾。

155. padStart/padEnd 有什么用?

  • 补齐字符串长度。

156. Set 有什么特点?

  • 值唯一。

157. Map 和 Object 区别?

  • Map 键类型更灵活,遍历更方便。

158. WeakMap/WeakSet 有什么特点?

  • 键是弱引用,不阻止垃圾回收。

159. 去重常见方式有哪些?

  • Set
  • filter + indexOf
  • Map

160. 数组 API 面试高频点是什么?

  • map/filter/reduce
  • slice/splice
  • sort
  • 去重扁平化

八、对象、拷贝与解构(161-175)

161. 浅拷贝和深拷贝区别?

  • 浅拷贝只复制一层
  • 深拷贝递归复制嵌套层

162. 常见浅拷贝方式有哪些?

  • Object.assign
  • 展开运算符
  • slice/concat

163. 常见深拷贝方式有哪些?

  • JSON
  • structuredClone
  • 递归 + WeakMap

164. JSON 深拷贝有什么问题?

  • 丢失函数、undefined、symbol
  • 不能处理循环引用

165. structuredClone 有什么优点?

  • 更原生、支持更多场景。

166. Object.assign 是深拷贝吗?

  • 不是,是浅拷贝。

167. 展开运算符是深拷贝吗?

  • 不是,是浅拷贝。

168. 解构赋值有什么用?

  • 快速提取对象或数组中的值。

169. 默认值什么时候生效?

  • 只有值为 undefined 时。

170. 剩余参数和展开运算符区别?

  • 一个收集参数
  • 一个展开内容

171. Object.keys / values / entries 区别?

  • 分别返回键、值、键值对数组。

172. for…in 和 for…of 区别?

  • for…in 遍历键
  • for…of 遍历可迭代值

173. 什么是可枚举属性?

  • 能被某些遍历方式枚举出来的属性。

174. Object.freeze 有什么用?

  • 冻结对象,不能再增删改属性。

175. Object.seal 有什么用?

  • 不能增删属性,但可修改已有属性。

九、ES6+ 新特性(176-190)

176. 模板字符串有什么用?

  • 更方便拼接字符串和换行。

177. 箭头函数有什么特点?

  • 没有自己的 this
  • 写法更简洁

178. 解构赋值有什么优点?

  • 简化取值代码。

179. 默认参数有什么用?

  • 函数参数可直接设置默认值。

180. rest 参数是什么?

  • 收集剩余参数为数组。

181. spread 语法是什么?

  • 展开数组、对象、可迭代对象。

182. class 是什么?

  • 原型继承语法糖。

183. module 是什么?

  • JS 模块化标准。

184. import 和 require 区别?

  • import 是静态编译
  • require 是运行时加载

185. export default 和 export 区别?

  • 一个默认导出
  • 一个具名导出

186. 可选链是什么?

  • ?. 安全访问深层属性。

187. 空值合并运算符是什么?

  • ?? 只在 null/undefined 时取默认值。

188. 逻辑与和空值合并区别?

  • || 会把 0、’’、false 也当假值
  • ?? 不会

189. 动态 import 有什么用?

  • 按需加载模块。

190. 顶层 await 是什么?

  • 在模块顶层直接使用 await。

十、浏览器、存储与场景题(191-200)

191. localStorage 和 sessionStorage 区别?

  • 生命周期和作用域不同。

192. cookie、localStorage、sessionStorage 区别?

  • cookie 会随请求发送
  • storage 不会
  • 体积有限,而且每次请求都可能带上。

194. 防抖是什么?

  • 停止触发一段时间后再执行。

195. 节流是什么?

  • 固定时间内只执行一次。

196. 防抖和节流场景区别?

  • 搜索输入常用防抖
  • 滚动监听常用节流

197. 什么是跨域?

  • 协议、域名、端口任一不同就是跨域。

198. 常见跨域方案有哪些?

  • CORS
  • 代理
  • JSONP(仅 GET)

199. 什么是同源策略?

  • 浏览器对不同源资源访问的安全限制。

200. JS 面试主线是什么?

  • 类型
  • 作用域闭包
  • 原型继承
  • this
  • 事件循环
  • Promise
  • 拷贝
  • ES6+

高频补充

  • 核心主线:类型 -> 作用域 -> 原型链 -> this -> 事件循环 -> Promise -> 拷贝
  • 高频对比:==/===、var/let/const、call/apply/bind、map/filter/reduce、slice/splice
  • 高频场景:防抖节流、深拷贝、并发请求、跨域、存储