哔哩哔哩面经总结
b站
HTML/CSS
BFC
- 触发BFC
- 浮动float
- 非静态定位position为absolute或fixed
- display为inline-block、table-cell、table-caption、flow-root、flex 或 inline-flex 元素的直接子元素、grid 或 inline-grid 元素的直接子元素
- overflow不为visible
- contain 值为 layout、content 或 paint 的元素
- BFC特性
- 内部box会在垂直方向,一个接一个地放置。
- Box垂直方向的距离由margin决定,在一个BFC中,两个相邻的块级盒子的垂直外边距会产生折叠。
- 在BFC中,每一个盒子的左外边缘(margin-left)会触碰到容器的左边缘(border-left)(对于从右到左的格式来说,则触碰到右边缘)
- 形成了BFC的区域不会与float box重叠
- 计算BFC高度时,浮动元素也参与计算
- 应用
- 解决浮动高度塌陷
- 实现左图右文
重绘重排
- 定义
- 重排:DOM 的变化影响到了预算内宿的几何属性比如宽高,浏览器重新计算元素的几何属性, 其他元素的几何属性也会受到影响,浏览器需要重新构造渲染树,这个过程称之为重排,
- 重绘:浏览器将受到影响的部分重新绘制在屏幕上 的过程称为重绘。
- 原因
- 添加或者删除可见的 DOM 元素
- 元素尺寸位置的改变 浏览器页面初始化,
- 浏览器窗口大小发生改变,重排一定导致重绘,重绘不一定导致重排,
- 优化方法
- 不在布局信息改变时做 DOM 查询
- 减少发生操作(合并多次对
DOM
和样式的修改、或者将样式事先设计好,动态去改变class
) - 脱离文档流(绝对定位、)
- 避免多层内联样式
- 平滑度换取速度、避免TABLE布局、CSS3硬件加速、调试
工具
git merge,git rebase
Merge会自动根据两个分支的共同祖先和两个分支的最新提交 进行一个三方合并,然后将合并中修改的内容生成一个新的 commit,即merge合并两个分支并生成一个新的提交,并且仍然后保存原来分支的commit记录
Rebase会从两个分支的共同祖先开始提取当前分支上的修改,然后将当前分支上的所有修改合并到目标分支的最新提交后面,如果提取的修改有多个,那git将依次应用到最新的提交后面。Rebase后只剩下一个分支的commit记录
JS
ES6新特性
let和const关键字、解构赋值、箭头函数、模块导入导出、Promise、symbol、Map和Set类型、class类
- Symbol (符号)是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险
深拷贝与浅拷贝
JavaScript
中存在两大数据类型:
- 基本类型
- 引用类型
基本类型数据保存在在栈内存中
引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中
浅拷贝
在JavaScript
中,存在浅拷贝的现象有:
Object.assign
Array.prototype.slice()
,Array.prototype.concat()
- 使用拓展运算符实现的复制
深拷贝
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式有:
- _.cloneDeep()
- jQuery.extend()
- JSON.stringify()
- 但是这种方式存在弊端,会忽略
undefined
、symbol
和函数
- 但是这种方式存在弊端,会忽略
- 手写循环递归
- 如何解决循环引用的问题
- 使用一个哈希表来跟踪已经拷贝的对象:在深拷贝过程中,维护一个哈希表,用于存储已经拷贝过的对象和对应的拷贝。在拷贝对象时,首先检查哈希表是否已经存在该对象的拷贝,如果存在,直接返回拷贝的引用而不再进行递归拷贝。
- 使用变量跟踪已经拷贝的对象:在深拷贝过程中,可以使用一个变量来跟踪已经拷贝的对象。在拷贝对象时,将该对象存储在变量中,并在递归拷贝其他属性时进行判断,遇到已经拷贝的对象则直接使用之前拷贝的引用。
- 如何解决循环引用的问题
function deepClone(obj,hash = new WeakMap()){
if(obj === null) return obj // 如果是null或者undefined 不进行操作
if(obj instanceof Date) return new Date(obj)
if(obj instanceof RegExp) return new RegExp(obj)
// 可能是对象或者普通的值 如果是函数的话不需要深拷贝
if(typeof obj !== "object") return obj
// 是对象的话就要进行深拷贝
// 如果是已经拷贝过的对象,则返回拷贝的引用
if(hash.get(obj)) return hash.get(obj)
let cloneObj = new obj.constructor()
// 找到的是所属类院校上的constructor,而原型上的constructor指向的是当前类本身
hash.set(obj,cloneObj)
for(let key in obj){
if(obj.hasOwnProperty(key)){
// 实现一个递归深拷贝
cloneObj[key] = deepClone(obj[key],hash)
}
}
}
原型原型链
原型
JavaScript
常被描述为一种基于原型的语言——每个对象拥有一个原型对象
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾
准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype
属性上,而非实例对象本身
原型链
原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法
在对象实例和它的构造器之间建立一个链接(它是__proto__
属性,是从构造函数的prototype
属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法
示例
- 现在有一个构造函数
Person
存在原型对象Person.prototype
- 构造函数生成实例对象
p1
,p1
的__proto__
就会指向构造函数Person
的原型对象 Person.prototype.__proto__
指向内置对象,因为Person.prototype
是个对象,默认是由Object
函数作为类创建的,而Object.prototype
为内置对象Function.prototype
和Function.__proto__
同时指向内置匿名函数anonymous
,这样原型链的终点就是null
支持异步的循环方法
哪些循环方法能支持异步 哪种循环支持等待异步结果返回
- for...of
- promise.all + map
- 传统for循环
- reduce
- generator
事件冒泡,事件捕获 事件代理
- 事件冒泡就是:当某元素执行某一种类型事件,那么从该元素起逐级向外层的元素检测是否存在与本身同样的事件。这一个过程,就叫做事件冒泡
- 事件捕获:事件捕获刚好就和事件冒泡反过来。当元素被触发事件时候,从该元素的根节点开始逐级向里寻找同类型事件。这个过程,就被称为事件捕获
- 顺序:先进行事件捕获 => 再到目标本身 => 最后再进行事件冒泡
- 事件代理/事件委托/event delegation: 事件代理就是利用事件冒泡或事件捕获的机制把一系列的内层元素事件绑定到外层元素。(这也就意味着子元素本身将不用注册自己的事件)
- 作用:事件代理的作用就是:避免大量事件注册
- 如何阻止:event.stopPropagation( )
事件循环
首先,JavaScript
是一门单线程的语言,意味着同一时间内只能做一件事,但是这并不意味着单线程就是阻塞,而实现单线程非阻塞的方法就是事件循环
在JavaScript
中,所有的任务都可以分为
- 同步任务:立即执行的任务,同步任务一般会直接进入到主线程中执行
- 异步任务:异步执行的任务,比如
ajax
网络请求,setTimeout
定时函数等
异步任务还可以细分为微任务与宏任务
在此之前,我们先了解下宏任务和微任务。
- 宏任务
- 事件触发的回调函数,例如
DOM Events
、I/O
、requestAnimationFrame
setTimeout
、setInterval
的回调函数- 一段新程序或子程序被直接执行时(比如从一个控制台,或在一个
<script>
元素中运行代码)。
- 事件触发的回调函数,例如
- 微任务
- promises:
Promise.then
、Promise.catch
、Promise.finally
- MutationObserver:提供了监视对 DOM 树所做更改的能力。
- queueMicrotask:将回调加入微任务队列。
- process.nextTick:Node独有
- promises:
TS
TS泛型
泛型允许我们在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型 在typescript
中,定义函数,接口或者类的时候,不预先定义好具体的类型,而在使用的时候在指定类型的一种特性
<T>
TS12种基本类型
工具类型 | 描述 | 发布版本 |
---|---|---|
Awaited<Type> | 获取 Promise 中的结果类型 | 4.5 |
Partial<Type> | 将Type中的所有属性设置为可选属性,返回一个新的类型。 | 2.1 |
Required<Type> | 将 Type 中的所有属性设置为必选属性,返回一个新的类型。 | 2.8 |
Readonly<Type> | 将Type中的所有属性设置为只读属性,返回一个新的类型。 | 2.1 |
Record<Keys, Type> | 新建一个由Keys指定的属性和Type指定的值组成的对象类型。 | 2.1 |
Pick<Type, Keys> | 从 Type 中选择一个或多个属性,并返回一个新的类型。 | 2.1 |
Omit<Type, Keys> | 从 Type 中删除一个或多个属性,并返回一个新的类型。 | 3.5 |
Exclude<UnionType, ExcludedMembers> | 从UnionType中排除ExcludedMembers 中的所有类型,并返回一个新的类型。 | 2.8 |
Extract<UnionType, ExtractedMembers> | 从 UnionType 中提取 ExtractedMembers 中的所有类型,并返回一个新的类型。 | 2.8 |
NonNullable<Type> | 从Type 中排除 null 和 undefined 类型,并返回一个新的类型。 | 2.8 |
Parameters<Type> | 获取函数类型 Type 的参数类型,以元组类型返回。 | 3.1 |
ReturnType<Type> | 获取函数类型 Type 的返回值类型。 | 2.8 |
Vue
vue2、vue3的区别
更好的ts支持
Vue2 响应式原理基础是 Object.defineProperty;Vue3 响应式原理基础是 Proxy。
- object.defineProperty来劫持数据的setter和getter方法,对象改变需要借助api去深度监听
- Proxy来实现数据劫持,删除了一些api($on,$once,$off) fiter等,优化了Block tree,solt,diff 算法)(变化的主要原因:无法监听对象或数组新增、删除的元素。)
vue3支持在template中写多个根
生命周期方面(名字变化多了一个on)
异步组件(Suspense)
- Vue3 提供 Suspense 组件,允许程序在等待异步组件加载完成前渲染兜底的内容,如 loading ,使用户的体验更平滑。使用它,需在模板中声明,并包括两个命名插槽:default 和 fallback。Suspense 确保加载完异步内容时显示默认插槽,并将 fallback 插槽用作加载状态。
Teleport(移出app之外渲染,如dialog)
diff算法优化:patchFlag 帮助 diff 时区分静态节点,以及不同类型的动态节点。一定程度地减少节点本身及其属性的比对。
打包优化:Tree-shaking:模块打包 webpack、rollup 等中的概念
vue23 v-if v-for 区别,优先级
- 在vue2中,
v-for
的优先级比v-if
更高。 - 在vue3中,
v-if
具有比v-for
更高的优先级。
高频代码题
防抖节流
- 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
- 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时
实现
节流实现
- 时间戳:使用时间戳写法,事件会立即执行,停止触发后没有办法再次执行
function throttled(fn,delay=500){
let oldtime = Dete.now()
return function(...args){
let newtime = Date.now()
if(newtime - oldtime >= delay){
fn.apply(null,args)
oldtime = Date.now()
}
}
}
- 定时器写法:使用定时器写法,
delay
毫秒后第一次执行,第二次事件停止触发后依然会再一次执行
function throttled2(fn,delay = 500){
let timer = null
return function(...args){
if(!timer){
timer = setTimeout(() => {
fn.apply(this,args)
timer = null
},delay);
}
}
}
防抖实现
立即防抖和非立即防抖
function debounce(func,wait,immediate){
let timeout;
return function(){
let context = this
let args = arguments
if(timeout) clearTimeout(timeout) // timeout 不为null
if(immediate){
// 立即防抖
let callNow = !timeout // 第一次会立即执行,以后只有事件执行后会触发
timeout =setTimerout(function(){
timeout = null
},wait)
if(callNow){
func.apply(context,args)
}
}else {
// 非立即防抖
timeout = setTimeout(function(){
func.apply(context,args)
},wait)
}
}
}
手写call、apply、bind
总结
- 三者都可以改变函数的
this
对象指向 - 三者第一个参数都是
this
要指向的对象,如果如果没有这个参数或参数为undefined
或null
,则默认指向全局window
- 三者都可以传参,但是
apply
是数组,而call
是参数列表,且apply
和call
是一次性传入参数,而bind
可以分为多次传入 bind
是返回绑定this之后的函数,apply
、call
则是立即执行
call
定义:call
方法的第一个参数也是this
的指向,后面传入的是一个参数列表
跟apply
一样,改变this
指向后原函数会立即执行,且此方法只是临时改变this
指向一次
Function.prototype.call1 = function(){
// 初始化
const [context,...args] = [...arguments]
// 在传入的对象上设置属性为待执行函数
context.fn = this
// 执行函数
const res= context.fn(args)
// 删除属性
delete context.fn
// 返回执行结果
return res
}
apply
定义: 调用一个具有给定 this 值的函数,及以一个数组的形式提供的参数。
改变this
指向后原函数会立即执行,且此方法只是临时改变this
指向一次
Function.prototype.apply1 = function (context, args) {
// 给传入的对象添加属性,值为当前函数
context.fn = this;
// 判断第二个参数是否存在,不存在直接执行,否则拼接参数执行
let res = !args ? context.fn() : context.fn(...args)
// 删除新增属性
delete context.fn
return res
}
bind
定义:创建一个新的函数,在 bind 被调用时,这个新函数的 this 被指定为 bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
Function.prototype.bind1 = function (context) {
// 将当前函数的this存放起来
var _self = this
// 绑定bind传入的参数,从第二个开始
var args = Array.prototype.slice.call(arguments,1);
// 声明一个空的构造函数
function fNOP(){}
var fBound = function(){
// 绑定bind返回新的函数,执行所带的参数
const bindArgs = Array.prototype.slice.apply(arguments)
// 合并数组
args.push.apply(args.bindArgs)
// 普通函数 this指向window
// 作为构造函数,this指向实例
return _self.apply(this instanceof fNOP ? this : context,args)
}
if(this.prototype){
// 修改返回函数的prototype为绑定函数的prototype,实例就可以继承绑定函数的原型中的值
fNOP.prototype = this.prototype
}
fBound.prototype = new fNOP();
return fBound;
}
并发请求
/**
* 并发请求
* @param {string[]} urls 待请求的url数组
* @param {number} maxNum 最大并发数
*/
function concurRequest(urls,maxNum){
return new Promise((resolve) =>{
if(urls.length === 0){
resolve([])
return
}
const result = []
let index = 0 // 下一个请求的下表
let count = 0 // 当前请求完成的数量
async function request(){
if(index === urls.length){
return;
}
const i = index
const url = urls[index];
index++
console.log(url);
try{
const resp = await fetch(url)
// resp加入到results
results[i] = resp
}catch(err){
// err 加如刀results
results[i] = err
}finally{
count++
if(count === urls.length){
console.log(over)
resolve(results)
}
}
}
})
}
settimeout定时的时间准确吗? 为什么不准确? 怎么解决?
- JS 是单线程执行任务,如果当前任务执行时间过久会导致定时器设置的任务被延迟执行。
- 如果
setTimeout
存在嵌套调用且超过 5 次,那么系统会设置最短时间间隔为4ms
。 - 未激活的页面,
setTimeout
的最小执行间隔是1000ms
。 setTimeout
的延迟执行时间有最大值2147483647ms
。
setTimeout系统时间补偿
function timer(){
var speed = 500,
counter = 1,
start = new Date().getTime();
function instance(){
var real = (counter * speed),
ideal = (new Date().getTime() - start);
counter++
var diff = (ideal - real);
form.diff.value = diff
window.setTimeout(function() {
instance();
},(speed - diff))// 通过系统时间进行修复
}
window.setTimeout(function(){
instance();
},speed)
}
手写Promise.all
特点:
- Promise.all() 方法接收一个promise的iterable类型(MDN)
- 只返回一个promise实例
- 当传入的参数promise全部成功时,最后的结果才会成功(成功的结果是所有的promise的成功的结果组成的数组),只要有一个promise失败,all返回的实例就是一个失败的promise(失败的结果是传入的参数中的第一个失败的promise的结果)
let p1 = new Promise(resolve => {
setTimeout(resolve, 200, 1)
});
let p2 = new Promise((resolve, reject) => reject(2));
let p3 = 3;
console.log(Promise.all([p1, p2, p3]));//all方法
let myAll = function(parr) {
let result = [],//最后成功的结果
count = 0,//累加器,与len比较判断是否全部成功了
len = parr.length;
return new Promise((resolve, reject) => {
for (let p of parr) {// 依次测试传入的参数(转化为promise)是否是成功的
Promise.resolve(p).then(res => {
result[count] = res;// 成功就加入到结果中
count++;// 累加器加一
if (count == len) {// 如果相等,说明都成功了,可以走成功resolve
resolve(res);
}
}, err => {
// 只要有一个失败了,直接走失败reject
reject(err);
})
}
})
}
console.log(myAll([p1, p2, p3]));
计算机基础
HTTP1.0 1.1 2.0
HTTP1.0:
- 浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接
HTTP1.1:
- 引入了持久连接,即TCP连接默认不关闭,可以被多个请求复用
- 在同一个TCP连接里面,客户端可以同时发送多个请求
- 虽然允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的,服务器只有处理完一个请求,才会接着处理下一个请求。如果前面的处理特别慢,后面就会有许多请求排队等着
- 新增了一些请求方法
- 新增了一些请求头和响应头
HTTP2.0:
- 采用二进制格式而非文本格式
- 完全多路复用,而非有序并阻塞的、只需一个连接即可实现并行
- 使用报头压缩,降低开销
- 服务器推送
TCP如何实现可靠
三次握手和四次挥手
流量控制
拥塞控制
序列号
超时重传
停止等待协议
参考
https://www.nowcoder.com/questionTerminal/bc8d4f4deea240d18b46459c6b7f6aff
https://juejin.cn/post/7234341843580174393
https://juejin.cn/post/6982081539249012766