京科八股文
推荐
链接: https://pan.baidu.com/s/1hQgXgjopiORp-kg_xXEwqw 提取码: 69yt 问题可以借鉴但是答案太乱
redux收集
Redux是什么?
redux
是集中管理状态的容器,遵循三大基本原则:
单一数据源
state是只读的
使用纯函数进行修改
工作原理?
首先,react 组件从 store 中获取原始的数据,然后渲染。当 react 中的数据发生改变时,react 就需要使用 action,让 action 携带新的数据值派发给 store,store 把 action 发给 reducer 函数,reducer 函数处理新的数据然后返回给 store,最后 react 组件拿到更新后的数据渲染页面,达到页面更新的目的。
为什么使用?
当应用变得越来越复杂的时候,各级组件相互传递数据,不借助redux,项目将难以维护,redux将这些数据维护在store里,store通过react-redux中的Provider组件可以传递到Provider组件下的所有组件,store也就是全局状态仓库。
web 存储
1、cookie
本身用于浏览器和 server 通讯。
被“借用”到本地存储来的。
可用 document.cookie = ‘…’ 来修改。
口诀:借用本地通讯
其缺点:
- 存储大小限制为 4KB。(大小)
- http 请求时需要发送到服务端,增加请求数量。(携带)
- 只能用 document.cookie = ‘…’ 来修改,太过简陋。(方便)
口诀:大小携带方便
2、localStorage 和 sessionStorage
- HTML5 专门为存储来设计的,最大可存 5M。
- API 简单易用, setItem getItem。
- 不会随着 http 请求被发送到服务端。
它们的区别:
- localStorage 数据会永久存储,除非代码删除或手动删除。
- sessionStorage 数据只存在于当前会话,浏览器关闭则清空。
- 一般用 localStorage 会多一些。
闭包
什么是闭包
闭包就是能够读取其他函数内部变量的函数
表现形式
返回一个函数 :函数里面返回的函数
var a = 1;
function foo(){
var a = 2;
// 这就是闭包
return function(){
console.log(a);
}
}
var bar = foo();
// 输出2,而不是1
bar();
这里可以联想到防抖与节流的应用案例。
作为一个函数参数传递 :无论通过何种手段将内部函数传递到它所在词法作用域之外,它都会持有对原始作用域的引用,无论在何处执行这个函数,都会产生闭包。
var a = 1;
function foo(){
var a = 2;
function baz(){
console.log(a);
}
bar(baz);
}
function bar(fn){
// 这就是闭包
fn();
}
// 输出2,而不是1
foo();
回调函数:在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
// 定时器
setTimeout(function timeHandler(){
console.log('timer');
},100)
// 事件监听
$('#container').click(function(){
console.log('DOM Listener');
})
IIFE:IIFE(立即执行函数表达式)并不是一个典型的闭包,但它确实创建了一个闭包。
var a = 2;
(function IIFE(){
// 输出2
console.log(a);
})();
使用场景(表现形式另一版)
- 创建私有变量
- 延长变量的生命周期
由于对象内的变量一直被引用,所以对象不会被垃圾回收机制进行回收,可以始终保持在内存中。
案例1
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
柯里化函数
柯里化的目的在于避免频繁调用相同参数函数的同时又能轻松的复用
// 假设我们有一个求长方形面积的函数
function getArea(width, height) {
return width * height
}
// 如果我们碰到的长方形的宽老是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)
// 我们可以使用闭包柯里化这个计算面积的函数
function getArea(width) {
return height => {
return width * height
}
}
const getTenWidthArea = getArea(10)
// 之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)
// 而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)
使用闭包模拟私有方法
var Counter = function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
};
var Counter1 = Counter();
var Counter2 = Counter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
上述通过使用闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也叫模块方式
两个计数器 Counter1
和 Counter2
是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量
其他
例如计数器、延迟调用、回调等闭包的应用,其核心思想还是创建私有变量和延长变量的生命周期
使用注意
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
经典问题
以下代码运行结果是什么,如何改进?
for(var i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i)
}, i*1000)
}
for
循环创建了5个定时器,并且定时器是在循环结束后才开始执行for
循环结束后,用var i
定义的变量i
此时等于6依次执行五个定时器,都打印变量
i
,所以结果是打印5次6
第一种改进方法:利用IIFE(立即执行函数表达式)
当每次for
循环时,把此时的i
变量传递到定时器中
for(var i=1;i<=5;i++){
(function(j){
setTimeout(function timer(){
console.log(j)
}, i*1000)
})(i)
}
第二种方法:setTimeout
函数的第三个参数,可以作为定时器执行时的变量进行使用
for(var i=1;i<=5;i++){
setTimeout(function timer(j){
console.log(j)
}, i*1000, i)
}
**第三种方法(推荐)**:在循环中使用let i
代替var i
for(let i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i)
}, i*1000)
}
EventLoop事件循环
进程和线程
进程: CPU在运行指令及加载和保存上下文所需的时间,放在应用上一个程序就是一个进程,一个浏览器tab选项卡就是一个进程
线程: 线程是进程中更小的单位,描述了执行一段指令所需的时间。
JavaScript
是单线程执行的,在JavaScript
运行期间,有可能会阻塞UI渲染,这在一方面说明JavaScript
引擎线程和UI渲染线程是互斥的。JavaScript
被设计成单线程的原因在于,JavaScript
可以修改DOM,如果在JavaScript
工作期间,UI还在渲染的话,则可能不会正确渲染DOM。单线程也有一些好处,如下:
- 节省内存空间
- 节省上下文切换时间
- 没有锁的问题存在
执行栈
可以把执行栈看成是一个存储函数调用的栈结构,遵循先进后出的原则,一个执行栈可能表现如下:

EventLoop
js主线程不断的循环往复的从任务队列中读取任务,执行任务,这种运行机制就是事件循环更详细的事件循环算法:
从 宏任务 队列(例如 “script”)中出队(dequeue)并执行最早的任务。
执行所有微任务,当微任务队列非空时,当微任务队列非空时。
如果有变更,则将变更渲染出来。
如果宏任务队列为空,则休眠直到出现宏任务。
转到步骤 1。
函数会在执行栈中执行,那么当遇到异步代码后,该如何处理呢?其实当遇到异步代码的时候,会被挂起在Task队列中,一旦执行栈为空,就会从Task中拿出需要执行的代码执行,所以本质上讲JS中的异步还是同步行为。

如上图,可以看到,不同的异步任务是有区别的,异步任务又可以划分如下:
- 宏任务(
script
、setTimeout
、setInterval
、setImmidiate
、I/O
、UI Rendering
)可以有多个队列 - 微任务(
procress.nextTick
、Promise.then
、Object.observe
、mutataionObserver
)只能有一个队列
执行顺序:当执行栈执行完毕后,会首先执行微任务队列,当微任务队列执行完毕再从宏任务中读取并执行,当再次遇到微任务时,放入微任务队列
setTimeout(() => {
console.log(1);
Promise.resolve().then(() => {
console.log(2);
})
}, 0)
setTimeout(() => {
console.log(3);
}, 0)
Promise.resolve().then(() => {
console.log(4);
})
console.log(5);
// 输出结果:5 4 1 2 3
代码分析:
console.log(5)
是唯一的同步任务,首先执行,输出5- 将所有异步任务放在Task队列中,挂起
- 同步任务执行完毕,开始执行微任务队列,即
Promise.then
,输出4 - 微任务队列执行完毕,执行宏任务队列
setTimeout
- 宏任务队列中首先执行同步任务,再次遇到微任务,放入微任务队列中,输出1
- 同步任务执行完毕,执行微任务队列,输出2
- 微任务队列执行完毕,执行宏任务队列
setTimeout
,输出3
参见:
技巧语法篇
判断类型的方式
// 怎么判断数组
const suo=[];
console.log(Array.isArray(suo)) // true
- typeof
判断基本数据类型
特例typeof null
,返回的是‘ object ’ - Object.prototype.toString.call(xx)
判断基本数据类型
若参数(xx)不为 null 或 undefined,则将参数转为对象,再作判断
转为对象后,取得该对象的 [Symbol.toStringTag] 属性值(可能会遍历原型链)作为 tag,然后返回 “[object “ + tag + “]” 形式的字符串。 - instanceof
- constructor
具体参见https://juejin.cn/post/7202904269535887418#heading-10
React篇
React的key与diff 虚比较规则
么是key?
说到key属性,就关联到了 Diff算法上了,key属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染。
因此key的值需要为每一个元素赋予一个确定的标识。
良好使用key属性是性能优化的非常关键的一步,注意事项为:
- key 应该是唯一的
- key不要使用随机值(随机数在下一次 render 时,会重新生成一个数字)
- 使用 index 作为 key值,对性能没有优化
diff 虚拟DOM 比较的规则
旧虚拟DOM与新虚拟DOM相同key。若虚拟DOM的内容没有发生变化,直接使用旧的虚拟DOM;若虚拟Dom发生改变了,则生成新真实的DOM,随后替换页面中的之前真实的DOM。
旧虚拟DOM中未找到新虚拟DOM相同的key,根据数据创建真实的DOM,随后渲染到页面。
简述React事件机制
简述
就是在我们写代码时,看似onclick绑定到DOM元素上,实际上并不会把事件代理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的外层上,使用一个统一的事件去监听。
这个事件监听器维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上进行插入和删除一些对象。
当事件发生时,首先被统一事件监听器处理,然后在映射里找到真正的事件处理函数并调用。
这样做简化了事件处理和回收机制,效率也有很大提升。
执行顺序
React 所有事件都挂载在 id=root DOM元素上
当真实 DOM 元素触发事件,会冒泡到 id=root DOM元素,再处理 React 事件
所以会先执行原生事件,然后处理 React 事件
最后真正执行 document 上挂载的事件
总结(也可做简述回答)
- React 上注册的事件最终会绑定在document这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件)
- React 自身实现了一套事件冒泡机制,所以这也就是为什么我们 event.stopPropagation()无效的原因。
- React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
- React 有一套自己的合成事件 SyntheticEvent
安全与性能优化篇
网络劫持有哪几种,如何防范?
⽹络劫持分为两种:
(1)DNS劫持: (输⼊京东被强制跳转到淘宝这就属于dns劫持)
- DNS强制解析: 通过修改运营商的本地DNS记录,来引导⽤户流量到缓存服务器
- 302跳转的⽅式: 通过监控⽹络出⼝的流量,分析判断哪些内容是可以进⾏劫持处理的,再对劫持的内存发起302跳转的回复,引导⽤户获取内容
(2)HTTP劫持: (访问⾕歌但是⼀直有贪玩蓝⽉的⼴告),由于http明⽂传输,运营商会修改你的http响应内容(即加⼴告)
DNS劫持由于涉嫌违法,已经被监管起来,现在很少会有DNS劫持,⽽http劫持依然⾮常盛⾏,最有效的办法就是全站HTTPS,将HTTP加密,这使得运营商⽆法获取明⽂,就⽆法劫持你的响应内容。
三次握手与四次挥手
第一次握手是客户端给服务端发送请求,请求建立连接。
第二次握手是服务端给客户端发送请求,表示已经收到客户端的请求,并且同意建立连接。
第三次握手是客户端向服务端发送请求,表示确认收到服务端发送的信息。
次握手的原因是为了确认客户端和服务端都有收发信息的能力,少一次确认不了,多一次浪费资源。
第一次挥手:先由客户端向服务器端发送一个 FIN,请求关闭数据传输。
第二次挥手:当服务器接收到客户端的 FIN 时,向客户端发送一个 ACK,其中 ACK的值等于 FIN+SEQ
第三次挥手:然后服务器向客户端发送一个 FIN,告诉客户端应用程序关闭。
第四次挥手:当客户端收到服务器端的 FIN 是,回复一个 ACK 给服务器端。其中 ACK 的值等于 FIN+SEQ
seq(Sequence Number):
32bits
,表示这个tcp
包的序列号。tcp
协议拼凑接收到的数据包时,根据seq
来确定顺序,并且能够确定是否有数据包丢失。
ack(Acknowledgment Number):
32bits
,表示这个包的确认号。首先意味着已经收到对方了多少字节数据,其次告诉对方接下来的包的seq
要从ack
确定的数值继续接力。