学习分享,共勉
题外话,毕竟我工作多年,深知技术改革和创新的方向,Flutter作为跨平台开发技术、Flutter以其美观、快速、高效、开放等优势迅速俘获人心
开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】
这里代码是有问题的,因为最后计算 bigRes 的大数相除(即 /)是会把小数部分截掉的,所以我很疑惑为什么网络上很多文章都说可以通过先转为整数运算再除回去,为了防止转为的整数超出 js 表示范围,还可以运用到 ES6 新增的大数类型,我真的很疑惑,希望有好心人能解答下。
- 使用 Number.EPSILON 误差范围。
functionisEqual(a, b) {
returnMath.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("");
};
functionisEqual(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 上一道原题:面试题库。区别在于原题没有考虑小数,但是也是很简单的,我们分为两个部分计算就行。
2、 原型和原型链
可以说这部分每家面试官都会问了。。首先理解的话,其实一张图即可,一段代码即可。
functionFoo() {}
let f1 = newFoo();
let f2 = newFoo();
复制代码
千万别畏惧下面这张图,特别有用,一定要搞懂,熟到提笔就能默画出来。
总结:
-
原型:每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性,其实就是 prototype 对象。
-
原型链:由相互关联的原型组成的链状结构就是原型链。
先说出总结的话,再举例子说明如何顺着原型链找到某个属性。
推荐的阅读:JavaScript 深入之从原型到原型链 掌握基本概念,再阅读这篇文章轻松理解 JS 原型原型链加深上图的印象。
3、 作用域与作用域链
-
作用域:规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性。(全局作用域、函数作用域、块级作用域)
-
作用域链:从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找 。这种层级关系就是作用域链。(由多个执行上下文的变量对象构成的链表就叫做作用域链,学习下面的内容之后再考虑这句话)
需要注意的是,js 采用的是静态作用域,所以函数的作用域在函数定义时就确定了。
推荐阅读:先阅读JavaScript 深入之词法作用域和动态作用域,再阅读深入理解 JavaScript 作用域和作用域链。
4、 执行上下文
总结:当 JavaScript 代码执行一段可执行代码时,会创建对应的执行上下文。对于每个执行上下文,都有三个重要属性:
-
变量对象(Variable object,VO);
-
作用域链(Scope chain);
-
this。(关于 this 指向问题,在上面推荐的深入系列也有讲从 ES 规范讲的,但是实在是难懂,对于应付面试来说以下这篇阮一峰的文章应该就可以了:JavaScript 的 this 原理)
5、 闭包
根据 MDN 中文的定义,闭包的定义如下:
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。可以在一个内层函数中访问到其外层函数的作用域。
也可以这样说:
闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是
函数参数也不是
函数的局部变量的
变量。 闭包 = 函数 + 函数能够访问的自由变量。
在经过上一小节“执行上下文”的学习,再来阅读这篇文章:JavaScript 深入之闭包,你会对闭包的实质有一定的了解。在回答时,我们这样答:
在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。
闭包应用: 函数作为参数被传递:
functionprint(fn) {
const a = 200;
fn();
}
const a = 100;
functionfn() {
console.log(a);
}
print(fn); // 100复制代码
函数作为返回值被返回:
functioncreate() {
const a = 100;
returnfunction () {
console.log(a);
};
}
const fn = create();
const a = 200;
fn(); // 100复制代码
闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。
应用实例:比如缓存工具,隐藏数据,只提供 API 。
functioncreateCache() {
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复制代码
6、 call、apply、bind 实现
这部分实现还是要知道的,就算工作中不会自己手写,但是说不准面试官就是要问,知道点原理也好,可以扩宽我们写代码的思路。
call
call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。
举个例子:
var obj = {
value: "vortesnail",
};
functionfn() {
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 (typeofthis !== "function") {
thrownewError("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 (typeofthis !== "function") {
thrownewError("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 (typeofthis !== "function") {
thrownewError("Type error");
}
// 获取参数const args = [...arguments].slice(1),
const fn = this;
returnfunctionFn() {
return fn.apply(
thisinstanceofFn ? this : context,
// 当前的这个 arguments 是指 Fn 的参数
args.concat(...arguments)
);
};
};
复制代码
7、 new 实现
-
首先创一个新的空对象。
-
根据原型链,设置空对象的 __proto__ 为构造函数的 prototype 。
-
构造函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)。
-
判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象。
functionmyNew(context) {
const obj = newObject();
obj.__proto__ = context.prototype;
const res = context.apply(obj, [...arguments].slice(1));
returntypeof res === "object" ? res : obj;
}
复制代码
大厂面试题分享 面试题库
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
8、 异步
这部分着重要理解 Promise、async awiat、event loop 等。
8.1 event loop、宏任务和微任务
首先推荐一个可以在线看代码流程的网站:loupe。 然后看下这个视频学习下:到底什么是 Event Loop 呢?
简单的例子:
console.log("Hi");
setTimeout(functioncb() {
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。
-
每次 Call Stack 清空(即每次轮询结束),即同步任务执行完。
-
都是 DOM 重新渲染的机会,DOM 结构有改变则重新渲染。
-
然后再去触发下一次 Event loop。
宏任务:setTimeout,setInterval,Ajax,DOM 事件。 微任务:Promise async/await。
两者区别:
-
宏任务:DOM 渲染后触发,如 setTimeout 、setInterval 、DOM 事件 、script 。
-
微任务:DOM 渲染前触发,如 Promise.then 、MutationObserver 、Node 环境下的 process.nextTick 。
从 event loop 解释,为何微任务执行更早?
-
微任务是 ES6 语法规定的(被压入 micro task queue)。
-
宏任务是由浏览器规定的(通过 Web APIs 压入 Callback queue)。
-
宏任务执行时间一般比较长。
-
每一次宏任务开始之前一定是伴随着一次 event loop 结束的,而微任务是在一次 event loop 结束前执行的。
8.2 Promise
关于这一块儿没什么好说的,最好是实现一遍 Promise A+ 规范,多少有点印象,当然面试官也不会叫你默写一个完整的出来,但是你起码要知道实现原理。
关于 Promise 的所有使用方式,可参照这篇文章:
ECMAScript 6 入门 - Promise 对象。 手写 Promise 源码的解析文章,可阅读此篇文章:
从一道让我失眠的 Promise 面试题开始,深入分析 Promise 实现细节。 关于 Promise 的面试题,可参考这篇文章:
要就来 45 道 Promise 面试题一次爽到底。
实现一个 Promise.all:
Promise.all = function (promises) {
returnnewPromise((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 的关系
-
async/await 是消灭异步回调的终极武器。
-
但和 Promise 并不互斥,反而,两者相辅相成。
-
执行 async 函数,返回的一定是 Promise 对象。
-
await 相当于 Promise 的 then。
-
tru…catch 可捕获异常,代替了 Promise 的 catch。
9、 浏览器的垃圾回收机制
这里看这篇文章即可:「硬核 JS」你真的了解垃圾回收机制吗。
总结一下:
有两种垃圾回收策略:
-
标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
-
引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收。
标记清除的缺点:
-
内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
-
分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。
解决以上的缺点可以使用 **标记整理(Mark-Compact)算法 **,标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)
引用计数的缺点:
-
需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
-
解决不了循环引用导致的无法回收问题。
V8 的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。
-
针对新生区采用并行回收。
-
针对老生区采用增量标记与惰性回收。
10、 实现一个 EventMitter 类
EventMitter 就是发布订阅模式的典型应用:
exportclassEventEmitter {
private_events: Record<string, Array<Function>>;
constructor() {
this._events = Object.create(null);
}
emit(evt: string, ...args: any[]) {
if (!this._events[evt]) returnfalse;
const fns = [...this._events[evt]];
fns.forEach((fn) => {
fn.apply(this, args);
});
returntrue;
}
on(evt: string, fn: Function) {
if (typeof fn !== "function") {
thrownewTypeError("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) {
constexecFn = () => {
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);
}
}
}
复制代码
四、web 存储
要掌握 cookie,localStorage 和 sessionStorage。
1、cookie
-
本身用于浏览器和 server 通讯。
-
被“借用”到本地存储来的。
-
可用 document.cookie = ‘…’ 来修改。
其缺点:
-
存储大小限制为 4KB。
-
http 请求时需要发送到服务端,增加请求数量。
-
只能用 document.cookie = ‘…’ 来修改,太过简陋。
2、localStorage 和 sessionStorage
-
HTML5 专门为存储来设计的,最大可存 5M。
-
API 简单易用, setItem getItem。
-
不会随着 http 请求被发送到服务端。
它们的区别:
-
localStorage 数据会永久存储,除非代码删除或手动删除。
-
sessionStorage 数据只存在于当前会话,浏览器关闭则清空。
-
一般用 localStorage 会多一些。
五、Http
前端工程师做出网页,需要通过网络请求向后端获取数据,因此 http 协议是前端面试的必考内容。
1、http 状态码
1.1 状态码分类
-
1xx - 服务器收到请求。
-
2xx - 请求成功,如 200。
-
3xx - 重定向,如 302。
-
4xx - 客户端错误,如 404。
-
5xx - 服务端错误,如 500。
1.2 常见状态码
-
200 - 成功。
-
301 - 永久重定向(配合 location,浏览器自动处理)。
-
302 - 临时重定向(配合 location,浏览器自动处理)。
-
304 - 资源未被修改。
-
403 - 没权限。
-
404 - 资源未找到。
-
500 - 服务器错误。
-
504 - 网关超时。
1.3 关于协议和规范
-
状态码都是约定出来的。
-
要求大家都跟着执行。
-
不要违反规范,例如 IE 浏览器。
2、http 缓存
-
关于缓存的介绍。
-
http 缓存策略(强制缓存 + 协商缓存)。
-
刷新操作方式,对缓存的影响。
4.1 关于缓存
什么是缓存? 把一些不需要重新获取的内容再重新获取一次
为什么需要缓存? 网络请求相比于 CPU 的计算和页面渲染是非常非常慢的。
哪些资源可以被缓存? 静态资源,比如 js css img。
4.2 强制缓存
Cache-Control:
-
在 Response Headers 中。
-
控制强制缓存的逻辑。
-
例如 Cache-Control: max-age=3153600(单位是秒)
Cache-Control 有哪些值:
-
max-age:缓存最大过期时间。
-
no-cache:可以在客户端存储资源,每次都必须去服务端做新鲜度校验,来决定从服务端获取新的资源(200)还是使用客户端缓存(304)。
-
no-store:永远都不要在客户端存储资源,永远都去原始服务器去获取资源。
4.3 协商缓存(对比缓存)
-
服务端缓存策略。
-
服务端判断客户端资源,是否和服务端资源一样。
-
一致则返回 304,否则返回 200 和最新的资源。
资源标识:
-
在 Response Headers 中,有两种。
-
Last-Modified:资源的最后修改时间。
-
Etag:资源的唯一标识(一个字符串,类似于人类的指纹)。
Last-Modified:
服务端拿到 if-Modified-Since 之后拿这个时间去和服务端资源最后修改时间做比较,如果一致则返回 304 ,不一致(也就是资源已经更新了)就返回 200 和新的资源及新的 Last-Modified。
Etag:
其实 Etag 和 Last-Modified 一样的,只不过 Etag 是服务端对资源按照一定方式(比如 contenthash)计算出来的唯一标识,就像人类指纹一样,传给客户端之后,客户端再传过来时候,服务端会将其与现在的资源计算出来的唯一标识做比较,一致则返回 304,不一致就返回 200 和新的资源及新的 Etag。
两者比较:
-
优先使用 Etag。
-
Last-Modified 只能精确到秒级。
-
如果资源被重复生成,而内容不变,则 Etag 更精确。
4.4 综述
4.4 三种刷新操作对 http 缓存的影响
-
正常操作:地址栏输入 url,跳转链接,前进后退等。
-
手动刷新:f5,点击刷新按钮,右键菜单刷新。
-
强制刷新:ctrl + f5,shift+command+r。
正常操作:强制缓存有效,协商缓存有效。 手动刷新:强制缓存失效,协商缓存有效。 强制刷新:强制缓存失效,协商缓存失效。
3. 面试
对于更多面试中可能出现的问题,我还是建议精读这篇三元的文章:HTTP 灵魂之问,巩固你的 HTTP 知识体系。
比如会被经常问到的: GET 和 POST 的区别。
-
从缓存的角度,GET 请求会被浏览器主动缓存下来,留下历史记录,而 POST 默认不会。
-
从编码的角度,GET 只能进行 URL 编码,只能接收 ASCII 字符,而 POST 没有限制。
-
从参数的角度,GET 一般放在 URL 中,因此不安全,POST 放在请求体中,更适合传输敏感信息。
-
从幂等性的角度,GET 是幂等的,而 POST 不是。(幂等表示执行相同的操作,结果也是相同的)
-
从 TCP 的角度,GET 请求会把请求报文一次性发出去,而 POST 会分为两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue), 然后发 body 部分。(火狐浏览器除外,它的 POST 请求只发一个 TCP 包)
HTTP/2 有哪些改进?(很大可能问原理)
-
头部压缩。
-
多路复用。
-
服务器推送。
关于 HTTPS 的一些原理,可以阅读这篇文章:这一次,彻底理解 https 原理。接着你可以观看这个视频进行更进一步的学习:HTTPS 底层原理,面试官直接下跪,唱征服!
关于跨域问题,大部分文章都是理论性比较强,还不如读这篇文章,聊聊跨域的原理与解决方法,讲的非常清晰,我个人觉得对付面试就是先知道使用流程,把这个流程能自己说出来,然后再讲下原理即可。
大厂面试题分享 面试题库
前端面试题库 (面试必备) 推荐:★★★★★
地址:前端面试题库
六、React
1、 React 事件机制,React 16 和 React 17 事件机制的不同
阅读这篇文章即可:一文吃透 react 事件系统原理。
为什么要自定义事件机制?
-
抹平浏览器差异,实现更好的跨平台。
-
避免垃圾回收,React 引入事件池,在事件池中获取或释放事件对象,避免频繁地去创建和销毁。
-
方便事件统一管理和事务机制。
2、class component
不排除现在还会有面试官问关于 class component 的问题。
2.1 生命周期
- 初始化阶段。
发生在 constructor 中的内容,在 constructor 中进行 state 、props 的初始化,在这个阶段修改 state,不会执行更新阶段的生命周期,可以直接对 state 赋值。
- 挂载阶段。
1. componentWillMount
发生在 render 函数之前,还没有挂载 Dom2. 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.staticgetDerivedStateFromProps(nextProps,nextState)
在组件实例化、接收到新的 props 、组件状态更新时会被调用
2. getSnapshotBeforeUpdate(prevProps,prevState)
在这个阶段我们可以拿到上一个状态 Dom 元素的坐标、大小的等相关信息。用于替代旧的生命周期中的 componentWillUpdate。
该函数的返回值将会作为 componentDidUpdate 的第三个参数出现。
复制代码
需要注意的是,一般都会问为什么要废弃三个生命周期,原因是什么。
2.2 setState 同步还是异步
setState 本身代码的执行肯定是同步的,这里的异步是指是多个 state 会合成到一起进行批量更新。 同步还是异步取决于它被调用的环境。
-
如果 setState 在 React 能够控制的范围被调用,它就是异步的。比如合成事件处理函数,生命周期函数, 此时会进行批量更新,也就是将状态合并后再进行 DOM 更新。
-
如果 setState 在原生 JavaScript 控制的范围被调用,它就是同步的。比如原生事件处理函数,定时器回调函数,Ajax 回调函数中,此时 setState 被调用后会立即更新 DOM 。
3、对函数式编程的理解
总结一下: 函数式编程有两个核心概念。
-
数据不可变(无副作用): 它要求你所有的数据都是不可变的,这意味着如果你想修改一个对象,那你应该创建一个新的对象用来修改,而不是修改已有的对象。
-
无状态: 主要是强调对于一个函数,不管你何时运行,它都应该像第一次运行一样,给定相同的输入,给出相同的输出,完全不依赖外部状态的变化。
纯函数带来的意义。
-
便于测试和优化:这个意义在实际项目开发中意义非常大,由于纯函数对于相同的输入永远会返回相同的结果,因此我们可以轻松断言函数的执行结果,同时也可以保证函数的优化不会影响其他代码的执行。
-
可缓存性:因为相同的输入总是可以返回相同的输出,因此,我们可以提前缓存函数的执行结果。
-
更少的 Bug:使用纯函数意味着你的函数中不存在指向不明的 this,不存在对全局变量的引用,不存在对参数的修改,这些共享状态往往是绝大多数 bug 的源头。
4、react hooks
现在应该大多数面试官会问 hooks 相关的啦。这里我强烈推荐三篇文章,即使没看过源码,也能比较好地理解一些原理:
4.1 为什么不能在条件语句中写 hook
hook 在每次渲染时的查找是根据一个“全局”的下标对链表进行查找的,如果放在条件语句中使用,有一定几率会造成拿到的状态出现错乱。
4.2 HOC 和 hook 的区别
hoc 能复用逻辑和视图,hook 只能复用逻辑。
4.3 useEffect 和 useLayoutEffect 区别
对于 React 的函数组件来说,其更新过程大致分为以下步骤:
-
因为某个事件 state 发生变化。
-
React 内部更新 state 变量。
-
React 处理更新组件中 return 出来的 DOM 节点(进行一系列 dom diff 、调度等流程)。
-
将更新过后的 DOM 数据绘制到浏览器中。
-
用户看到新的页面。
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 元素时会用到。
6、react hooks 与 class 组件对比
react hooks 与 class 组件对比 函数式组件与类组件有何不同
7、介绍 React dom diff 算法
8、对 React Fiber 的理解
关于这块儿我觉得可以好好阅读下这篇无敌的博客了:Build your own React。 它可以教你一步步实现一个简单的基于 React Fiber 的 React,可以学到很多 React 的设计思想,毕竟为了面试我们可能大多数人是没有时间或能力去阅读源码的了。
然后我们再阅读下其它作者对于 React Fiber 的理解,再转化为我们自己的思考总结,以下是推荐文章: 这可能是最通俗的 React Fiber(时间分片) 打开方式
9、React 性能优化手段
推荐文章:面试题库
-
使用 React.memo 来缓存组件。
-
使用 React.useMemo 缓存大量的计算。
-
避免使用匿名函数。
-
利用 React.lazy 和 React.Suspense 延迟加载不是立即需要的组件。
-
尽量使用 CSS 而不是强制加载和卸载组件。
-
使用 React.Fragment 避免添加额外的 DOM。
七、webpack
热更新原理:Webpack HMR 原理解析
这里要注意,应该还会考 webpack5 和 4 有哪些区别。
八、模块化
前端模块化详解(完整版) (这里面没有讲 umd)
九、性能优化
代码层面:
-
防抖和节流(resize,scroll,input)。
-
减少回流(重排)和重绘。
-
事件委托。
-
css 放 ,js 脚本放 最底部。
-
减少 DOM 操作。
-
按需加载,比如 React 中使用 React.lazy 和 React.Suspense ,通常需要与 webpack 中的 splitChunks 配合。
构建方面:
-
压缩代码文件,在 webpack 中使用 terser-webpack-plugin 压缩 Javascript 代码;使用 css-minimizer-webpack-plugin 压缩 CSS 代码;使用 html-webpack-plugin 压缩 html 代码。
-
开启 gzip 压缩,webpack 中使用 compression-webpack-plugin ,node 作为服务器也要开启,使用 compression。
-
常用的第三方库使用 CDN 服务,在 webpack 中我们要配置 externals,将比如 React, Vue 这种包不打倒最终生成的文件中。而是采用 CDN 服务。
其它:
-
使用 http2。因为解析速度快,头部压缩,多路复用,服务器推送静态资源。
-
使用服务端渲染。
-
图片压缩。
-
使用 http 缓存,比如服务端的响应中添加 Cache-Control / Expires 。
十、常见手写
以下的内容是上面没有提到的手写,比如 new 、Promise.all 这种上面内容中已经提到了如何写。
1、防抖
JavaScript 专题之跟着 underscore 学防抖
functiondebounce(func, wait, immediate) {
let timeout;
returnfunction () {
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);
}
};
}
复制代码
2、节流
JavaScript 专题之跟着 underscore 学节流
// 使用时间戳functionthrottle(func, wait) {
let preTime = 0;
returnfunction () {
let nowTime = +newDate();
let context = this;
let args = arguments;
if (nowTime - preTime > wait) {
func.apply(context, args);
preTime = nowTime;
}
};
}
// 定时器实现functionthrottle(func, wait) {
let timeout;
returnfunction () {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
}
};
}
复制代码
3、快速排序
这里对快排思想不太明白的同学可以看下这个讲解的很清晰的视频:快速排序算法。
functionsortArray(nums) {
quickSort(0, nums.length - 1, nums);
return nums;
}
functionquickSort(start, end, arr) {
if (start < end) {
const mid = sort(start, end, arr);
quickSort(start, mid - 1, arr);
quickSort(mid + 1, end, arr);
}
}
functionsort(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;
}
复制代码
4、instanceof
这个手写一定要懂原型及原型链。
functionmyInstanceof(target, origin) {
if (typeof target !== "object" || target === null) returnfalse;
if (typeof origin !== "function")
thrownewTypeError("origin must be function");
let proto = Object.getPrototypeOf(target); // 相当于 proto = target.__proto__;while (proto) {
if (proto === origin.prototype) returntrue;
proto = Object.getPrototypeOf(proto);
}
returnfalse;
}
复制代码
5、数组扁平化
重点,不要觉得用不到就不管,这道题就是考察你对 js 语法的熟练程度以及手写代码的基本能力。
functionflat(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();
}
复制代码
6、手写 reduce
先不考虑第二个参数初始值:
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;
};
复制代码
7、带并发的异步调度器 Scheduler
JS 实现一个带并发限制的异度调度器 Scheduler,保证同时运行的任务最多有两个。完善下面代码中的 Scheduler 类,使得以下程序能正确输出。
classScheduler {
add(promiseMaker) {}
}
consttimeout = (time) =>
newPromise((resolve) => {
setTimeout(resolve, time);
});
const scheduler = newScheduler();
constaddTask = (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 类就行:
classScheduler {
constructor() {
### 最后
推荐一些系统学习的途径和方法。
![路线图](https://img-blog.csdnimg.cn/img_convert/abffda3a0cfe5b9baa70f132ab55f248.png)
每个Web开发人员必备,很权威很齐全的Web开发文档。作为学习辞典使用,可以查询到每个概念、方法、属性的详细解释,注意使用英文关键字搜索。里面的一些 HTML,CSS,HTTP 技术教程也相当不错。
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0)**
HTML 和 CSS:
![html5知识](https://img-blog.csdnimg.cn/img_convert/2ee0d943c2e65e1bf84360c1c8a40cf6.png)
![css基础知识](https://img-blog.csdnimg.cn/img_convert/f660a2fef40322a6e2c45716d4a97e02.png)
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;
};
复制代码
7、带并发的异步调度器 Scheduler
JS 实现一个带并发限制的异度调度器 Scheduler,保证同时运行的任务最多有两个。完善下面代码中的 Scheduler 类,使得以下程序能正确输出。
classScheduler {
add(promiseMaker) {}
}
consttimeout = (time) =>
newPromise((resolve) => {
setTimeout(resolve, time);
});
const scheduler = newScheduler();
constaddTask = (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 类就行:
classScheduler {
constructor() {
### 最后
推荐一些系统学习的途径和方法。
![路线图](https://img-blog.csdnimg.cn/img_convert/abffda3a0cfe5b9baa70f132ab55f248.png)
每个Web开发人员必备,很权威很齐全的Web开发文档。作为学习辞典使用,可以查询到每个概念、方法、属性的详细解释,注意使用英文关键字搜索。里面的一些 HTML,CSS,HTTP 技术教程也相当不错。
**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/forums/4304bb5a486d4c3ab8389e65ecb71ac0)**
HTML 和 CSS:
![html5知识](https://img-blog.csdnimg.cn/img_convert/2ee0d943c2e65e1bf84360c1c8a40cf6.png)
![css基础知识](https://img-blog.csdnimg.cn/img_convert/f660a2fef40322a6e2c45716d4a97e02.png)