作者什么时候能有一个毛孩子啊~~~
BFC(1次)
定义:
MDN:区块格式化上下文(Block Formatting Cofntext,BFC)是 Web 页面的可视 CSS 渲染的一部分,是块级盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。
通俗来讲:BFC是一个独立的布局环境,可以理解为一个容器,在这个容器中按照一定规则进行物品摆放,并且不会影响其它环境中的物品。如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。
解决布局问题: 浮动、外边距重叠
构成BFC方法:
- HTML根元素
- float: left | right(除none)
- position: absolute | fixed
- display: inline-block | flex | inline-flex | grid | inline-grid | table-cell |table-caption | flow-root(专门用来创建BFC属性)
- overflow: 除了visible
- contain: layout | content | paint(现代浏览器支持)
清除浮动
浮动会造成父元素高度塌陷(没有设置height)
方法:BFC、clear: both、伪元素(不影响布局复杂度)
像素单位(3次)
- px:绝对单位
- em:相对单位,相对父元素的字体大小
- rem:相对单位,相对根元素(html)的字体大小
- vw/vh:视口宽高的百分比
CSS选择器以及优先级(2次)
| 选择器 | 格式 | 优先级权重 |
| id | #id | 100 |
| 类 | .class | 010 |
| 伪类 注意区分 | li : last-child | 010 |
| 属性 | a[class="refValue"] | 010 |
| 元素 | div | 001 |
| 伪元素 注意区分 | div :: after | 001 |
| 相邻兄弟 | h1+p | 000 |
| 子 | ul>li | 000 |
| 后代 | li a | 000 |
| * 通配符 | * | 000 |
提示:内联样式的优先级是1000(比id还高) ,!important声明的样式的优先级最高,在样式中更改UI库的样式经常用到 :global, 设置某个节点下的全局样式;
伪类和伪元素的区别(3次,字节一面)
伪类
将特殊效果添加到特定选择器上,在已有元素上添加类别,不产生新元素
a:hover {color: #FF00FF}
p:first-child {color: red}
p:nth-child(odd) {color: pink}
伪元素
在内容元素的前后插入额外元素/样式,插入的元素实际上并不在文档中生成。它们只在外部显示可见(渲染树存在,DOM树不存在),但不会在文档的源代码中找到它们,因此,称为“伪”元素。
p::before {content:"第一章:";}
p::after {content:"Hot!";}
p::first-line {background:red;}
p::first-letter {font-size:30px;}
行内元素、块级元素、行内块元素区别,
| 元素 | 排列方式 | 样式效果 |
| 行内 inline | 同行排列内容超出自动换行(盒子占两行) | 设置宽高无效,垂直margin、padding、border不占空间 |
| 块级 block | 独占一行,前后自动换行 | 设置宽高有效,margin、padding、border均生效并影响布局 |
| 行内块 inline-block | 同行排列内容超出整个盒子换到下一行(注意区分inline) | 同block |
置换元素
置换元素(Replaced Element) :是指其内容由外部资源决定,而非由 HTML 直接描述的元素。
常见置换元素:、、、、、
作者小问答
<div classname="block">块盒子</div>
// 书写样式时,div.block 和 div .block有区别吗?
让我想想...
看着很像,但是既然这么问了,应该有区别吧~
emmm
答案揭晓:
- div.block:div元素中类名为block的元素(“且”关系,筛选范围更小)
- div .block(带空格):div元素下类名为block的后代(后代选择器,匹配范围更大)
H5C3新特性
H5
- 语义化标签:header footer article section main nav
好处: 1)便于搜索引擎爬虫准确抓取页面,提高页面在搜索结果中的排名。2)增加代码易读性,便于代码维护和更新
- 多媒体:video audio
- canvas绘图(画布)
- 本地存储:localStorage sessionStorage cookie
扩展: 三者区别
- 地理位置的API
- 拖放
CSS3
- 弹性布局: display: flex 重要
- 网格布局: display: grid
- 渐变、阴影、过渡、动画
- 媒体查询:依据设备的屏幕尺寸、分辨率 使用不同的样式规则,实现响应式设计
- 自定义字体:@font-face
如何画一个三角形(基础)
把一个元素的宽高设置为0,然后给不同方向的边框设置颜色和宽度,接着想要哪个方向的三角就留下颜色,其余改为透明
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.traingle {
width: 0;
height: 0;
border: 10px solid transparent;
border-top-color: pink;
}
</style>
</head>
<body>
<!-- 画一个三角形 -->
<div class="traingle"></div>
</body>
</html>
如何画一条0.5px的线
// 1
.border {
width: 1000px;
height: calc(1px/2);
}
// 2
.border {
width: 100px;
height: 1px;
transform: scaleY(0.5);
/* transform: scale(1, 0.5); */
transform-origin: 0 0; /* 防止缩放后偏移 */
}
// 3 用边框+缩放
.border {
width: 100px;
border-top: 1px solid black;
transform: scaleY(0.5);
transform-origin: 0 0; /* 防止缩放后偏移 */
}
自适应布局和响应式布局(1次)
自适应: 随着视口变化,元素进行放大缩小 (淘宝无限适配方案:可以动态调整根元素font-size的值 -> rem)
响应式: 通过媒体查询来设置特定屏幕尺寸的样式规则(750px 1080px 2000px)
<style>
/* 小屏幕设备样式 */
@media (max-width: 767px) {
body {
background-color: lightblue;
font-size: 14px;
}
.container {
padding: 10px;
}
}
/* 大屏幕设备样式 */
@media (min-width: 768px) {
body {
background-color: lightgreen;
font-size: 16px;
}
.container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
}
</style>
如何实现垂直居中对齐(5次,高频,唯品会一面,字节一面)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* 绝对定位 1.*/
/* #parent {
position: relative;
width: 500px;
height: 500px;
background-color: aqua;
}
#center {
position: absolute;
width: 100px;
height: 100px;
line-height: 100px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background-color: brown;
} */
/* 绝对定位 2.*/
/* #parent {
position: relative;
width: 500px;
height: 500px;
background-color: aqua;
}
#center {
position: absolute;
width: 100px;
height: 100px;
line-height: 100px;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
text-align: center;
background-color: brown;
} */
/* flex布局 */
/* #parent {
display: flex;
justify-content: center;
align-items: center;
width: 500px;
height: 500px;
background-color: aqua;
}
#center {
width: 100px;
height: 100px;
line-height: 100px;
text-align: center;
background-color: brown;
} */
/* flex中用margin布局 */
/* #parent {
display: flex;
width: 500px;
height: 500px;
background-color: aqua;
} */
/* #center {
width: 100px;
height: 100px;
margin:auto;
line-height: 100px;
text-align: center;
background-color: brown;
} */
/*grid布局*/
/* #parent {
display: grid;
place-items: center; //place-content: center;也可
width: 500px;
height: 500px;
background-color: aqua;
}
#center {
width: 100px;
height: 100px;
background-color: brown;
}*/
</style>
</head>
<body>
<!-- 实现三栏布局 -->
<div id="parent">
<div id="center">center</div>
</div>
</body>
</html>
效果图如下:
编辑
延伸:说一下flex布局(6次,超高频,必会)
说一下flex怎么布局的,然后说一下常用属性的用法,让面试官觉得你会;
编辑
如何实现三栏布局(笔试3次,面试3次,CSS基础)
flex、绝对定位、浮动、双飞翼(1次)、圣杯(后两者布局还没理解到)
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
/* flex布局 */
/* #parent {
width: 1000px;
height: 100px;
display: flex;
}
#left {
width: 100px;
text-align: center;
background-color: palegoldenrod;
}
#right {
width: 200px;
text-align: center;
background-color: blue;
}
#center {
flex: 1;
text-align: center;
background-color: brown;
} */
/* 绝对定位 */
#parent {
position: relative;
width: 1000px;
height: 100px;
}
#left {
position: absolute;
height: 100px;
width: 100px;
text-align: center;
background-color: palegoldenrod;
}
#right {
position: absolute;
width: 200px;
height: 100px;
top: 0;
right: 0;
text-align: center;
background-color: blue;
}
#center {
/* position: absolute;
left: 100px;
right: 200px;*/
height: 100%;
margin-left: 100px;
margin-right: 200px;
text-align: center;
background-color: brown;
}
/* 浮动:注意中间元素center要在末尾 */
/* #parent {
width: 1000px;
height: 100px;
}
#left {
float: left;
height: 100px;
width: 100px;
text-align: center;
background-color: palegoldenrod;
}
#right {
float: right;
width: 200px;
height: 100px;
text-align: center;
background-color: blue;
}
#center {
height: 100%;
margin-left: 100px;
margin-right: 200px;
text-align: center;
background-color: brown;
} */
</style>
</head>
<body>
<!-- 实现三栏布局 -->
<div id="parent">
<div id="left">left</div>
<div id="center">center</div>
<div id="right">right</div>
</div>
</body>
</html>
效果图都是这样,最好自己动手实操,加深印象~
编辑
盒子模型(4次)
width和height属性的范围:
- box-sizing: content-box,标准盒子模型总宽度:width(content) + padding + border + margin
- box-sizing: border-box,IE(怪异)盒子模型总宽度: margin + width(content + padding + border)
实现隐藏元素的方法(1次)
- opacity: 0 (占位)
- visibility: hidden (占位)
- display: none | hidden(脱离文档流)
- HTML5
hidden属性 (同display: hidden)
面向对象(纪传体)vs面向过程(编年体)
面向过程(ex:C语言):负责完成某个具体任务的代码(可以理解为函数),核心:将要完成的事情拆分成一个个步骤,依次完成
面向对象(Object Oriented Programming ex:JAVA):以对象为核心,考虑各个对象有什么性质、能做什么事情;把事务先分解到对象身上,描述各个对象的作用,然后才是它们之间的交互
提取性质定义类创建对象(类是创建对象的模板,对象是类的实例),用对象绑定相关属性(放在类里面的变量)有利于让程序逻辑更加清晰,数据流动更加清晰
用对象绑定对象能实现的方法(放在类里面的函数)
结合方法和属性,能更优雅地处理逻辑
面向对象特性:
封装:写类的人将内部实现细节隐藏起来,使用类的人只通过外部接口(方法)访问和使用
继承:面向对象编程允许创建有层次的类
多态:同样的接口,因为对象具体类不同而有不同的表现
new操作符做了什么
- 创建了一个空对象
- 将空对象的原型指向构造函数的原型
- 将空对象作为构造函数的上下文(改变this指向)
- 对构造函数有返回值的处理判断
function Fun(age, name){
this.age = age;
this.name = name;
}
function create(fn, ...args){
// 1.创建了一个空对象
let obj = {};
// 2.将空对象的原型指向构造函数的原型
Object.setPrototypeOf(obj, fn.prototype);
// 3.将空对象作为构造函数的上下文(改变this指向)
let res = fn.apply(obj, args)
// 4.对构造函数有返回值的处理判断
return res instanceof Object ? res : obj;
}
console.log(create(Fun, 18, '王五'));
原型和原型链(4次,重点!)
原型:每一个函数都有prototype显式属性,称为原型(原型对象),原型有属性和方法与实例对象共享,可以继承
原型链:对象都有隐式__proto__属性,指向它的原型对象,原型对象也是对象, 也有__proto__属性,指向原型对象的原型对象,这样一层一层形成的链式结构称原型链(最顶层为null),原型链是通过对象间的原型链接形成的属性查找路径
画图加深记忆(理解理解才能吸收!!)
编辑
继承
原型链继承:父类的实例赋值给子类的原型
优:子类可以共享父类的方法
劣:不能给父类传参、父类的引用数据类型可能被共享
<script>
function Person(name) {
this.name = name
}
Person.prototype.say = function () {
console.log(123);
}
function Son(age) {
this.age = age
}
Son.prototype = new Person()
console.log(new Son())
</script>
构造函数继承:在子类构造函数中调用父类构造函数
优:可以给父类传递属性
劣:无法共享父类方法
<script>
function Person(name) {
this.name = name
}
Person.prototype.say = function () {
console.log(123);
}
function Son(age) {
Person.call(this, age)
this.age = age
}
console.log(new Son(18)) // {name:18, age:18}
</script>
组合式继承:结合原型链继承和构造函数继承的优点
劣:复杂度增加、增加了父类对象的创建
<script>
function Person(name) {
this.name = name
}
Person.prototype.say = function () {
console.log(123);
}
function Son(age) {
Person.call(this, age) // 1
this.age = age
}
Son.prototype = new Person() // 2
console.log(new Son(18)) // {name:18, age:18}
</script>
class继承:本质基于原型链和构造函数继承
劣:低版本浏览器不支持
<script>
// 父类
class Person {
constructor(name) {
this.name = name
}
say() {
console.log(123)
}
}
// 子类
class Son extends Person {
constructor(name, age) {
super(name)
this.age = age
}
write() {
console.log(456)
}
}
const son = new Son('fu', 'zi')
console.log(son) // {name: 'fu', age: 'zi'}
son.say() // 123
son.write() // 456
</script>
区分undefined和null(1次)
undefined
- 定义变量未初始化,默认值为undefined
- 函数没有返回值,默认返回undefined
- 调用函数未传值,形参值为undefined
- 访问对象/数组不存在的属性/元素
null--表示"空对象指针"
- 不是默认值,一般需要主动赋值,表示"没有值"或"空",例:定义对象初始化为null
- 对象的原型链顶端是null
ES6新特性(n次,必背必背!!!)
1、Symbol、BigInt数据类型
2、模版字符串:${}
3、新增let、const关键字
var、let、const区别:
- var没有块级作用域(有函数作用域),let、const有(块级作用域 : { } )
- var存在变量提升(变量只能在声明之后使用,否在会编译报错且打印为'undefined'),let(暂时性死区)、const没有 (tips:优先级:函数提升 > 变量提升)
- var可以重复声明,let、const不可以
- var、let不用设置初始值,const必须设置(初始化)
- let创建的变量可以重新赋值,const不可以
- 在全局作用域下var定义的变量挂载到window对象,let不会
4、箭头函数()=>{}
箭头函数和普通函数的区别:
- 箭头函数更简洁
- 箭头函数不绑定this,会捕获其所在上下文的this,作为自己的this。箭头函数中this的指向在它在定义时已经确定了,不会改变。普通函数的this指向调用者(谁调用就指向谁)。
- 箭头函数不能作为构造函数使用(如上因为不能绑定this)
- call()、apply()、bind()等方法不能改变箭头函数中this的指向
- 箭头函数没有prototype,当然就不存在原型;没有自己的arguments对象
- 箭头函数不能用作Generator函数,不能使用yeild关键字(作者自己标红,还不懂呜呜呜呜)
5、扩展运算符
6、class类
7、解构赋值
如何在不添加第三个变量的情况下交换两个变量的值?
let a = 1;
let b = 2;
[a, b] = [b, a]
console.log(a, b) // 2 1
8、新增Map、Set数据结构
Map、Set、json区别:
- JSON 对象在 JavaScript 中以键值对的形式表示,但键必须是字符串,且整个 JSON 对象必须是一个字符串。
- Map存储键值对的集合,键和值都能是任意类型,并且键具有唯一性,可迭代(用于
for...of循环)。 - Set存储唯一的值的集合,可迭代(用于
for...of循环)。
Map和weakMap区别:待续...
Set和weakSet区别:待续...
扩展: 将Set转换为数组方法
- 展开运算符:
const set= new Set([1, 2, 3, 4, 5]);
const arr = [...set];
- Array.from() + Set / Set.prototype.keys() / Set.prototype.values()
const mySet = new Set([1,2,3,4,5]);
const myArr1 = Array.from(mySet);
const myArr2 = Array.from(mySet.keys());
const myArr3 = Array.from(mySet.values()); // keys 和 values相同
console.log(Array.isArray(myArr1, myArr2, myArr3));
- for...of
- Set.prototype.forEach()
const mySet = new Set([1, 2, 3, 4, 5])
const myArr1 = []
const myArr2 = []
// forEach
mySet.forEach((item) => {
myArr1.push(item)
})
for...of
for (let item of mySet) {
myArr2.push(item)
}
console.log(myArr1, myArr2)
9、默认参数
即给函数设置默认参数
function greet(name, message = "Hello") {
console.log(`${message}, ${name}!`);
}
10、Promise:处理异步操作的对象
11、async/await
扩展:promise和async/await区别
async/await基于promise的语法糖(语法糖:简化,语法盐:复杂化)
12、模块导入导出:import export
13、??:空值合并运算符
- 若左侧操作数为
null或者undefined时,返回右侧操作数 - 若左侧操作数不为
null或undefined,则返回左侧操作数
14、?. :可选链操作符
const uer = {
name: 'dili',
address:{
street: 'Main Street'
}
}
console.log(user.address?.street)
// 若address属性存在,则继续访问其street属性并输出值
// 若address属性不存在,则返回undefined避免引用错误
15、replaceAll()(String的api)
let str = 'hello world, hello agin'
console.log(str.replaceAll('h', 'H')) // Hello world, Hello agin
16、顶层await
<script type="module">
// 模拟一个异步操作
function fetchData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve('Data fetched successfully');
}, 1000);
});
}
// 顶层使用 await
const data = await fetchData();
console.log(data);
</script>
JS的组成部分
ECMAScript、DOM(文档对象模型)、BOM(浏览器对象模型)
JavaScript中的原生请求方法(两种)
1. XMLHttpRequest
传统的原生方法,具有良好的兼容性
2.Fetch API
更强大、更灵活的原生请求方法,返回Promise对象,处理异步请求更便捷;可以使用Promise/fetch的API实现对老浏览器的支持(兼容)。
async和defer区别
作用:控制外部脚本的加载和执行顺序
- async:异步加载脚本,脚本文件会并行下载,不会阻塞页面的解析,下载完就直接执行
- defer:延迟加载脚本,脚本文件会并行下载,在文档完全解析之后,按顺序执行
javascript执行过程
预编译: 代码执行前,先进行编译
JS代码执行过程:
- 通篇检查代码是否有语法错误
- 预编译
- 解释一行执行一行
变量声明和函数声明
- var存在变量提升(声明提升,赋值不会提升)
- 函数声明的提升是提升整个函数,包括里面的代码
注意: 如果给一个没有进行声明的变量赋值,那么这个变量默认为全局变量
函数执行期上下文
活跃对象AO: activation object
- 创建AO对象 AO = {}
- 找函数的形参和变量声明,并赋值为undefined
- 把实参赋值给形参
- 找函数声明,并赋值函数体
- 执行代码 在预编译阶段执行过的代码,在函数执行期不再执行
dom的事件模型
事件: 用户在浏览器中操作时自动触发的信号,JavaScript可以使用事件监听器来监听这些"事件",并执行回调函数作为响应
事件触发时,事件流的传播路径是从根节点自上而下地传播至目标节点(event.target),再从目标节点向上传播至根节点。
三个阶段:
事件捕获: 事件从最外层祖先节点(window对象:浏览器环境的全局对象)开始,自上而下传播,直至到达目标节点
目标阶段: 事件到达触发事件的目标元素,在这里事件会被处理
事件冒泡:事件委托利用的阶段,事件从目标元素开始,自下而上传播,直至到达window
扩展: 事件委托通过将事件监听器绑定到父元素上,利用事件冒泡机制处理子元素的事件;这样做可以减少事件监听器的数量,优化性能
遍历数组的方式有哪些?(1次)
map forEach filter reduce some/every find/findIndex flatMap
操作数组常用的方法有哪些?哪些方法会改变原数组?
不改变原数组,方法会返回一个新的数组或某个结果,不会修改原来数组的内容
slice、reduce、filter、map
改变原数组
push、pop、uhshift(+)、shift(-)、splice、sort、reverse
扩展:sort方法(参数可选)就地对数组进行排序,返回的是对相同数组的引用,默认排序是将元素转换为字符串,然后按照它们的 UTF-16 码元值升序排序。
不改变原数组的排序方法用toSorted
tips: 数组方法均是浅拷贝
forEach和map区别
| forEach | map |
| 无返回值,仅读取数组元素 | 返回原数组的映射结果,返回新数组 |
| 不可链式调用 | 可链式调用 |
深浅拷贝(3次)
- 浅拷贝: 只复制对象/数组的一层属性/元素,如果对象/数组的属性/元素是基本数据类型复制其值;但如果属性/元素是引用数据类型,则只会复制其引用,(即新对象/数组和原对象/数组的该属性/元素会指向同一个内存地址)
实现方式: Object.assign()、扩展运算符、Array.slice()
- 深拷贝:递归地复制对象/数组的所有属性,包括嵌套的对象和数组,创建一个完全独立的新对象/数组,新对象/数组和原对象/数组在内存中占用不同的地址,修改新对象/数组不会影响原对象/数组,反之亦然
实现方式:
1、JSON的序列化+反序列化(JSON.parse(JSON.stringfy()))
缺点:
- 不能识别BigInt类型
- 不能拷贝undefined、Date、RegExp、Map、Set、symbol、函数类型的值
- 不能处理循环引用(对象的属性直接或间接地引用了对象本身)
2、手搓递归
<script>
function deepCopy(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj
}
let copy = Array.isArray(obj) ? [] : {}
for (let key in obj) {
copy[key] = deepCopy(obj[key])
}
return copy
}
let obj = { a: 1, b: { c: 2 } }
let copy = deepCopy(obj)
copy.b.c = 4
console.log(obj, copy)
// { a: 1, b: { c: 2 } } { a: 1, b: { c: 4 } }
</script>
3、使用第三方库(lodash中的cloneDeep)
将类数组对象转换为数组
Array.from()(es6+推荐)
展开运算符:[...arguments](es6+推荐)
Array.prototype.slice.call(类数组对象)(es5传统方法)
Array.prototype.concat.call([], 类数组对象)(邪修)
Promise(有空还是去看看源码吧,看看作者的逻辑就懂Promise了,嗨嗨嗨!)
Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,改善了回调地狱
如何捕获Promise中的错误信息(1次)
try/catch,Promise.then(,reject), Promise.catch(),Promise.finally()(无论成功失败都会执行)
Promise实例的三个状态(1次)
- Pending(进行中)
- Fulfilled(已完成)
- Rejected(已拒绝)
两个过程(pending起手):状态凝固
- pending -> fulfilled : Resolved(已完成)
- pending -> rejected:Rejected(已拒绝)
注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
tips: 在回调中执行resolve(),函数中resolve后面的代码依然会继续执行
处理成功、失败
.then() 处理成功的情况 .then(onResolve,onRejected) 接受两个函数,第一个处理成功,第二个处理失败
.catch() 处理失败的情况 本质:.then(null,onRejected)语法糖
***Promise.all()和.race()(1次 美团)
.all() 接受一个数组,返回一个新的promise,只有所有的promise都成功才会成功,如果有一个失败,那么立刻返回失败。
成功: 将所有成功的结果收集到一个数组中,并将这个数组作为最终的 resolve 值
失败: 将失败的 Promise 的 reject 值作为最终的 reject 值
(注意: Promise.all 本身会立即终止并抛出错误(被最近的 .catch() 或 try/catch 捕获),但不会阻止整个程序的执行。后续代码(位于 Promise.all 调用之后)会继续运行,但依赖 Promise.all 结果的代码将无法执行。)
场景题: 我要上传多张图片,但是我要等所有图片上传成功后,再去调提交,应该怎么做?(待更新...)
.race() 接受一个数组,返回一个新的promise,只要有一个promsie完成(成功/失败),就立刻决定整个promsie的结果
***原理
- 初始化promise:创建promise对象,构造函数会立刻调用一个函数,该函数有两个参数resolve(函数,把promise状态从pending -> fullfilled),reject((函数,把promise状态从pending -> rejected))
- 执行异步操作:在执行函数的时候,它一般是个异步任务(发送请求),异步任务的结果决定了promise的状态(任务成功则调用resolve函数,失败则调用reject函数,改变promise状态),一旦执行状态不可变
如何实现Promis功能(要看Promis源码)(1次)
this/call/apply/bind区别(6次,背背背!!!)
这三个方法都显式指定调用函数的 this 指向
- call,第一个参数this绑定的对象,其余参数需要依次列举出来,立即执行
- apply,第一个参数this绑定的对象,第二个传参数数组,立即执行
- bind,第一个参数this绑定的对象,仅定义(创建新函数)未执行
// bind
<script>
function say(arg, msg) {
return `${arg}, ${this.name}, ${msg}`
}
const obj = { name: 'dili' }
const newSay = say.bind(obj, 'Hello')
console.log(newSay('I'm liuyifei'))
</script>
this指向(1次)
全局this --> window
普通函数中this --> 谁调用指向谁(默认window)
对象中this --> 函数作为对象的方法被调用,this指向调用该方法的对象
构造函数this --> 新创建的实例对象
箭头函数this --> 外层普通函数/外层作用域的this
定时器中this -> window(定义在window对象下)
if语句条件判断归纳(1次)
| 值/类型 | if判断结果 |
|---|---|
| false | false |
| 0, -0, NaN | false |
| "", '' | false |
| null | false |
| undefined | false |
| 其他所有值 | true |
注意: 负值, ****{}, []都判定为true, 想判断空对象/数组可用length属性
浏览器事件循环(必要)
JS是单线程的(why?设计为多线程如果同时添加删除同一DOM节点会出问题),为了防止阻塞,将代码分为同步and异步
常见宏任务:script(代码块),setTimeout/setInterval,setImmediate定时器、事件、ajax
常见微任务:Promise.then()/catch(),ASync/Await,Object.observe,process.nextTick(node)
执行顺序:执行栈中的同步代码 -> 微任务队列(直到没有微任务) -> 宏任务队列
编辑
说完概念面试官一般会让你做一道关于时间循环的题(穿插seetTimeout、Promise),我的建议是多看多练,就会对事件循环理解更深刻。
先自己做一下哦~
setTimeout(() => {
console.log('setTimeout');
Promise.resolve().then(() => {
console.log('setPro1');
}).then(() => {
console.log('setPro2');
})
}, 0);
setTimeout(() => {
console.log('set');
}, 0)
Promise.resolve().then(() => {
console.log('then1');
}).then(() => {
console.log('then2');
}).then(() => {
console.log('then3');
})
公布答案啦~
编辑
下面出一个纯Promise的,很绕很绕,直接晕厥
Promise.resolve().then(() => {
console.log(0);
return Promise.resolve(4);
}).then(res => {
console.log(res);
})
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
})
浏览器内核
定义: 通过取得页面内容、整理信息(应用CSS)、计算和组合最终输出可视化的图像结果,又称作渲染引擎
一个浏览器通常由以下常驻线程组成:
GUI渲染线程,主要负责页面的渲染,解析HTML、CSS(两者并行),构建DOM树、CSS规则树,合并成渲染树,布局和绘制...
javascript引擎线程,主要负责处理javascript脚本,执行代码
定时触发器线程,负责执行异步定时器一类的函数的线程,如:setTimeout、setInterval
事件触发线程,主要负责将准备好的事件(定时器结束、异步请求成功触发的回调,点击事件)交给JS引擎执行
异步http请求线程,负责执行异步请求一类的函数的线程,如Promise、fetch、ajax...
tips: GUI渲染线程与javascript引擎互斥(性能问题)
Node的事件循环
主要关注的三个阶段:timers poll check(考点)
编辑
timer阶段: 会执行setTimeout和setInterval回调,并且是由poll控制的(注意:在node.js中定时器指定的事件也不是准确时间,只能是尽快执行)
poll阶段: 至关重要的阶段,系统会做两件事:回到timer阶段执行回调;执行I/O回调
如果该阶段没有设定了timers的话,会发生以下两件事情:
如果poll队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
如果poll为空,会发生两件事:
如果有setImmediate回调需要执行,poll阶段会停止并进入到check阶段执行回调
如果没有setImmediate回调需要执行,会等待回调被加入到队列中并立即执行回调(这里会设置超时时间防止一直等待)
编辑
check阶段:setImmediated()的回调会被加入check队列中,从事件循环的阶段图可以知道,check阶段的执行顺序在poll阶段之后。(注意:当二者在异步I/O callback内部调用时,总是先执行setImmediate,再执行setTimeout)
问: 简述一下Node.js中的事件循环和浏览器环境的事件循环有何不同?
Node.js事件循环的6个阶段,该循环的执行顺序为:
外部输入数据 - 轮询(poll) - 检查(check) - 关闭时间回调(close callback) - 定时器检测(timers) - I/O事件回调(I/O callbacks) - 闲置(idle、prepare) - 轮询(poll)
浏览器和Node.js环境下,微任务队列的执行时机不同:
- 在Node.js中,每个任务队列的每个任务执行完毕之后,就会清空这个微任务队列
- 在浏览器环境下就两个队列,分别是宏任务和微任务队列。微任务的任务队列是每个宏任务执行完之后执行(微任务->渲染->下一次宏任务,微任务会阻塞渲染)
DOM树、CSSOM树和渲染树在结构不一致的场景
| DOM | CSSOM | 渲染树 | |
| 使用伪元素(::before/::after...) | × | √ | √(若可见) |
| display: none; | √ | √ | ×(被过滤) |
tips: visibility:hidden;在三个树中均存在,只是在渲染树中占位不可见,控制台中的Elements属于DOM树
栈、队列、堆区别
| 栈 | 队列 | 堆 |
| 先进后出 | 先进先出 | |
| 内存块小、自动分配/释放 | 内存块小、自动分配/释放 | 内存块大、需手动分配/释放 |
| 线性结构 | 线性结构 | 树形结构 |
闭包(3次)
MDN:闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。
内部函数可以访问外部函数作用域且被外部函数返回就形成闭包,闭包可以使变量私有(不会造成变量污染)(优点)
经典面试题:循环中使用闭包解决 var 定义函数的问题
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j)
}, j * 1000)
})(i)
}
首先使用了立即执行函数(5次)将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变(j取到的值都是对应循环中的i值),当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。
弊端以及注意点:
- 避免内存泄漏:确保在不需要闭包时,手动释放捕获的变量。
- 优化性能:避免在嵌套很深的函数中使用闭包,减少作用域链的长度。
- 保持代码清晰:尽量避免复杂的闭包逻辑,确保代码易于理解和维护。
注意:内存泄漏可能会延伸垃圾回收机制(V8)
websocket和SSE区别
| SSE | websocket | |
| 协议 | 基于HTTP | 基于TCP |
| 通信 | 单工,只能服务端单向发送消息 | 全双工,可以同时发送和接受消息 |
| 资源消耗 | 单向,服务端只需要推送数据,资源消耗更少 | 全双工通信,服务器需要监听来自客户端的消息,增大资源消耗 |
| 适用场景 | 天气、新闻消息推送 | 聊天室、实时游戏 |
高阶函数
满足下列条件之一:
- 接收一个或多个函数作为参数
- 返回一个函数
常见的应用场景
- 回调函数:map、filter、reduce
- 事件处理:监听事件
- 闭包:防抖、节流
- 柯里化
高阶组件(Higher-Order-Components)
本质: 一个接受组件并返回新组件的函数,用于封装通用的逻辑和行为,以便在多个组件中重复使用
解决的核心问题:
- 横切关注点的复用:将通用逻辑抽离为装饰器,避免重复代码
- 渲染劫持:通过控制render方法修改输出(ex:条件渲染、样式注入)
- Props动态扩展:向子组件注入新的props或覆盖现有props
浏览器的跨域问题
跨域问题其实就是浏览器的同源策略造成的。同源策略是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议、域名、端口号必须一致。
如何解决跨域问题
跨域问题涉及后端,建议找视频看看,文字内容比较抽象实践一下印象更深;
(1)CORS(跨域资源共享),CORS需要浏览器和服务器同时支持,整个CORS过程都是浏览器完成的,无需用户参与。因此实现CORS的关键就是服务器,只要服务器实现了CORS请求,就可以跨源通信了。
浏览器将CORS分为简单请求和非简单请求:
简单请求(不会触发预检请求OPTIONS):请求方法:HEAD/GET/POST,请求头信息默认不改变
编辑
浏览器会直接发出CORS请求,它会在请求的头信息中增加一个Origin(协议+域名+端口)字段, 如果Orign指定的域名在许可范围之内,服务器返回的响应头字段中至少有Access-Control-Allow-Origin(和Origin字段存储的数据相等)
非简单请求: 简单请求的要求之外的情况,非简单请求的CORS请求会在正式通信之前进行一次HTTP查询请求,称为预检请求。预检请求使用的请求方法是OPTIONS, 他的头信息中的关键字段有Origin,Access-Control-Request-Method,Access-Control-Request-Headers, 服务器在收到浏览器的预检请求之后,会根据头信息的三个字段来进行判断,如果返回的头信息在中有Access-Control-Allow-Origin这个字段就是允许跨域请求,如果没有,就是不同意这个预检请求,就会报错。
(2)nginx反向代理接口跨域;
(3)JSONP(已过时):利用<script>标签没有跨域限制,通过<script>标签src属性,发送带有callback参数的GET(只能是GET)请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。
XSS攻击(3次)
定义:跨站脚本注入攻击,攻击者通过在网站注入恶意脚本,使之在用户的浏览器上运行,从而盗取用户的信息如 cookie 等。
本质: 网站没有对恶意代码进行过滤,与正常的代码混合在一起了,浏览器没有办法分辨哪些脚本是可信的,从而导致了恶意代码的执行。
攻击类型:
- 存储型:攻击者将恶意脚本存储在服务端数据库中,当有请求发送过来,跟随响应数据一起返回,浏览器解析并执行,完成攻击
- DOM型:通过修改页面DOM节点形成xss
- 反射型:攻击者诱导用户访问带有恶意代码的URL,服务端处理请求并返回带有恶意代码的数据,浏览器把这段数据当脚本解析并执行,完成攻击
如何预防?(2次)
1.对用户输入进行严格验证
明确规定用户输入的格式、长度和允许的字符范围。(脚本通常需要很长的字段)
2.过滤危险字符
对于用户输入的内容,过滤掉可能会被用于 XSS 攻击的危险字符,如 <、>、& 等
3.设置HTTP头Content-Security-Policy(CSP)
Content-Security-Policy: default-src'self'; script-src'self' example.com
4.对敏感的 Cookie 设置 HttpOnly 标志,这样javascript脚本就无法访问(服务端)
setcookie('session_id', '123456', time() + 3600, '/', '', false, true);
CSRF(Cross-site request )攻击
定义:跨站请求伪造攻击,攻击者诱导用户进入一个第三方网站,然后该网站向被攻击网站发送跨站请求(和被攻击网站同源)。如果用户在被攻击网站中保存了登录状态,那么攻击者就可以利用这个登录状态,绕过后台的用户验证,冒充用户向服务器执行一些操作。
本质: 利用 cookie 会在同源请求中携带发送给服务器的特点(服务器根据域名判断),以此来实现用户的冒充。
常见攻击类型:
- GET型:ex: 在网站中的一个 img 标签里构建一个请求,当用户打开这个网站的时候就会自动发起提交。
- POST型:ex: 构建一个表单,然后隐藏它,当用户进入页面时,自动提交这个表单。
- 链接型:ex: 在 a 标签的 href 属性里构建一个请求,然后诱导用户去点击。
预防:
CSRF token验证:服务端返回一个随机token,客户端再次发送请求时需携带
设置cookie属性:可以设置samesite属性,有两种模式,严格模式下任何情况下不允许第三方使用;宽松模式下仅允许被实现页面跳转的get请求使用
中间人攻击(Man-in-the-middle attack, MITM)
指攻击者与通讯的两端分别创建独⽴的联系, 并交换其所收到的数据, 使通讯的两端认为他们正在通过⼀个私密的连接与对⽅直接对话, 但事实上整个会话都被攻击者完全控制。在中间⼈攻击中,攻击者可以拦截通讯双⽅的通话并插⼊新的内容。
攻击过程: 客户端发送请求 -> 中间人截获 -> 服务端接收请求
服务端响应发送公钥 -> 中间人截获,伪造公钥 -> 客户端接受假公钥
客户端发送假公钥加密hash值 -> 中间截获,用私钥获取真hash值,伪造hash值 -> 服务端私钥解假hash值(获取假密钥)
HTTPS加密
前言
对称加密:用一个密钥进行加密/解密
非对称加密:产生一对密钥:公钥-私钥,公钥加密,私钥解密(私钥加密,公钥解密)
对称+非对称:服务端生成一对密钥(公钥/私钥key1),将公钥key1发送给客户端-->客户端生成key2用公钥key1进行加密发送给服务端-->服务端用私钥key1解密得到key2-->二者用key2进行加密通信(可能会有中间人攻击,截获公钥key1,生成假公钥发送给客户端)
正文:
https的加密流程(ssl):服务端生成一对密钥(公钥/私钥key1),将公钥key1提交给CA生成数字证书(电子版)-->服务端发送数字证书给客户端校验-->客户端校验完成后生成key2用公钥key1加密发送给服务端-->服务端用私钥key1解密得到key2-->二者用key2进行加密通信
注意: 数字证书不能被篡改(除非机构的私钥泄露),CA会对主体信息(域名,公钥,有效期...)计算出哈希值,通过CA私钥加密生成数字签名,客户端用公钥key1解密得到的信息可以和计算证书信息得到的哈希值比较,判断证书是否被篡改
常见的状态码(待更新...)
当输入一个网址按下Enter键会发生什么(美团一面,2次)
解析URL
|
判断缓存
|
DNS解析
|
获取MAC地址
|
TCP三次握手
|
HTTPS握手(TLS/SSL)
|
发送请求,返回数据
|
页面渲染
|
TCP四次挥手
重排一定会触发重绘,重绘不一定触发重排
布局没有发生变化,跳过layout+layer阶段,直接进行paint(绘制)阶段
编辑
触发回流(重排):
- 首次渲染页面
- 添加/删除元素
- 改变元素大小/位置/内容、字体大小
- 调整浏览器窗口大小
- 查询某些属性或调用某些方法(clientWidth/clientLeft/clientHeight..., getcomputerdStyle(),getBoundingClientReact())
如何避免回流:避免频繁操作样式、DOM(脱离文档流,隐藏修改再显示)、transform/opacity/filters/will-change不会引起回流重绘
触发重绘:没有出现几何图形的变化的CSS样式
浏览器缓存机制(百度一面,浏览器原理必需知道,2次)
我将缓存机制分为四种类型
浏览器首次加载资源,服务器返回 200,浏览器从服务器下载资源文件,并缓存资源文件与 response header(响应头),以供下次加载时对比使用;
- cache-control: max-age; 请求数据时,响应头中的cache-control字段,表示有效时间内的重复请求浏览器无需再次访问服务器,直接使用缓存资源;
- expires: 指定过期时间,同样是服务端的响应头字段,在有效的时间点之前,浏览器无需再次访问服务端,直接使用缓存资源;
- Etag(服务端)/If-None-Match(客户端): 首次请求资源时,服务端将缓存结果签名并设置在响应头的Etag字段中发送给客户端,客户端缓存Etag和结果数据,并在下一次请求时将Etag设置在请求头的If-None-Match字段中,服务端接收请求会比较Etag和If-None-Match是否一致, 若一致,返回304告诉客户端资源没有发生变化,客户端接收304状态码直接访问之前缓存的结果数据;
- Last-Modified(服务端)/If-Modified-Since(客户端): 首次请求资源时,服务端将缓存Last-Modified(结果数据最新的更改时间)并设置在响应头的Last-Modified字段中发送给客户端,客户端缓存Last-Modified和结果数据,并在下一次请求时将Last-Modified设置在请求头的If-Modified-Since字段中,服务端接收请求比较Last-Modified和If-Modified-Since,如果Last-Modified < If-Modified-Since(请求资源的时间在结果数据变化之后,资源是最新的),说明结果数据并没有发生变化,返回304告诉客户端资源没有发生变化,客户端接收304状态码直接访问之前缓存的结果数据;
下一次加载时,强缓存优先级更高,先开始强缓存,判断cahe-control中的max-age(时间点)是否过期(若没有max-age,则判断expirse(时间段)是否过期);
若没过期,命中强缓存,直接使用本地资源;
若过期,开始协商缓存,在请求头中携带If-None-Match/If-Modified-Since向服务端发送请求,服务端判断If-None-Match中的etag值(若没有etag,比较If-Modified-Since是否在Last-Modified之后)是否改变;
如果没有改变(在后面),命中协商缓存,返回304;
如果改变(在之前),返回200+新的资源文件+新etag(Last-Modified)
编辑
总的来说,就是设置资源过期时间和判断结果数据是否发生变化两种方式,理解记忆哦~
OSI七层模型(字节一面)
| 应用层 | 为应用程序提供服务(http发生在这一层) |
| 表示层 | 数据格式转化、数据加密 |
| 会话层 | 建立、管理和维护会话 |
| 传输层 | 建立、管理和维护端到端的连接(TCP、UDP在这一层,规定了数据包的传输方式) |
| 网络层 | IP选址以及路由选择(规定了数据包的传输路线) |
| 数据链路层 | 提供介质访问和链路管理(传输路线) |
| 物理层 | 物理层(通过物理介质传输比特流) |
标红要考滴,加深印象不吃亏不上当~ 那么我问你::http发生在哪一层?TCP发生在哪一层?
REACT的钩子useEffect和useLayoutEffect区别
- useEffect:浏览器重新绘制屏幕之后触发,尽量将每个Effect作为一个独立的过程编写,并且每次只考虑一个单独的setup/cleanup(可以有多个)
useLayoutEffect: 浏览器重新绘制屏幕之前触发,内部的代码和所有计划的状态更新阻塞了浏览器重新绘制屏幕。如果过度使用,这会使你的应用程序变慢。
REACT中useCallback和useMemo区别(3次)
当尝试优化子组件时,它们都很有用。他们会 记住(或者说,缓存)正在传递的东西:
主要区别是使用场景的不同:
- useMemo缓存调用的结果
- useCallback缓存缓存函数本身
useMemo返回一个函数等价于使用useCallback,useCallback的出现是为了避免使用useMemo编写额外嵌套函数
REACT组件通信
通过props:父子、兄弟
使用useContext HOOK跨层级通信
redux/zustand状态管理
事件总线(eventBus):通过发布-订阅者模式实现任意组件通信
useRef获取子组件实例(慎用!!!避免频繁操作DOM)
受控组件和非受控组件(1次)
受控组件
定义: 由react控制并管理其内部状态的组件。它的状态通常由props传递给子组件,通过事件处理程序更新。受控组件提供了更精确的控制和验证,但更新会触发渲染(存在性能问题)
应用场景: 即时验证(密码强度),表单值依赖其他状态,遵循react单项数据流
非受控组件
定义: 由组件本身管理其内部状态的组件。它的状态通常通过ref从DOM中获取,不依赖于react处理状态的更新
应用场景: 处理大型表单,第三方集成,文件上传
useEffect和useLayoutEffect区别
useEffect允许你 将组件与外部系统同步。useEffect在浏览器完成对 DOM 的绘制后执行,不会阻塞渲染进程
useLayoutEffect 是 useEffect 的一个版本,在浏览器重新绘制屏幕之前触发。useLayoutEffect 在浏览器完成对 DOM 的布局(layout)和绘制(paint)之前执行。这意味着它可以同步访问和修改 DOM,但会阻塞浏览器的渲染过程。性能影响较大,使用不当会造成页面卡顿和性能问题
REACT事件机制
REACT的diff算法(2次,待更新...)
核心思想:
REACT的Fiber
问题: REACTv15在渲染时会递归对比虚拟DOM,找到需要更新的节点然后同步更新他们,一气呵成。整个过程REACT会占用浏览器资源,,这会导致用户触发的事件得不到响应,并且会导致掉帧,导致用户感觉到卡顿
解决:
为了给用户制造一种应用很快的“假象”,不能让一个任务长期霸占着资源。 可以将浏览器的渲染、布局、绘制、资源加载(例如 HTML 解析)、事件响应、脚本执行视作操作系统的“进程”,需要通过某些调度策略合理地分配 CPU 资源,从而提高浏览器的用户响应速率, 同时兼顾任务执行效率。
所以 React 通过Fiber 架构,让这个执行过程变成可被中断。“适时”地让出 CPU 执行权,除了可以让浏览器及时地响应用户的交互,还有其他好处:
- 分批延时对DOM进行操作,避免一次性操作大量 DOM 节点,可以得到更好的用户体验;
- 给浏览器一点喘息的机会,它会对代码进行编译优化(JIT)及进行热代码优化,或者对 reflow 进行修正。
核心思想: Fiber也称协程或纤程,它和线程不一样,本身不具有并发或并行的能力(需要线程配合),它是一种控制流程让出机制。让出CPU的执行权,让CPU先执行优先级高的任务(如与用户交互)。渲染的过程可以被中断,可以将控制权交回浏览器,让位给高优先级的任务,浏览器空闲后再恢复渲染。
V8的垃圾回收机制——GC算法分代式垃圾回收机制
V8将内存(堆)分为新生代和老生代。
新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
对象会出现在老生代空间中的情况:
- 新生代中的对象是否已经经历过一次 Scavenge 算法,如果经历过的话,会将对象从新生代空间移到老生代空间中。
- To 空间的对象占比大小超过 25 %。在这种情况下,为了不影响到内存分配,会将对象从新生代空间移到老生代空间中。
在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行。
清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。
V8数组模式-触发机制(性能优化)
快速模式
概念: 对应C语言的数组,速度快,紧凑
触发条件: 索引[0, length-1]且无空洞 || 预分配数组长度 < 100000(10w),无论有无空洞
字典模式
概念: 对应C语言的哈希表,速度慢,松散
触发条件: 预分配的数组长度 >= 100000(10w)且有空洞
优化策略
- 从0开始连续地初始化数组,避免进入字典模式
- 避免预分配>=10w的数组
- 删除元素避免使用delete,让数组保持紧凑
- 不要访问未初始化/已删除的数组元素
防抖和节流
先介绍具体含义和可能使用的场景(一定一定一定不要游戏回城技能起手,典型的B站大学选手)
- 防抖: 确保在指定时间间隔内,仅最后一次触发的事件才会执行函数
应用场景:输入框输入、窗口大小调整
function debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
// 使用示例
const inputHandler = debounce(() => {
console.log('Input event triggered');
}, 300);
document.getElementById('myInput').addEventListener('input', inputHandler);
- 节流: 防止高频触发,在一段时间内仅触发一次
应用场景:滚动、鼠标移动事件
function throttle(func, limit) {
let inThrottle;
return function() {
const args = arguments;
const context = this;
if (!inThrottle) {
func.apply(context, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// 使用示例
window.addEventListener('resize', throttle(() => {
console.log('Window resized');
}, 1000));
扩展: 为什么要绑定传入函数上下文?
当防抖节流函数执行时,执行环境可能发生变化,给函数绑定this可以保证this指向的是预期对象
复杂表单的性能优化的方法
- 分页加载
- 虚拟滚动
- 懒加载
Webpack热更新原理(4次,唯品会一面,字节一面,待更新...)
TS篇
interface和type区别(3次 重点!)
interface用于定义类的实现和对象类型的结构,type用于创建类型别名,多用于元组,联合类型
type比interface更灵活,能表示一些interface无法表示的类型,如联合类型(|)、交叉类型(&)。- 重复定义:
interface具有可扩展性,可以重复定义并合并,而type重复定义会报错(不被允许) - interface用extends实现继承性, type需要用&交叉类型来组合多种类型
如何实现子接口仅继承部分父接口且具有额外属性
使用typescript的工具类型Pick筛选出子接口想要继承的变量
Pick属于 TypeScript 内置的工具类型,其用途是从某个类型里选取特定的属性,进而构建出一个新类型
interface Parent {
P1: string;
P2: number;
P3: boolean;
}
type PartialParent = Pick<Parent, "P1" | "P2">;
// 添加额外属性1.
interface Child extends PartialParent {
C1: string;
}
let children: Child = {
P1: "hello",
P2: 123,
C1: "world",
};
console.log(children);
// 添加额外属性2.添加一个字符串索引签名 可自定义添加0-n个属性
interface Child {
[propName: string]: any;
}
let children1: Child = {
P1: "hello",
P2: 123,
C1: "world",
extra: "extra",
name:"name"
}
console.log(children1);
接口引入文件形式
import VS import type
- import type: 仅导入类型信息,编译后会被移除,纯类型引入
- import: 同时导入类型和值(ex: 类, 函数)
Record<K, T>
TypeScript 内置的工具类型,用于构造一个键类型为 K、值类型为 T 的对象类型。
type UserRole = 'admin' | 'user' | 'guest';
type RolePermissions = Record<UserRole, boolean>;
const permissions: RolePermissions = {
admin: true,
user: false,
guest: false,
};
手写代码篇
数组去重(5次)
// 数组去重的方法
const equal = function (list) {
// 1.利用Set特性
// let _list = [...new Set(list)];
// 2.数组indexOf、includes方法
// for (let i = 0; i < list.length; i++) {
// // if (_list.indexOf(list[i]) < 0) {
// if (!_list.includes(list[i])) {
// _list.push(list[i]);
// }
// }
// 3.数组filter方法和indexOf组合技
// let _list = list.filter((item, index, array) => {
// return array.indexOf(item) === index;
// })
// 4.数组reduce和indexOf/includes组合技
let _list = list.reduce((pre, cur) => {
if (!pre.includes(cur)) {
pre.push(cur);
}
return pre;
}, [])
return _list;
}
console.log(equal([1, 2, 3, 4, 5, 23, 1, 2, 3, 4, 5]));
//4. 利用Map映射+循环 Map.has()的时间复杂度是O(1)!!!!!!
//总体时间复杂度为O(n)
function uniqueArray(arr: number[]) {
const map = new Map<number, boolean>();
const uniArr: number[] = [];
for (let index in arr) {
if (!map.has(arr[index])) {
map.set(arr[index], true);
uniArr.push(arr[index]);
}
}
return uniArr;
}
console.log(uniqueArray([1, 1, 1, 1, 2, 2, 2, 3, 4, 5, 5, 6, 6, 7, 8, 8]));
对象扁平化
// 处理对象,使得 { a: {b: { c: 1, d:2 } } } 变成 { abc: 1, abd:2 }
function flatObj(obj: any, prefix: string = "", result: object = {}) {
for (let key in obj) {
if (typeof obj[key] === "object" && obj[key] !== null) {
flatObj(obj[key], prefix + key, result);
} else {
result[prefix + key] = obj[key];
}
}
return result;
}
const obj = { a: { b: { c: 1, d: 2 }, e: 3 } }; // { abc: 1, abd: 2, ae: 3 }
console.log(flatObj(obj));
判断对象是否相等
// 判断对象是否相等
function objEqual (obj1, obj2) {
if (obj1 === obj2) return true;
if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null) return false;
let keys1 = Object.keys(obj1);
let keys2 = Object.keys(obj2);
if (keys1.length !== keys2.length) return false;
for (let i of keys1) {
if (!obj2.hasOwnProperty(i) || !objEqual(obj1[i], obj2[i])) return false;
}
return true;
}
let o1 = { name: 'Tom', age: 18, ability: { basketball: true, run: true, swimming: true }, sex: 'woman' };
let o2 = { age: 18, name: 'Tom', ability: { swimming: true, basketball: true, run: true }, sex: 'woman' };
console.log(objEqual(o1, o2));
用setTimeout来实现setInterval
// 用setTimeout 实现 setInterval效果
function mySetInterval(fn, intervalTime) {
let timeId;
function interval(){
fn();
timeId = setTimeout(interval, intervalTime)
}
timeId = setTimeout(interval, 0)
return {
clear:function () {
clearTimeout(timeId);
}
}
}
const interval1 = mySetInterval(()=>{
console.log(new Date())
}, 1000)
setTimeout(interval1.clear, 5000) // 5s之后清空定时器mySetInterval
手写Promise(必须记住的干货)
不要死记硬背捏(也是告诫我寄己),要跟着作者的逻辑走,消化前辈的思路
/**
* 定义全局的状态常量,避免魔法字符串
* pending: 初始状态,既不是成功,也不是失败
* fulfilled: 成功状态,操作成功完成
* rejected: 失败状态,操作失败
*/
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
private _state
private _result
private handlers: any[] = []
constructor(executor: Function) {
this._state = PENDING
this._result = null
const resolve = (value: any) => {
this.changeState(FULFILLED, value)
}
const reject = (reason: any) => {
this.changeState(REJECTED, reason)
}
// 捕获错误,如果executor执行过程中抛出错误,直接reject
try {
executor(resolve, reject)
} catch (error) {
reject(error)
}
}
private changeState = (state: string, result: any) => {
/** 只有状态为pending时才能改变状态(状态不可逆) */
if (this._state !== PENDING) return
this._state = state
this._result = result
this.run() // 状态转变后,执行then方法中存储的回调函数
}
then(onFulfilled: any, onRejected?: any) {
/** 因为可链式调用,所以then方法需要返回一个新的Promise实例 */
return new MyPromise((resolve: Function, reject: Function) => {
// 因为状态不会立刻转变
// 需要把onFulfilled和onRejected存储起来,等状态转变后再执行
// 同一实例可以多次调用then方法,所以handlers是一个数组
// 可以封装一个run方法,在then/changeState中调用run方法
this.handlers.push({
onFulfilled,
onRejected,
resolve,
reject,
})
this.run()
})
}
catch(onRejected: any) {
//直接复用then,简简单单
return this.then(undefined, onRejected)
}
/** 执行handlers中存储的回调函数 */
run() {
// then调用的时候可能是pending状态
if (this._state === PENDING) return
/** 状态一旦改变,需要执行同一实例所有then的回调函数 */
while (!!this.handlers.length) {
const { onFulfilled, onRejected, resolve, reject } = this.handlers.shift()
if (this._state === FULFILLED) {
this.runOne(onFulfilled, resolve, reject)
} else {
this.runOne(onRejected, resolve, reject)
}
}
}
runOne(callBack: Function, resolve: Function, reject: Function) {
// 放入微队列中
this.runMicroTask(() => {
if (typeof callBack !== 'function') {
const settledState = this._state === FULFILLED ? resolve : reject
// 不是函数,直接把结果/原因传递给下一个then(穿透)
settledState(this._result)
} else {
try {
const data = callBack(this._result)
// 需要判断返回值是否是一个Promise
if (this.isPromiseLike(data)) {
// 如果是Promise,需要等待这个Promise完成后再决定下一个then的状态
data.then(resolve, reject)
} else {
resolve(data)
}
} catch (error) {
reject(error)
}
}
})
}
isPromiseLike(value: any) {
// 满足PromiseA+规范的对象/函数必须具有then方法(函数)
if (!!value && (typeof value === 'object' || typeof value === 'function')) {
return typeof value.then === 'function'
}
}
runMicroTask(fn: any) {
// node环境
if (typeof process === 'object' && typeof process.nextTick === 'function') {
process.nextTick(fn)
} else if (typeof MutationObserver === 'function') {
// 浏览器环境,MutationObserver是一个监听DOM变化的API,可以用来模拟微任务
const observer = new MutationObserver(fn)
const textNode = document.createTextNode('1')
observer.observe(textNode, { characterData: true })
textNode.data = '2'
} else {
// 脱离环境,setTimeout也是一个宏任务,但在没有更好的选择时可以用来模拟微任务
setTimeout(fn, 0)
}
}
}
const p = new MyPromise((resolve, reject) => {
// setTimeout(() => {
resolve('等了2秒')
// }, 2000)
})
setTimeout(() => {
console.log('打印了')
}, 1000)
p.then(132, (err: any) => {
console.log('MyPromise error', err)
return '错误被捕获了'
}).then(
(res) => {
console.log('resolve2', res)
},
(err: any) => {
console.log('MyPromise error2', err)
},
)
p.then(
(res: any) => {
console.log('MyPromise', res)
},
(err: any) => {
console.log('MyPromise error', err)
},
)
编辑
算法篇
二叉树
递归三部曲
- 确定递归函数的参数和返回值
- 确定终止条件
- 确定单层递归的逻辑
巧记前-中-后序遍历
tips:左节点始终比右节点先遍历,根节点随顺序变换位置
- 前:根 - 左 - 右
- 中:左 - 根- 右
- 后:左 - 右 - 根
性能优化
这里记录开发过程中优化性能的手段
- 在组件中定义变量/函数,使用useMemo/useCallback包裹
- 自身调用的递归函数谨慎使用,推荐while(数据量太大容易堆栈溢出)
- 避免大量的嵌套循环,用映射代替Array.incules 获取数据(O(1)优于O(n))
- Array.push在大量数据场景中,用concat代替push(push参数有限制,大数据量push可能会卡死)(性能:concat > push > 展开运算符)
- Array.unshift 大数据量下性能不好,推荐使用push+reverse或for循环倒序
- forEach优于map
场景题
登录无感刷新方案
1.单点登录
概念: 一种身份认证机制,允许用户通过一次登录访问多个相互信任的关联系统或应用,无需重复登录验证(ex:闲鱼和淘宝都是阿里巴巴旗下产品)
模式:
SESSION + COOKIE:
(同一浏览器)统一通过认证中心进行登录校验,管理一个session表格,表格记录有效登录状态的用户信息(唯一标识(sid)+用户信息),第一次登录时,认证中心返回cookie(sid),浏览器保存,登录其他子系统时只需发送cookie到认证中心即可完成登录
编辑
TOKEN
单TOKEN:(同一浏览器)通过认证中心登录校验拿到token,使用其他子系统时,可以在系统内完成token验证(加密解密算法,系统与认证中心进行约定),无需再向认证中心验证
劣势:失去对用户的控制
编辑
双TOKEN:提高对用户的控制,(同一浏览器)通过认证中心登录校验拿到两个TOKEN(TOKEN:所有子系统系统能够识别,REFRESHTOKEN:仅认证中心识别)
编辑
2.无感刷新(双token)
首次登录校验成功得到短token&长token,短token时效很短,当短token失效时,自动发起刷新(短)token请求(携带有效长token),即可获得有效短token;
无需用户手动刷新,增加用户体验,保证安全性
核心代码:
// request.js
import axios from 'axios'
import { getToken, setToken, setRefreshToken } from './token' //存在localStorage中
import { refreshToken, isRefreshRequest } from './refreshToken'
const request = axios.create({
baseURL: 'https://api.example.login.com',
headers: {
Authorization: `Bearer ${getToken}`
}
})
request.interceptors.response.use(
async (response) => {
if (response.headers.authorization) {
const token = response.headers.authorization.split(' ')[1]
setToken(token)
request.defaults.headers['Authorization'] = `Bearer ${token}`
}
if (response.headers.refreshtoken) {
const refreshToken = response.headers.refreshtoken.split(' ')[1]
setRefreshToken(refreshToken)
}
//短token过期&不是刷新token的请求
if (response.data.code === 401 && !isRefreshRequest(response.config)) {
// 刷新token
const isSuccess = await refreshToken()
if (isSuccess) {
// 重新请求
console.log('重新请求')
response.config.headers['Authorization'] = `Bearer ${getToken()}`
const resp = await request.request(response.config)
return resp
}else{
//无权限
console.log('无权限,跳转至登录页')
return response.data
}
}
return response.data
}, (error) => {
// 对响应错误做些什么
return Promise.reject(error)
}
)
export default request
// refreshToken.js
import request from './request'
import { getRefreshToken } from './token'
// 防止多次刷新请求
let promise = null
export async function refreshToken () {
console.log('刷新token')
if (promise) return promise
promise = new Promise(async (resolve, reject) => {
const res = await request.get('/refresh_token', {
headers: {
Authorization: `Bearer ${getRefreshToken()}`
},
__isRefreshToken: true,
})
resolve(res.code === 200)
}).finally(() => {
// 每次刷新结束都销毁promise
promise = null
})
return promise
}
export function isRefreshRequest (config) {
return config.__isRefreshToken
}