Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

CSS

盒模型

  1. 定义:HTML中每个元素都可以看成一个盒子
  2. 组成:内容(content),内边距(padding),边框(border),外边距(margin)
  3. 分类:
    1. 标准盒模型(content-box)
      1. 占据空间为margin + border + padding + content
    2. IE盒模型(border-box)
      1. 占据空间为margin + content(包含padding + border)
  4. 可以使用box-sizing:content-box(默认)/border-box切换模式

选择器优先级

  1. CSS特性:
    1. 继承性:子元素继承父元素样式
    2. 层叠性:样式发生冲突时按照优先级进行选择
  2. 选择器类型:
    1. 0级别
      1. !important
    2. 1级
      1. 行内样式(1级别)
    3. 二级
      1. id选择器
      2. 类/伪类/属性选择器
      3. 标签/伪元素选择器
      4. 全局选择器
  3. 二级优先级计算:
    1. 百分位:ID选择器
    2. 十分位:类/伪类/属性选择器
    3. 个位:元素/伪元素选择器

隐藏元素的方法

  1. display: none;
    1. 不占据位置
    2. 不可触发点击事件
    3. 子元素设置样式不可以显形
    4. 会修改DOM引起回流
  2. visibility: hidden;
    1. 占据位置
    2. 不可触发点击事件
    3. 子元素设置样式可以显形
    4. 不会修改DOM,进行重绘即可
  3. opacity: 0
    1. 占据位置
    2. 可以触发点击事件
    3. 子元素设置样式不能显形
    4. 不会修改DOM,进行重绘即可
  4. display: absolute
  5. clip-path

px/em/rem/vh/vw/百分比的区别

  1. px:绝对单位,元素按照标准像素进行显示
  2. em:相对单位,相对于父元素的font-size发生改变
  3. rem:相对单位,相对于根节点(html)的font-size发生变化
    1. 当给html设置font-size = 62.5%时,1rem = 10px(16px * 62.5% = 10px)
  4. vh:相对单位,相对视口高度 1vw = 3.75px
  5. vw:相对单位,相对视口宽度
  6. 百分比:相对单位,相对于父元素

重绘与重排(回流)的区别⭐

  1. 回流(重排):布局引擎计算DOM元素在页面上的位置,大小的过程
  2. 重绘:布局引擎将DOM元素绘制到对应位置上的过程
  3. 浏览器渲染机制:
    1. 解析HTML
    2. 生成DOM树
    3. 解析CSS
    4. 生成CSS样式规则
    5. 将DOM树与样式规则结合形成渲染树
    6. 根据渲染树,计算DOM元素在页面中的位置(回流)
    7. 将DOM元素渲染到对应位置(重绘)
  4. 如何触发回流:
    1. 使用JS增删DOM元素
    2. 使用JS修改DOM元素宽高
    3. 使用JS修改DOM元素位置
  5. 如何触发重绘
    1. 更改DOM元素background-color
    2. 更改DOM元素color
    3. 更改DOM元素box-shadow
  6. 回流一定触发重绘,但重绘不一定触发回流

让一个元素水平/垂直居中的方法

  1. 标准流 + margin
    1. 标准流下使用margin: 0 auto实现水平居中
  2. 定位流 + margin
    1. 子绝父相
    2. 子设置top:0;left:0;right:0;bottom:0;margin:auto;
  3. 定位流 + transform
    1. 父相对定位
    2. 子绝对定位
    3. 子设置top:50%;left:50%;transform:translate(-50%,-50%)
  4. flex布局
    1. 父设置display:flex;
    2. 父设置justify-content:cneter;设置主轴居中(默认主轴为水平)
    3. 父设置align-item: center;设置交叉轴居中(默认交叉轴为垂直)
  5. grid布局
    1. 父设置display:grid
    2. 子设置justify-self: center水平居中
    3. 子设置align-self: center垂直居中
    4. 或子设置place-self: center水平、且垂直居中
  6. table-cell
    1. 父设置display: table-cell
    2. 父设置text-align:center 水平居中
    3. 父设置vertical-align:middle 垂直居中
    4. 子设置display:inline-block

CSS中可以继承的属性

  1. 字体属性font
    1. font-size
    2. font-weight…
  2. 文本属性
    1. line-height
    2. text-align
    3. color
  3. 可见性属性
    1. visibility
  4. 表格属性
    1. border-spaceing
  5. 列表属性
    1. list-style
  6. 页面样式属性
    1. page
  7. 声音样式属性

预处理器(sass,less)

使用过Sass,为css提供了更多功能

  1. 变量@
  2. 混入实现BME
  3. 函数
  4. 样式嵌套

丰富了css的结构性,可复用性,可维护性

Flex: 1是哪些属性

表示设置:

1
2
flex-groth: 1	// 子元素按父元素剩余空间放大
flex-shrink: 1 // 子元素按父元素空间缩小

BFC

  • 块级上下文
  • 解决的问题:
    • 边塌陷
      • 元素之间没有非空内容、padding、border或clear分隔
      • 相邻元素之间
        • margin-top和第一个子元素的margin-top
        • margin-bottom和最后一个子元素的margin-bottom(height为auto)
        • margin-bottom和相邻兄弟的margin-top
      • 元素自身不是BFC,没有子元素,height为0时,margin-top和margin-bottom会塌陷
    • 清除浮动
      • 浮动元素会造成父元素高度塌陷
      • 原因是div高度默认为子元素高度,宽度默认为父元素的100%
      • 当子元素设置浮动时,不再占据空间,因此父元素发生高度塌陷
      • 解决方法:
        • 浮动元素后添加一个元素设置clear: both
        • 父元素设置伪元素并设置clear: both
        • 父元素设置overflow: hidden
  • 触发条件
    • float:left/right
    • overflow: hidden | auto | scroll
    • display: inline-block | table-cell | table-caption | flex | inline-flex
    • position: absolute | fixed

JS

节流和防抖

  • 节流和防抖的区别
  • 怎么写

节流:一段时间内的连续点击只执行最后一次

可以应用在搜索框联想

1
2
3
4
5
6
7
8
9
10
11
function debounce(fn: Function, delay: number) {
let timer = null
let that = this
return function(...args) {
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(that, args)
timer = null
}, delay)
}
}

节流:一段时间内的连续点击只执行第一次

可以应用在按钮点击和滚动事件

1
2
3
4
5
6
7
8
9
10
11
function throttle(fn: Function, delay: number) {
let timer = null
let that = this
return function(...args) {
if(timer) return
timer = setTimeout(() => {
fn.apply(that, args)
timer = null
}, delay)
}
}

typeof和instanceof

typeof只能对比基本数据类型

instanceof对比prototype的类型

JS由哪三部分组成

  1. ECMAScript
    1. JS核心内容
    2. 描述了语言的基础语法
  2. DOM(文档对象模型)
    1. 将HTML元素规划为元素构成的文档
  3. BOM(浏览器对象模型)
    1. 对浏览器进行访问和操作

JS内置对象及常用方法

  1. Boolean
  2. Number
  3. String
    1. slice()
    2. split()
    3. concat()
    4. length
  4. Array
    1. from()
    2. of()
    3. map()
  5. Object
  6. Function
  7. Date
    1. new Date()
    2. getYear()
  8. Math
    1. abs()
    2. sqrt()
    3. min()
    4. max()
  9. Map
  10. Set
  11. Symbol
  12. RegExp

Array常用方法

  1. ES5
    1. push()
    2. pop()
    3. shift()
    4. unshift()
    5. sort()
    6. reverse()
    7. concat()
    8. join()
    9. isArray()
  2. ES6新增
    1. 展开运算符…
    2. find,findIndex()
    3. Array.from()
      1. 类似数组对象
      2. 可便利对象
    4. Array.of()
      1. 接收一组值构成数组
    5. filter(),map(),reduce(),forEach()
    6. fill()
    7. entries(),keys,values()
    8. includes()
    9. flat(),flatMap()
      1. flatMap只能展开一层数组
      2. flatMap接收一个函数,相当于对数组进行了map()后有进行了flat()
  3. 会修改原数组的方法:
    1. pop()
    2. push()
    3. shift()
    4. unshift()
    5. sort()
    6. reverse()
    7. splice()

判断数据类型的方法

  1. typeof()
    • 只能判断除null外的基本数据类型
    • 引用数据类型均返回object
  2. instanceof()
    • 返回true/false
    • 只能判断引用数据类型
    • 会顺着原型链查找,有则返回true
    • 可以任意修改原型链的指向导致判断失效
  3. constructor
    • 几乎可以判断基本和引用数据类型
    • 但构造函数的值可以任意修改导致判断失效
  4. Object.prototype.toString().call()
    • 最终解决方案

基本数据类型与引用数据类型的区别

  1. 基本数据类型
    1. Number
    2. Boolean
    3. String
    4. Null
    5. Undefined
  2. 引用数据类型
    1. Object
    2. Array
    3. Function

基本数据类型的值直接保存在栈内存中

引用数据类型的值保存在堆内存中,其在堆内存中的地址将被保存在栈内存中

当两个引用类型同时指向同一地址时,修改其中一个其中的值,另一个里面的值也会一起改变

闭包及其特点

闭包:

  1. 函数嵌套函数
  2. 内部函数访问外部函数的数据
  3. 内部函数被外部函数返回出来并保存

特点:

  1. 可重复利用
  2. 不会污染全局作用域
  3. 这个变量会一直存在不被垃圾回收机制清除

缺点:

  1. 由于变量不会被垃圾回收机制清除,如果有太多闭包,会消耗内存,导致页面性能下降,在IE浏览器中会导致内存泄露

使用场景:

  1. 节流
  2. 防抖
  3. 函数嵌套以避免全局污染

前端内存泄露怎么理解

  1. 定义:JS中已经声明并分配内存,由于长时间未被使用或清除,导致长期占据内存,让内存资源大幅浪费,使得运行速度慢,甚至崩溃的情况
  2. 造成内存泄露的原因:
    1. 未声明直接赋值的变量
    2. 过多的闭包
    3. 未清理的定时器
    4. 一些引用元素未被清理

事件委托

  1. 是什么:又叫事件代理,是利用事件冒泡机制,将由子元素触发的事件委托给父元素进行处理的方法,如果子元素阻止了事件冒泡event.stopPropagation()则无法进行事件委托
  2. 优点:
    1. 当许多子元素需要绑定同一事件时,可以减少重复的代码,增强代码可读性
    2. 减少了事件绑定,即减少了内存占用
    3. 提高性能

说一下原型链⭐

  1. 原型:也就是原型对象,是JS用来为构造函数的实例共享属性和方法的对象,使得所有实例只需要存一次该方法或属性(类似静态方法)
  2. 构造函数的原型对象通过Constructor.prototype访问
  3. 实例在调用函数时会从自身出发:
    1. 先查找自身是否具有该函数
    2. 如果没有则通过__proto__访问其构造函数的原型对象
    3. 如果仍然没有则会访问其原型对象的原型对象

说一下用new创建一个实例发生了什么事情(含手撕)⭐

  1. 首先创建一个空对象
  2. 其次把空对象的原型链绑定到构造函数的原型对象上
  3. 然后将构造函数的this绑定为这个空对象
  4. 最后如果构造函数返回的时对象则直接返回,否则返回对象
1
2
3
4
5
6
7
8
9
10
11
12
function myNew(Fn, ...args) {
// 1. 创建空对象
let newObj = {}
// 2. 绑定原型链
// 不推荐使用newObj.__proto__ = Fn.prototype,因为__proto__已弃用
Object.setPrototypeOf(newObj, Fn.prototype)
// 3. 将构造函数的this绑定为新的对象
let resObj = Fn.apply(newObj, args)
// 4. 如果是对象则返回引用,否则返回对象
return resObj instanceof Object ? resObj : newObj

}

JS如何实现继承⭐

  1. 原型链继承

    1. 将子构造函数的原型对象设为父构造函数的一个实例

    2. 对子原型对象的构造函数重新赋值

    3. function Parent(name) {
          this.name = name;
          this.grade = [1, 2, 3]
      }
      
      Parent.prototype.say = function() {
          console.log("I am your father")
      }
      
      function Child(gender) {
          this.gender = gender
      }
      
      // 将子类原型对象指向新的父类实例
      // 以此构造出
      // childObj.__proto__ -> Child.prototype
      // Child.prototype.__proto__ -> Parent.prototype
      Child.prototype = new Parent()
      // 注意此处需要重新链接构造函数
      Child.prototype.constructor = Child
      
      let c1 = new Child('男')
      let c2 = new Child('女')
      c1.grade.push(4)
      console.log(c1.grade) // [1, 2, 3, 4]
      console.log(c2.grade) // [1, 2, 3, 4]
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34

      4. 优点:书写简单

      5. 缺点:

      1. 无法向父构造函数传参
      2. 所有子实例共享一个父实例,那么修改一个子实例中的引用父属性将导致其他子实例中的该属性一起改变

      2. 构造函数继承

      1. 在子构造函数中使用`call`等方法将父this绑定为子构造函数的this,并调用父构造函数,使得父构造函数中的属性绑定到子构造函数的this上
      2. ```js
      function Parent(name) {
      this.name = name;
      this.grade = [1, 2, 3]
      }

      Parent.prototype.say = function() {
      console.log("I am your father")
      }

      function Child(gender, ...args) {
      Parent.apply(this, args)
      this.gender = gender
      }

      let c1 = new Child('男', '小张')
      let c2 = new Child('女', '小花')
      c1.grade.push(4)
      console.log(c1.name) // 小张
      console.log(c2.name) // 小花
      console.log(c1.grade) // [1, 2, 3]
      console.log(c2.grade) // [1, 2, 3, 4]
      c1.say() // 报错:没有say方法
    4. 优点:

      1. 书写简单
      2. 不存在引用赋值的问题
      3. 父构造函数可以接收参数
    5. 缺点

      1. 原型链丢失
      2. 无法访问父原型对象上的方法
  2. 组合继承

    1. 同时使用方法1和2

    2. function Parent(name) {
          this.name = name;
          this.grade = [1, 2, 3]
      }
      
      Parent.prototype.say = function() {
          console.log("I am your father")
      }
      
      function Child(gender, ...args) {
          Parent.apply(this, args)
          this.gender = gender
      }
      Child.prototype = new Parent()
      Child.prototype.constructor = Child()
      
      let c1 = new Child('男', '小张')
      let c2 = new Child('女', '小花')
      c1.grade.push(4)
      console.log(c1.name) // 小张
      console.log(c2.name) // 小花
      console.log(c1.grade) // [1, 2, 3]
      console.log(c2.grade) // [1, 2, 3, 4]
      c1.say() // "I am your father"
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44

      3. 优点:

      1. 可以解决上述提到的所有问题

      4. 缺点:

      1. 会调用两次父构造函数,浪费资源

      4. 寄生组合继承

      1. 使用`Object.create()`代替组合继承中的new,因为该方法不会调用构造函数

      2. ```js
      function clone(child, parent) {
      // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
      child.prototype = Object.create(parent.prototype)
      child.prototype.constructor = child
      }

      function Parent(name) {
      this.name = name;
      this.grade = [1, 2, 3]
      }

      Parent.prototype.say = function() {
      console.log("I am your father")
      }

      function Child(gender, ...args) {
      Parent.apply(this, args)
      this.gender = gender
      }

      clone(Child, Parent)

      let c1 = new Child('男', '小张')
      let c2 = new Child('女', '小花')
      c1.grade.push(4)
      console.log(c1.name) // 小张
      console.log(c2.name) // 小花
      console.log(c1.grade) // [1, 2, 3]
      console.log(c2.grade) // [1, 2, 3, 4]
      c1.say() // "I am your father"
    3. 优点:

      1. 解决上述所有问题,且只需要调用一次父构造函数,是ES6之前最好的方式
  3. ES6继承

    1. 先构造出父构造函数的this

    2. 用子构造函数修改this

    3. 子类的this对象继承了父类的this对象,然后对其进行加工

    4. class Parent {
          constructor(name) {
                this.name = name;
              this.grade = [1, 2, 3]   
          }
          say() {
              console.log("I am your father")
          }
      }
      
      class Child extends Parent {
          constructor(gender, ...args) {
              super(...args)
              this.gender = gender
          }
      }
      
      let c1 = new Child('男', '小张')
      let c2 = new Child('女', '小花')
      c1.grade.push(4)
      console.log(c1.name) // 小张
      console.log(c2.name) // 小花
      console.log(c1.grade) // [1, 2, 3]
      console.log(c2.grade) // [1, 2, 3, 4]
      c1.say() // "I am your father"
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201

      5. 优点:

      1. 书写简单易懂
      2. 操作方便

      6. 缺点

      1. 部分浏览器不支持ES6语法


      ### JS设计原理

      1. JS引擎
      1. 编译JS脚本
      2. 目前毕竟流行V8引擎
      2. 运行上下文
      1. BOM API
      2. DOM API
      3. 事件循环
      3. 调用栈
      1. 保证JS单线程运行

      ### JS中this指向问题

      1. 全局对象中的this指向
      - 指向window对象
      2. 全局作用域/普通函数中的this
      - 指向window对象
      3. this永远指向最后调用它的对象
      - 需要注意的是在不是箭头函数的情况下满足上述条件
      4. new 关键词会改变this的指向
      5. 可以使用bind,call,apply可以改变非箭头函数的this指向
      6. 箭头函数的this
      - 其指向在定义时就已确定
      - 如果箭头函数外层有函数,则箭头函数的this指向外层函数的this
      7. 匿名函数的this
      - 永远指向window
      - 因为匿名函数的执行环境具有全局性

      ### JavaScript标签中defer和async的区别

      1. 在js标签不添加defer和async标签时,默认情况是:遇到js标签,则阻塞后续html元素加载,先将js脚本加载并执行后,再进行后续元素的加载与渲染
      2. async(h5新增)(与同台插入JS表现一致)
      1. 遇到包含async的js标签,浏览器会在渲染后续元素的同时,加载脚本
      2. 当脚本加载完成,浏览器会立即阻塞正在进行的渲染,转而执行加载好的脚本,知道脚本执行完成
      3. 多个async先下载完的先执行
      3. defer
      1. 遇到包含defer的js标签时,浏览器会在渲染后续元素的同时,加载脚本。
      2. 当所有DOM元素解析完成后执行下载好的脚本
      3. 多个defer需等待所有脚本均下载完成后再按HTML顺序执行

      | script标签 | JS执行顺序 | 是否阻塞HTML | DOMContentLoaded回调 |
      | ---------------- | --------------------------------- | ------------------- | ---------------------- |
      | `<script>` | 依次 | 是 | 等待脚本执行完成后回调 |
      | `<script async>` | 先下载完的先执行 | DOM未完成解析时阻塞 | 无需等待脚本执行完成 |
      | `<script defer>` | 下载完所有defer脚本后,再依次执行 | 不阻塞 | 等待脚本完成后回调 |

      ### setTimeout最小执行时间

      HTML5规定:(不同浏览器可能不同,当设置小于最小值是会自动调整为最小值)

      - setTimeout的最小延迟时间是4ms
      - setInterval的最小循环时间是10ms

      同时如果setTimeout嵌套超过5层会自动有一个4ms的延时,导致js不能精准计时

      ### ES6和ES5的区别,以及ES6的新特性

      JS的组成:

      - ECMAScript
      - DOM
      - BOM

      ES5即ECMAScript2005,第五次修订版

      ES6即SCMAScript2015,第六次修订版,是JS的下一个版本标准

      ES6新特性:

      1. 块级作用域
      1. 不存在变量提升
      2. 有暂时性死区的问题
      3. 多了成块级作用域的内容
      4. 同一作用域内不能重复声明
      2. 函数的拓展
      1. 箭头函数
      1. 不能作为构造函数,也就是不能用new
      2. 因此箭头函数没有原型对象
      3. 箭头函数没有arguments
      4. 箭头函数没有this,或者说箭头函数中的this指向第一个外层函数的this
      5. 箭头函数不能使用call,apply,bind改变this指向
      2. 参数默认值
      3. 剩余参数
      3. 新数据类型
      1. symbol
      - 代表了独一无二的数据类型
      - 无法使用new关键字
      - 无法使用四则运算
      4. 新数据结构
      1. set
      - 自动排序
      - 元素具有唯一性
      2. map
      - map可以自定义key和value的类型
      5. 新数组API
      1. 数组解构赋值
      1. 两数交换`[x, y] = [y, x]`
      2. Array.from/Array.of
      3. Array.concat
      4. find/findIndex()
      5. entries(),keys(),values()
      6. includes()
      6. 类的拓展
      1. 类语法糖
      7. 对象的拓展
      1. 解构赋值
      2. 属性名与变量名相同时可省略属性名
      3. 变量可以作为key
      8. 模板字符串
      9. Promise
      - 用于缓解回调地狱的问题
      - 自身具有的方法:
      1. all()
      2. race()
      3. resolve()
      4. reject()
      - 原型上的方法:
      1. then()
      2. catch()
      - Promise可以把异步操作队列化,保证执行顺序
      - 具有三种状态:
      - padding初始状态
      - fullfilled成果状态
      - rejected失败状态
      10. Generator语法糖
      1. async
      - 会将函数返回的结果封装为Promise对象
      2. await
      - 结果取决于其所等待的结果,如果是Promise对象,则结果为Promise的结果,如果是普通函数则直接返回函数结果
      - await等待的Promise如果被rejected,则整个async函数都会中断
      3. 两种觉果需要搭配使用
      11. 新增模块化
      1. import
      2. export
      3. export default

      ### call,bind,apply三者的区别

      1. 三者都可以改变this指向
      2. call接收一个参数列表作为函数参数,并立即执行
      3. apply接收一个数组作为函数参数,并立即执行
      4. bind并不会立即执行,返回一个新函数,接收一个参数列表作为函数参数
      5. call的性能要优于apply,因此用得更多

      ### 用递归的时候有没有遇到什么问题

      1. 递归式一个函数调用自身的过程
      2. 递归必须有递归出口,否则会造成调用栈溢出的问题

      ### 深拷贝与浅拷贝

      1. 定义:
      1. 对于一个对象而言,深拷贝即在堆内存中开辟一片新的空间,使其中保存的数据与原空间中数据相同
      2. 深拷贝可以由一下方式实现:
      1. 展开运算符
      1. 只能深拷贝单层对象
      2. JSON.stringify()
      1. 无法拷贝对象中的方法
      3. 递归实现
      2. 实现

      ```ts
      const copy = (obj: Object, deep: boolean): obj|null => {
      let newObj = {}
      if(obj instanceof Array) {
      newObj = []
      }
      for(let key in obj) {
      let value = obj[key]
      newObj[key] = (!!deep && typeof value === "object" && value !== null) ? copy(value, deep) : value
      }
      return newObj
      }

      const o1 = {
      name: "ender",
      say() {
      console.log("Hello Ender")
      },
      arr: [[1, 2, 3], 4, 5],
      sub: {
      name: "hello",
      arr: [1,2, 3, 4, [5, 6]]
      }
      }

      const o2 = copy(o1, true)
      o2.arr[0].unshift(0)
      console.log(o1, o2)

Ajax是什么,是怎么实现的⭐

ajax是一个创建交互式网页应用的网页开发技术

它可以在不重新加载页面的情况下与服务器发生数据交换并更新内容

ajax是通过XmlHttpRequest对象向服务器发送异步的请求,然后从服务器拿到响应数据,并通过JS操作DOM更新页面

  1. 创建xmh对象
  2. xmh通过open()函数与服务器建立连接
  3. 构造请求参数,并通过xmh的send()方法向服务器发送参数
  4. 通过onreadystate changes事件监听服务器与我的通讯
  5. 处理服务器响应的结果
  6. 将结果渲染到页面上去

浏览器存储方式

  1. cookies
    1. 兼容性好,请求头自带
    2. H5标准前的本地存储方式
    3. 存储容量小,安全性差,浪费资源,使用麻烦(需要封装
  2. localStorage
    1. H5加入的以键值对形式存储的存储方式
    2. 操作方便,永久存储,兼容性好
    3. 存储类型被限制只能是字符串和数字,不可被爬虫爬取,隐私模式下不可获取
    4. 在同一个域名下有效,跨域不同(http和https不同)
  3. sessionStorage
    1. 浏览器关闭即清除
    2. 会话级别的存储方式
    3. 在同一个域名下有效,跨域不同(http和https不同)
  4. IndexDB
    1. H5标准的存储方式
    2. 键值对形式存储
    3. 可快速读取
    4. 它数据量更大
    5. 支持字符串之外的类型,如二进制类型
    6. 支持事务
    7. 异步工作
  5. websql
    1. websql是浏览器支持的关系数据库API,但现在不维护了

token存在哪里

  1. 定义:token是一种身份认证令牌,是用户通过账号密码登陆后,服务器把这些凭证信息通过加密等一系列操作后得到的字符串
  2. 存储位置及影响:
    1. 存在localStorage中时:
      1. 可以在同一浏览器的所有标签页和窗口中共享
      2. 每次向服务端发送请求时,都需要读取后当作一个字段传递给后端
      3. 容易受到XSS(跨站脚本)攻击,但做好了应对措施则利大于弊
    2. 存在cookies时:
      1. 即使在浏览器关闭后,cookie仍然存在
      2. 每次请求将会自动将token携带在请求中
      3. 不支持跨域,容易受到CSRF(跨站请求伪造)攻击
    3. 存在session时:
      1. 只存在于当前会话中,用户关闭浏览器后,sessionStorage中的数据将被清除。
      2. 浏览器中打开新的标签页或窗口,那么新的页面将无法访问sessionStorage中的数据。

token登录流程

  1. 客户端使用账号密码请求登录
  2. 服务端收到用户的登录信息后验证密码
  3. 验证成功后,服务端会向客户端签发一个token
  4. 客户端收到token后保存在cookies或localstorage或sessionstorage中
  5. 当客户端需要向服务端请求数据时,需要在请求中携带保存的token
  6. 服务端收到请求后验证token,验证成功才会返回客户请求的数据

get和post的区别

  1. get一般用于获取数据,get一般用于上传数据
  2. get参数会放在url上,安全性较差,有长度限制;post参数放在body中参数不会限制长度
  3. get参数只支持url编码,post参数支持多种编码(表单等等)
  4. get请求会被浏览器缓存,post不会
  5. get请求在浏览器刷新或回退时不会处理,post请求则会重新发送
  6. get请求会被保存在浏览器历史记录中

浏览器从输入URL到看到页面发生了什么⭐

  1. 检查缓存
    • 如果浏览器有本地静态资源缓存,且未过期,则直接从缓存中读取,无需进行网络请求
  2. DNS解析
    • 将URL解析未IP地址,解析过程根据:本机host文件,本地DNS缓存,根据本地域名服务器的优先级解析域名
    • 域名服务器解析域名可能涉及本机与顶级域名服务器、根域名服务器间的数据交换
  3. 建立TCP连接(三次握手
  4. 发送HTTP请求
    • 如果是HTTPS则好需要建立TLS连接
    • 接收响应后如果状态码是301/302还需要进行重定向
  5. 将响应结果交给渲染线程
  6. 构建DOM
    • 解析HTML形成DOM树
  7. 计算样式
    • 计算样式形成渲染树
  8. 布局
    • 根据DOM树和渲染树,计算所有元素的坐标并生成布局树
  9. 分层
    • 根据布局树生成不同图层,得到分层树
  10. 绘制
    • 对分层树的每个层生成渲染质量,然后渲染引擎将质量交给合成线程处理
  11. 分块
    • 合成线程将每个图层分块,优先渲染靠近视口的快,合成线程会把块交给光栅化线程
  12. 光栅化
    • 光栅化线程把每个块转化为位图,写入显存
    • 所有块光栅化完成后合成线程会生成DrawQuad指令交给浏览器,其中包含了每个块在显存中存放的位置
  13. 合成
    • 浏览器接收到DQ指令后,将块合称为帧,调用GPU进程将渲染后的帧绘制到屏幕上
  14. 断开TCP连接(四次挥手

SVG了解多少

  1. svg是基于XML语法规则的图片格式
  2. svg是可缩放矢量图,无论如何缩放都不会是真
  3. svg的本质是文本文件,体积小
  4. svg可以直接插入HTML文件,称为DOM的一部分,并且可以使用JS或CSS操作
  5. SVG也可以作为单独的文件被img标签引用
  6. SVG可转化为base64编码引入页面

JWT了解多少⭐

JSON Web Token,通过json形式作为web应用中的令牌,可以在各方之间安全地把信息作为json对象传输

常用于信息传输、授权等(单点登录最常用JWT

事件捕获、冒泡、代理

事件流动:

  1. 事件捕获阶段
    • 该阶段由顶层对象windows开始,沿DOM树逐级向下查找出发事件的元素
  2. 目标阶段
    • 找到出发事件的最底层元素
  3. 事件冒泡阶段
    • 从最底层元素开始,逐级向上触发事件

先捕获,后冒泡

不管对于非目标阶段或者目标阶段的元素,事件响应执行顺序都是遵循先捕获后冒泡的原则;通过使用定时器暂缓执行捕获事件,可以达到先冒泡后捕获的效果;

绑定事件时通过第三个参数useCapture设置回调触发的阶段,为true表示在捕获阶段触发,为false表示在冒泡阶段触发,默认为false:

1
2
a.addEventListener('click', () => {console.log("冒泡a")})
a.addEventListener('click', () => {console.log("捕获a")}, true)

如果希望捕获事件在冒泡事件后触发,可以通过setTimeOut函数来讲函数的执行延后:

1
2
3
4
5
6
a.addEventListener('click', () => {console.log("冒泡a")})
a.addEventListener('click', () => {
setTimeOut(() => {
console.log("捕获a")
})
}, true)

事件委托则是通过冒泡或捕获,实现的,由于事件会通过冒泡或捕获传递到他的父级元素,因此当需要给很多元素添加相同的事件时,可以通过为其父节点添加该事件,点击子元素时会由于事件冒泡触发父元素的

但实际使用中事件冒泡对浏览器的兼容性要优于事件捕获,因此更推荐使用使事件冒泡

1
2
3
4
5
6
<ul id="item-list">
<li>item1</li>
<li>item2</li>
<li>item3</li>
<li>item4</li>
</ul>
1
2
3
4
5
let itemList = document.getElementById("item-list")
//事件捕获实现事件代理
itemList.addEventListener('click', (e) => {console.log('捕获:click ',e.target.innerHTML)}, true);
//事件冒泡实现事件代理
items.addEventListener('click', (e) => {console.log('冒泡:click ',e.target.innerHTML)}, false);

如果需要阻止事件冒泡则使用:

1
2
3
4
5
6
7
$("#div1").mousedown(function(e){
var e=event||window.event;
// 阻止事件冒泡
event.stopPropagation();
// 阻止默认事件
event.preventDefault();
});

函数柯里化

函数柯里化是一种将接收多个参数的函数转化为接收单一参数的函数,并且返回接受余下的参数而且返回结果的新函数的技术

函数柯里化的作用:

  1. 参数复用

    • 通过闭包机制,可以仅调用外层函数起到参数固定的作用

    • function curryingCheck(reg) {
        return function(txt) {
            return reg.test(txt)
        }
      }
      
      var hasDigit = curryingCheck(/\d+/g)
      var hasLetter = curryingCheck(/[a-z]+/g)
      
      hasDigit('test1')     // true
      hasDigit('test')   		// false
      hasLetter('123')      // false
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25

      2. 提前确认

      - 参数固定的另一个好处是参数在传递前就可以对其进行确认,避免每次传递参数都进行确认

      - ```ts
      const on = (document, element, event, handler) => {
      if (document.addEventListener) {
      element.addEventListener(event, handler, false);
      }
      else {
      element.attachEvent('on' + event, handler);
      }
      };

      // 提前确认注册事件的方法,就不用在每次调用on时候确认
      const on = (doc => {
      return doc.addEventListener
      ? (element, event, handler) => {
      element.addEventListener(event, handler, false);
      }
      : (element, event, handler) => {
      element.attachEvent('on' + event, handler);
      }
      })(document);
  2. 延时运行

    • 即调用方法时不会立即执行方法,只有当参数符合规定的时候才会执行返回想要的结果

    • function add(...args) {
          return args.reduce((prev, current) => prev + current);
      }
      
      function currylize(fn) {
          let args = []
          return function cb() {
              // 当不传入参数调用时才返回结果
              if(arguments.length < 1) {
                  return fn(...args)
              } else {
                  // 当传入参数时将参数保存起来
                  args = [...args, arguments]
                  return cb
              }
          }
      }
      
      add(1, 2, 3, 4);
      cAdd = currylize(add);
      cAdd(1)(2)(3)(4)();
      
      1
      2
      3
      4
      5
      6
      7
      8
      9

      经典面试题:实现一个add方法,使其可以使用任意数量的值作为参数,并可以调用任意多次:



      ```ts
      // add(1)(2)(3) == 6 // true
      // add(1, 2, 3)(4) == 10 // true
      // add(1)(2)(3)(4)(5) == 15 // true

答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function add() {
// 定义一个list存储参数
let args = Array.proptotype.slice.call(arguments)
// 创建一个闭包,利用闭包特性将函数外的数据与函数绑定
let adder = function() {
args.push(...arguments)
return adder
}
// 利用隐式转换时调用的toString方法求值:
adder.toString = function() {
return args.reduce(function(a, b) {
return a + b
})
}
return adder
}

通用柯里化函数可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function currylize(fn, ...rest) {
// 获取参数列表
let args = Array.prototype.slice.call(rest)
// 否则就保存参数不计算,并返回新的闭包
let curry = function() {
args.push(...arguments)
console.log(args)
if(args.length >= fn.length) {
let res = fn.apply(this, args)
// 由于闭包的存在,调用结束需要将args清空
args = []
return res
}
return curry
}
return curry
}

function add(a, b, c, d) {
return a + b + c + d;
}

const curriedAdd = currying(add);

curriedAdd(1)(2)(3)(4); // 10
curriedAdd(1, 2, 3)(4); // 10
curriedAdd(1, 2, 3, 4); // 10

Promise

promise对象具有三个状态:

  1. pending
  2. fullfilled
  3. rejected

该对象以函数作为参数,当new Promise接收到函数时,立即执行该函数,这个函数以resolve、reject作为参数:

  1. 调用resolve则将promise的状态变为fullfilled
  2. 调用reject则将promise的状态变为rejected

其余时间状态为pending,当执行任务抛出异常时,状态也会变为rejected

状态变化后,会执行通过then注册的回调。执行顺序和调用then方法的顺序相同。调用then方法时候,如果状态是pending则注册回调,等到状态改变时候执行,如果状态已经改变则执行相应的回调。

then

then方法返回包含两个参数:

  1. onFullfilled()
  2. onRejected()

then返回一个新的Promise对象,可以链式调用then方法来查看其返回的Promise的情况:

1
2
3
4
let p = new Promise(resolve => {resolve('test')});

// 继续调用then方法返回的Promise对象中的then方法
p.then(() => {return true}, e => {throw new Error('2')}).then( data => {console.log('resolve', data)}, e => {console.error('reject', e)})

Promise对象的特性:

  • 返回一个新的Promise对象,该对象的状态与then的两个参数回调函数以及这两个回调函数的返回值有关
  • 返回的Promise具有如下特性:
    • 如果不传入参数
      • 则返回的Promise对象的状态,与调用then的Promise对象的状态保持一致
      • 可以简单地理解:如果上一个promise不处理,那就下一个promise处理。
    • 如果then中的两个回调函数均无返回值
      • 则会将Promise的状态设为fullfilled,且值为undefined
    • 如果then中两个函数(不论fullfilled或rejected)均将返回一个没有then方法的值(非thenable对象)
      • 则返回的Promise状态为fullfilled,值为调用then方法的promise对象的状态对应的then返回函数中返回的值
    • 如果then中两个函数返回promise对象
      • 则返回的Promise状态和值均与then中返回的promise对象相同
    • 如果then中两个函数返回thenable对象
      • 则返回的Promise状态取决于该thenable对象的状态,如果then中返回的thenable对象调用了resolvePromise,则返回的promise状态置为fullfilled,如果then中调用了rejectPromise,或者then中抛出异常,则返回的Promise状态置为rejected,在调用resolvePromise或者rejectPromise之前,返回的promise处于pending状态。
    • 如果then中两个函数中抛出异常
      • 则返回的Promise状态为rejected,值为抛出的异常中的信息

catch

catch方法实际上等同于then中的onRejected()回调

Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数

——《ES6入门教程》 阮一峰

1
2
3
4
5
6
p.then((val) => console.log('fulfilled:', val))
.catch((err) => console.log('rejected', err));

// 等同于
p.then((val) => console.log('fulfilled:', val))
.then(null, (err) => console.log("rejected:", err));

Primise.all

all方法用于多个异步执行任务,

  • 接收一个Promise[]作为参数
  • 返回一个Promise对象
    • 当Promise[]中所有Promise状态均为fullfilled时,其状态为fullfilled
    • 当Promise[]由一个Promise状态为rejected时,其状态为rejected
    • 该Promise对象的then方法中的onFullfilled回调函数的参数也是一个数组,对应Promise[]中每个Promise的值

可以理解为多任务的&操作

如下代码的执行结果:

  • 由于p1中的reject会立即执行,因此p1的状态变为rehected
  • 那么Promise.all方法将直接返回一个状态为rejected的Promise对象
  • 并且其值为引发rejected的对象的值
  • 1秒后,p2的resolve回调打印p2 resolve 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const p1 = Promise.reject(1);
const p2 = new Promise(resolve => {
setTimeout(() => {
console.log("p2 resolve", 2)
resolve(2);
}, 1000);
});

Promise.all([p1, p2])
.then(
([result1, result2]) => {console.log('resolve', result1, result2);},
e => console.log('reject', e)
);

// 执行结果
reject 1
p2 resolve 2

Promise.race

Promise.race方法用于多个异步任务执行

  • 接收一个Promise[]
  • 当有其中一个任务完成或失败时候,就执行后续处理的场景。
  • 返回一个新的promise
    • 当参数数组中其中一个promise resolve或者reject,返回的promise就相应地改变状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var p1 = Promise.reject(1);
var p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, 1000);
});

Promise.race([p1, p2])
.then(
data => {console.log('resolve', data);},
e => {console.log('reject', e);}
);

// 执行结果
reject 1

Promise.allSettled

Promise.allSettled用于多个异步任务

  • 接收一个Promise[]
  • 返回一个新的Promise
    • 当Promise[]中所有Promise的状态均不为pending时,状态变为fullfilled
    • 其onFullfilled回调函数接受一个Object[]作为参数,其中每一个对象格式固定为{status, value, reason}:
      • 标识状态
      • resolve返回值(状态为rejected时没有该参数)
      • reject原因。(状态为fullfilled时没有该参数)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var p1 = Promise.reject(1);
var p2 = new Promise(resolve => {
setTimeout(() => {
resolve(2);
}, 1000);
});

Promise.allSettled([p1, p2])
.then(
data => {console.log('resolve', data);},
);

// 执行结果
resolve [{status: "rejected", reason: 1}, {status: "fulfilled", value: 2}]

async/await

  • async/await是promise对象的语法糖

  • 用于解决promise对象样板代码太多的问题

  • 当函数中使用await时,函数需要用async修饰

  • await后根一个promise对象,await表达式的结果为promise resolve的值

  • 而async方法返回一个promise对象

    • 状态为resolve时的值就是async函数的返回值
  • 该方法的缺点是:当await后的Promise状态变为rejected时,需要使用try/catch才能捕获到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
const task = () => {
return new Promise(resolve => {
setTimeout(() => {
console.log('1');
resolve('2');
}, 1000);
});
};

async function test() {
console.log(0);
const res = await task();
console.log(res);
}

test();

// 执行结果
0
1
2

async function task1() {
return 'test';
}


task1()
.then(console.log);

// 执行结果
test

const task = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('test-reject');
}, 1000);
});
};

async function test() {
try {
const res = await task();
}
catch (e) {
console.log('error', e);
}
}

test();

// 执行结果
error test-reject

使用Promise实现并发请求控制

对于并发请求,可以使用类似线程池的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import axios from "axios"

function multiRequest(urls: string[], maxNum: number): Promise<any> {
return new Promise<any>((resolve: Function, reject: Function) => {
// 请求列表
const requestURLS = [...urls]
// 请求结果
const result: {[key:string]:any} = {}
// 线程池
let urlPool:string[] = [];
const createTask = (url: string) => {
urlPool.push(url);
const onComplete = (err:any | null, res:any | null = null) => {
if(urlPool.length === 0) {
resolve(urls.map(url => result[url]))
}
result[url] = err || res;
urlPool = urlPool.filter(value => value !== url);
if (urlPool.length < maxNum && requestURLS.length) {
createTask(requestURLS.pop() as string)
}
}
axios.get(url).then(
data => { onComplete(null, data) },
error => { onComplete(error) }
)
}
while(requestURLS.length && urlPool.length < maxNum) {
createTask(requestURLS.pop() as string)
}
})
}

事件循环

JS将任务划分为宏任务与微任务,宏任务队列可以有多个,但微任务队列只有一个

宏任务包括:

  1. script(全局任务)

  2. setTimeout

  3. setInterval

  4. setImmediate

  5. I/O

  6. UI rendering

微任务包括:

  1. process.nextTick
  2. Promise
  3. Object.observer
  4. MutationObserver

微任务优先级大于宏任务

任务执行过程如下:

  1. 执行栈为空
  2. 将script(全局)加入宏任务队列
  3. 执行栈为空则从宏任务队列中取出一个任务,放入执行栈
  4. 执行执行栈中的任务
  5. 遇到异步操作则交由异步处理块处理,异步块将回调函数根据类型放入宏任务队列or微任务队列
  6. 当执行栈为空时从微任务队列中取任务,并执行,直到微任务队列为空
  7. 再从宏任务队列中取一个任务执行
  8. 执行微任务队列中的所有任务

所有同步任务都在主线程执行,形成一个执行栈,主线程之外,还存在一个任务队列,异步任务执行队列中先执行宏任务,然后清空当次宏任务中的所有微任务,然后进行下一个tick(帧)如此循环

下面这段代码输出的结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const first = () => (new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
console.log(5);
resolve();
}, 0);
resolve(1);
});
resolve(2);
p.then((arg) => {
console.log(arg);
});
}));
first().then((arg) => {
console.log(arg);
});
console.log(4);

首先将srcipt加入宏任务队列,然后取出该任务执行:

  1. 执行first函数打印3
  2. 构造p,打印7
  3. 将setTimeout的回调加入宏任务队列
  4. 执行resolve(1),将p状态变为fullfilled,值为1
  5. 执行resolve(2),将first()状态变为fullfilled,值为2
  6. 将p.then的回调加入微任务队列
  7. 将first().then的回调加入微任务队列
  8. 打印4
  9. 从微任务队列中取出任务按顺序执行:
    1. 打印1
    2. 打印2
  10. 执行宏任务队列中的任务,打印5

因此最终结果为374125

下面代码输出的结果:

1
2
3
4
5
6
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
Promise.resolve().then(() => console.log(3))
console.log(4)
  1. 将script加入宏任务队列,然后取出该任务加入调用栈
  2. 执行调用栈中的script打印1
  3. 执行setTimeout并将回调加入宏任务队列
  4. 执行Promise.resolve()将状态变为fullfilled
  5. 执行then将回调函数加入微任务队列
  6. 执行最后一条语句打印4
  7. 从微任务中取出微任务并加入调用栈执行:
    1. 打印3
  8. 从宏任务队列取出宏任务并加入调用栈执行:
    1. 打印2

回流与重绘

首先来看看浏览器的渲染过程:

  1. html转DOM树
  2. 计算style
  3. 生成布局树
  4. 分层,生成分层树
  5. 主线程给每个图层生成绘制列表,交给合成线程处理
  6. 合成线程将图层分块
  7. 合成线程在光栅化线程池中将图块转成位图
  8. 合成线程发送绘制图块的命令drawquad给浏览器进程
  9. 浏览器根据命令绘制,并显示在显示器上

如果JavaScript做了修改DOM元素的几何属性(位置、尺寸)等操作,将会重新计算style,并且需要更新布局树,然后执行后面的渲染操作,即从1~9的步骤需要重新执行一遍。这个过程叫“重排”。

如果JavaScript修改了DOM的非几何属性,如修改了元素的背景颜色,不需要更新布局树和重新构建分层树,只需要重新绘制,即省略了3、4两个阶段。

在页面运行中,应该尽量避免重排和重绘,以提升渲染性能。

引发重排的操作

  1. 首次加载
  2. DOM元素移动、增加、删除和内容改变会触发回流
  3. 当DOM元素的几何属性(width / height / padding / margin /border)发生变化就会触发回流。
  4. 读写元素的offset / scroll / client等属性会触发回流。
  5. 调用window.getComputedStyle会触发回流。
  6. 激活伪类
  7. 浏览器窗口动作(拖拽,拉伸)
  8. 添加或删除样式表

但需要注意的是浏览器自身会包含一些优化,一些属性的连续修改只会触发一次回流

1
2
3
4
document.getElementById('root').stlye.width = '100px';
document.getElementById('root').stlye.height = '100px';
document.getElementById('root').stlye.top = '10px';
document.getElementById('root').stlye.left = '10px';

上面代码只会触发一次回流,这是因为浏览器自身有优化机制。

但是获取offset等元素属性,每获取一次都会触发一次回流,这是因为offset等属性,要回流完才能获取到最准确的值。

减少重排与回流

  1. 避免元素影响到所在文档流

    1. 用绝对定位position: absolute;)使元素脱离文档流,这样元素的变化不会导致其他元素的布局变化,也就不会引起重排。
    2. 如果使用CSS的transform属性实现动画,则不需要重排和重绘,直接在合成线程合成动画操作,即省略了3、4、5三个阶段。由于没有占用主线程资源,并且跳过重排和重绘阶段,因此这样性能最高。
  2. 减少table使用,table属性变化使用会直接导致布局重排或者重绘

  3. 读写分离

    1. 由于浏览器对于JS的样式写操作会采用渲染队列机制,将写操作放入异步渲染队列,异步批量执行。当JS遇到读操作时候(offset / scroll / client),会把异步队列中相关的操作提前执行,以便获取到准确的值。

    2. 因此下面代码执行后,浏览器并不会触发4次重排,而是会将3个操作放入一个渲染队列中,异步批量执行,因此可能只会触发一次重排。

    3. div.style.left = '10px';
      div.style.top = '10px';
      div.style.width = '20px';
      div.style.height = '20px';
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12

      4. 但当遇到读操作时候,则立刻执行渲染队列中相关操作,从而马上触发重排。

      5. ```js
      div.style.left = '10px';
      console.log(div.offsetLeft);
      div.style.top = '10px';
      console.log(div.offsetTop);
      div.style.width = '20px';
      console.log(div.offsetWidth);
      div.style.height = '20px';
      console.log(div.offsetHeight);
    4. 因此需要将读写分离来减少回流次数:

    5. div.style.left = '10px';
      div.style.top = '10px';
      div.style.width = '20px';
      div.style.height = '20px';
      console.log(div.offsetLeft);
      console.log(div.offsetTop);
      console.log(div.offsetWidth);
      console.log(div.offsetHeight);
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13

      8. 如果要根据当前样式设置新样式则需要缓存:

      9. ```js
      // bad 强制刷新,触发两次重排
      div.style.left = div.offsetLeft + 1 + 'px';
      div.style.top = div.offsetTop + 1 + 'px';

      // good 缓存布局信息,读写分离
      var curLeft = div.offsetLeft;
      var curTop = div.offsetTop;
      div.style.left = curLeft + 1 + 'px';
      div.style.top = curTop + 1 + 'px';
  4. 集中改变样式

    1. 虽然浏览器有异步渲染队列的机制,但是异步flush的时机我们没有办法控制,为了保证性能,还是应该集中改变样式。

    2. 修改样式改用className控制(在用js修改盒子的多个样式时,尽量使用className来一次性对盒子进行修改。)操作如下:

    3. // bad
      var left = 10;
      var top = 10;
      el.style.left = left + "px";
      el.style.top  = top  + "px";
      
      // good 
      el.className += " theclassname";
      // good
      el.style.cssText += "; left: " + left + "px; top: " + top + "px;";
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13

      5. 离线改变DOM

      1. 如果需要进行多个DOM操作(添加、删除、修改),不要在当前的DOM中连续操作(如循环插入`li`)

      2. 在要操作DOM之前,通过display隐藏DOM,当操作完成之后,才将元素的display属性为可见,因为不可见的元素不会触发重排和重绘。

      3. ```js
      dom.display = 'none';

      // 执行DOM操作...

      dom.display = 'block';
    4. 通过使用DocumentFragment创建一个dom碎片,在它上面批量操作DOM,操作完成之后,再添加到文档中,这样只会触发一次重排。

    5. 复制节点,在副本上操作,然后替换原节点。

瓶颈分析

使用chrome的performance查看main部分火焰图,检查是否有过长时间的js block,避免大的js循环和大列表渲染,控制循环和列表的上限。导致页面卡顿,如果js执行超过几百ms就需要警惕了。

数组拍平的五种方法

数组拍平,即将一个多维嵌套数组转化为1维数组,同时如有需要,可以输入n用于控制拍平的深度,只有当嵌套的数组深度大于 n 时,才应该执行扁平化操作

1.toString

即利用toString方法将数组展开转化为字符串,然后利用split方法以,分隔,分开后需要利用+运算符将字符串转化为数字

缺点:是无法控制拍平深度

优点:书写简单

1
2
3
4
5
6
7
8
9
10
11
12
//答案:
function flatten(arr) {
return arr.toString().split(',').map(function(item){
return +item;//类型准换为
})
}
//解析:
arr=[[1,2,3],[4,5,6],[7,8,9]]
console.log(arr.toString());
//输出 1,2,3,4,5,6,7,8,9
console.log(arr.toString().split(","));
//输出 (9) ['1', '2', '3', '4', '5', '6', '7', '8', '9']

2.展开运算符

利用...将数组展开,并且此处运用了Array.some进行类型检测,方法用于测试数组中是否至少有一项元素通过了指定函数的测试.

缺点:是无法控制拍平深度

优点:书写简单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function flatten(arr) {
//检查 arr 中是否有元素是数组:
while(arr.some((item)=>{Array.isArray(item)})){
arr = [].concat(...arr);//逐层拨开洋葱的心,直到没有数组
}
return arr;
}

//解析:
let arr = [1,2,3]
console.log(...arr);// 1 2 3
let arr2 = [[1,2,3],[4,5,6],[7,8,9]];
console.log(...arr2);//[1,2,3] [4,5,6] [7,8,9]
res = [].concat(...arr2)//[].concat([1,2,3],[4,5,6],[7,8,9])
console.log(res);//[1, 2, 3, 4, 5, 6, 7, 8, 9]

3.递归

遇到数组则递归调用外层函数

优点:可以方便的设置拍平深度

缺点:书写复杂

1
2
3
4
5
6
7
8
9
10
11
12
13
var flat = function (arr, n){
const res = []
if (n === 0)
return arr
arr.forEach((value) => {
if(Array.isArray(value)) {
res.push(...flat(value, n - 1))
} else {
res.push(value)
}
})
return res
};

4.flat方法

直接使用ES6中新增的方法Array.flatten(n)

优点:书写最简单且可设置深度

缺点:自定义程度不高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function flatten(arr) {
return arr.flat(Infinity);
}
//解析:
var arr1 = [1, 2, [3, 4]];
arr1.flat(); // [1, 2, 3, 4]

var arr2 = [1, 2, [3, 4, [5, 6]]];
arr2.flat();// [1, 2, 3, 4, [5, 6]] 默认展开一层深度

var arr3 = [1, 2, [3, 4, [5, 6]]];
arr3.flat(2);// [1, 2, 3, 4, 5, 6]

arr3.flat(Infinity); //使用 Infinity 作为深度,展开任意深度的嵌套数组

5.reduce+递归

利用reduce方法遍历数组,省去了额外定义遍历存放元素的代码

1
2
3
4
5
function flatten(arr) {
return arr.reduce((pre,cur)=>{
pre.concat(cur instanceof Array?flatten(cur):)
},[])
}

面试提示

  1. 解释扁平化多维数组的概念。为什么在某些情景下扁平化很有用?

    • 扁平化多维数组意味着通过删除任何嵌套数组并替换为它们的实际元素将其转换为单维数组。这在需要将数组作为扁平列表进行处理、而不考虑其原始嵌套结构时很有用。它简化了搜索、过滤或转换数组元素的操作。
  2. 是否存在需要考虑的特殊情况或边缘情景?你的解决方案如何处理这些情况?

    • 是的,我们应该考虑空数组或没有嵌套数组的情况。在这种情况下,函数应该返回原始数组,因为没有需要扁平化的嵌套数组。此外,我们需要处理深度 n 为负数或超出数组的实际深度的情况。在这些情况下,函数也应返回原始数组,而不进行扁平化。
  3. 你的解决方案如何处理输入数组中的循环引用或自引用数组?

    • 循环引用或自引用数组可能导致无限递归。提供的解决方案不显式处理循环引用。如果输入数组包含循环引用,递归扁平化过程可能导致无限循环或堆栈溢出错误。

try…catch可以捕获到异步代码的错误吗

不能

异步代码需要使用Promise, async/await来捕获

不用Websocket原生实现多个Tab相互通信(本地实现)

  1. localStorage,发消息时保存到localStorage,用window.addEventListener("storage"(e) = >)监听变化
  2. Broadcast Channel API,一个Tab广播,另一个监听广播频道,支持浏览器不同上下文
  3. SharedWorker,多个浏览器上下文共享脚本的机制,不同Tab链接到同一个SharedWorker即可共享数据

target和currentTarget的区别

  • e.target:实际触发的元素
  • e.currentTarget:绑定事件监听的元素

迭代器与生成器的关系⭐

Vue

Vue2和Vue3的区别

  1. 数据绑定
    1. Vue2使用getter/setter
    2. Vue3使用Proxy
  2. 对外API,Vue使用setup
  3. Diff算法
    1. Vue2使用双端diff
    2. Vue3使用快速diff
      1. 借助了文本diff的思想
      2. 其中包含一个最长上升子序列的DP
  4. 编译上的优化
    1. Vue3使用静态方法解决重复渲染的问题
    2. Vue3会标记节点类型,直接根据类型进行高效更新

Diff算法

Computer和Watch的区别

VueRouter路由守卫

组合式API对比选项式API的优点

浏览器

从URL输入到页面展示过程中发生了什么

  1. 检查缓存
  2. 解析URL
    1. 使用DNS服务查找IP
    2. 得到MAC地址
    3. HTTP2.0以前采用TCP
      1. TCP三次握手四次挥手
      2. HTTPS的SSL握手
      3. HTTP3.0使用基于QUIC的UDP
  3. 构建HTTP request
    1. request和respond的区别
  4. CDN
  5. API/BFF 微服务 缓存 mysql读写分离
  6. 页面渲染
    1. Vue、React的渲染过程
    2. 虚拟DOM
    3. 事件代理/绑定
  7. 首屏时间
  8. CSR/SSR

渲染页面流程

浏览器存储机制

重绘与回流

如何减少回流

大量请求时如何进行优化

WebPack

对WabPack的理解

plugin和loader的区别

常见优化方式

Babel

Vite和Wabpack的区别

TS

TS项目搭建

ts支持的基本步骤:

  1. 转义ts
  2. 识别.ts/.tsx/.d.ts文件
  3. 语法检测与提示(vscode默认支持,通过tsconfig.json检测类型给出提示)
  4. eslint实现代码规范(tslint已不推荐)

webpack如何支持ts

  1. 使用babel解析器
  2. 使用tsc解析器

babel配置

babel主要通过@babel/preset-typescript预设 编译TypeScript代码。

首先安装babel工具:

1
npm install @babel/core babel-loader @babel/preset-typescript

然后在webpack配置文件中进行如下配置

1
2
3
4
5
6
7
8
9
10
11
12
// webpack.config.js
module.exports = {
// ......other configure
module: {
rules: [
{
test: /\.ts$/,
use: ['babel-loader']
}
]
}
};

在babel配置文件中进行如下配置:

1
2
3
4
// .babelrc
{
"presets": ["@babel/preset-typescript"]
}

但babel有如下缺点:

  1. 并不会读取tsconfig.json,babel只会读取自己的配置文件
  2. 不支持类型检查,需要使用其他工具,如fork-ts-checker-webpack-plugin,该插件也是依赖tsc进行代码转换。当前版本的create-react-app的ts模板就是使用的这两个插件配合
  3. babel-loader的@babel/preset-typescript包并不完全支持ts所使用的先进es语法(如可选链)需要配合babel-preset
  4. 不支持import modelName = require('path'),推荐使用import moduleName from 'path'

TSC

使用ts-loader,该loader调用tsc编译ts代码,并会进行类型检测

使用如下命令安装:

1
npm install ts-loader

在webpack的配置文件中加入如下配置

1
2
3
4
5
6
7
8
9
10
11
12
// webpack.config.js
module.exports = {
// ......other configure
module: {
rules: [
{
test: /\.ts$/,
use: ['babel-loader', 'ts-loader']
}
]
}
};

使用ts-loader需要配置tsconfig.json

React

组件生命周期,哪些hooks替换生命周期

Diff算法

对于Fiber框架的理解

把整个不可中断的渲染过程变得可以终端了。使得渲染过程可以断点续传(类似于操作系统中的分时间片进行抢占式调度)

组件间通讯的方式

父组件与子组件间通信

  1. 父组件向子组件传递props:

    • 父组件:

      • class Parent extends React.Component {
            state = { name:"wang" }
            render() {
                return {
                    <div>
                        <Child name={this.state.lastName}></Child>
                    </div>
                }
            }
        }
        
        1
        2
        3
        4
        5
        6
        7

        - 子组件

        - ```jsx
        function Child(props) {
        return <div>{props.name}</div>
        }
  2. 子组件向父组件传递参数需要利用父组件回调

    • 父组件

      • class Parent extends React.Component {
            // 提供回调
            getChildMsg = (msg) => {
                console.log('接收到子组件数据', msg)
            }
            render() {
                return (
                    // 传递给子组件
                    <div>
                        子组件:<Child getMsg={this.getChildMsg} />
                    </div>
                )
            }
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16

        - 子组件

        - ```jsx
        class Child extends React.Component {
        states = {ChildMsg: 'React'}
        handleClick = () => {
        this.props.getMsg(this.state.childMsg)
        }
        render() {
        return (
        // 传递子组件
        <button onClick={this.handleClick}>传参</button>
        )
        }
        }
  3. 兄弟组件之间的通信需要借助状态提升:

    1. 将共享状态提升到最近的公共父组件
    2. 进行子到父通信
    3. 进行父到子通信
  4. 借助Context通信

    1. 调用React.createContext()创建ProviderConsumer两个组件

    2. 使用Provider组件作为父节点

    3. 设置Value属性,表示要传递的数据

    4. 效用Consumer组件接收数据

    5. 即Provider包裹父组件,Consumer被子组件包裹

      • 
        // 创建context
        const {Provider, Consumer} = React.getContext() 
        
        class App extends React.Component {
            render() {
                return (
                    // 使用Provider作为父节点
                    // 设置要传递的数据
                    <Provider value="pink">
                        <div className='app'>
                            <Node />
                        </div>
                    </Provider>
                )
            }
        }
        
        const Node = props => {
            return (
                <div className="node">
                    <subNode />
                </div>
            )
        }
        
        const SubNode = props => {
            return (
                <div className="subnode">
                    <Child />
                </div>
            )
        }
        
        const Child = props => {
            return <div className='child'>
                <!-- 使用consumer组件接受数据 -->
                <Consumer>
                    {
                        data => <span>我是子节点 -- {data}</span>
                    }
                </Consumer>
            </div>
        }
        
        ReactDOm.render(<App />, document.getElementById('root'))
        

React-Native

ReactNative的有点是什么

1、跨平台兼容性

使用React Native,您可以编写一次代码并多次部署到Android和iOS操作系统。对于创业公司来说,这样可以节省成本,并为程序员腾出时间完成其他重要任务。

2、React Native卓越性能

除了React Native外跨平台框架还有:Cordova、AppCan、APICloud、Phonegap、Ionic、Dcloud等,这些框架基本都是在一个WebView上进行渲染,也就是说他们的性能最多就是原生app中WebView的性能。而ReactNaitve是采用JS桥接加Native桥接两个方式合并起来的。React Native产出的并不是“网页应用”, 或者说“HTML5应用”,又或者“混合应用”。 最终产品是一个真正的移动应用,从使用感受上和用Objective-C或Java编写的应用相比几乎是无法区分的。 React Native所使用的基础UI组件和原生应用完全一致。 你要做的就是把这些基础组件使用JavaScript和React的方式组合起来。

3、社区力量

有着Facebook的支撑,相信会发展的很好。目前github的星数已经快7 万了,还有很多开源的组件和框架可以使用。

4、学习成本低

用的是react的框架和css的布局,有前端开发经验降低了不少学习成本,也大大减少了代码量。但是对于iOS或者安卓开发者来说,刚开始接触的时候,得接受一些思想上的转变。

5、调试方便

ipa安装好之后,就不需要频繁编译了,只需要reload一下!

把js代码从云服务器下载下来就可以呈现改变代码后的效果。而且RN支持hotReload,在调试界面的时候非常方便,修改代码之后保存,界面就自动跟着变化,这一点在调试的时候很方便,不过有时候有点慢,需要reload。Chrome在线调试也可以打断点,看日志。

6、热更新

频繁的app升级会让用户很烦,毕竟繁多的业务迭代,每次都通过APP审核,也算是噩梦。而且苹果的审核也很麻烦。现在很多大型app都使用了RN,通过微软提供的codepush可以很简单的实现热更新。

如何提升Cookie的安全性

  1. 对Cookie中存储的铭感内容进行加密
  2. 设置HttpOnly为true
    1. 该属性值的作用是防止Cookie值被脚本读取
    2. 但HttpOnly属性只是增加了攻击者的难度,Cookie盗窃的威胁并没有彻底消除,因为Cookie还是有可能在传递的过程中被监听捕获后信息泄露
  3. 设置Secure为true
    1. 给Cookie设置该属性时,只有在https协议下访问时,浏览器才会发送该cookie
    2. 把cookie设置为secure,只保证cookie与Web服务器之间的数据传输过程加密,而保存在本地的cookie文件并不加密。如果想让本地cookie也加密,得自己加密数据
  4. 给Cookie设置有效期
    1. 如果不设置有效期,万一用户获取到用户的Cookie后,就可以一直使用用户身份登录。
    2. 在设置Cookie认证的时候,需要加入两个时间,一个是“即使一直在活动,也要失效”的时间,一个是“长时间不活动的失效时间”,并在Web应用中,首先判断两个时间是否已超时,再执行其他操作。

业务

说一说用户登录发生了什么

  1. 用户点击登陆时会带着账号密码(加密)调用后端的登录接口
  2. 后端接口接收到用户信息后根据账号查找数据库得到用户信息,如果没有或信息不匹配,则返回错误;如果验证通过则向前端返回一个token
  3. 前端拿到token后将其保存在Vuex或localStorage中并提示登录成功进行页面跳转
  4. 前端每次页面路由都需要验证token是否存在,这一操作通常封装到路由守卫中
  5. 在需要向后端发送请求时,会将token包含在请求头中一起发送给后端,这一操作通常封装在请求拦截器中
  6. 后端受到请求会判断请求头中是否包含token,且token是否失效,如果失效则返回错误信息
  7. 前端受到错误信息则清空存储的token,并提示身份过期跳转到登录界面,验证token一般封装在响应拦截器中,跳转到登录页面时需要携带当前跳转路径,登录成功后应该返回该路径

说一说前后端分别如何实现用户鉴权

评论