以前我在CSDN上看到面试贴就直接刷掉的,从不会多看一眼,直到去年 9 月份我开始准备面试时,才发现很多面试经验贴特别有用,看这些帖子(我不敢称之为文章,怕被杠)的过程中对我的复习思维形成影响很大,所以我现在把之前自己好好整理的面试计划分享出来,希望能帮到接下来要找工作的朋友,不喜勿喷哈~
简历在找工作过程中是非常非常重要的,无论你是什么途径去面试的,面试你的人一定会看你的简历。
推荐阅读:
如何写「前端简历」,能敲开字节跳动的大门?
一份优秀的前端开发工程师简历是怎么样的?
2.1 基本信息
2.2 专业技能
2.3 工作经历
2.4 项目经历
2.5 社区贡献
HTML 和 CSS 面试题答不出来基本可以回去了。
以下是针对 HTML 相关的面试题,一般来说这地方不会出太多题,面试官也不愿意花太多时间在这上面。
1.1 如何理解 HTML 语义化?
1.2 script 标签中 defer 和 async 的区别?
script
:会阻碍 HTML 解析,只有下载好并执行完脚本才会继续解析 HTML。async script
:解析 HTML 过程中进行脚本的异步下载,下载成功立马执行,有可能会阻断 HTML 的解析。defer script
:完全不会阻碍 HTML 的解析,解析完成之后再按照顺序执行脚本。下图清晰地展示了三种 script
的过程:
推荐文章:
- 图解 script 标签中的 async 和 defer 属性
1.3 从浏览器地址栏输入 url 到请求返回发生了什么
先阅读这篇科普性质的:从 URL 输入到页面展现到底发生什么? 先阅读篇文章:从输入 URL 开始建立前端知识体系。
DNS 域名解析。(字节面试被虐后,是时候搞懂 DNS 了)
TCP 连接。
总是要问:为什么需要三次握手,两次不行吗?其实这是由 TCP 的自身特点可靠传输决定的。客户端和服务端要进行可靠传输,那么就需要确认双方的接收和发送能力。第一次握手可以确认客服端的发送能力,第二次握手,确认了服务端的发送能力和接收能力,所以第三次握手才可以确认客户端的接收能力。不然容易出现丢包的现象。
http 请求。
服务器处理请求并返回 HTTP 报文。
浏览器渲染页面。
以下是针对 CSS 相关的面试题,这些题答不出来会给人非常不好的技术印象。
2.1 盒模型介绍
CSS3 中的盒模型有以下两种:标准盒模型、IE(替代)盒模型。
两种盒子模型都是由 content + padding + border + margin
构成,其大小都是由 content + padding + border
决定的,但是盒子内容宽/高度(即 width/height
)的计算范围根据盒模型的不同会有所不同:
content
。content + padding + border
。可以通过 box-sizing
来改变元素的盒模型:
box-sizing: content-box
:标准盒模型(默认值)。box-sizing: border-box
:IE(替代)盒模型。2.2 css 选择器和优先级
首先我们要知道有哪些选择器:选择器参考表。
常规来说,大家都知道样式的优先级一般为 !important > style > id > class
,但是涉及多类选择器作用于同一个元素时候怎么判断优先级呢?相信我,你在改一些第三方库(比如 antd )样式时,理解这个会帮助很大!
这篇文章写的非常清晰易懂,强烈推荐,看完之后就没啥问题了:深入理解 CSS 选择器优先级。
上述文章中核心内容: 优先级是由 A 、B、C、D 的值来决定的,其中它们的值计算规则如下:
- 如果存在内联样式,那么
A = 1
,否则A = 0
;- B 的值等于
ID选择器(#id)
出现的次数;- C 的值等于
类选择器(.class)
和属性选择器(a[href="https://example.org"])
和伪类(:first-child)
出现的总次数;- D 的值等于
标签选择器(h1,a,div)
和伪元素(::before,::after)
出现的总次数。
从左至右比较,如果是样式优先级相等,取后面出现的样式。
2.3 重排(reflow)和重绘(repaint)的理解
简单地总结下两者的概念:
如何减少重排和重绘?
.class
或 cssText
。offsetWidth
属性存到一个临时变量,再去使用,而不是频繁使用这个计算属性;又比如利用 document.createDocumentFragment()
来添加要被添加的节点,处理完之后再插入到实际 DOM 中。**absolute**
或 **fixed**
使元素脱离文档流,这在制作复杂的动画时对性能的影响比较明显。transform
、will-change
等,比如改变元素位置,我们使用 translate
会比使用绝对定位改变其 left
、top
等来的高效,因为它不会触发重排或重绘,transform
使浏览器为元素创建⼀个 GPU 图层,这使得动画元素在一个独立的层中进行渲染。当元素的内容没有发生改变,就没有必要进行重绘。这里推荐腾讯 IVWEB 团队的这篇文章:你真的了解回流和重绘吗,好好认真看完,面试应该没问题的。
2.4 对 BFC 的理解
BFC 即块级格式上下文,根据盒模型可知,每个元素都被定义为一个矩形盒子,然而盒子的布局会受到尺寸,定位,盒子的子元素或兄弟元素,视口的尺寸等因素决定,所以这里有一个浏览器计算的过程,计算的规则就是由一个叫做视觉格式化模型的东西所定义的,BFC 就是来自这个概念,它是 CSS 视觉渲染的一部分,用于决定块级盒的布局及浮动相互影响范围的一个区域。
BFC 具有一些特性:
margin
会重叠,创建新的 BFC 可以避免外边距重叠。margin
值和容器的左 border
相接触。利用这些特性,我们可以解决以下问题:
4
和 6
,我们可以实现三栏(或两栏)自适应布局。2
,我们可以避免 margin
重叠问题。3
,我们可以避免高度塌陷。创建 BFC 的方式:
position
为 absolute
或 fixed
)。display
为 inline-block
。overflow
的值不为 visible
。推荐文章:可能是最好的 BFC 解析了...
2.5 实现两栏布局(左侧固定 + 右侧自适应布局)
现在有以下 DOM 结构:
左侧
右侧
margin-left
设为固定宽度 。注意,因为右边元素的 width
默认为 auto
,所以会自动撑满父元素。.outer {
height: 100px;
}
.left {
float: left;
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
margin-left: 200px;
height: 100%;
background: lightseagreen;
}
overflow: hidden;
这样右边就触发了 BFC
,BFC
的区域不会与浮动元素发生重叠,所以两侧就不会发生重叠。.outer {
height: 100px;
}
.left {
float: left;
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
overflow: auto;
height: 100%;
background: lightseagreen;
}
flex
布局,左边元素固定宽度,右边的元素设置 flex: 1
。.outer {
display: flex;
height: 100px;
}
.left {
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
flex: 1;
height: 100%;
background: lightseagreen;
}
absolute
定位,宽度固定。右边元素的 margin-left
的值设为左边元素的宽度值。.outer {
position: relative;
height: 100px;
}
.left {
position: absolute;
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
margin-left: 200px;
height: 100%;
background: lightseagreen;
}
absolute
定位, left
为宽度大小,其余方向定位为 0
。.outer {
position: relative;
height: 100px;
}
.left {
width: 200px;
height: 100%;
background: lightcoral;
}
.right {
position: absolute;
left: 200px;
top: 0;
right: 0;
bottom: 0;
height: 100%;
background: lightseagreen;
}
2.6 实现圣杯布局和双飞翼布局(经典三分栏布局)
圣杯布局和双飞翼布局的目的:
圣杯布局和双飞翼布局的技术总结:
float
布局。margin
负值,以便和中间内容横向重叠。padding
,双飞翼布局用 margin
。圣杯布局: HTML 结构:
我是中间
我是左边
我是右边
CSS 样式:
#container {
padding-left: 200px;
padding-right: 150px;
overflow: auto;
}
#container p {
float: left;
}
.center {
width: 100%;
background-color: lightcoral;
}
.left {
width: 200px;
position: relative;
left: -200px;
margin-left: -100%;
background-color: lightcyan;
}
.right {
width: 150px;
margin-right: -150px;
background-color: lightgreen;
}
.clearfix:after {
content: "";
display: table;
clear: both;
}
双飞翼布局: HTML 结构:
main
left
right
CSS 样式:
.float {
float: left;
}
#main {
width: 100%;
height: 200px;
background-color: lightpink;
}
#main-wrap {
margin: 0 190px 0 190px;
}
#left {
width: 190px;
height: 200px;
background-color: lightsalmon;
margin-left: -100%;
}
#right {
width: 190px;
height: 200px;
background-color: lightskyblue;
margin-left: -190px;
}
tips:上述代码中 margin-left: -100%
相对的是父元素的 content
宽度,即不包含 paddig
、 border
的宽度。
其实以上问题需要掌握 margin 负值问题 即可很好理解。
2.7 水平垂直居中多种实现方式
left: 50%
和 top: 50%
现将子元素左上角移到父元素中心位置,然后再通过 translate
来调整子元素的中心点到父元素的中心。该方法可以不定宽高。.father {
position: relative;
}
.son {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
0
,将 margin
设置为 auto
,由于宽高固定,对应方向实现平分,该方法必须盒子有宽高。.father {
position: relative;
}
.son {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0px;
margin: auto;
height: 100px;
width: 100px;
}
left: 50%
和 top: 50%
现将子元素左上角移到父元素中心位置,然后再通过 margin-left
和 margin-top
以子元素自己的一半宽高进行负值赋值。该方法必须定宽高。.father {
position: relative;
}
.son {
position: absolute;
left: 50%;
top: 50%;
width: 200px;
height: 200px;
margin-left: -100px;
margin-top: -100px;
}
flex
,最经典最方便的一种了,不用解释,定不定宽高无所谓的。.father {
display: flex;
justify-content: center;
align-items: center;
}
其实还有很多方法,比如 display: grid
或 display: table-cell
来做,有兴趣点击下面这篇文章可以了解下:
面试官:你能实现多少种水平垂直居中的布局(定宽高和不定宽高)。
2.8 flex 布局
这一块内容看 Flex 布局教程 就够了。
这里有个小问题,很多时候我们会用到 flex: 1
,它具体包含了以下的意思:
flex-grow: 1
:该属性默认为 0
,如果存在剩余空间,元素也不放大。设置为 1
代表会放大。flex-shrink: 1
:该属性默认为 1
,如果空间不足,元素缩小。flex-basis: 0%
:该属性定义在分配多余空间之前,元素占据的主轴空间。浏览器就是根据这个属性来计算是否有多余空间的。默认值为 auto
,即项目本身大小。设置为 0%
之后,因为有 flex-grow
和 flex-shrink
的设置会自动放大或缩小。在做两栏布局时,如果右边的自适应元素 flex-basis
设为 auto
的话,其本身大小将会是 0
。2.9 line-height 如何继承?
line-height
写了具体数值,比如 30px
,则子元素 line-height
继承该值。line-height
写了比例,比如 1.5 或 2
,则子元素 line-height
也是继承该比例。line-height
写了百分比,比如 200%
,则子元素 line-height
继承的是父元素 font-size * 200%
计算出来的值。js 的考察其实来回就那些东西,不过就我自己而已学习的时候理解是真的理解了,但是忘也确实会忘(大家都说理解了一定不会忘,但是要答全的话还是需要理解+背)。
以下是比较重要的几个 js 变量要掌握的点。
1.1 基本的数据类型介绍,及值类型和引用类型的理解
在 JS 中共有 8
种基础的数据类型,分别为: Undefined
、 Null
、 Boolean
、 Number
、 String
、 Object
、 Symbol
、 BigInt
。
其中 Symbol
和 BigInt
是 ES6 新增的数据类型,可能会被单独问:
值类型的赋值变动过程如下:
let a = 100;
let b = a;
a = 200;
console.log(b); // 100
值类型是直接存储在**栈(stack)**中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
引用类型的赋值变动过程如下:
let a = { age: 20 };
let b = a;
b.age = 30;
console.log(a.age); // 30
引用类型存储在**堆(heap)**中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;
1.2 数据类型的判断
object
。console.log(typeof undefined); // undefined
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof "str"); // string
console.log(typeof Symbol("foo")); // symbol
console.log(typeof 2172141653n); // bigint
console.log(typeof function () {}); // function
// 不能判别
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
class People {}
class Student extends People {}
const vortesnail = new Student();
console.log(vortesnail instanceof People); // true
console.log(vortesnail instanceof Student); // true
其实现就是顺着原型链去找,如果能找到对应的 Xxxxx.prototype
即为 true
。比如这里的 vortesnail
作为实例,顺着原型链能找到 Student.prototype
及 People.prototype
,所以都为 true
。
Object.prototype.toString.call(2); // "[object Number]"
Object.prototype.toString.call(""); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Math); // "[object Math]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(function () {}); // "[object Function]"
在面试中有一个经常被问的问题就是:如何判断变量是否为数组?
Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true
Object.prototype.toString.call(arr); // "[object Array]"
1.3 手写深拷贝
这个题一定要会啊!笔者面试过程中疯狂被问到!
文章推荐:如何写出一个惊艳面试官的深拷贝?
/**
* 深拷贝
* @param {Object} obj 要拷贝的对象
* @param {Map} map 用于存储循环引用对象的地址
*/
function deepClone(obj = {}, map = new Map()) {
if (typeof obj !== "object") {
return obj;
}
if (map.get(obj)) {
return map.get(obj);
}
let result = {};
// 初始化返回结果
if (
obj instanceof Array ||
// 加 || 的原因是为了防止 Array 的 prototype 被重写,Array.isArray 也是如此
Object.prototype.toString(obj) === "[object Array]"
) {
result = [];
}
// 防止循环引用
map.set(obj, result);
for (const key in obj) {
// 保证 key 不是原型属性
if (obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(obj[key], map);
}
}
// 返回结果
return result;
}
1.4 根据 0.1+0.2 ! == 0.3,讲讲 IEEE 754 ,如何让其相等?
建议先阅读这篇文章了解 IEEE 754 :硬核基础二进制篇(一)0.1 + 0.2 != 0.3 和 IEEE-754 标准。 再阅读这篇文章了解如何运算:0.1 + 0.2 不等于 0.3?为什么 JavaScript 有这种“骚”操作?。
原因总结:
进制转换
:js 在做数字计算的时候,0.1 和 0.2 都会被转成二进制后无限循环 ,但是 js 采用的 IEEE 754 二进制浮点运算,最大可以存储 53 位有效数字,于是大于 53 位后面的会全部截掉,将导致精度丢失。对阶运算
:由于指数位数不相同,运算时需要对阶运算,阶小的尾数要根据阶差来右移(0舍1入
),尾数位移时可能会发生数丢失的情况,影响精度。解决办法:
function add(a, b) {
const maxLen = Math.max(
a.toString().split(".")[1].length,
b.toString().split(".")[1].length
);
const base = 10 ** maxLen;
const bigA = BigInt(base * a);
const bigB = BigInt(base * b);
const bigRes = (bigA + bigB) / BigInt(base); // 如果是 (1n + 2n) / 10n 是等于 0n的。。。
return Number(bigRes);
}
这里代码是有问题的,因为最后计算 bigRes
的大数相除(即 /
)是会把小数部分截掉的,所以我很疑惑为什么网络上很多文章都说可以通过先转为整数运算再除回去,为了防止转为的整数超出 js 表示范围,还可以运用到 ES6 新增的大数类型,我真的很疑惑,希望有好心人能解答下。
Number.EPSILON
误差范围。function isEqual(a, b) {
return Math.abs(a - b) < Number.EPSILON;
}
console.log(isEqual(0.1 + 0.2, 0.3)); // true
Number.EPSILON
的实质是一个可以接受的最小误差范围,一般来说为 Math.pow(2, -52)
。
// 字符串数字相加
var addStrings = function (num1, num2) {
let i = num1.length - 1;
let j = num2.length - 1;
const res = [];
let carry = 0;
while (i >= 0 || j >= 0) {
const n1 = i >= 0 ? Number(num1[i]) : 0;
const n2 = j >= 0 ? Number(num2[j]) : 0;
const sum = n1 + n2 + carry;
res.unshift(sum % 10);
carry = Math.floor(sum / 10);
i--;
j--;
}
if (carry) {
res.unshift(carry);
}
return res.join("");
};
function isEqual(a, b, sum) {
const [intStr1, deciStr1] = a.toString().split(".");
const [intStr2, deciStr2] = b.toString().split(".");
const inteSum = addStrings(intStr1, intStr2); // 获取整数相加部分
const deciSum = addStrings(deciStr1, deciStr2); // 获取小数相加部分
return inteSum + "." + deciSum === String(sum);
}
console.log(isEqual(0.1, 0.2, 0.3)); // true
这是 leetcode 上一道原题:415. 字符串相加。区别在于原题没有考虑小数,但是也是很简单的,我们分为两个部分计算就行。
可以说这部分每家面试官都会问了。。首先理解的话,其实一张图即可,一段代码即可。
function Foo() {}
let f1 = new Foo();
let f2 = new Foo();
千万别畏惧下面这张图,特别有用,一定要搞懂,熟到提笔就能默画出来。
总结:
prototype
对象。先说出总结的话,再举例子说明如何顺着原型链找到某个属性。
推荐的阅读:JavaScript 深入之从原型到原型链 掌握基本概念,再阅读这篇文章轻松理解 JS 原型原型链
加深上图的印象。
需要注意的是,js 采用的是静态作用域,所以函数的作用域在函数定义时就确定了。
推荐阅读:先阅读JavaScript 深入之词法作用域和动态作用域
,再阅读深入理解 JavaScript 作用域和作用域链
。
这部分一定要按顺序连续读这几篇文章,必须多读几遍:
总结:当 JavaScript 代码执行一段可执行代码时,会创建对应的执行上下文。对于每个执行上下文,都有三个重要属性:
根据 MDN 中文的定义,闭包的定义如下:
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。可以在一个内层函数中访问到其外层函数的作用域。
也可以这样说:
闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。 闭包 = 函数 + 函数能够访问的自由变量。
在经过上一小节“执行上下文”的学习,再来阅读这篇文章:JavaScript 深入之闭包
,你会对闭包的实质有一定的了解。在回答时,我们这样答:
在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]]
中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。
闭包应用: 函数作为参数被传递:
function print(fn) {
const a = 200;
fn();
}
const a = 100;
function fn() {
console.log(a);
}
print(fn); // 100
函数作为返回值被返回:
function create() {
const a = 100;
return function () {
console.log(a);
};
}
const fn = create();
const a = 200;
fn(); // 100
闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。
应用实例:比如缓存工具,隐藏数据,只提供 API 。
function createCache() {
const data = {}; // 闭包中被隐藏的数据,不被外界访问
return {
set: function (key, val) {
data[key] = val;
},
get: function (key) {
return data[key];
},
};
}
const c = createCache();
c.set("a", 100);
console.log(c.get("a")); // 100
这部分实现还是要知道的,就算工作中不会自己手写,但是说不准面试官就是要问,知道点原理也好,可以扩宽我们写代码的思路。
call
call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
举个例子:
var obj = {
value: "vortesnail",
};
function fn() {
console.log(this.value);
}
fn.call(obj); // vortesnail
通过 call
方法我们做到了以下两点:
call
改变了 this 的指向,指向到 obj
。fn
函数执行了。那么如果我们自己写 call
方法的话,可以怎么做呢?我们先考虑改造 obj
。
var obj = {
value: "vortesnail",
fn: function () {
console.log(this.value);
},
};
obj.fn(); // vortesnail
这时候 this 就指向了 obj
,但是这样做我们手动给 obj
增加了一个 fn
属性,这显然是不行的,不用担心,我们执行完再使用对象属性的删除方法(delete)不就行了?
obj.fn = fn;
obj.fn();
delete obj.fn;
根据这个思路,我们就可以写出来了:
Function.prototype.myCall = function (context) {
// 判断调用对象
if (typeof this !== "function") {
throw new Error("Type error");
}
// 首先获取参数
let args = [...arguments].slice(1);
let result = null;
// 判断 context 是否传入,如果没有传就设置为 window
context = context || window;
// 将被调用的方法设置为 context 的属性
// this 即为我们要调用的方法
context.fn = this;
// 执行要被调用的方法
result = context.fn(...args);
// 删除手动增加的属性方法
delete context.fn;
// 将执行结果返回
return result;
};
apply
我们会了 call
的实现之后,apply
就变得很简单了,他们没有任何区别,除了传参方式。
Function.prototype.myApply = function (context) {
if (typeof this !== "function") {
throw new Error("Type error");
}
let result = null;
context = context || window;
// 与上面代码相比,我们使用 Symbol 来保证属性唯一
// 也就是保证不会重写用户自己原来定义在 context 中的同名属性
const fnSymbol = Symbol();
context[fnSymbol] = this;
// 执行要被调用的方法
if (arguments[1]) {
result = context[fnSymbol](...arguments[1]);
} else {
result = context[fnSymbol]();
}
delete context[fnSymbol];
return result;
};
bind
bind
返回的是一个函数,这个地方可以详细阅读这篇文章,讲的非常清楚:解析 bind 原理,并手写 bind 实现
。
Function.prototype.myBind = function (context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new Error("Type error");
}
// 获取参数
const args = [...arguments].slice(1),
const fn = this;
return function Fn() {
return fn.apply(
this instanceof Fn ? this : context,
// 当前的这个 arguments 是指 Fn 的参数
args.concat(...arguments)
);
};
};
__proto__
为构造函数的 prototype
。function myNew(context) {
const obj = new Object();
obj.__proto__ = context.prototype;
const res = context.apply(obj, [...arguments].slice(1));
return typeof res === "object" ? res : obj;
}
这部分着重要理解 Promise、async awiat、event loop 等。
8.1 event loop、宏任务和微任务
首先推荐一个可以在线看代码流程的网站:loupe
。 然后看下这个视频学习下:到底什么是 Event Loop 呢?
简单的例子:
console.log("Hi");
setTimeout(function cb() {
console.log("cb"); // cb 即 callback
}, 5000);
console.log("Bye");
它的执行过程是这样的:
Web APIs 会创建对应的线程,比如 setTimeout
会创建定时器线程,ajax
请求会创建 http 线程。。。这是由 js 的运行环境决定的,比如浏览器。
看完上面的视频之后,至少大家画 Event Loop 的图讲解不是啥问题了,但是涉及到宏任务和微任务,我们还得拜读一下这篇文章:这一次,彻底弄懂 JavaScript 执行机制
。如果意犹未尽,不如再读下这篇非常详细带有大量动图的文章:做一些动图,学习一下 EventLoop
。想了解事件循环和页面渲染之间关系的又可以再阅读这篇文章:深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示)
。
注意:1.Call Stack 调用栈空闲 -> 2.尝试 DOM 渲染 -> 触发 Event loop。
宏任务:setTimeout,setInterval,Ajax,DOM 事件。 微任务:Promise async/await。
两者区别:
setTimeout
、setInterval
、DOM 事件
、script
。Promise.then
、MutationObserver
、Node 环境下的 process.nextTick
。从 event loop 解释,为何微任务执行更早?
8.2 Promise
关于这一块儿没什么好说的,最好是实现一遍 Promise A+ 规范,多少有点印象,当然面试官也不会叫你默写一个完整的出来,但是你起码要知道实现原理。
关于 Promise 的所有使用方式,可参照这篇文章:ECMAScript 6 入门 - Promise 对象。 手写 Promise 源码的解析文章,可阅读此篇文章:从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节
。 关于 Promise 的面试题,可参考这篇文章:要就来 45 道 Promise 面试题一次爽到底
。
实现一个 Promise.all:
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
// 参数可以不是数组,但必须具有 Iterator 接口
if (typeof promises[Symbol.iterator] !== "function") {
reject("Type error");
}
if (promises.length === 0) {
resolve([]);
} else {
const res = [];
let count = 0;
const len = promises.length;
for (let i = 0; i < len; i++) {
//考虑到 promises[i] 可能是 thenable 对象也可能是普通值
Promise.resolve(promises[i])
.then((data) => {
res[i] = data;
if (++count === len) {
resolve(res);
}
})
.catch((err) => {
reject(err);
});
}
}
});
};
8.3 async/await 和 Promise 的关系
这里看这篇文章即可:「硬核 JS」你真的了解垃圾回收机制吗
。
总结一下:
有两种垃圾回收策略:
标记清除的缺点:
解决以上的缺点可以使用 **标记整理(Mark-Compact)算法 **,标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)
引用计数的缺点:
V8 的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。
EventMitter 就是发布订阅模式的典型应用:
export class EventEmitter {
private _events: Record>;
constructor() {
this._events = Object.create(null);
}
emit(evt: string, ...args: any[]) {
if (!this._events[evt]) return false;
const fns = [...this._events[evt]];
fns.forEach((fn) => {
fn.apply(this, args);
});
return true;
}
on(evt: string, fn: Function) {
if (typeof fn !== "function") {
throw new TypeError("The evet-triggered callback must be a function");
}
if (!this._events[evt]) {
this._events[evt] = [fn];
} else {
this._events[evt].push(fn);
}
}
once(evt: string, fn: Function) {
const execFn = () => {
fn.apply(this);
this.off(evt, execFn);
};
this.on(evt, execFn);
}
off(evt: string, fn?: Function) {
if (!this._events[evt]) return;
if (!fn) {
this._events[evt] && (this._events[evt].length = 0);
}
let cb;
const cbLen = this._events[evt].length;
for (let i = 0; i < cbLen; i++) {
cb = this._events[evt][i];
if (cb === fn) {
this._events[evt].splice(i, 1);
break;
}
}
}
removeAllListeners(evt?: string) {
if (evt) {
this._events[evt] && (this._events[evt].length = 0);
} else {
this._events = Object.create(null);
}
}
}
要掌握 cookie,localStorage 和 sessionStorage。
其缺点:
它们的区别:
前端工程师做出网页,需要通过网络请求向后端获取数据,因此 http 协议是前端面试的必考内容。
1.1 状态码分类
1.2 常见状态码
1.3 关于协议和规范
4.1 关于缓存
什么是缓存? 把一些不需要重新获取的内容再重新获取一次
为什么需要缓存? 网络请求相比于 CPU 的计算和页面渲染是非常非常慢的。
哪些资源可以被缓存? 静态资源,比如 js css img。
4.2 强制缓存
Cache-Control:
Cache-Control 有哪些值:
4.3 协商缓存(对比缓存)
资源标识:
Last-Modified:
服务端拿到 if-Modified-Since 之后拿这个时间去和服务端资源最后修改时间做比较,如果一致则返回 304 ,不一致(也就是资源已经更新了)就返回 200 和新的资源及新的 Last-Modified。
Etag:
其实 Etag 和 Last-Modified 一样的,只不过 Etag 是服务端对资源按照一定方式(比如 contenthash)计算出来的唯一标识,就像人类指纹一样,传给客户端之后,客户端再传过来时候,服务端会将其与现在的资源计算出来的唯一标识做比较,一致则返回 304,不一致就返回 200 和新的资源及新的 Etag。
两者比较:
4.4 综述
4.4 三种刷新操作对 http 缓存的影响
正常操作:强制缓存有效,协商缓存有效。 手动刷新:强制缓存失效,协商缓存有效。 强制刷新:强制缓存失效,协商缓存失效。
对于更多面试中可能出现的问题,我还是建议精读这篇三元的文章:HTTP 灵魂之问,巩固你的 HTTP 知识体系
。
比如会被经常问到的: GET 和 POST 的区别。
HTTP/2 有哪些改进?(很大可能问原理)
关于 HTTPS 的一些原理,可以阅读这篇文章:这一次,彻底理解 https 原理
。接着你可以观看这个视频进行更进一步的学习:HTTPS 底层原理,面试官直接下跪,唱征服!
关于跨域问题,大部分文章都是理论性比较强,还不如读这篇文章,聊聊跨域的原理与解决方法
,讲的非常清晰,我个人觉得对付面试就是先知道使用流程,把这个流程能自己说出来,然后再讲下原理即可。
阅读这篇文章即可:一文吃透 react 事件系统原理
。
为什么要自定义事件机制?
不排除现在还会有面试官问关于 class component 的问题。
2.1 生命周期
发生在 constructor
中的内容,在 constructor
中进行 state
、props
的初始化,在这个阶段修改 state
,不会执行更新阶段的生命周期,可以直接对 state
赋值。
1. componentWillMount
发生在 render 函数之前,还没有挂载 Dom
2. render
3. componentDidMount
发生在 render 函数之后,已经挂载 Dom
更新阶段分为由 state
更新引起和 props
更新引起。
props 更新时:
1. componentWillReceiveProps(nextProps,nextState)
这个生命周期主要为我们提供对 props 发生改变的监听,如果你需要在 props 发生改变后,相应改变组件的一些 state。在这个方法中改变 state 不会二次渲染,而是直接合并 state。
2. shouldComponentUpdate(nextProps,nextState)
这个生命周期需要返回一个 Boolean 类型的值,判断是否需要更新渲染组件,优化 react 应用的主要手段之一,当返回 false 就不会再向下执行生命周期了,在这个阶段不可以 setState(),会导致循环调用。
3. componentWillUpdate(nextProps,nextState)
这个生命周期主要是给我们一个时机能够处理一些在 Dom 发生更新之前的事情,如获得 Dom 更新前某些元素的坐标、大小等,在这个阶段不可以 setState(),会导致循环调用。
**一直到这里 this.props 和 this.state 都还未发生更新**
4. render
5. componentDidUpdate(prevProps, prevState)
在此时已经完成渲染,Dom 已经发生变化,state 已经发生更新,prevProps、prevState 均为上一个状态的值。
state 更新时(具体同上)
1. shouldComponentUpdate
2. componentWillUpdate
3. render
4. componentDidUpdate
1. componentWillUnmount
在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作,例如,清除 timer,取消网络请求或清除在 componentDidMount 中创建的订阅等。componentWillUnmount 中不应调用 setState,因为该组件将永远不会重新渲染。组件实例卸载后,将永远不会再挂载它。
在 React 16 中官方已经建议删除以下三个方法,非要使用必须加前缀:UNSAVE_
。
componentWillMount;
componentWillReceiveProps;
componentWillUpdate;
取代这两三个生命周期的以下两个新的。
1. static getDerivedStateFromProps(nextProps,nextState)
在组件实例化、接收到新的 props 、组件状态更新时会被调用
2. getSnapshotBeforeUpdate(prevProps,prevState)
在这个阶段我们可以拿到上一个状态 Dom 元素的坐标、大小的等相关信息。用于替代旧的生命周期中的 componentWillUpdate。
该函数的返回值将会作为 componentDidUpdate 的第三个参数出现。
需要注意的是,一般都会问为什么要废弃三个生命周期,原因是什么。
2.2 setState 同步还是异步
setState
本身代码的执行肯定是同步的,这里的异步是指是多个 state 会合成到一起进行批量更新。 同步还是异步取决于它被调用的环境。
setState
在 React 能够控制的范围被调用,它就是异步的。比如合成事件处理函数,生命周期函数, 此时会进行批量更新,也就是将状态合并后再进行 DOM 更新。setState
在原生 JavaScript 控制的范围被调用,它就是同步的。比如原生事件处理函数,定时器回调函数,Ajax 回调函数中,此时 setState
被调用后会立即更新 DOM 。这篇文章写的真的太好了,一定要读:简明 JavaScript 函数式编程——入门篇
。
总结一下: 函数式编程有两个核心概念。
纯函数带来的意义。
现在应该大多数面试官会问 hooks 相关的啦。这里我强烈推荐三篇文章,即使没看过源码,也能比较好地理解一些原理:
用动画和实战打开 React Hooks(一):useState 和 useEffect
用动画和实战打开 React Hooks(二):自定义 Hook 和 useCallback
用动画和实战打开 React Hooks(三):useReducer 和 useContext
4.1 为什么不能在条件语句中写 hook
推荐这篇文章:我打破了 React Hook 必须按顺序、不能在条件语句中调用的枷锁
。
hook 在每次渲染时的查找是根据一个“全局”的下标对链表进行查找的,如果放在条件语句中使用,有一定几率会造成拿到的状态出现错乱。
4.2 HOC 和 hook 的区别
hoc 能复用逻辑和视图,hook 只能复用逻辑。
4.3 useEffect 和 useLayoutEffect 区别
对于 React 的函数组件来说,其更新过程大致分为以下步骤:
state
发生变化。state
变量。useEffect
在第 4 步之后执行,且是异步的,保证了不会阻塞浏览器进程。 useLayoutEffect
在第 3 步至第 4 步之间执行,且是同步代码,所以会阻塞后面代码的执行。
4.4 useEffect 依赖为空数组与 componentDidMount 区别
在 render
执行之后,componentDidMount
会执行,如果在这个生命周期中再一次 setState
,会导致再次 render
,返回了新的值,浏览器只会渲染第二次 render
返回的值,这样可以避免闪屏。
但是 useEffect
是在真实的 DOM 渲染之后才会去执行,这会造成两次 render
,有可能会闪屏。
实际上 useLayoutEffect
会更接近 componentDidMount
的表现,它们都同步执行且会阻碍真实的 DOM 渲染的。
4.5 React.memo() 和 React.useMemo() 的区别
memo
是一个高阶组件,默认情况下会对 props
进行浅比较,如果相等不会重新渲染。多数情况下我们比较的都是引用类型,浅比较就会失效,所以我们可以传入第二个参数手动控制。useMemo
返回的是一个缓存值,只有依赖发生变化时才会去重新执行作为第一个参数的函数,需要记住的是,useMemo
是在 render
阶段执行的,所以不要在这个函数内部执行与渲染无关的操作,诸如副作用这类的操作属于 useEffect
的适用范畴。4.6 React.useCallback() 和 React.useMemo() 的区别
useCallback
可缓存函数,其实就是避免每次重新渲染后都去重新执行一个新的函数。useMemo
可缓存值。有很多时候,我们在 useEffect
中使用某个定义的外部函数,是要添加到 deps
数组中的,如果不用 useCallback
缓存,这个函数在每次重新渲染时都是一个完全新的函数,也就是引用地址发生了变化,这就会导致 useEffect
总会无意义的执行。
4.7 React.forwardRef 是什么及其作用
这里还是阅读官方文档来的清晰:React.forwardRef。 一般在父组件要拿到子组件的某个实际的 DOM 元素时会用到。
react hooks 与 class 组件对比
函数式组件与类组件有何不同
让虚拟 DOM 和 DOM-diff 不再成为你的绊脚石
。
关于这块儿我觉得可以好好阅读下这篇无敌的博客了:Build your own React。 它可以教你一步步实现一个简单的基于 React Fiber 的 React,可以学到很多 React 的设计思想,毕竟为了面试我们可能大多数人是没有时间或能力去阅读源码的了。
然后我们再阅读下其它作者对于 React Fiber 的理解,再转化为我们自己的思考总结,以下是推荐文章: 这可能是最通俗的 React Fiber(时间分片) 打开方式
推荐文章:React 性能优化的 8 种方式了解一下?
React.memo
来缓存组件。React.useMemo
缓存大量的计算。React.lazy
和 React.Suspense
延迟加载不是立即需要的组件。React.Fragment
避免添加额外的 DOM。Redux 包教包会(一):介绍 Redux 三大核心概念
原理初探:当面试官问 Webpack 的时候他想知道什么
简易实现:面试官:webpack 原理都不会
,手写一个 webpack,看看 AST 怎么用
加料:简单易懂的 webpack 打包后 JS 的运行过程
,Webpack 手写 loader 和 plugin
热更新原理:Webpack HMR 原理解析
面试题:「吐血整理」再来一打 Webpack 面试题
这里要注意,应该还会考 webpack5 和 4 有哪些区别。
前端模块化详解(完整版)
(这里面没有讲 umd)
可能是最详细的 UMD 模块入门指南
代码层面:
React.lazy
和 React.Suspense
,通常需要与 webpack 中的 splitChunks
配合。构建方面:
terser-webpack-plugin
压缩 Javascript 代码;使用 css-minimizer-webpack-plugin
压缩 CSS 代码;使用 html-webpack-plugin
压缩 html 代码。compression-webpack-plugin
,node 作为服务器也要开启,使用 compression
。其它:
Cache-Control / Expires
。以下的内容是上面没有提到的手写,比如 new
、Promise.all
这种上面内容中已经提到了如何写。
JavaScript 专题之跟着 underscore 学防抖
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
let callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (callNow) func.apply(context, args);
} else {
timeout = setTimeout(function () {
func.apply(context, args);
}, wait);
}
};
}
JavaScript 专题之跟着 underscore 学节流
// 使用时间戳
function throttle(func, wait) {
let preTime = 0;
return function () {
let nowTime = +new Date();
let context = this;
let args = arguments;
if (nowTime - preTime > wait) {
func.apply(context, args);
preTime = nowTime;
}
};
}
// 定时器实现
function throttle(func, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
}
};
}
这里对快排思想不太明白的同学可以看下这个讲解的很清晰的视频:快速排序算法
。
function sortArray(nums) {
quickSort(0, nums.length - 1, nums);
return nums;
}
function quickSort(start, end, arr) {
if (start < end) {
const mid = sort(start, end, arr);
quickSort(start, mid - 1, arr);
quickSort(mid + 1, end, arr);
}
}
function sort(start, end, arr) {
const base = arr[start];
let left = start;
let right = end;
while (left !== right) {
while (arr[right] >= base && right > left) {
right--;
}
arr[left] = arr[right];
while (arr[left] <= base && right > left) {
left++;
}
arr[right] = arr[left];
}
arr[left] = base;
return left;
}
这个手写一定要懂原型及原型链。
function myInstanceof(target, origin) {
if (typeof target !== "object" || target === null) return false;
if (typeof origin !== "function")
throw new TypeError("origin must be function");
let proto = Object.getPrototypeOf(target); // 相当于 proto = target.__proto__;
while (proto) {
if (proto === origin.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
重点,不要觉得用不到就不管,这道题就是考察你对 js 语法的熟练程度以及手写代码的基本能力。
function flat(arr, depth = 1) {
if (depth > 0) {
// 以下代码还可以简化,不过为了可读性,还是....
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flat(cur, depth - 1) : cur);
}, []);
}
return arr.slice();
}
先不考虑第二个参数初始值:
Array.prototype.reduce = function (cb) {
const arr = this; //this就是调用reduce方法的数组
let total = arr[0]; // 默认为数组的第一项
for (let i = 1; i < arr.length; i++) {
total = cb(total, arr[i], i, arr);
}
return total;
};
考虑上初始值:
Array.prototype.reduce = function (cb, initialValue) {
const arr = this;
let total = initialValue || arr[0];
// 有初始值的话从0遍历,否则从1遍历
for (let i = initialValue ? 0 : 1; i < arr.length; i++) {
total = cb(total, arr[i], i, arr);
}
return total;
};
JS 实现一个带并发限制的异度调度器 Scheduler,保证同时运行的任务最多有两个。完善下面代码中的 Scheduler 类,使得以下程序能正确输出。
class Scheduler {
add(promiseMaker) {}
}
const timeout = (time) =>
new Promise((resolve) => {
setTimeout(resolve, time);
});
const scheduler = new Scheduler();
const addTask = (time, order) => {
scheduler.add(() => timeout(time).then(() => console.log(order)));
};
addTask(1000, "1");
addTask(500, "2");
addTask(300, "3");
addTask(400, "4");
// output:2 3 1 4
// 一开始,1,2两个任务进入队列。
// 500ms 时,2完成,输出2,任务3入队。
// 800ms 时,3完成,输出3,任务4入队。
// 1000ms 时,1完成,输出1。
根据题目,我们只需要操作 Scheduler
类就行:
class Scheduler {
constructor() {
this.waitTasks = []; // 待执行的任务队列
this.excutingTasks = []; // 正在执行的任务队列
this.maxExcutingNum = 2; // 允许同时运行的任务数量
}
add(promiseMaker) {
if (this.excutingTasks.length < this.maxExcutingNum) {
this.run(promiseMaker);
} else {
this.waitTasks.push(promiseMaker);
}
}
run(promiseMaker) {
const len = this.excutingTasks.push(promiseMaker);
const index = len - 1;
promiseMaker().then(() => {
this.excutingTasks.splice(index, 1);
if (this.waitTasks.length > 0) {
this.run(this.waitTasks.shift());
}
});
}
}
set
关键字:function unique(arr) {
return [...new Set(arr)];
}
filter
方法:function unique(arr) {
return arr.filter((item, index, array) => {
return array.indexOf(item) === index;
});
}