📝 前端面试题汇总
本篇文章旨在利用一天时间,剖析经典前端经典面试题,带你快速建立前端面试知识体系,让面试更加 “有底气”,欢迎您的指正和点赞。
HTML
如何理解 HTML 语义化
- 让人更容易读懂(代码可读性)
- 让搜索引擎更容易读懂(SEO)
默认情况下,哪些 HTML 标签是块级元素,哪些是内联元素
- display: block/table:div、h1 - h6、table、ul、ol、p 等
- display: inline/inline-block:img、span、input、button 等
CSS
布局
盒子模型的宽度如何计算

答案:offsetWidth = 122px = 内容宽度 + 内边距 + 边框
补充:如果让 offsetWidth 等于 100px,该如何做?
答案:box-sizing: border-box
margin 纵向重叠的问题

- 相邻元素的 margin-top 和 margin-bottom 会发生重叠
- 空白内容的也会重叠
答案:15px
margin 负值的问题
- margin-top 和 margin-left 负值,元素向上、向左移动
- margin-right 负值,自身不受影响,右侧元素左移
- margin-bottom 负值,自身不受影响,下方元素上移
BFC 理解和应用
块级格式化上下文,一块独立的渲染区域,内部元素的渲染不会影响边界以外的元素。
形成 BFC 的常见条件:
- float 不是 none
- position 是 absolute 或 fixed
- overflow 不是 visible
- display 是 flex、inline-block 等
BFC 的常见应用
- 清除浮动
float 布局的问题,以及手写 clearfix
圣杯布局和双飞翼布局:
- 三栏布局,中间一栏最先加载和渲染
- 两侧内容固定,中间内容随宽度自适应
技术实现:
- 使用 float 布局
- 两侧使用 margin 负值,以便和中间内容横向重叠
- 防止中间内容被两侧覆盖,一个用 padding 一个用 margin

flex 画色子

定位
absolute 和 relative 分别依据什么定位
- relative 依据自身定位
- absolute 依据最近一层的定位元素(absolute、relative、fixed、body)定位
居中对齐有哪些实现方式
水平居中:
- inline 元素:text-align: center
- block 元素:margin: auto
- absolute 元素:left: 50% + margin-left 负值
垂直居中:
- inline 元素:line-height 的值等于 height 的值
- absolute 元素:top: 50% + margin-top 负值
- absolute 元素:transform: translate(-50%, -50%)
- absolute 元素:top、left、bottom、right = 0 + margin: auto
图文样式
line-height 的继承问题

- 写具体数值,如 30px,则继承该值
- 写比例,如 2,则继承该比例
- 写百分比,如 200%,则继承计算出来的值
响应式
rem 是什么?
- px,绝对长度单位,最常用
- em,相对长度单位,相对于父元素,不常用
- rem,相对长度单位,相对于根元素,常用于响应式布局
如何实现响应式
- media-query,根据不同的屏幕宽度设置根元素的 font-size
- rem,基于根元素的相对单位
JS 基础
变量类型和计算
typeof 能判断哪些类型
- 识别所有值类型
- 识别函数
- 判断是否是引用类型(不可再细分)
何时使用 === 何时使用 ==
除了 == null(null == undefined) 外,其他都一律用 ===
值类型和引用类型的区别
值类型:栈
引用类型:栈 + 堆
值类型 / 引用类型的拷贝
手写深拷贝 ⭐️
- 判断值类型和引用类型
- 判断是数组还是对象
- 递归
function deepClone(obj = {}) {
if (typeof obj !== "object" || obj == null) {
return obj;
}
let result = Array.isArray(obj) ? [] : {};
for (let key in obj) {
// 自己的属性
if (obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(obj[key]);
}
}
return result;
}
原型和原型链
如何准确判断一个变量是不是数组
- a instanceof Array
- Array.isArray()
class 的原型本质,怎么理解
class 实际上是函数,语法糖
- 每个 class 都有显示原型 prototype
- 每个实例都有隐式原型 proto
- 实例的 proto 指向对应 class 的 prototype

手写一个简易的 jQuery,考虑插件和扩展性
class jQuery {
constructor(selector) {
const result = document.querySelectorAll(selector);
const length = result.length;
for (let i = 0; i < length; i++) {
this[i] = result[i];
}
this.length = length;
this.selector = selector;
}
get(index) {
return this[index];
}
each(fn) {
for (let i = 0; i < this.length; i++) {
const elem = this[i];
fn(elem);
}
}
on(type, fn) {
return this.each((elem) => {
elem.addEventListener(type, fn, false);
});
}
}
// 插件
jQuery.prototype.dialog = function (info) {
alert(info);
};
// "造轮子"
class MyJQuery extends jQuery {
constructor(selector) {
super(selector);
}
// 扩展自己的方法
addClass(className) {
// ...
}
}
作用域和闭包
this 的不同应用场景,如何取值 ⭐️
this 的取值是在函数执行时确定,不是在函数定义时确定!!!
- 当做普通函数被调用(window)
- 使用 call、apply、bind(传入的对象)
- 作为对象方法被调用(对象)
- 在 class 的方法中调用
- 箭头函数(定义时的上级作用域)
手写 bind 函数 ⭐️
Function.prototype.bind = function () {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments);
// 获取 this(数组第一项)
const t = args.shift();
// fn.bind(...) 中的 fn
const self = this;
// 返回一个函数
return function () {
return self.apply(t, args);
};
};
实际开发中闭包的应用场景,举例说明
- 隐藏数据,只提供 API
function createCache() {
const data = {};
return {
set: function (key, val) {
data[key] = val;
},
get: function (key) {
return data[key];
},
};
}

闭包:所有的自由变量的查找,是在函数定义的地方,向上级作用域查找,不是在执行的地方(静态作用域)!!!
答案:100, 100
异步
同步和异步的区别是什么?
- 基于 JS 是单线程语言
- 异步不会阻塞代码执行
- 同步会阻塞代码执行
手写用 Promise 加载一张图片 ⭐️
function loadImg(src) {
return new Promise((resolve, reject) => {
const img = document.createElement("img");
img.onload = () => {
resolve(img);
};
img.onerror = () => {
reject(new Error("图片加载失败"));
};
img.src = src;
});
}
Callback Hell

前端使用异步的场景有哪些?
- 网络请求,如 ajax 图片加载
- 定时任务,如 setTimeout、setInterval
场景题

答案:1、3、5、4、2
异步进阶
请描述 event loop 的机制,可画图 ⭐️
- 同步代码,一行一行放在 Call Stack 中执行
- 遇到异步,会先 “记录” 下,等待时机(定时、网络请求等)
- 时机到了,就移动到 Callback Queue
- 如果 Call Stack 为空(即同步代码执行完),Event Loop 开始工作
- 轮询查找 Callback Queue,如有则移动到 Call Stack 执行
- 然后继续轮询查找(永动机)

Promise 有哪三种状态,如何变化
三种状态:
- pending、resolved、rejected
- pending → resolved 或 pending → rejected
- 变化不可逆
状态表现:
- pending 状态,不会触发 then 和 catch
- resolved 状态,会触发后续的 then 回调函数
- rejected 状态,会触发后续的 catch 回调函数
async/await 和 Promise 的关系
- 执行 async 函数,返回的是 Promise 对象
- await 相当于 Promise 的 then
- try…catch 可捕获异常,代替了 Promise 的 catch
Event Loop 和 DOM 渲染
- JS 是单线程,而且和 DOM 渲染共用一个线程
- JS 执行时,得留一些时机供 DOM 渲染
- 每次 Call Stack 清空,即同步任务执行完
- 都是 DOM 重新渲染的机会,DOM 结构如有改变则重新渲染
- 然后再去触发下一次 Event Loop
什么是宏任务和微任务,两者有什么区别
- 宏任务:setTimeout、setInterval、ajax、dom 事件
- 微任务:Promise、async/await
- 微任务执行时机比宏任务要早
- 宏任务在 DOM 渲染后触发,如 setTimeout
- 微任务在 DOM 渲染前触发,如 Promsie
手写 Promise⭐️
- 初始化 & 异步调用
- then catch 链式调用
- API .resolve .reject .all .race
class MyPromise {
state = "pending";
value = undefined;
reason = undefined;
resolveCallbacks = []; // pending 状态下,存储成功的回调
rejectCallbacks = []; // pending 状态下,存储失败的回调
constructor(fn) {
const resolveHandler = (value) => {
if (this.state === "pending") {
this.state = "fulfilled";
this.value = value;
this.resolveCallbacks.forEach((fn) => fn(this.value));
}
};
const rejectHandler = (reason) => {
if (this.state === "pending") {
this.state = "rejected";
this.reason = reason;
this.rejectCallbacks.forEach((fn) => fn(this.reason));
}
};
try {
fn(resolveHandler, rejectHandler);
} catch (err) {
rejectHandler(err);
}
}
then(fn1, fn2) {
// pending 状态下,fn1、fn2 被保存
fn1 = typeof fn1 === "function" ? fn1 : (v) => v;
fn2 = typeof fn2 === "function" ? fn2 : (e) => e;
if (this.state === "pending") {
return new MyPromise((resolve, reject) => {
this.resolveCallbacks.push(() => {
try {
const newValue = fn1(this.value);
resolve(newValue);
} catch (err) {
reject(err);
}
});
this.rejectCallbacks.push(() => {
try {
const newReason = fn2(this.reason);
reject(newReason);
} catch (err) {
reject(err);
}
});
});
}
if (this.state === "fulfilled") {
return new MyPromise((resolve, reject) => {
try {
const newValue = fn1(this.value);
resolve(newValue);
} catch (err) {
reject(err);
}
});
}
if (this.state === "rejected") {
return new MyPromise((resolve, reject) => {
try {
const newReason = fn2(this.reason);
reject(newReason);
} catch (err) {
reject(err);
}
});
}
}
catch(fn) {
return this.then(null, fn);
}
}
MyPromise.resolve = function (value) {
return new MyPromise((resolve, reject) => resolve(value));
};
MyPromise.reject = function (reason) {
return new MyPromise((resolve, reject) => reject(reason));
};
MyPromise.all = function (promiseList = []) {
return new MyPromise((resolve, reject) => {
const result = [];
const length = promiseList.length;
let resolveCount = 0;
promiseList.forEach((p) => {
p.then((data) => {
result.push(data);
// resolveCount 必须在 then 里面做 ++
resolveCount++;
if (resolveCount === length) {
resolve(result);
}
}).catch((err) => {
reject(err);
});
});
});
};
MyPromise.race = function (promiseList = []) {
let resolved = false;
return new MyPromise((resolve, reject) => {
promiseList.forEach((p) => {
p.then((data) => {
if (!resolved) {
resolve(data);
resolved = true;
}
}).catch((err) => {
reject(err);
});
});
});
};
场景题
then 和 catch 改变状态:
- then 正常返回 resolved promise,里面有报错则返回 rejected promise
- catch 正常返回 resolved promise,里面有报错则返回 rejected promise

第一题:1、3
第二题:1、2、3
第三题:1、2

第一题:a → Promise 对象、b → 100
第二题:‘start’、100、200、报错…

答案:100、400、300、200

答案:‘script start’ → ‘async1 start’ → ‘async2’ → ‘promise1’ → ‘script end’ → ‘async1 end’ → ‘promise2’ → ‘setTimeout’
执行时机:同步代码执行完毕 → 执行微任务 → 触发 DOM 渲染 → 触发 Event Loop,执行宏任务。
要点:初始化 promise 时,传入的函数会立刻被执行;await 后面的都作为回调内容(微任务)。
JS-Web-API
DOM
DOM 是哪种数据结构
树(DOM 树)
attribute 和 property 的区别
- property:修改对象属性,不会体现到 html 结构中
- attribute:修改 html 属性,会改变 html 结构
- 两者都有可能引起 DOM 重新渲染
一次性插入多个 DOM 节点,考虑性能

BOM
分析拆解 url 各个部分

事件
编写一个通用的事件监听函数
function bindEvent(elem, type, selector, fn) {
if (fn == null) {
fn = selector;
selector = null;
}
elem.addEventListener(type, (event) => {
const target = event.target;
if (selector) {
// 代理绑定
if (target.matches(selector)) {
fn.call(target, event);
}
} else {
// 普通绑定
fn.call(target, event);
}
});
}
描述事件冒泡的流程
- 基于 DOM 树形结构
- 事件会顺着触发元素向上冒泡
- 应用场景:代理
无限下拉的图片列表,如何监听每个图片的点击
- 事件代理
- 用 event.target 来获取触发元素
- 用 matches 来判断是否是触发元素
Ajax
手写一个简易的 ajax
function ajax(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else if (xhr.status === 404) {
reject(new Error("404 not found"));
}
}
};
xhr.send(null);
});
}
跨域的常用方式
JSONP:
- <script> 可绕过跨域限制
- 服务器可以任意动态拼接数据返回
- 所以,<script>就可以获得跨域的数据,只要服务端愿意返回

CORS:

存储
描述 cookie、localStorage、sessionStorage 区别
- 容量:cookie 4kb,localStorage/sessionStorage 5M
- API 易用性:cookie document.cookie,localStorage/sessionStorage setItem、getItem
- 是否跟随 http 请求发送出去
http
http
http 常见的状态码有哪些
- 1xx 服务器收到请求
- 2xx 请求成功,如 200(成功)
- 3xx 重定向,如 301(永久重定向)302(临时重定向)304(资源未被修改)
- 4xx 客户端请求错误,如 403(没有权限)404(资源未找到)
- 5xx 服务端错误,如 500(服务器错误)504(网关超时)
http 常见的 header 有哪些
request headers:
- Accept 浏览器可接收的数据格式
- Accept-Encoding 浏览器可接收的压缩算法,如 gzip
- Accept-Languange 浏览器可接收的语言,如 zh-CN
- Connection: keep-alive 一次 TCP 连接重复使用
- Host
- cookie
- User-Agent 浏览器信息
- Content-type 发送数据的格式,如 application/json
response headers:
- Content-Type 返回数据的格式,如 application/json
- Content-Length 返回数据的大小,多少字节
- Content-Encoding 返回数据的压缩算法,如 gzip
- Set-Cookie
什么是 restful api
- 传统 API 设计:把每个 url 当做一个功能
- restful api 设计:把每个 url 当做一个唯一的资源
描述一下 http 的缓存机制
- 正常操作:地址栏输入 url、跳转链接、前进后退等(强制缓存有效,协商缓存有效)
- 手动刷新:F5、点击刷新按钮、点击菜单刷新(强制缓存失效,协商缓存有效)
- 强制刷新:ctrl + F5(强制缓存失效,协商缓存失效)
强制缓存:
Cache-Control: max-age、no-cache、no-store


协商缓存:
- 服务端缓存策略
- 服务器判断客户端资源,是否和服务端资源一样
- 一致则返回 304,否则返回 200 和最新的资源

Last-Modified

Etag

https
http 与 https 的区别
- http 是明文传输,敏感信息容易被中间劫持
- https = http + 加密,劫持了也无法解密
加密方式
- 对称加密:一个 key 同时负责加密、解密
- 非对称加密:一对 key,A 加密之后,只能用 B 来解密
- https 同时用到了这两种加密方式(安全、成本、高效)

https 证书
- 中间人攻击
- 使用第三方证书
- 浏览器校验证书

开发环境
git
- git branch
- git status
- git diff
- git add .
- git commit -m “xxx”
- git checkout xxx
- git checkout -b xxx
- git push origin main
- git pull origin main
- git fetch
- git merge xxx
- git stash
- git stash pop
调试工具
- chrome 调试工具
抓包
- 移动端 h5 页,查看网络请求,需要用工具抓包
- windows 一般用 fiddler
- Mac OS 一般用 charles
- 手机和电脑连同一个局域网
- 将手机代理到电脑上
- 手机浏览网页,即可抓包
- 查看网络请求
- 网址地理
- https
webpack
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "development",
entry: path.join(__dirname, "src", "index.js"),
output: {
filename: "bundle.js",
path: path.join(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.js$/,
loader: ["babel-loader"],
include: path.join(__dirname, "src"),
exclude: /node_modules/,
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "index.html"),
filename: "index.html",
}),
],
devServer: {
port: 3000,
contentBase: path.join(__dirname, "dist"),
},
};
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
mode: "production",
entry: path.join(__dirname, "src", "index.js"),
output: {
filename: "bundle.[contenthash].js",
path: path.join(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.js$/,
loader: ["babel-loader"],
include: path.join(__dirname, "src"),
exclude: /node_modules/,
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: path.join(__dirname, "src", "index.html"),
filename: "index.html",
}),
],
};
babel
{
"presets": ["@babel/preset-env"]
}
linux 常用命令
- ssh root@xxx.xxx.xx.xx
- ls、ls -a、ll
- mkdir xxx
- touch xxx
- vi xxx
- vim xxx
- rm -rf xxx
- cd xxx
- mv xxx xxx
- cp xxx xxx
- cat xxx
- grep xxx xxx
运行环境
页面加载过程
从输入 url 到渲染出页面的整个过程
加载过程:
- DNS 解析:域名 -> ip 地址
- 浏览器根据 ip 地址向服务器发送 http 请求
- 服务器处理 http 请求,返回给浏览器
渲染过程:
- 根据 HTML 代码生成 DOM Tree
- 根据 CSS 代码生成 CSSOM
- 将 DOM Tree 和 CSSOM 整合形成 Render Tree
- 根据 Render Tree 渲染页面
- 遇到 <script> 则暂停渲染,优先加载并执行 js 代码,完成再继续
- 直至把 Render Tree 渲染完成
window.onload 和 DOMContentLoaded 的区别
- window.onload: 资源全部加载完才能执行,包括视频、图片等
- DOMContentLoaded: DOM 渲染完即可执行,此时图片、视频可能还没加载完
性能优化
性能优化原则
- 多使用内存、缓存或其他方法
- 减少 CPU 计算量,减少网络加载耗时
- 适用于所有编程的性能优化 - 空间换时间
从何入手
- 让加载更快
- 让渲染更快
让加载更快
- 减少资源体积:压缩代码
- 减少访问次数:合并代码、SSR、缓存
- 使用更快的网络:CDN
让渲染更快
- CSS 放在 head,JS 放在 body 最下面
- 尽早开始执行 JS,用 DOMContentLoaded 触发
- 懒加载(图片懒加载,上滑加载更多)
- 对 DOM 查询进行缓存
- 频繁 DOM 操作,合并到一起插入 DOM 结构
- 节流 throttle、防抖 debounce
防抖 debounce
- 用户输入结束或暂停时,才会触发 change 事件
function debounce(fn, delay = 500) {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
};
}
节流 throttle
- 无论拖拽速度多快,都会每隔 delay 触发一次
function throttle(fn, delay = 500) {
let timer = null;
return function () {
if (timer) {
return;
}
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
};
}
安全
常见的 web 前端攻击方式有哪些
xss 跨站请求攻击:
- 一个博客网站,我发表一篇博客,其中嵌入 <script> 脚本
- 脚本内容:获取 cookie(document.cookie),发送到我的服务器
- 发布这篇博客,有人查看它,我轻松收割访问者的 cookie
xss 预防:
- 替换特殊符号,如 < 变为 < > 变为 >
- <script> 变为 <script>,直接显示,而不会作为脚本执行
- 前端要替换,后端也要替换,都做总不会错
xsrf 跨站请求伪造:
- 你正在购物,看中了某个商品,商品 id 是 100
- 付费接口是 xxx.com/pay?id=100,没有任何验证
- 我是攻击者,看中了一个商品,id 是 200
- 我向你发送一封邮件,邮件标题很吸引人
- 邮件正文隐藏着 <img src=xxx.com/pay?id=200>
- 你一查看邮件,就帮我购买了 id 是 200 的商品
xsrf 预防:
- 使用 post 接口
- 增加验证,例如指纹、短信验证码等
真题模拟
var、let、const 区别
- var 是 es5 语法,let const 是 es6 语法;var 有变量提升
- var let 是变量,可修改;const 是常量,不可修改
- let const 有块级作用域,var 没有
typeof 能判断哪些类型
- string、number、boolean、undefined、symbol
- object (typeof null === ‘object’)
- function
列举强制类型转换和隐式类型转换
- 强制类型转换:parseInt、parseFloat、toString 等
- 隐式类型转换:if、逻辑运算、==、+ 拼接字符串
手写深度比较,模拟 lodash isEqual
function isObject(obj) {
return typeof obj === "object" && obj !== null;
}
function isEqual(obj1, obj2) {
if (!isObject(obj1) || !isObject(obj2)) {
// 值类型(注意:参与 equal 的一般不会是函数)
return obj1 === obj2;
}
if (obj1 === obj2) {
return true;
}
// 两个都是对象或数组,而且不相等
// 1. 先取出 obj1 和 obj2 的 keys,比较个数
const obj1Keys = Object.keys(obj1);
const obj2Keys = Object.keys(obj2);
if (obj1Keys.length !== obj2Keys.length) {
return false;
}
// 2. 以 obj1 为基础,和 obj2 依次递归比较
for (let key in obj1) {
// 比较当前 key 的val
const res = isEqual(obj1[key], obj2[key]);
if (!res) {
return false;
}
}
// 3. 全相等
return true;
}
split() 和 join() 的区别

数组的 pop、push、shift、unshift 分别是什么
- 功能是什么
- 返回值是什么
- 是否会对原数组造成影响
// const arr = [10, 20, 30, 40]
// // pop
// const popRes = arr.pop()
// console.log(popRes, arr)
// // shift
// const shiftRes = arr.shift()
// console.log(shiftRes, arr)
// // push
// const pushRes = arr.push(50) // 返回 length
// console.log(pushRes, arr)
// // unshift
// const unshiftRes = arr.unshift(5) // 返回 length
// console.log(unshiftRes, arr)
数组的 API,有哪些是纯函数
// // 纯函数:1. 不改变源数组(没有副作用);2. 返回一个新的数组
// const arr = [10, 20, 30, 40]
// // concat
// const arr1 = arr.concat([50, 60, 70])
// // map
// const arr2 = arr.map(num => num * 10)
// // filter
// const arr3 = arr.filter(num => num > 25)
// // slice
// const arr4 = arr.slice()
数组的 API,有哪些是非纯函数
// // push pop shift unshift
// // forEach
// // some every
// // reduce
// splice
数组 slice 和 splice 区别
- slice-切片 splice-剪接
- 参数和返回值
- 是否是纯函数
// const arr = [10, 20, 30, 40, 50]
// // slice 纯函数
// const arr1 = arr.slice()
// const arr2 = arr.slice(1, 4) // startIndex endIndex
// const arr4 = arr.slice(-3) // 从最后开始截取
var myFish = ["angel", "clown", "drum", "mandarin", "sturgeon"];
var removed = myFish.splice(3, 1); // 从索引3位置开始删除1个元素
var myFish = ["angel", "clown", "mandarin", "sturgeon"];
var removed = myFish.splice(2, 0, "drum", "guitar"); // 从索引2位置开始添加两个元素
[10, 20, 30].map(parseInt) 返回结果是什么
[10, 20, 30].map((num, index) => {
return parseInt(num, index);
});
答案:[10, NaN, NaN]
ajax 请求 get 和 post 的区别
- get 一般用于查询操作,post 一般用于用户提交操作
- get 参数拼接在 url 上,post 放在请求体内
- post 易于防止 xsrf
函数 call 和 apply 的区别

事件代理是什么

闭包是什么?有什么特性?有什么负面影响
- 回顾作用域和自由变量
- 回顾闭包应用场景:作为参数被传入,作为返回值被返回
- 回顾:自由变量的查找,要在函数定义的地方(而不是执行的地方)
- 影响:变量会常驻内存,得不到释放(不一定导致内存泄漏)
如何阻止事件冒泡和默认行为
- event.stopPropagation()
- event.preventDefault()
查找、添加、删除、移动 DOM 节点的方法
- 回顾之前内容
如何减少 DOM 操作
- 缓存 DOM 查询结果
- 多次 DOM 操作合并到一次插入
解析 jsonp 的原理,为何它不是真正的 ajax
- 浏览器的同源策略(服务端没有同源策略)和跨域
- 哪些 html 标签能绕过跨域
- jsonp 的原理
window.load 和 DOMContentLoaded 的区别

== 和 === 的不同
- == 会尝试类型转换
- === 严格相等
- 哪些场景才用 == 如:if (null == undefined)
函数声明和函数表达式的区别
- 函数声明 function fn() {…}
- 函数表达式 const fn = function () {…}
- 函数声明会有变量提升,函数表达式没有
new Object() 和 Object.create()的区别
- {} 等同于 new Object(),原型是 Object.prototype
- Object.create(null) 没有原型
- Object.create({…}) 可指定原型
关于 this 的场景题

答案:1 window
关于作用域和自由变量的场景题

答案:4

答案:100 10 10
判断字符串以字母开头,后面字母数字下划线,长度 6-30
答案:/^[a-zA-Z]\w{5, 29}$/ \w 字母数字下划线 . 字符.
// 邮政编码
/\d{6}/
// 小写英文字母
/^[a-z]+$/
// 英文字母
/^[a-zA-Z]+$/
// 日期格式 2019.12.1
/^\d{4}-\d{1,2}-\d{1,2}$/
// 用户名
/^[a-zA-Z]\w{5, 17}$/
// 简单的 IP 地址匹配
/\d+\.\d+\.\d+\.\d+/
手写字符串 trim 方法,保证浏览器兼容性

如何获取多个数字中的最大值


如何用 js 实现继承
- class 继承
- prototype 继承
如何捕获 js 程序中的异常

什么是 JSON
- 是一种数据格式,本质是一段字符串
- 和 js 对象结构一致,对 js 语言更好
- JSON.parse JSON.stringify
获取当前页面的 url 参数
- 传统方式 location.search
- 新 API,URLSearchParams
function query(name) {
const search = location.search.substr(1); // 类似 array.slice(1)
// search: 'a=10&b=20&c=30'
const reg = newRegExp(`(^|&)${name}=([^&]*)(&|$)`, "i");
const res = search.match(reg);
if (res === null) {
return null;
}
return res[2];
}
将 url 参数解析为 JS 对象


手写数组 flatern,考虑多层级
function flat(arr) {
// 验证 arr 中,还有没有深层数组
const isDeep = arr.some((item) => item instanceof Array);
if (!isDeep) {
return arr;
}
const res = Array.prototype.concat.apply([], arr);
return flat(res);
}
数组去重
- 传统方式,遍历元素挨个比较、去重
- 使用 Set
function unique(arr) {
const res = [];
arr.forEach((item) => {
if (res.indexOf(item) === -1) {
res.push(item);
}
});
return res;
}
function unique(arr) {
const set = newSet(arr);
return [...set];
}
手写深拷贝

介绍一下 RAF requestAnimatioFrame
- 要想动画流畅。更新频率要 60 帧/s,即 16.67ms 更新一次视图
- setTimeout 要手动控制频率,RFA 浏览器会自动控制
- 后台标签或隐藏 iframe 中,RFA 会暂停,而 setTimeout 依然执行
// 3s 把宽度从 100px 变为 640px ,即增加 540px
// 60帧/s ,3s 180 帧 ,每次变化 3px
const $div1 = $("#div1");
let curWidth = 100;
const maxWidth = 640;
// // setTimeout
// function animate() {
// curWidth = curWidth + 3
// $div1.css('width', curWidth)
// if (curWidth < maxWidth) {
// setTimeout(animate, 16.7) // 自己控制时间
// }
// }
// animate()
// RAF
function animate() {
curWidth = curWidth + 5;
$div1.css("width", curWidth);
if (curWidth < maxWidth) {
window.requestAnimationFrame(animate); // 时间不用自己控制
}
}
animate();
前端性能如何优化?一般从几个地方考虑

Map 和 Set 有序和无序
有序:顺序,增删查改慢(数组)
无序:散乱,增删查改快(对象)

Map 和 Object 的区别
- API 不同,Map 可以以任意类型为 key
- Map 是有序结构(重要)
- Map 操作同样很快
Set 和数组的区别
- API 不同
- Set 元素不能重复
- Set 是无序结构,操作很快
weakMap 和 weakSet
- 弱引用,防止内存泄漏
- weakMap 只能用对象作为 key,weakSet 只能用对象作为 value
- 没有 forEach 和 size,只能用 add delete has
数组 reduce 用法
- 求和
constarr = [10, 20, 30, 40, 50];
constres = arr.reduce((sum, curVal) => sum + curVal, 0);
- 计数
constarr = [10, 20, 30, 40, 50, 10, 20, 30, 20];
constn = 30;
constcount = arr.reduce((count, val) => {
return val === n ? count + 1 : count;
}, 0);
- 输出字符串
constarr = [
{ name: "张三", age: "20" },
{ name: "李四", age: "21" },
{ name: "小明", age: "22" },
];
conststr = arr.reduce((s, item) => {
return `${s}${item.name} - ${item.age}\n`;
}, "");
console.log(str);
React
基本使用
JSX 基本使用
- 变量、表达式
- class、style
- 子元素和组件
render () {
// // 获取变量 插值
const pElem = <p>{this.state.name}</p>
return pElem
// // 表达式
const exprElem = <p>{this.state.flag ? 'yes' : 'no'}</p>
return exprElem
// // 子元素
const imgElem = <div>
<p>我的头像</p>
<img src="xxxx.png"/>
<img src={this.state.imgUrl}/>
</div>
return imgElem
// // class
const classElem = <p className="title">设置 css class</p>
return classElem
// // style
const styleData = { fontSize: '30px', color: 'blue' }
const styleElem = <p style={styleData}>设置 style</p>
// 内联写法,注意 {{ 和 }}
// const styleElem = <p style={{ fontSize: '30px', color: 'blue' }}>设置 style</p>
return styleElem
// // 原生 html
const rawHtml = '<span>富文本内容<i>斜体</i><b>加粗</b></span>'
const rawHtmlData = {
__html: rawHtml // 注意,必须是这种格式
}
const rawHtmlElem = <div>
<p dangerouslySetInnerHTML={rawHtmlData}></p>
<p>{rawHtml}</p>
</div>
return rawHtmlElem
// // 加载组件
const componentElem = <div>
<p>JSX 中加载一个组件</p>
<hr/>
<List/>
</div>
return componentElem
}
条件判断和渲染列表
条件判断
- if else
- 三元表达式
- 逻辑运算符 && ||
render() {
const blackBtn = <button className="btn-black">black btn</button>
const whiteBtn = <button className="btn-white">white btn</button>
// // if else
if (this.state.theme === 'black') {
return blackBtn
} else {
return whiteBtn
}
// // 三元运算符
return <div>
{ this.state.theme === 'black' ? blackBtn : whiteBtn }
</div>
// &&
return <div>
{ this.state.theme === 'black' && blackBtn }
</div>
}
渲染列表
- map
- key
render() {
return <ul>
{ /* vue v-for */
this.state.list.map(
(item, index) => {
// 这里的 key 和 Vue 的 key 类似,必填,不能是 index 或 random
return <li key={item.id}>
index {index}; id {item.id}; title {item.title}
</li>
}
)
}
</ul>
}
事件
- bind this
- 关于 event 参数
- 传递自定义参数
this.clickHandler1 = this.clickHandler1.bind(this)
clickHandler1() {
// console.log('this....', this) // this 默认是 undefined
this.setState({
name: 'lisi'
})
}
// // this - 使用 bind
return <p onClick={this.clickHandler1}>
{this.state.name}
</p>
// 静态方法,this 指向当前实例
clickHandler2 = () => {
this.setState({
name: "lisi",
});
};
return <p onClick={this.clickHandler2}>clickHandler2 {this.state.name}</p>;
clickHandler3 = (event) => {
event.preventDefault(); // 阻止默认行为
event.stopPropagation(); // 阻止冒泡
console.log("target", event.target); // 指向当前元素,即当前元素触发
console.log("current target", event.currentTarget); // 指向当前元素,假象!!!
// 注意,event 其实是 React 封装的。可以看 __proto__.constructor 是 SyntheticEvent 组合事件
console.log("event", event); // 不是原生的 Event ,原生的是 MouseEvent
console.log("event.__proto__.constructor", event.__proto__.constructor);
// 原生 event 如下。其 __proto__.constructor 是 MouseEvent
console.log("nativeEvent", event.nativeEvent);
console.log("nativeEvent target", event.nativeEvent.target); // 指向当前元素,即当前元素触发
console.log("nativeEvent current target", event.nativeEvent.currentTarget); // 指向 document !!!(绑定事件的元素)
// 1. event 是 SyntheticEvent ,模拟 DOM 事件所有能力
// 2. event.nativeEvent 是原生事件对象
// 3. 所有的事件,都被挂载到 document 上(React17 后绑定到 root 组件,有利于多个版本共存)
// 4. 和 DOM 事件不一样,和 Vue 事件也不一样
};
// // event
// return <a href="https://imooc.com/" onClick={this.clickHandler3}>
// click me
// </a>

// 传递参数 - 用 bind(this, a, b)
return <ul>{this.state.list.map((item, index) => {
return <li key={item.id} onClick={this.clickHandler4.bind(this, item.id, item.title)}>
index {index}; title {item.title}
</li>
})}</ul>
// 传递参数
clickHandler4(id, title, event) {
console.log(id, title)
console.log('event', event) // 最后追加一个参数,即可接收 event
}
表单
- 受控组件(state 和 value 绑定,onChange 事件)
- 非受控组件
- onChange 事件
- input textarea select 用 value
- checkbox radio 用 checked
render() {
// // 受控组件(非受控组件,后面再讲)
return <div>
<p>{this.state.name}</p>
<label htmlFor="inputName">姓名:</label> {/* 用 htmlFor 代替 for */}
<input id="inputName" value={this.state.name} onChange={this.onInputChange}/>
</div>
// textarea - 使用 value
return <div>
<textarea value={this.state.info} onChange={this.onTextareaChange}/>
<p>{this.state.info}</p>
</div>
// // select - 使用 value
return <div>
<select value={this.state.city} onChange={this.onSelectChange}>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="shenzhen">深圳</option>
</select>
<p>{this.state.city}</p>
</div>
// // checkbox
return <div>
<input type="checkbox" checked={this.state.flag} onChange={this.onCheckboxChange}/>
<p>{this.state.flag.toString()}</p>
</div>
// // radio
return <div>
male <input type="radio" name="gender" value="male" checked={this.state.gender === 'male'} onChange={this.onRadioChange}/>
female <input type="radio" name="gender" value="female" checked={this.state.gender === 'female'} onChange={this.onRadioChange}/>
<p>{this.state.gender}</p>
</div>
// 非受控组件 - 后面再讲
}
onInputChange = (e) => {
this.setState({
name: e.target.value
})
}
onTextareaChange = (e) => {
this.setState({
info: e.target.value
})
}
onSelectChange = (e) => {
this.setState({
city: e.target.value
})
}
onCheckboxChange = () => {
this.setState({
flag: !this.state.flag
})
}
onRadioChange = (e) => {
this.setState({
gender: e.target.value
})
}
组件使用
- props 传递数据
- props 传递函数
- props 类型检查
import PropTypes from 'prop-types'
class Input extendsReact.Component {
constructor(props) {
super(props)
this.state = {
title: ''
}
}
render() {
return <div>
<input value={this.state.title} onChange={this.onTitleChange}/>
<button onClick={this.onSubmit}>提交</button>
</div>
}
onTitleChange = (e) => {
this.setState({
title: e.target.value
})
}
onSubmit = () => {
const { submitTitle } = this.props
submitTitle(this.state.title) // 'abc'
this.setState({
title: ''
})
}
}
// props 类型检查
Input.propTypes = {
submitTitle: PropTypes.func.isRequired
}
class List extendsReact.Component {
constructor(props) {
super(props)
}
render() {
const { list } = this.props
return <ul>{list.map((item, index) => {
return <li key={item.id}>
<span>{item.title}</span>
</li>
})}</ul>
}
}
// props 类型检查
List.propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired
}
setState
- 不可变值(不能直接修改 state)
- 可能是异步更新
- 可能会被合并(批处理)
不可变值
// // 不要直接修改 state ,使用不可变值 ----------------------------
// // this.state.count++ // 错误
// this.setState({
// count: this.state.count + 1 // SCU
// })
// 操作数组、对象的的常用形式
// // 不可变值(函数式编程,纯函数) - 数组
// const list5Copy = this.state.list5.slice()
// this.setState({
// list1: this.state.list1.concat(100), // 追加
// list2: [...this.state.list2, 100], // 追加
// list3: this.state.list3.slice(0, 3), // 截取
// list4: this.state.list4.filter(item => item > 100), // 筛选
// list5: list5Copy // 其他操作
// })
// // 注意,不能直接对 this.state.list 进行 push pop splice 等,这样违反不可变值
// // 不可变值(浅拷贝) - 对象
// this.setState({
// obj1: Object.assign({}, this.state.obj1, {a: 100}),
// obj2: {...this.state.obj2, a: 100}
// })
// // 注意,不能直接对 this.state.obj 进行属性设置,这样违反不可变值
可能是异步更新
// // 直接使用是异步
this.setState({
count: this.state.count + 1
}, () => {
// 联想 Vue $nextTick - DOM
console.log('count by callback', this.state.count) // 回调函数中可以拿到最新的 state
})
console.log('count', this.state.count) // 异步的,拿不到最新值
// // setTimeout 中 setState 是同步的
setTimeout(() => {
this.setState({
count: this.state.count + 1
})
console.log('count in setTimeout', this.state.count)
}, 0)
// // 自己定义的 DOM 事件,setState 是同步的, 在 componentDidMount 中
bodyClickHandler = () => {
this.setState({
count: this.state.count + 1
})
console.log('count in body event', this.state.count)
}
componentDidMount() {
// 自己定义的 DOM 事件,setState 是同步的
document.body.addEventListener('click', this.bodyClickHandler)
}
componentWillUnmount() {
// 及时销毁自定义 DOM 事件
document.body.removeEventListener('click', this.bodyClickHandler)
// clearTimeout
}
可能会被合并
// // 传入对象,会被合并(类似 Object.assign )。执行结果只执行一次 +1
this.setState({
count: this.state.count + 1,
});
this.setState({
count: this.state.count + 1,
});
this.setState({
count: this.state.count + 1,
});
// 传入函数,不会被合并。执行结果是 +3
this.setState((prevState, props) => {
return {
count: prevState.count + 1,
};
});
this.setState((prevState, props) => {
return {
count: prevState.count + 1,
};
});
this.setState((prevState, props) => {
return {
count: prevState.count + 1,
};
});
React <= 17 setState
- React 组件事件:异步更新 + 合并 state(批处理)
- DOM 事件,setTimeout:同步更新,不合并 state
React 18 setState
- React 组件事件:异步更新 + 合并 state
- DOM 事件,setTimeout:异步更新 + 合并 state
- Automatic Batching 自动批处理
生命周期
- 单组件生命周期
- 父子组件生命周期,和 Vue 的一样

高级特性
函数组件
- 纯函数组件,输入 props,输出 JSX
- 没有实例,没有生命周期,没有 state
- 不能扩展其它方法

非受控组件
- ref
- defaultValue、defaultChecked
- 手动操作 DOM 元素
适用场景
- 必须手动操作 DOM 元素,setState 实现不了
- 文件上传 <input type=“file”>
- 某些富文本编辑器,需要传入 DOM 元素
受控组件 vs 非受控组件
- 优先使用受控组件,符合 React 设计原则
- 必须操作 DOM,则使用非受控组件
constructor(props) {
super(props)
this.state = {
name: 'oweqian',
flag: true,
}
this.nameInputRef = React.createRef() // 创建 ref
}
render() {
// input defaultValue
return <div>
{/* 使用 defaultValue 而不是 value ,使用 ref */}
<input defaultValue={this.state.name} ref={this.nameInputRef}/>
{/* state 并不会随着改变 */}
<span>state.name: {this.state.name}</span>
<br/>
<button onClick={this.alertName}>alert name</button>
</div>
}
alertName = () => {
const elem = this.nameInputRef.current // 通过 ref 获取 DOM 节点
alert(elem.value) // 不是 this.state.name
}
React Portals
- 组件默认按照既定层次嵌套渲染
- 如何让组件渲染到父组件以外?
适用场景
- 对话框 / 模态框
- overflow: hidden
- fixed 需要放在 body 第一层级
- 父组件 z-index 值太小
import React from 'react'
import ReactDOM from 'react-dom'
render() {
// 使用 Portals 渲染到 body 上。
// fixed 元素要放在 body 上,有更好的浏览器兼容性。
return ReactDOM.createPortal(<div className="modal">{this.props.children}</div>, document.body)
}
React Context
- 公共信息(语言、主题)如何传递给每个组件?
- 用 props 太繁琐
- 用 redux 小题大做
import React from 'react'
// 创建 Context 填入默认值(任何一个 js 变量)
const ThemeContext = React.createContext('light')
// 底层组件 - 函数是组件
function ThemeLink (props) {
// 函数式组件可以使用 Consumer,返回函数传参value
return <ThemeContext.Consumer>
{ value => <p>link's theme is {value}</p> }
</ThemeContext.Consumer>
}
// 底层组件 - class 组件
class ThemedButton extendsReact.Component {
// 指定 contextType 读取当前的 theme context。
// static contextType = ThemeContext // 也可以用 ThemedButton.contextType = ThemeContext
render() {
const theme = this.context // React 会往上找到最近的 theme Provider,然后使用它的值。
return <div>
<p>button's theme is {theme}</p>
</div>
}
}
ThemedButton.contextType = ThemeContext // 指定 contextType 读取当前的 theme context。
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
return (
<div>
<ThemedButton />
<ThemeLink />
</div>
)
}
class App extendsReact.Component {
constructor(props) {
super(props)
this.state = {
theme: 'light'
}
}
render() {
return <ThemeContext.Provider value={this.state.theme}>
<Toolbar />
<hr/>
<button onClick={this.changeTheme}>change theme</button>
</ThemeContext.Provider>
}
changeTheme = () => {
this.setState({
theme: this.state.theme === 'light' ? 'dark' : 'light'
})
}
}
export default App
异步组件
- import
- React.lazy
- React.Suspense
import React from 'react'
const ContextDemo = React.lazy(() => import('./ContextDemo'))
class App extendsReact.Component {
constructor(props) {
super(props)
}
render() {
return <div>
<p>引入一个动态组件</p>
<hr />
<React.Suspense fallback={<div>Loading...</div>}>
<ContextDemo/>
</React.Suspense>
</div>
// 1. 强制刷新,可看到 loading (看不到就限制一下 chrome 网速)
// 2. 看 network 的 js 加载
}
}
export default App
性能优化
- shouldComponentUpdate(简称 SCU)
- PureComponent 和 React.memo
- 不可变值 immutable.js
SCU 基本用法

SCU 默认返回什么
React 默认父组件有更新,子组件则无条件也更新,SCU 默认返回 true(即可以渲染)
性能优化对 React 更加重要!!!
SCU 一定要每次都用吗? – 需要的时候才优化
SCU 一定要配合 state 不可变值

注:配合 state 不可变值部分(很重要!!!)
PureComponent 和 React.memo
- PureComponent(适用于类组件),实现了浅比较
- React.memo,函数组件中的 PureComponent
- 浅比较已适用大部分情况(尽量不要做深度比较)
immutable.js
- 彻底拥抱 “不可变值”
- 基于共享数据(不是深拷贝),速度好
- 有一定学习和迁移成本,谨慎使用

公共逻辑抽离
- 高阶组件 HOC
- Render Props
HOC

import React from 'react'
// 高阶组件
const withMouse = (Component) => {
class withMouseComponent extendsReact.Component {
constructor(props) {
super(props)
this.state = { x: 0, y: 0 }
}
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY
})
}
render() {
return (
<div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
{/* 1. 透传所有 props 2. 增加 mouse 属性 */}
<Component {...this.props} mouse={this.state}/>
</div>
)
}
}
return withMouseComponent
}
const App = (props) => {
const a = props.a
const { x, y } = props.mouse // 接收 mouse 属性
return (
<div style={{ height: '500px' }}>
<h1>The mouse position is ({x}, {y})</h1>
<p>{a}</p>
</div>
)
}
export default withMouse(App) // 返回高阶函数
Render Props

HOC vs Render Props
- HOC:模式简单,但会增加组件层级
- Render Props:代码简介,学习成本较高
- 按需使用
Redux
- 基本概念
- 单向数据流
- React-Redux
- 异步 action
- 中间件
基本概念
- store state
- action
- reducer
单向数据流
- dispatch action
- reducer -> new State
- subscribe 触发通知(视图更新)

React-Redux
connect - mapStateToProps mapDispatchToProps
异步 action


中间件原理
对 dispatch 做改造。


中间件 logger 实现

React-Router
- hash 模式
- h5 history 模式
- history 模式需要 server 端支持
React Hooks
class 组件的问题
- 大型组件难以拆分和重构,很难测试
- 相同业务逻辑,分散到各个方法中,逻辑混乱
- 复用逻辑变的复杂,如 HOC、Render Props
React 组件更易用函数表达
- React 提倡函数式编程,view = fn (props)
- 函数组件更灵活,更易拆分,更易测试
- 但函数组件太简单,需要增强能力 - Hooks
命名规范
- 规定所有的 Hooks 都以 use 开头,如 useXxx
- 自定义 Hooks 也要以 use 开头
- 非 Hooks 的地方,尽量不要使用 useXxx 写法
useState
- useState(0) 传入初始值,返回数组 [state, setState]
- 通过 state 获取值
- 通过 setState(1) 修改值
import React, { useState } from "react";
function ClickCounter() {
// 数组的解构
// useState 就是一个 Hook “钩”,最基本的一个 Hook
const [count, setCount] = useState(0); // 传入一个初始值
const [name, setName] = useState("oweqian老师");
function clickHandler() {
setCount(count + 1);
setName(name + "2020");
}
return (
<div>
<p>
你点击了 {count} 次 {name}
</p>
<button onClick={clickHandler}>点击</button>
</div>
);
}
export default ClickCounter;
useEffect
- 模拟 componentDidMount - useEffect 依赖 []
- 模拟 componentDidUpdate - useEffect 无依赖或依赖 [a, b]
- 模拟 componentWillUnMount - useEffect 中返回一个函数
import React, { useState, useEffect } from "react";
function LifeCycles() {
const [count, setCount] = useState(0);
const [name, setName] = useState("oweqian老师");
// // 模拟 class 组件的 DidMount 和 DidUpdate
// useEffect(() => {
// console.log('在此发送一个 ajax 请求')
// })
// // 模拟 class 组件的 DidMount
// useEffect(() => {
// console.log('加载完了')
// }, []) // 第二个参数是 [] (不依赖于任何 state)
// // 模拟 class 组件的 DidUpdate
// useEffect(() => {
// console.log('更新了')
// }, [count, name]) // 第二个参数就是依赖的 state
// 模拟 class 组件的 DidMount
useEffect(() => {
let timerId = setInterval(() => {
console.log(Date.now());
}, 1000);
// 返回一个函数
// 模拟 WillUnMount
return () => {
clearInterval(timerId);
};
}, []);
function clickHandler() {
setCount(count + 1);
setName(name + "2020");
}
return (
<div>
<p>
你点击了 {count} 次 {name}
</p>
<button onClick={clickHandler}>点击</button>
</div>
);
}
export default LifeCycles;
模拟 willUnMount,但不完全等同于 willUnMount
// class component
componentDidMount() {
console.log(`开始监听 ${this.props.friendId} 的在线状态`)
}
componentWillUnMount() {
console.log(`结束监听 ${this.props.friendId} 的在线状态`)
}
// friendId 更新
componentDidUpdate(prevProps) {
console.log(`结束监听 ${prevProps.friendId} 在线状态`)
console.log(`开始监听 ${this.props.friendId} 在线状态`)
}
// function component
// DidMount 和 DidUpdate
useEffect(() => {
console.log(`开始监听 ${friendId} 在线状态`)
// 【特别注意】
// 此处并不完全等同于 WillUnMount
// props 发生变化,即更新,也会执行结束监听
// 准确的说:返回的函数,会在下一次 effect 执行之前,被执行
return () => {
console.log(`结束监听 ${friendId} 在线状态`)
}
})
useEffect 中返回函数 fn:
- useEffect 依赖 [],组件销毁时执行 fn,等同于 willUnMount
- useEffect 无依赖或依赖 [a, b],组件更新时执行 fn
- 即,下一次执行 useEffect 之前,就会执行 fn,无论更新或卸载
useRef
import React, { useRef, useEffect } from "react";
function UseRef() {
const btnRef = useRef(null); // 初始值
useEffect(() => {
console.log(btnRef.current); // DOM 节点
}, []);
return (
<div>
<button ref={btnRef}>click</button>
</div>
);
}
export default UseRef;
useContext
import React, { useContext } from "react";
// 主题颜色
const themes = {
light: {
foreground: "#000",
background: "#eee",
},
dark: {
foreground: "#fff",
background: "#222",
},
};
// 创建 Context
const ThemeContext = React.createContext(themes.light); // 初始值
function ThemeButton() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
hello world
</button>
);
}
function Toolbar() {
return (
<div>
<ThemeButton></ThemeButton>
</div>
);
}
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Toolbar></Toolbar>
</ThemeContext.Provider>
);
}
export default App;
useReducer
import React, { useReducer } from "react";
const initialState = { count: 0 };
const reducer = (state, action) => {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
};
function App() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
count: {state.count}
<button onClick={() => dispatch({ type: "increment" })}>increment</button>
<button onClick={() => dispatch({ type: "decrement" })}>decrement</button>
</div>
);
}
export default App;
useReducer 和 Redux 的区别:
- useReducer 是 useState 的代替方案,用于 state 复杂变化
- useReducer 是单组件状态管理,组件通讯还需要 props
- Redux 是全局状态管理,多组件共享数据
useMemo
- React 默认会更新所有子组件
- class 组件使用 SCU 和 PureComponent 做优化
- Hooks 中使用 useMemo,但优化的原理是相同的
import React, { useState, memo, useMemo } from "react";
// 子组件
// function Child({ userInfo }) {
// console.log('Child render...', userInfo)
// return <div>
// <p>This is Child {userInfo.name} {userInfo.age}</p>
// </div>
// }
// 类似 class PureComponent ,对 props 进行浅层比较
const Child = memo(({ userInfo }) => {
console.log("Child render...", userInfo);
return (
<div>
<p>
This is Child {userInfo.name} {userInfo.age}
</p>
</div>
);
});
// 父组件
function App() {
console.log("Parent render...");
const [count, setCount] = useState(0);
const [name, setName] = useState("oweqian老师");
// const userInfo = { name, age: 20 }
// 用 useMemo 缓存数据,有依赖
const userInfo = useMemo(() => {
return { name, age: 21 };
}, [name]);
return (
<div>
<p>
count is {count}
<button onClick={() => setCount(count + 1)}>click</button>
</p>
<Child userInfo={userInfo}></Child>
</div>
);
}
export default App;
useCallback
- useMemo 缓存数据
- useCallback 缓存函数
- 两者是 React Hooks 的常见优化策略 (搭配 React.memo)
import React, { useState, memo, useMemo, useCallback } from "react";
// 子组件,memo 相当于 PureComponent
const Child = memo(({ userInfo, onChange }) => {
console.log("Child render...", userInfo);
return (
<div>
<p>
This is Child {userInfo.name} {userInfo.age}
</p>
<input onChange={onChange}></input>
</div>
);
});
// 父组件
function App() {
console.log("Parent render...");
const [count, setCount] = useState(0);
const [name, setName] = useState("oweqian老师");
// 用 useMemo 缓存数据
const userInfo = useMemo(() => {
return { name, age: 21 };
}, [name]);
// function onChange(e) {
// console.log(e.target.value)
// }
// 用 useCallback 缓存函数
const onChange = useCallback((e) => {
console.log(e.target.value);
}, []);
return (
<div>
<p>
count is {count}
<button onClick={() => setCount(count + 1)}>click</button>
</p>
<Child userInfo={userInfo} onChange={onChange}></Child>
</div>
);
}
export default App;
自定义 Hook
- 封装通用的功能
- 开发和使用第三方 Hooks
- 自定义 Hook 带来了无限的扩展性,解耦代码
- 本质是一个函数,以 use 开头
- 内部正常使用 useState、useEffect、其它 Hooks
- 自定义返回格式
import { useState, useEffect } from "react";
import axios from "axios";
// 封装 axios 发送网络请求的自定义 Hook
function useAxios(url) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState();
const [error, setError] = useState();
useEffect(() => {
// 利用 axios 发送网络请求
setLoading(true);
axios
.get(url) // 发送一个 get 请求
.then((res) => setData(res))
.catch((err) => setError(err))
.finally(() => setLoading(false));
}, [url]);
return [loading, data, error];
}
export default useAxios;
// 第三方 Hook
// https://nikgraf.github.io/react-hooks/
// https://github.com/umijs/hooks
import Reactfrom 'react'
import useAxios from '../customHooks/useAxios'
function App() {
const url = 'http://localhost:3000/'
// 数组解构
const [loading, data, error] = useAxios(url)
if (loading) return <div>loading...</div>
return error
? <div>{JSON.stringify(error)}</div>
: <div>{JSON.stringify(data)}</div>
}
export default App
总结
- 命名规范 useXxx
- Hooks 使用规范,重要
- 关于 Hooks 的调用顺序
使用规范
- 只能用于 React 函数组件和自定义 Hook 中,其它地方不可以
- 只能用于顶层代码,不能在循环、判断中使用 Hooks
- eslint 插件 eslint-plugin-react-hooks 可以帮到你
Hooks 调用顺序
- 函数组件,纯函数,执行完即销毁
- 无论是 render 还是 re-render,Hooks 调用顺序必须一致
- 如果 Hooks 出现在循环、判断里,则无法保证顺序一致
- Hooks 严重依赖调用顺序!重要!
import React, { useState, useEffect } from "react";
function Teach({ couseName }) {
// 函数组件,纯函数,执行完即销毁
// 所以,无论组件初始化(render)还是组件更新(re-render)
// 都会重新执行一次这个函数,获取最新的组件
// 这一点和 class 组件不一样
// render: 初始化 state 的值 '张三'
// re-render: 读取 state 的值 '张三'
const [studentName, setStudentName] = useState("张三");
// if (couseName) {
// const [studentName, setStudentName] = useState('张三')
// }
// render: 初始化 state 的值 'oweqian'
// re-render: 读取 state 的值 'oweqian'
const [teacherName, setTeacherName] = useState("oweqian");
// if (couseName) {
// useEffect(() => {
// // 模拟学生签到
// localStorage.setItem('name', studentName)
// })
// }
// render: 添加 effect 函数
// re-render: 替换 effect 函数(内部的函数会重新定义)
useEffect(() => {
// 模拟学生签到
localStorage.setItem("name", studentName);
});
// render: 添加 effect 函数
// re-render: 替换 effect 函数(内部的函数会重新定义)
useEffect(() => {
// 模拟开始上课
console.log(`${teacherName} 开始上课,学生 ${studentName}`);
});
return (
<div>
课程:{couseName}, 讲师:{teacherName}, 学生:{studentName}
</div>
);
}
export default Teach;
class 组件逻辑复用
- 高阶组件 HOC(组件嵌套、props 透传)
- Render Props(学习成本高,只能传递纯函数)
Hooks 组件逻辑复用
- 完全符合 Hooks 原有规则,没有其他要求,易理解记忆
- 变量作用域明确
- 不会产生组件嵌套
import { useState, useEffect } from "react";
function useMousePosition() {
const [x, setX] = useState(0);
const [y, setY] = useState(0);
useEffect(() => {
function mouseMoveHandler(event) {
setX(event.clientX);
setY(event.clientY);
}
// 绑定事件
document.body.addEventListener("mousemove", mouseMoveHandler);
// 解绑事件
return () =>
document.body.removeEventListener("mousemove", mouseMoveHandler);
}, []);
return [x, y];
}
export default useMousePosition;
注意事项
- useState 初始化值,只有第一次有效
import React, { useState } from "react";
// 子组件
function Child({ userInfo }) {
// render: 初始化 state
// re-render: 只恢复初始化的 state 值,不会再重新设置新的值
// 只能用 setName 修改
const [name, setName] = useState(userInfo.name);
return (
<div>
<p>Child, props name: {userInfo.name}</p>
<p>Child, state name: {name}</p>
</div>
);
}
function App() {
const [name, setName] = useState("oweqian");
const userInfo = { name };
return (
<div>
<div>
Parent
<button onClick={() => setName("慕课网")}>setName</button>
</div>
<Child userInfo={userInfo} />
</div>
);
}
export default App;
- useEffect 内部不能修改 state (闭包)
import React, { useState, useEffect } from "react";
function UseEffectChangeState() {
const [count, setCount] = useState(0);
// 模拟 DidMount
// useEffect中的纯函数执行完则销毁,除非是DidUpdate
useEffect(() => {
console.log("useEffect...", count);
// 定时任务
const timer = setInterval(() => {
console.log("setInterval...", count); // 会一直输出0
setCount(count + 1);
}, 1000);
// 清除定时任务
return () => clearTimeout(timer);
}, []); // 依赖为 []
// 依赖为 [] 时: re-render 不会重新执行 effect 函数
// 没有依赖:re-render 会重新执行 effect 函数
return <div>count: {count}</div>;
}
export default UseEffectChangeState;
解决方法:
import React, { useState, useRef, useEffect } from "react";
function UseEffectChangeState() {
const [count, setCount] = useState(0);
// 模拟 DidMount
const countRef = useRef(0); // 增加一个useRef
useEffect(() => {
console.log("useEffect...", count);
// 定时任务
const timer = setInterval(() => {
console.log("setInterval...", countRef.current);
setCount(++countRef.current);
}, 1000);
// 清除定时任务
return () => clearTimeout(timer);
}, []);
return <div>count: {count}</div>;
}
export default UseEffectChangeState;
- useEffect 可能出现死循环(依赖项里有引用类型)

真题演练
组件之间如何通讯
- 父子组件 props
- 自定义事件
- Redux 和 Context
JSX 本质是什么
- createElement
- 执行返回 vnode
Context 是什么,如何应用
- 父组件,向其下所有子孙组件传递信息
- 如一些简单的公共信息:主题色、语言等
- 复杂的公共信息,请用 Redux
shouldComponentUpate 用途
- 性能优化
- 配合 “不可变值” 一起使用,否则容易出错
组件生命周期
- 单组件生命周期
- 父子组件生命周期
- 注意 SCU
Ajax 应该放在哪个生命周期
- ComponentDidMount
渲染列表,为何用 key
- diff 算法中,通过 tag 和 key 来算判断是否是 sameNode
- 减少渲染次数,提升渲染性能
函数组件和 class 组件的区别
- 纯函数,输入 props,输出 JSX
- 没有实例,没有生命周期,没有 state
- 不能扩展方法
什么是受控组件
- 表单的 value 受 state 控制
- 需要自行监听 onChange,更新 state
- 对比非受控组件
何时使用异步组件
- 加载大组件
- 路由懒加载
多个组件有公共逻辑,如何抽离
- HOC
- Render Props
Redux 如何进行异步请求
- 使用异步 action
- 如 redux-thunk
PureComponent 有何区别
- 实现了浅比较的 shouldComponentUpdate
- 优化性能
- 要结合不可变值使用
React 事件和 DOM 事件的区别
- 所有事件均挂载到 root element 上
- event 不是原生的,是 SyntheticEvent 合成事件对象
- dispatchEvent
性能优化
- 渲染列表时使用 key
- 自定义事件、DOM 事件及时销毁
- 合理使用异步组件
- 减少函数 bind this 的次数
- 合理使用 SCU、PureComponent 和 memo
- 合理使用 immutable.js
- webpack 层面的优化
- 前端通用的性能优化(图片懒加载等)
- 使用 SSR
React 和 Vue 的区别
相同点:
- 都支持组件化
- 都是数据驱动视图
- 都有虚拟 DOM 的概念
不同点:
- React 使用 JSX 拥抱 JS,Vue 使用模板拥抱 html
- React 是函数式编程,Vue 是声明式编程
- React 更适合大型业务开发,Vue 更适合中小型业务开发
为什么会有 React Hooks,它解决了什么问题
- 完善函数组件的能力,函数更适合 React 组件
- 组件逻辑复用,Hooks 表现更好
- class 组件复杂,不易拆解,不易测试,逻辑混乱
React Hooks 如何模拟组件生命周期
- 模拟 componentDidMount - useEffect 依赖 []
- 模拟 componentDidUpdate - useEffect 无依赖或依赖 [a, b]
- 模拟 componentWillUnMount - useEffect 中返回一个函数
React Hooks 性能优化
- useMemo 缓存数据
- useCallback 缓存函数
使用 React Hooks 遇到哪些坑
- useState 初始化值,只初始化一次
- useEffect 内部不能修改 state
- useEffect 依赖引用类型,会出现死循环
Hooks 相比 HOC 和 Render Props 有哪些优点
- 完全符合 Hooks 原有规则,没有其他要求,易理解记忆
- 变量作用域很明确
- 不会产生组件嵌套
TypeScript
TS 优点和缺点,适用场景是什么
优点:
- 静态类型检查,编译时报错
- 智能提示,提高开发效率和稳定性
缺点:
- 有一定学习成本
- 应用不规范,“anyscript”
适用场景:
- 大型项目、业务复杂、维护人员多
- 逻辑性较强的代码,需要类型更稳固
TS 基础类型有哪些
- boolean
- number
- string
- null
- undefined
- symbol
- 数组 Array
- 元组 Tuple
- 枚举 enum
- interface
any void unknown never 的区别
- any:不进行类型检查
- void:函数没有显式返回一个值
- unknown:未知类型(有类型检查)
- never:永远不存在的类型
TS 的访问修饰符有哪几个
- public - 全部可访问
- private - 只有自己可访问
- protected - 自己和派生类可访问
TS 私有属性 # 和 private 有什么区别
- 私有属性,不能在构造函数的参数中定义
- private 属性,可以通过 as any 强制获取,但私有属性不行
type 和 interface 的区别,以及使用场景
共同点:
- 都可以描述一个对象结构
- 都可以被 class 实现
- 都可以被扩展
不同点:
- type 可以声明基础类型
- type 可以使用联合类型(或)、交叉类型(合并)
- type 可以通过 typeof、keyof 等赋值
TS 中交叉类型和联合类型是什么
交叉类型:
- 多个类型合并为一个类型 T1 & T2 & T3
- 如果属性类型冲突了,则属性类型为 never
- 基础类型无法交叉,会返回 never
联合类型:
- 联合多个类型 T1 | T2 | T3
- 一种 “或” 的关系
- 基础类型可以联合
TS 中有哪些特殊符号,分别是什么意思
- ?:可选
- ?.:可选链
- ??:空值合并运算符
- !:非空断言操作符
- _:数字分隔符
TS 常见工具类型有哪些
- Partial
- Required
- Pick<T, K>
- Omit<T, K>
- Readonly
如何扩展 window 属性
// interface 合并声明
declare interface Window {
test: string;
}
window.test = "xxx";
TS 如何处理第三方模块的类型
- 常见的第三方插件,都有
@types/xxx
包,安装即可使用 - 外部模块,可通过 declare module
- 内部模块,可通过 namespace 命名空间