常见知识点补充

402 阅读40分钟

一、HTML

1.1 如何理解 HTML 中的语义化标签

  1. 语义化标签是一种写 HTML 标签的方法论
  2. 实现方法是遇到标题就用 h1 到 h6,遇到段落就用 p,遇到文章就用 article,主要内容用 main,侧边栏用 aside,导航用 nav......
  3. 明确了 HTML 的书写规范。
  4. 优点:
    • 适合搜索引擎检索
    • 适合人类阅读,利于团队维护

1.2 HTML5 有哪些新标签

  • 文章相关:header main footer nav section article figure mark
  • 多媒体相关:video audio svg canvas
  • 表单相关:type=email type=tel PS:具体详见这里

1.3 canvas 和 svg 的区别

  1. Canvas 主要是用笔刷来绘制 2D 图形的。
  2. SVG 主要是用标签来绘制不规则矢量图的。
  3. 相同点:都是主要用来画 2D 图形的。
  4. 不同点:
    • Canvas 画的是位图,SVG画的是矢量图
    • SVG 节点过多时渲染慢,Canvas 性能更好一点,但写起来更复杂。
    • SVG 支持分层和事件,Canvas 不支持,但可以用库实现。

二、CSS

2.1 BFC

  1. BFC 是块级格式化上下文

  2. BFC 触发条件:

    • 浮动元素(元素的 float 不是 none)
    • 绝对定位元素(元素的 position 为 absolute 或 fixed)
    • 行内块(inline-block)元素
    • overflow 值不为 visible 的块元素
    • 弹性元素(display 为 flex 或 inline-flex 元素的直接子元素)
  3. 解决了什么问题:

    • 清除浮动
    • 防止上下 margin 合并
  4. 缺点:

    • 有副作用
  5. 解决缺点:

    • 使用最新的 display:flow-root 来触发 BFC 就没有副作用了。

2.2 实现垂直居中

2.2.1 父元素没有写死高度

当父元素没有写死高度时,子元素垂直居中时比较容易实现的,给父元素上下 padding 即可。

<div class="parent">
  <div class="child">
    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Facilis iusto praesentium dolorem qui, accusamus labore fugit nulla quibusdam eius explicabo minima libero, obcaecati, odio rerum quae inventore vel quo totam!
  </div>
</div>

.parent {
  border: 5px solid red;
  padding: 50px 0;
}

.child {
  border: 5px solid green;
}

2.2.2 父元素设置固定高度

  1. flex
<div class="parent">
  Lorem ipsum dolor sit amet, consectetur adipisicing elit. Distinctio rerum hic praesentium veniam fugiat, vel dolor cumque molestiae labore. Assumenda consequuntur ratione temporibus, voluptatem quidem tenetur totam mollitia accusamus illum!
</div>

.parent {
  border: 1px solid red;
  height: 400px;
  display: flex;
  align-items: center;
}

2. absolute + translate-50%

<div class="parent">
  <div class="child">
    Lorem ipsum dolor sit amet, consectetur adipisicing elit. Tenetur amet ipsa modi, iure rem iusto esse sed minima cupiditate, molestias deleniti recusandae. Recusandae quam delectus id sint
    eius repellendus eveniet.
  </div>
</div>

.parent {
  border: 1px solid red;
  height: 400px;
  position: relative;
}
.child {
  border: 1px solid green;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}

3. absolute + 负 margin

.parent {
  height: 400px;
  position: relative;
}

.child {
  width: 300px;
  height: 200px;
  position: absolute;
  left: 50%;
  top: 50%;
  margin-left: -150px;    /* 子元素 width 的一半 */
  margin-top: -100px;     /* 子元素 height 的一半 */
}

4. absolute + margin:auto

.parent {
  height: 400px;
  position: relative;
}

.child {
  width: 300px;
  height: 200px;
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  margin: auto;
}

5. table 标签

<table>
  <tr>
    <td>
      Lorem ipsum dolor sit amet, consectetur adipisicing elit. Tempora harum voluptates quae velit eveniet, accusamus, quas ratione placeat! Nisi perferendis facere, error sed possimus molestias
      et. Quas accusantium, maiores aliquid?
    </td>
  </tr>
</table>

table {
  border: 1px solid red;
  height: 400px;
}

td {
  border: 1px solid green;
}

6. div 模拟 table 标签

<div class="parent">
  <div class="child">
    尼采于是又回到思想的起源与苏格拉底以前的观点。苏格拉底以前的观点消除了最终的原因,以保持他们想像出的原则的永恒性。惟有无目的力量,赫拉克利特的“游戏”是永恒的。尼采的全部努力就是指出变化之中有规律,必然性中有偶然。“儿童就是纯真与忘记,重新开始,就是一种游戏,一个自己转动的轮子,就是最初的运动,说‘是’的神圣天赋。”世界是神圣的,因为世界是无报酬的,因而惟有艺术同样是无报酬的而能使世界担忧。没有任何判断可阐述世界,但艺术可以教我们重复它,犹如世界在永恒的运动中重复自己一样。最初的海洋在同一个海滩上重复着相同的话语,抛掷着为活着而感到吃惊的相同的生物。然而对于同意自己返回与一切都返回,而他自己又在对不断地重复的人来说,他分享了世界的神圣性。
  </div>
</div>

.parent {
  border: 5px solid red;
  height: 400px;
  display: table;
}

.child {
  border: 5px solid green;
  display: table-cell;
  vertical-align: middle;
}

7. 高度为 100% 的 after、before + inline-block

<div class="parent">
  <span class="before">
  <div class="child">
    尼采于是又回到思想的起源与苏格拉底以前的观点。苏格拉底以前的观点消除了最终的原因,以保持他们想像出的原则的永恒性。惟有无目的力量,赫拉克利特的“游戏”是永恒的。尼采的全部努力就是指出变化之中有规律,必然性中有偶然。“儿童就是纯真与忘记,重新开始,就是一种游戏,一个自己转动的轮子,就是最初的运动,说‘是’的神圣天赋。”世界是神圣的,因为世界是无报酬的,因而惟有艺术同样是无报酬的而能使世界担忧。没有任何判断可阐述世界,但艺术可以教我们重复它,犹如世界在永恒的运动中重复自己一样。最初的海洋在同一个海滩上重复着相同的话语,抛掷着为活着而感到吃惊的相同的生物。然而对于同意自己返回与一切都返回,而他自己又在对不断地重复的人来说,他分享了世界的神圣性。
  </div>
  <span class="after">
</div>

.parent {
  border: 5px solid red;
  height: 400px;
  text-align: center;
}

.parent .before,
.parent .after {
  content: "";
  outline: 3px solid blue;
  display: inline-block;
  height: 100%;
  vertical-align: middle;
}

.child {
  border: 5px solid green;
  display: inline-block;
  width: 300px;
  vertical-align: middle;
}

优化:高度为 100% 的伪元素 + display:inline-block

<div class="parent">
  <div class="child">
    尼采于是又回到思想的起源与苏格拉底以前的观点。苏格拉底以前的观点消除了最终的原因,以保持他们想像出的原则的永恒性。惟有无目的力量,赫拉克利特的“游戏”是永恒的。尼采的全部努力就是指出变化之中有规律,必然性中有偶然。“儿童就是纯真与忘记,重新开始,就是一种游戏,一个自己转动的轮子,就是最初的运动,说‘是’的神圣天赋。”世界是神圣的,因为世界是无报酬的,因而惟有艺术同样是无报酬的而能使世界担忧。没有任何判断可阐述世界,但艺术可以教我们重复它,犹如世界在永恒的运动中重复自己一样。最初的海洋在同一个海滩上重复着相同的话语,抛掷着为活着而感到吃惊的相同的生物。然而对于同意自己返回与一切都返回,而他自己又在对不断地重复的人来说,他分享了世界的神圣性。
  </div>
</div>

.parent {
  border: 5px solid red;
  height: 400px;
  text-align: center;
}

.parent:before,
.parent:after {
  content: "";
  outline: 3px solid blue;
  display: inline-block;
  height: 100%;
  vertical-align: middle;
}

.child {
  border: 5px solid green;
  display: inline-block;
  width: 300px;
  vertical-align: middle;
}

2.3 CSS 选择器优先级如何确定

2.3.1 类型

  1. 通配符选择器(*) 0 0 0 0
  2. 元素名、伪元素(::before ::after) 0 0 0 1
  3. 属性、类、伪类(:active :nth-child) 0 0 1 0
  4. id选择器 0 1 0 0
  5. 内联样式(style="") 1 0 0 0
  6. !important 最高权重

2.3.2 范围

  1. 内部样式
  2. 外部样式
  3. 内联样式

2.4 清除浮动

  1. 给父元素加上 .clearfix
.clearfix:after {
  content: '',
  display: block; /* 或者 table */
  clear: both;
}
.clearfix {
  zoom: 1; /*  IE 兼容 */
}

2. 给父元素加上 overflow: hidden

2.5 两种盒模型(box-sizing)的区别

  1. 第一种盒模型 content-box,即 width 指定的是 content 区域宽度,而不是实际宽度,公式为

实际宽度 = width + padding + border

  1. 第二种盒模型为 border-box,即 width 指定的是左右边框外侧的距离,公式为

实际宽度 = width

相同点都是用来指定宽度的,不同点是 border-box 更好用。

3. JS

3.1 JS 的数据类型

字符串、数字、布尔、undefined、null、大整数、符号、对象

3.2 原型链

  1. 什么是原型链

假设有个一个数组对象 a=[],a 有一个隐藏属性,叫做__proto__,这个属性指向 Array.prototype,即

a.proto === Array.prototype

此时,我们说 a 的原型是 Array.prototype,记为 x,x 也有一个隐藏属性,指向 Object.prototype,即

x.proto === Object.prototype

这样一来,a 就有两层原型:

  1. a 的原型是 Array.prototype
  2. a 的原型的原型是 Object.prototype

于是就通过隐藏属性__proto__形成了一个链条:

a ===> Array.prototype ===> Object.prototype

  1. 如何修改原型

    • x.proto = 原型 (不推荐)
    • const x = Object.create(原型)
    • const x = new 构造函数() // 会导致 x.proto === 构造函数.prototype
  2. 解决了什么问题

在没有 Class 的情况下实现继承,以上面的原型链为例:

  1. a 是 Array 的实例,a 拥有 Array.prototype 里的属性
  2. Array 继承了 Object
  3. a 是 Object 的简介实例, a 拥有 Object.prototype 里的属性

这样一来,a 就既拥有 Array.prototype 里的属性,又拥有 Object.prototype 里的属性

  1. 优点

简单、优雅

  1. 缺点

跟 class 相比,不支持私有属性

  1. 如何解决缺点

使用 ES6 的 class

3.3 this

var length = 4
function callback() {
  console.log(this.length) // => 打印出什么? 4
}
const obj = {
  length: 5,
  method(callback) {
    callback() // callback.call(undefined)
  }
}
obj.method(callback, 1, 2)

3.4 jS 的 new 做了什么

  1. 创建临时对象/新对象
  2. 绑定原型
  3. 执行 this = 临时对象
  4. 执行构造函数
  5. 返回临时对象

3.5 JS 的立即执行函数

  1. 是什么

声明一个匿名函数,然后立即执行它。这种做法就是立即执行函数。

  1. 怎么做
(function(){alert('我是匿名函数')} ()) // 用括号把整个表达式包起来
(function(){alert('我是匿名函数')}) () // 用括号把函数包起来
!function(){alert('我是匿名函数')}() // 求反,我们不在意值是多少,只想通过语法
+function(){alert('我是匿名函数')}()
-function(){alert('我是匿名函数')}()
~function(){alert('我是匿名函数')}()
void function(){alert('我是匿名函数')}()
new function(){alert('我是匿名函数')}()
var x = function(){return '我是匿名函数'}()

3. 解决了什么问题

在 ES6 之前,只能通过它来 创建局部作用域

  1. 优点

兼容性好。

  1. 缺点

丑。

  1. 怎样解决缺点

使用 ES6 的 block + let 语法,即

{
  let a = '我是局部变量'
  console.log(a)  // 能读取到 a
}
console.log(a) // 找不到 a

3.5 JS 的闭包是什么?怎么用?

  1. 是什么

闭包是 JS 的一种语法特性。

闭包 = 函数 + 自由变量

  1. 怎么做
var die2 = function {
  let lives = 3
  return () => { lives -=1 }
}

// game code
die2() // lives -= 1

3. 解决了什么问题 * 避免污染全局变量。(因为用的是全局变量) * 提供对局部变量的间接访问。(因为只能 count += 1 不能 count -= 1) * 维持变量,使其不被垃圾回收。

  1. 优点

简单、好用

  1. 缺点

闭包使用不当可能导致内存泄漏。

3.6 JS 如何实现类

  1. 使用原型
function Dog(name) {
  this.name = name
  this.legsNumber = 4
}
Dog.prototype.kind = "狗"
Dog.prototype.say = function () {
  console.log(`汪汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`)
}
Dog.prototype.run = function () {
  console.log(`${this.legsNumber}条腿跑起来。`)
}
const d1 = new Dog("啸天") // Dog 函数就是一个类
d1.say()

2. 使用 class

class Dog {
  kind = "狗" // 等价于在 constructor 里写 this.kind = '狗'
  constructor(name) {
    this.name = name
    this.legsNumber = 4
    // 思考:kind 放在哪,放在哪都无法实现上面的一样的效果
  }
  say() {
    console.log(`汪汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`)
  }
  run() {
    console.log(`${this.legsNumber}条腿跑起来。`)
  }
}
const d1 = new Dog("啸天")
d1.say()

3.7 JS 实现继承

  1. 使用原型链
function Animal(legsNumber) {
  this.legsNumber = legsNumber
}
Animal.prototype.kind = "动物"
function Dog(name) {
  this.name = name
  Animal.call(this, 4) // 关键代码1
}
Dog.prototype.__proto__ = Animal.prototype // 关键代码2,但这句代码被禁用了,怎么办,见如下代码
Dog.prototype.kind = "狗"
Dog.prototype.say = function () {
  console.log(`汪汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`)
}
const d1 = new Dog("啸天") // Dog 函数就是一个类
console.dir(d1)

// 被 ban 掉后使用如下三行代码
var f = function () {}
f.prototype = Animal.prototype
Dog.prototype = new f()

  1. 使用 class
class Animal {
  constructor(legsNumber) {
    this.legsNumber = legsNumber
  }
  run() {}
}
class Dog extends Animal {
  constructor(name) {
    super(4)
    this.name = name
  }
  say() {
    console.log(`汪汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`)
  }
}

3.8 手写节流、防抖

3.8.1 节流

一般用在点击按钮时只希望一段时间内触发一次事件。

单位时间内,频繁触发事件,只执行一次。

  • 典型场景:高频事件快速点击、鼠标滑动、resize 事件、scroll 事件
  • 代码思路:利用定时器,等定时器执行完毕,才开启定时器。
const throttle = (fn, time) => {
  let flag = false
  return (...args) => {
    if (flag) return
    fn.call(undefined, ...args)
    flag = true
    setTimeout(() => {
      flag = false
    }, time)
  }
}

// 简洁版,删掉 flag,直接使用 timer 代替
const throttle = (fn, time) => {
let timer = null
  return (...args) => {
    if (timer) return
    fn.call(undefined, ...args)
    timer = setTimeout(() => {
      timer = null
    }, time)
  }
}

// 使用
const f = throttle(() => console.log('hi'), 3000)
f() // 打印 hi
f() // 技能冷却中

3.8.2 防抖

一般用在一段时间内频繁触发某操作时只希望在这段时间内没有触发时才执行此函数。

单位时间内、频繁触发事件,只执行最后一次。

  • 典型场景:搜索框输入
  • 代码思路:利用定时器,每次触发先清掉之前的定时器。
const debounce = (fn, time) => {
  let timer = null
  return (...args) => {
    timer && clearTimeout(timer)
    timer = setTimeout(() => {
      fn.call(undefined, ...args)
      timer = null
    }, time)
  }
}

3.9 手写发布订阅

const eventHub = {
  map: {
    // click: [f1, f2]
  },
  on: (name, fn) => {
    //  同时对 eventHub.map[name] 进行读写操作,会改变指针
    eventHub.map[name] = eventHub.map[name] || []
    eventHub.map[name].push(fn)
  },
  emit: (name, data) => {
    const q = eventHub.map[name]
    if (!q) return
    q.map(f => f.call(null, data))
    return undefined
  },
  off: (name, fn) => {
    // 只对 eventHub.map[name] 进行读操作
    const q = eventHub.map[name]
    if (!q) return
    const index = q.indexOf(fn)
    if (index < 0) return
    q.splice(index, 1)
  }
}

eventHub.on('click', console.log)
eventHub.on('click', console.error)

setTimeout(() => {
  eventHub.emit('click', 'frank')
}, 3000)



// 类方式实现
class EventHub {
  map = {}
  on(name, fn) {
    this.map[name] = this.map[name] || []
    this.map[name].push(fn)
  }
  emit(name, data) {
    const fnList = this.map[name] || []
    fnList.forEach(fn => fn.call(undefined, data))
  }
  off(name, fn) {
    const fnList = this.map[name] || []
    const index = fnList.indexOf(fn)
    if (index < 0) return
    fnList.splice(index, 1)
  }
}
// 使用
const e = new EventHub()
e.on('click', name => {
  console.log('hi ' + name)
})
e.on('click', name => {
  console.log('hello ' + name)
})
setTimeout(() => {
  e.emit('click', 'frank')
}, 3000)

3.10 手写 Ajax(async Javascript and XML(JSON))

const ajax = (method, url, data, success, fail) => {
  var reqeust = new XMLHttpRequest()
  reqeust.open(method, url) // request.open('GET', '/xxx)
  reqeust.onreadystatechange = function () {
    if (reqeust.readyState === 4) {
      if ((reqeust.status >= 200 && reqeust.status < 300) || reqeust.status == 304) {
        success(reqeust)
      } else {
        fail(reqeust)
      }
    }
  }
  // get 发送体为空,post 发送体 request.send('{"name": "frank"}')
  reqeust.send()
}

3.11 手写简化版 Promise

class Promise2 {
  #status = 'pending'
  constructor(fn) {
    this.q = []
    const resolve = data => {
      this.#status = 'fulfilled'
      const f1f2 = this.q.shift()
      if (!f1f2 || !f1f2[0]) return
      const x = f1f2[0].call(undefined, data)
      if (x instanceof Promise2) {
        x.then(
          data => {
            resolve(data)
          },
          reason => {
            reject(reason)
          }
        )
      } else {
        resolve(x)
      }
    }
    const reject = reason => {
      this.#status = 'rejected'
      const f1f2 = this.q.shift()
      if (!f1f2 || !f1f2[1]) return
      const x = f1f2[1].call(undefined, reason)
      if (x instanceof Promise2) {
        x.then(
          data => {
            resolve(data)
          },
          reason => {
            reject(reason)
          }
        )
      } else {
        resolve(x)
      }
    }
    fn.call(undefined, resolve, reject)
  }
  then(f1, f2) {
    this.q.push([f1, f2])
  }
}

const p = new Promise2(function (resolve, reject) {
  setTimeout(function () {
    resolve('成功')
    // reject('出错')
  }, 3000)
})

p.then(data => console.log(data), r => console.error(r))

3.12 手写 Promise.all

要点:

  • 要在 Promise 上写而不是在原型上写
  • 知道 all 的参数(Promise 数组)和返回值(新 Promise 对象)
  • 知道要用数组来记录结果
  • 知道只要有一个 reject 就整体 reject
Promise.myAll = function (list) {
  const results = []
  let count = 0
  return new Promise((resolve, reject) => {
    list.map((promise, index) => {
      promise.then(
        r => {
          results[index] = r
          count += 1
          if (count >= list.length) resolve(results)
        },
        reason => {
          reject(reason)
        }
      )
    })
  })
}

3.13 手写深拷贝

  1. JSON 方式
const b = JSON.parse(JSON.stringify(a))

缺点:

  • 不支持 Date、正则、undefined、函数等数据
  • 不支持引用(即环状结构)
  1. 递归

要点:

  • 递归
  • 判断类型
  • 检查环
  • 不拷贝原型上的属性
function deepClone(a, cache) {
  if (!cache) {
    cache = new WeakMap() // 缓存不能全局,最好临时创建并递归传递
  }
  // 不考虑 iframe
  if (a instanceof Object) {
    if (cache.get(a)) return cache.get(a)
    let result = undefined
    // object
    // 判断是不是函数
    if (a instanceof Function) {
      // 不能 100% 拷贝,还有 Generator 和 async 函数
      if (a.prototype) {
        // 有 prototype 就是普通函数
        result = function () {
          return a.apply(this, arguments)
        }
      } else {
        // 箭头函数
        result = (...args) => {
          return a.call(undefined, ...args)
        }
      }
    } else if (a instanceof Array) {
      result = []
    } else if (a instanceof Date) {
      result = new Date(a - 0)
    } else if (a instanceof RegExp) {
      result = new RegExp(a.source, a.flags)
    } else {
      result = {}
    }
    cache.set(a, result)
    for (let key in a) {
      if (a.hasOwnProperty(key)) {
        result[key] = deepClone(a[key], cache)
      }
    }
    return result
  } else {
    // string number boolean null undefined symbol bigint
    return a
  }
}

const a = {
number:1, bool:false, str: 'hi', empty1: undefined, empty2: null,
array: [
{name: 'frank', age: 18},
{name: 'jacky', age: 19}
],
date: new Date(2000,0,1,20,30,0),
regex: /\.(j|t)sx/i,
obj: { name:'frank', age: 18},
f1: (a, b) => a + b,
f2: function(a, b) { return a + b }
}
a.self = a

const b = deepClone(a)
b.self === b  // true
b.self === 'hi'
a.self !== 'hi'  // true

3.14 手写数组去重

  1. 使用计数排序的思路,缺点是只支持字符串
  2. 使用 Set(面试禁用,太简单)
  3. 使用 Map,缺点是兼容性差一点(IE6 不支持)
var a = [1, 1, 2, 3, 3, 3, '1', ,]
// 下面这两行代码禁用
// var b = Array.from(new Set(a))
// var b = [...new Set(a)]
function uniq(a) {
  const result = new Map()
  for (let i = 0; i < a.length; i++) {
    let number = a[i]
    if (!number) continue
    if (result.has(number)) continue
    result.set(number, true)
  }
  return [...result.keys()]
}

console.log(uniq(a))

4 DOM 相关内容

4.1 请简述 DOM 事件模型

  1. 先经历从上到下的捕获阶段,再经历从下到上的冒泡阶段。
  2. addEventListener('click', fn, true/fasle) true 为捕获,false 为冒泡。
  3. 可以使用 event.stopPropagation() 来阻止捕获或冒泡。

4.2 手写事件委托

  1. 方式一
ul.addEventListener('click', function(e) {
  // e.target 事件真实触发的对象 span
  // e.currentTarget 事件监听的对象 ul
  if (e.target.tagName.toLowerCase() === 'li') {
    fn()  // 执行某个函数
  }
})

bug 在于,如果用户点击的是 li 里面的 span,就没法触发 fn,这显然不对。 好处:

  1. 节省监听器:如果有 100 个 li 元素,只需要监听 ul 即可。
  2. 实现动态监听:当 ul 里面没有任何内容时,监听 ul 可以当里面内容发生变化时代码不发生变化。

坏处:

  • 调试比较复杂,不容易确定监听者。

解决坏处:

  • 使用 Chrome 中自带的 event listener。
  1. 方式二
function delegate(element, eventTarget, selector, fn) {
  element.addEventListener(eventTarget, e => {
    let el = e.target
    while (!el.matches(selector)) {
      if (element === el) {
        el = null
        break
      }
      el = el.parentNode
    }
    el && fn.call(el, e, el)
  })
  return element
}

delegate(ul, 'click', 'li', f1)

4.3 手写 div 拖拽事件

要点:

  1. 注意监听范围,不能只监听 div
  2. 不要使用 drag 事件,很难用
  3. 使用 transform 会比 top/left 性能更好,因为可以避免 reflow 和 repaint
// HTML
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>JS Bin</title>
</head>
<body>
<div id="xxx"></div>
</body>
</html>

// JS
var dragging = false
var position = null

xxx.addEventListener('mousedown', function (e) {
  dragging = true
  position = [e.clientX, e.clientY]
})

document.addEventListener('mousemove', function (e) {
  if (!dragging) return
  console.log('hi')
  const x = e.clientX
  const y = e.clientY
  const deltaX = x - position[0]
  const deltaY = x - position[1]
  const left = parseInt(xxx.style.left || 0)
  const top = parseInt(xxx.style.top || 0)
  xxx.style.left = left + deltaX + 'px'
  xxx.style.top = top + deltaY + 'px'
  position = [x, y]
})
document.addEventListener('mouseup', function (e) {
  dragging = false
})

5. HTTP 相关内容

5.1 GET 和 POST 区别

区别一:幂等性

  1. 由于 GET 是读,POST 是写,所以 GET 是幂等的,POST 不是幂等的。
  2. 由于 GET 是读,POST 是写,所以用浏览器打开网页会发送 GET 请求,想要 POST 打开网页要用 form 标签。
  3. 由于 GET 是读,POST 是写,所以 GET 打开的页面刷新是无害的,POST 打开的 页面刷新需要确认。
  4. 由于 GET 是读,POST 是写,所以 GET 结果会被缓存,POST 结果不会被缓存。
  5. 由于 GET 是读,POST 是写,所以 GET 打开的页面可被书签收藏,POST 打开的 不行。

区别二:请求参数

  1. 通常,GET 请求参数放在 url 里,POST 请求数据放在 body(消息体)里。(注意)
  2. GET 比 POST 更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信 息。
  3. GET 请求参数放在 url 里是有长度限制的,而 POST 放在 body 里没有长度限制。

区别三:TCP packet

  1. GET 产生一个 TCP 数据包;POST 产生两个或以上 TCP 数据包。

5.2 HTTP 缓存方案

image.png

HTTP 1.1 时代,强缓存中有一个 Cache-Control:记录最大过期时间Etag:标志,在这一段时间内,使用的都是缓存中的内容,不会重新发送请求,当超过这个时间后,浏览器进行内容协商,根据里面的 If-None-Match 与强缓存中的 Etag 是否相同 来判断是否需要重新请求,如果响应状态码是 304,表示可以继续使用缓存中的内容,当状态码是 200,表示需要重新发送请求。

HTTP 1.0 时代,强缓存中会存过期时间(Expires) 和最后修改时间(Last-Modified),内容协商(弱缓存)中存修改时间(If-Modified-Since)和响应状态码,浏览器根据时间是否超过过期时间来发送不同的状态码。

1.0 时代出现2个 bug:

  • 用户时间错乱
  • 1s中可能修改多次,到底是否需要修改

5.3 HTTP 与 HTTPS 的区别

HTTPS = HTTP + SSL/TLS(安全层)

  1. HTTP 是明文传输的,不安全;HTTPS 是加密传输的,非常安全。
  2. HTTP 使用 80 端口,HTTPS 使用 443 端口。
  3. HTTP 较快,HTTPS 较慢。
  4. HTTPS 的证书一般需要购买(但也有免费的),HTTP 不需要证书。

HTTPS 细节

5.4 TCP 三次握手与四次挥手

建立 TCP 连接时 server 与 client 会经历三次握手

  1. 浏览器向服务器发送 TCP 数据:SYN(seq=x)
  2. 服务器向浏览器发送 TCP 数据:ACK(seq=x+1) SYN(y)
  3. 浏览器向服务器发送 TCP 数据:ACK(seq=y+1)

关闭 TCP 连接时 server 与 client 会经历四次挥手

  1. 浏览器向服务器发送 TCP 数据:FIN(seq=x)
  2. 服务器向浏览器发送 TCP 数据:ACK(seq=x+1)
  3. 服务器向浏览器发送 TCP 数据:FIN(seq=y)
  4. 浏览器向服务器发送 TCP 数据:ACK(seq=y+1)

为什么 2、3 步骤不合并起来呢?

答案:2、3 中间服务器很可能还有数据要发送,不能提前发送 FIN。

5.5 同源策略和跨域

  1. 同源策略是什么

如果两个 URL 的协议、端口和域名都完全一致的话,则这两个 URL 是同源的。

www.baidu.com/s www.baidu.com/sdafssd

  1. 同源策略怎么做

只要在浏览器中打开页面,就默认遵守同源策略。

  1. 优点

保证用的隐私安全和数据安全。

  1. 缺点

很多时候,前端需要访问另一个域名的后端接口,会被浏览器阻止其获取响应。比如甲站点通过 AJAX 访问乙站点的 /money 查询余额接口,请求会被发出,但是响应会被浏览器屏蔽。

  1. 怎样解决缺点

使用跨域手段。

  1. JSONP

网页通过添加一个 <script> 标签的方式,向服务端请求数据,为了得到请求数据,地址的参数中通常会包含一个包 callback 的参数,服务端在接收到这样的请求后,会调用回调函数的 JavaScript 代码,并且将需要传递的数据以参数的形式传入 callback。

       a. 甲站点利用 script 标签可以跨域的特性,向乙站点发送 get 请求。
       b. 乙站点后端改造 JS 文件的内容,将数据传进回调函数。
       c. 甲站点通过回调函数拿到乙站点的数据。
  • 优点:改动代码很少,只需要改动 js 内容;兼容性好
  • 缺点:只支持 get 请求,不支持 post 请求;若 jsonp 跨域的网址恶意攻击,可能会对网页存在安全风险。
  1. CORS

a. 对于简单请求,乙站点在响应头里添加 Access-Control-Allow-Origin: http://甲站点 即可。

b. 对于复杂请求,如 PATCH,乙站点需要:

  • i. 响应 OPTIONS 请求,在响应中添加如下的响应头

    Access-Control-Allow-Origin: https://甲站点

    Access-Control-Allow-Methods: POST, GET, OPTIONS, PATCH

    Access-Control-Allow-Headers: Content-Type

  • ii. 响应 POST 请求,在响应中添加 Access-Control-Allow-Origin 头。

c. 如果需要附带身份信息,JS 中需要在 AJAX 里设置 xhr.withCredentials = true 。

  1. Nginx 代理 / Node.js 代理 a. 前端 ⇒ 后端 ⇒ 另一个域名的后端

5.6 Session、Cookie、LocalStorage、SessionStorage 的区别

  • Cookie V.S. LocalStorage
  1. 主要区别是 Cookie 会被发送到服务器,而 LocalStorage 不会
  2. Cookie 一般最大 4k,LocalStorage 可以用 5Mb 甚至 10Mb(各浏览器不 同)
  • LocalStorage V.S. SessionStorage
  1. LocalStorage 一般不会自动过期(除非用户手动清除)
  2. SessionStorage 在回话结束时过期(如关闭浏览器之后,具体由浏览器自行 决定)
  • Cookie V.S. Session
  1. Cookie 存在浏览器的文件里,Session 存在服务器的文件里
  2. Session 是基于 Cookie 实现的,具体做法就是把 SessionID 存在 Cookie 里

5.7 HTTP/1.1 和 HTTP/2 的区别

  1. HTTP/2 使用了二进制传输,而且将 head 和 body 分成来传输;HTTP/1.1 是字 符串传输。
  2. HTTP/2 支持多路复用,HTTP/1.1 不支持。多路复用简单来说就是一个 TCP 连接 从单车道(不是单行道)变成了几百个双向通行的车道。
  3. HTTP/2 可以压缩 head,但是 HTTP/1.1 不行。
  4. HTTP/2 支持服务器推送,但 HTTP/1.1 不支持。(实际上没多少人用)

6 TS 相关内容

6.1 TS 与 JS 的区别

  1. 语法层面:TypeScript = JavaScript + Type(TS 是 JS 的超集)
  2. 执行环境层面:浏览器、Node.js 可以直接执行 JS,但不能执行 TS(Deno 可以 执行 TS)
  3. 编译层面:TS 有编译阶段,JS 没有编译阶段(只有转译阶段和 lint 阶段)
  4. 编写层面:TS 更难写一点,但是类型更安全
  5. 文档层面:TS 的代码写出来就是文档,IDE 可以完美提示。JS 的提示主要靠 TS

6.2 any、unknown、never

  1. any VS unknown

二者都是顶级类型(top type),任何类型的值都可以赋值给顶级类型变量:

let foo: any = 123 // 不报错 let bar: unkonw = 134 // 不报错

但是 unknown 比 any 的类型检查更严格,any 是什么类型都不做检查,unknown 要求类型收窄:

const value: unknown = "Hello World";
const someString: string = value;
// 报错:Type 'unknown' is not assignable to type 'string'.(2322)

const value: unknown = "Hello World";
const someString: string = value as string; // 不报错

如果改成 any,基本在哪都不报错。所以能用 unknown 就优先使用 unknown,类型更安全一点。

  1. never

never 是底类型,表示不应该出现的类型,这里有一个尤雨溪给出的例子:

interface A {
  type: "a"
}
interface B {
  type: "b"
}
type All = A | B
function handleValue(val: All) {
  switch (val.type) {
    case "a":
      // 这里 val 被收窄为 A
      break
    case "b":
      // val 在这里是 B
      break
    default:
      // val 在这里是 never
      const exhaustiveCheck: never = val
      break
  }
}

6.3 type 和 interface 区别

  1. 组合方式:interface 使用 extends 来实现继承,type 使用 & 来实现联合类型。
  2. 扩展方式:interface 可以重复声明用来扩展,type 一个类型只能声明一次
  3. 范围不同:type 适用于基本类型,interface 一般不行。
  4. 命名方式:interface 会创建新的类型名,type 只是创建类型别名,并没有新创建 类型。

具体区别看官方文档

6.4 TS 工具类型 Partial、Required 等的作用和实现

  1. 英翻中

    • a. Partial 部分类型
    • b. Required 必填类型
    • c. Readonly 只读类型
    • d. Exclude 排除类型
    • e. Extract 提取类型
    • f. Pick/Omit 排除 key 类型
    • g. ReturnType 返回值类型

7. vue2 相关内容

7.1 生命周期钩子函数

  1. create x 2 (before + ed) - SSR多次渲染问题
  2. mount x 2
  3. update x 2
  4. destroy x 2
  5. activated
  6. deactivated
  7. errorCaptured

请求放在 mounted 里面,因为放在其他地方都不合适。

7.2 Vue2 组件间通信方式

  1. 父子组件:使用 props和事件 进行通信

  2. 爷孙组件:

    • 使用两次父子组件间通信来实现
    • 使用 provide + inject 来通信
  3. 任意组件:使用 eventBus = new Vue() 来通信

  • 主要 API 是 eventBus.oneventBus.on 和 eventBus.emit
  1. 任意组件:使用 Vuex 通信(Vue3 可用 Pinia 代替 Vuex)

7.3 Vuex 相关内容

  1. Vuex 是一个专为 Vue.js 应用程序开发的状态管理库
  2. 说出核心概念的名字和作用:store/State/Getter/Mutation/Action/Module
    • store 是个大容器,包含以下所有内容
    • State 用来读取状态,带有一个 mapState 辅助函数
    • Getter 用来读取派生状态,附有一个 mapGetters 辅助函数
    • Mutation 用于同步提交状态变更,附有一个 mapMutations 辅助函数
    • Action 用于异步变更状态,但它提交的是 mutation,而不是直接变更状态。
    • Module 用来给 store 划分模块,方便维护代码 常见追问:Mutation 和 Action 为什么要分开? 答案:为了让代码更易于维护。(可是 Pinia 就把 Mutation 和 Action 合并了呀:方便使用)

7.4 VueRouter 相关内容

  1. 背下文档第一句:Vue Router 是 Vue.js 的官方路由
  2. 说出核心概念的名字和作用: router-link router-view 嵌套路由、Hash 模式 和 History 模式、导航守卫、懒加载
  3. 常见追问:
    • a. Hash 模式和 History 模式的区别?

      • i. 一个用的 Hash,一个用的 History API
      • ii. 一个不需要后端 nginx 配合,一个需要
    • b. 导航守卫如何实现登录控制? router.beforeEach((to, from, next) => { if (to.path === '/login') return next() if (to是受控页面 && 没有登录) return next('/login') next() })

路由守卫相关链接

7.5 Vue2 如何实现双向绑定

推荐链接

  1. 说明一般使用 v-model / .sync 实现,v-model 是 v-bind:value 和 von:input 的语法糖
    • a. v-bind:value 实现了 data ⇒ UI 的单向绑定
    • b. v-on:input 实现了 UI ⇒ data 的单向绑定 c. 加起来就是双向绑定了
  2. 这两个单向绑定是如何实现的呢?
    • a. 前者通过 Object.defineProperty API 给 data 创建 getter 和 setter,用于监听data 的改变,data 一变就会安排改变 UI
    • b. 后者通过 template compiler 给 DOM 添加事件监听,DOM input 的值变了就会去修改 data。

7.6 v-model 修饰符

7.6.1 native

native 修饰符是用在 vue2 里面的,在 vue3 中已经舍弃。 主要是用来监听自定义组件上的原生事件。

<my-component @click=rawClick @vclick=customClick />

export default {
  methods: {
    rawClick() {
      console.log('触发原生事件') // 不会执行
    },
    customClick() {
      console.log('触发自定义事件')  // 会执行
    }
  }
}

// 加上 native rawClick 事件监听函数就会执行
<my-component @click.native=rawClick @vclick=customClick />

native 的作用就是将自定义组件看成是类似于原生的 HTML 元素,这样一来原生监听事件比如(click 事件)就可以被监听到,从而执行此监听函数。

8. Vue3 相关内容

8.1 Vue3 为什么使用 Proxy

  1. 弥补 Object.defineProperty 的两个不足
    • a. 动态创建的 data 属性需要用 Vue.set 来赋值,Vue 3 用了 Proxy 就不需要了
    • b. 基于性能考虑, Vue 2 篡改了数组的 7 个 API,Vue 3 用了 Proxy 就不需要了
  2. defineProperty 需要提前递归地遍历 data 做到响应式,而 Proxy 可以在真正用到 深层数据的时候再做响应式(惰性)

8.2 Vue3 为什么使用Composition API

答案参考尤雨溪的博客: Vue Function-based API RFC - 知乎 (zhihu.com)

  1. Composition API 比 mixins、高阶组件、extends、Renderless Components 等更 好,原因有三:
    • a. 模版中的数据来源不清晰。
    • b. 命名空间冲突。
    • c. 性能。
  2. 更适合 TypeScript

8.3 Vue3 对比 Vue2 做了哪些改动

详见官方文档

  1. createApp() 代替了 new Vue()
  2. v-model 代替了以前的 v-model 和 .sync
  3. 根元素可以有不止一个元素了
  4. 新增 Teleport 传送门
  5. destroyed 被改名为 unmounted 了(before 当然也改了)
  6. ref 属性支持函数了

9 React 相关内容

9.1 虚拟 DOM 的原理

虚拟 DOM 就是虚拟节点。React 用 JS 对象来模拟 DOM 节点,然后将其渲染成真实的 DOM 节点。

  1. 怎么做

第一步是模拟,用 JSX 语法写出来的 div 其实是一个虚拟节点

<div id="x">
  <span class="red">hi</span>
</div>

这段代码会得到这样一个对象

{
  tag: 'div',
  props: {
    id: 'x'
  },
  children: [
    {
      tag: 'span',
      props: {
        className: 'red'
      },
      children: [
        'hi'
      ]
    }
  ]
}

能做到这一点是因为 JSX 语法会被转译为 createElement 函数调用(也成 h 函数),如下:

React.createElement("div", { id: "x"},
  React.createElement("span", { class: "red" }, "hi")
)

第二步是将虚拟节点渲染为真实节点

function render(vdom) {
  // 如果是字符串或者数字,创建一个文本节点
  if (typeof vdom === 'string' || typeof vdom === 'number') {
    return document.createTextNode(vdom)
  }
  const { tag, props, children } = vdom
  // 创建真实DOM
  const element = document.createElement(tag)
  // 设置属性
  setProps(element, props)
  // 遍历子节点,并获取创建真实DOM,插入到当前节点
  children.map(render).forEach(element.appendChild.bind(element))
  // 虚拟 DOM 中缓存真实 DOM 节点 (虚拟节点与真实节点建立关联,不然不知道保存在哪里)
  vdom.dom = element
  // 返回 DOM 节点
  return element
}
function setProps // 略
function setProp // 略

具体详见

注意,如果节点发生变化,并不会直接把虚拟节点渲染到真实节点,而是先进过 diff 算法得到一个 patch 在更新到真实节点上。

  1. 解决了什么问题
  • DOM 操作性能问题。通过虚拟 DOM 和 diff 算法减少不必要的 DOM 操作,保证性能不太差
  • DOM 操作不方便问题。以前各种 DOM API 要记,现在只有 setState
  1. 优点
  • 为 React 带来了跨平台能力,因为虚拟节点除了渲染为真实节点,还可以渲染为其他东西
  • 让 DOM 操作的整体性能更好,能(通过 diff)减少不必要的 DOM 操作
  1. 缺点
  • 性能要求极高的地方,还是得用真实的 DOM 操作
  • React 为虚拟 DOM 创造了 合成事件,跟原生 DOM 事件不太一样,工作中要额外注意
    • 所有 React 事件都绑定到根元素,自动实现事件委托
    • 如果混用合成事件和原生 DOM 事件,有可能会出 bug

9.2 React 或 Vue 的 DOM diff 算法是怎样的

DOM diff 就是对比两棵虚拟 DOM 树的算法。当组件变化时,会 render 出一个新的虚拟 DOM,diff 算法对比新旧虚拟 DOM 之后,得到一个 patch, 然后 React 用 patch 来更新真实 DOM。

怎么做

首先,对比两棵树的根节点

  1. 如果根节点的类型改变了,比如 div 变成了 p,那么直接认为整棵树都变了,不 再对比子节点。此时直接删除对应的真实 DOM 树,创建新的真实 DOM 树。
  2. 如果根节点的类型没变,就看看属性变了没有
    • a. 如果没变,就保留对应的真实节点
    • b. 如果变了,就只更新该节点的属性,不重新创建节点。
      • i. 更新 style 时,如果多个 css 属性只有一个改变了,那么 React 只更新改变的。

然后,同时遍历两棵树的子节点,每个节点的对比过程同上,不过存在如下两种情况。

  1. 情况一
<ul>
  <li>A</li>
  <li>B</li>
</ul>
<ul>
  <li>A</li>
  <li>B</li>
  <li>C</li>
</ul>

React 依次对比 A-A、B-B、空-C,发现 C 是新增的,最终会创建真实 C 节点插入页面。

  1. 情况二
<ul>
  <li>A</li>
  <li>B</li>
</ul>
<ul>
  <li>A</li>
  <li>B</li>
  <li>C</li>
</ul>

React 对比 B-A,会删除 B 文本新建 A 文本;对比 C-B,会删除 C 文本,新建 B 文本;(注意,并不是边对比边删除新建,而是把操作汇总到 patch 里再进行 DOM 操作。)对比空-C,会新建 C 文本。你会发现其实只需要创建 A 文本,保留 B 和 C 即可,为什么 React 做不到呢?

因为 React 需要你加 key 才能做到:

<ul>
  <li key="b">B</li>
  <li key="c">C</li>
</ul>
<ul>
  <li key="a">A</li>
  <li key="b">B</li>
  <li key="c">C</li>
</ul>

React 先对比 key 发现 key 只新增一个,于是保留 b 和 c,新建 a。 Vue 的 双端交叉对比算法详见 文章1文章2

9.3 React 或 Vue 的 DOM diff 的区别

9.4 React 生命周期钩子函数以及数据请求放到那个钩子中

React 声明周期官方文档以及声明周期官方图

总的来说:

  1. 挂载时调用 constructor,更新时不调用
  2. 更新时调用 shouldComponentUpdate 和 getSnapshotBeforeUpdate,挂载时不调用
  3. should... 在 render 之前,getSnapshot... 在 render 之后
  4. 请求最好放在 componentDidMount 中。

9.5 React 如何实现组件间通信

  1. 父子组件通信:props + 函数
  2. 爷孙组件通信:两层父子通信使用 Context.Provider 和 Context.Consumer
  3. 任意组件通信:使用状态管理库
    • a. Redux
    • b. Mobx
    • c. Recoil

9.6 如何理解 Redux

  1. Redux 是一个 状态管理库/状态容器

  2. Redux 的核心概念

    • State
    • Action = type + payload 荷载
    • Reducer
    • Dispatch 派发
    • Middleware
  3. ReactRedux 的核心概念

    • connect()(Component)
    • mapStateToProps
    • mapDispatchToProps
  4. 两个常见的中间件 redux-thunk redux-promise 具体详见

9.7 什么是高阶组件 HOC

参数是组件,返回值也是组件 具体说明即可:

    1. React.forwardRef
    1. ReactRedux 的 connect
  • ReactRouter 的 withRouter

参考阅读

9.8 React Hooks 如何模拟组件生命周期

  1. 模拟 componentDidMount
  2. 模拟 componentDidUpdate
  3. 模拟 componentWillUnmount
import { useEffect, useState, useRef } from 'react'
import './styles.css'
export default function App() {
  const [visible, setNextVisible] = useState(true)
  const onClick = () => {
    setNextVisible(!visible)
  }
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      {visible ? <Frank /> : null}
      <div>
        {' '}
        <button onClick={onClick}>toggle</button>{' '}
      </div>
    </div>
  )
}
function Frank(props) {
  const [n, setNextN] = useState(0)
  const first = useRef(true)
  useEffect(() => {
    if (first.current === true) {
      return
    }
    console.log('did update')
  })
  useEffect(() => {
    console.log('did mount')
    first.current = false
    return () => {
      console.log('did unmount')
    }
  }, [])
  const onClick = () => {
    setNextN(n + 1)
  }
  return (
    <div>
      Frank
      <button onClick={onClick}>+1</button>
    </div>
  )
}

10. Nodejs 相关知识

10.1 Nodejs 的 EventLoop 是什么

背景知识

Node.js 将各种函数(也叫任务或回调)分成至少 6 类,按先后顺序执行,因此将时间分为六个阶段:

    1. timers 阶段(setTimeout)
    1. I/O callbacks 该阶段不用管
    1. idle,prepare 该阶段不用管
    1. poll 轮询阶段,停留时间最长,可以随时离开。
      • a. 主要用来处理 I/O 事件,该阶段中 Node 会不停询问操作系统有没有文件数据、网络数据等
      • b. 如果 Node 发现 timer 快到时间了或者有 setImmediate 任务,就会主动离开 poll 阶段
    1. check 阶段,主要处理 setImmediate 任务
    1. close callback 该阶段不用管

Node.js 会不停的从 1 到 6 循环处理各种事件,这个过程叫做事件循环(Event Loop)

nextTick

process.nextTick(fn) 的 fn 会在什么时候执行呢?

在 Node.js 11 之前,会在每个阶段的末尾集中执行(俗称队尾执行)。

在 Node.js 11 之后,会在每个阶段的任务间隙执行(俗称插队执行)。

浏览器跟 Node.js 11 之后的情况类似。可以用 window.queueMicrotask 模拟 nextTick。

Promise

Promise.resolve(1).then(fn) 的 fn 会在什么时候执行?

这要看 Promise 源码是如何实现的,一般都是用 process.nextTick(fn) 实现的,所以直接参考 nextTick。

async / await

这是 Promise 的语法糖,所以直接转为 Promise 写法即可。

练习题1

setTimeout(() => {
  console.log('setTimeout')
})
setImmediate(() => {
  console.log('setImmediate')
})
// Node.js 的底层使用 C++ 实现,而 JS 的启动顺序和 C++ 的启动顺序不能确定谁先谁后
// 在 Node.js 运行会输出什么? D
// A setTimeout setImmediate
// B setImmediate setTimeout
// C 出错
// D A 或 B
// 在浏览器执行会怎样? B setImmediate 需要改成 window.queueMicrotask

练习题2

async function async1() {
  console.log('1') // 2
  /*
   * await async2()
   * console.log('2')
   * 等价于下面的代码
  */
  async2().then(() => {
    console.log('2')
  })
}
async function async2() {
  console.log('3') // 3
}
console.log('4') // 1
setTimeout(function () {
  console.log('5')
}, 0)
async1()
new Promise(function (resolve) {
  console.log('6') // 4
  resolve()
}).then(function () {
  console.log('7')
})
console.log('8') // 5
//4 1 3 6 8 2 7 5

10.2 浏览器里的微任务和任务是什么

浏览器中并不存在宏任务,宏任务(Macrotask)是 Node.js 发明的术语。

浏览器中只有任务(Task)和微任务(Microtask)。

    1. 使用 script 标签、setTimeout 可以创建任务。
    1. 使用 Promise.then、window.queueMicrotask、MutationObserver、Proxy 可以创建微任务。

执行顺序:微任务会在任务间隙执行(俗称插队执行)

! 注意,微任务不能插微任务的队,微任务只能插任务的队。

练习题

Promise.resolve()
  .then(() => {
    console.log(0);
    // return Promise.resolve(...) 等于两个 then
    /*
     * Promise.resolve(null).then(() => {
         Promise.resolve(null).then(() => console.log('4x'))
       })
    */
    return Promise.resolve('4x');
  })
.then((res) => {console.log(res)})

Promise.resolve().then(() => {console.log(1);})
                 .then(() => {console.log(2);}, ()=>{console.log(2.1)})
                 .then(() => {console.log(3);})
                 .then(() => {console.log(5);})
                 .then(() => {console.log(6);})
                 
// 0 1 2 3 4x 5 6

10.3 express.js 和 koa.js 的区别

  1. 中间件模型不同:express 的中间件模型为线型,而 koa 的为U型(洋葱模型)。
  2. 对异步的处理不同:express 通过回调函数处理异步,而 koa 通过generator 和 async/await 使用同步的写法来处理异步,后者更易维护,但彼时 Node.js 对 async 的兼容性和优化并不够好,所以没有流行起来。
  3. 功能不同:express 包含路由、渲染等特性,而 koa 只有 http 模块。 总得来说,express 功能多一点,写法烂一点,兼容性好一点,所以当时更流行。虽 然现在 Node.js 已经对 await 支持得很好了,但是 koa 已经错过了风口。

11. Webpack 和 Vite 相关内容

11.1 常见 loader 和 plugin 有哪些?二者区别是什么?

常见 loader

可以记住几个比较重要的:

  1. baber-loader 把 JS/TS 变成 JS
  2. ts-loader 把 TS 变成 JS,并提示类型错误
  3. markdown-loader 把 Markdown 变成 html
  4. html-loader 把 html 变成 JS 字符串
  5. sass-loader 把 SASS/SCSS 变成 CSS
  6. css-loader 把 CSS 变成 JS 字符串
  7. style-loader 把 JS 字符串变成 style 标签
  8. postcss-loader 把 CSS 变成更优化的 CSS
  9. vue-loader 把单文件组件(SFC)变成 JS 模块
  10. thread-loader 用于多进程打包

常见 plugin

可以记住几个比较重要的:

  1. html-webpack-plugin 用于创建 HTML 页面并自动引入 JS 和 CSS
  2. clean-webpack-plugin 用于 清理之前打包的残余文件
  3. mini-css-extract-plugin 用于将 JS 中的 CSS 抽离成单独的 CSS 文件
  4. SplitChunksPlugin 用于代码分包(Code Split)
  5. DllPlugin + DLLReferencePlugin 用于避免大依赖被频繁重新打包,大幅降低打包时间
  6. eslint-webpack-plugin 用于检查代码中的错误
  7. DefinePlugin 用于在 webpack config 里添加全局配置
  8. copy-webpack-plugin 用于拷贝静态文件到 dist

二者的区别

  • loader 是文件加载器

    • 功能:能够对文件进行编译、优化、混淆(压缩)等,比如 babel-loader/vue-loader
    • 运行时机:在创建最终产物之前运行
  • plugin 是 webpack 插件

    • 功能:能实现更多功能,比如定义全局变量、Code Split、加速编译等
    • 运行时机:在整个打包过程(以及前后)都能运行

11.2 webpack 如何解决开发时的跨域问题

在开发时,我们的页面在 localhost:8080, JS 直接访问后端接口(如 www.baidu.comhttp://localhost:3000) 会跨域报错。为了解决这个问题,可以在 webpack.config.js 中添加如下配置:

module.exports = {
  //...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://www.baidu.com',
        changeOrigin: true
      }
    }
  }
}

此时,在 JS 中请求 /api/users 就会被自动代理到 www.baidu.com/api/users

如果希望请求中的Origin 从 8080 修改为 www.baidu.com ,可以添加 changeOrigin:true。

如果要访问的是 HTTPS API,那么就需要配置 HTTPS 证书,否则会报错。不过,如果在 target 下面添加 secure:false,就可以不配置证书且忽略 HTTPS 报错。

11.3 如何实现 tree-shaking

tree-shaking 就是将没有用到的 JS 代码不打包,以减小包的体积。

  1. 怎么删

    • a. 使用 ES Modules 语法(即 ES6 的 import 和 export 关键字)
    • b. CommonJS 语法无法 tree-shaking(即 require 和 exports 语法)
    • c. 引入的时候只引用需要的模块
      • 要写 import {cloneDeep} from 'lodash-es' 因为方便 tree-shaking
      • 不要写 import _ from 'lodash' 因为会导致无法 tree-shaking 无用模块
  2. 怎么不删:在 packjson.json 中配置 sideEffects,防止某些文件被删掉

    • 比如我 import 了 x.js,而 x.js 只是添加了 window.x 属性,那么 x.js 就要放到 sideEffects 中
    • 比如所有被 import 的 CSS 都要放到 sideEffects 中
  3. 怎么开启:在 webpack config 中将 mode 设置为 production(开发环境中没必要 tree-shaking)

    • mode:production 给 webpack 加了非常多优化

11.4 如何提高 webpack 的构建速度

  1. 使用 DllPlugin 将不常变化的代码提前打包,并复用,如 vue、react
  2. 使用 thread-loader 或 HappyPack(过时)进行多线程打包
  3. 处于开发环境时,在 webpack config 中将 cache 设置为 true,也可用 cache-loader(过时)
  4. 处于生产环境时,关闭不必要的环节,比如可以关闭 source map
  5. 网传的 HardSourceWebpackPlugin 久未更新,谨慎使用

11.5 webpack 与 vite 的区别

  1. 开发环境区别

    • vite 自己实现 server,不对代码打包,充分利用浏览器对 script type=module 的支持
      • 假设 main.js 引入了 vue
      • 该 server 会把 import { createApp } from 'vue' 改为 import { createApp } from '/node_modules/.vite/vue.js' 这样浏览器就知道去哪里找 vue.js 了
    • webpack-dev-server 常使用 babel-loader 基于内存打包,比 vite 慢很多很多
      • 该 server 会把 vue.js 的代码(递归地)打包进 main.js
  2. 生产环境区别

    • vite 使用 rollup + esbuild 来打包 JS 代码
    • webpack 使用 babel 来打包 JS 代码,比 esbuild 慢很多很多
      • webpack 可以使用 esbuild,但需要自己配置
  3. 文件处理时机

    • vite 只会在你请求某个文件的时候处理该文件
    • webpack 会提前打包好 main.js,等你请求的时候直接输出打包哈的 JS 给你

目前已知 vite 的缺点有:

  • 热更新常常失败,原因不清楚
  • 有些功能 rollup 不支持,需要自己写 rollup 插件
  • 不支持非现代浏览器

11.6 webpack 配置多页应用

这是对应的 webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
  entry: {
    app: './src/app.js',
    admin: './src/admin.js'
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: 'index.html',
      chunks: ['app']
    }),
    new HtmlWebpackPlugin({
      filename: 'admin.html',
      chunks: ['admin']
    })
  ]
}

但是,这样配置会有一个 重复打包 的问题:假设 app.js 和 admin.js 都引入了 vue.js,那么 vue.js 的代码既会打包进 app.js,也会打包进 admin.js。我们需要使用 optimization.splitChunks 将共同依赖单独打包成 common.js(HtmlWebpackPlugin 会自动引入 common.js)

支持无限多页面

const HtmlWebpackPlugin = require('html-webpack-plugin')
const fs = require('fs')
const path = require('path')
const filenames = fs
  .readdirSync('./src/pages')
  .filter(file => file.endsWith('.js'))
  .map(file => path.basename(file, '.js'))
const entries = filenames.reduce((result, name) => ({ ...result, [name]: `./src/pages/${name}.js` }), {})
const plugins = filenames.map(
  name =>
    new HtmlWebpackPlugin({
      filename: name + '.html',
      chunks: [name]
    })
)
module.exports = {
  entry: {
    ...entries
  },
  plugins: [...plugins]
}

11.7 swc、esbuild是什么

**swc **

实现语言:Rust

功能:编译 JS/TS、打包 JS/TS

优势:比 babel 快很多很多很多(20倍以上) 能否集成进 webpack:能 使用者:Next.js、Parcel、Deno、Vercel、ByteDance、Tencent、Shopify……

做不到:

  1. 对 TS 代码进行类型检查(用 tsc 可以)
  2. 打包 CSS、SVG

esbuild

实现语言:Go

功能:同上

优势:比 babel 快很多很多很多很多很多很多(10~100倍)

能否集成进 webpack:能

使用者:vite、vuepress、snowpack、umijs、blitz.js 等

做不到:

  1. 对 TS 代码进行类型检查
  2. 打包 CSS、SVG

12. 算法相关内容

12.1 大数相加

const add = (a, b) => {
  const maxLength = Math.max(a.length, b.length)
  let overflow = false
  let sum = ''
  for (let i = 1; I <= maxLength; i++) {
    const ai = a[a.length - i] || '0'
    const bi = b[b.length - i] || '0'
    let ci = parseInt(ai) + parseInt(bi)
    overflow = ci >= 10
    ci = overflow ? ci - 10 : ci
    sum = ci + sum
  }
  sum = overflow ? '1' + sum : sum
  return sum
}

// 15 位加速版
// 浏览器最大支持长度为 15 位,速度要比上面的快一点
const add = (a, b) => {
  const maxLength = Math.max(a.length, b.length)
  let overflow = false
  let sum = ''
  for (let i = 0; i < maxLength; i += 15) {
    const ai = a.substring(a.length - i - 15, a.length - i) || '0'
    const bi = b.substring(b.length - i - 15, b.length - i) || '0'
    let ci = parseInt(ai) + parseInt(bi)
    overflow = ci > 999999999999999 // 159
    ci = overflow ? ci - (999999999999999 + 1) : ci
    sum = ci + sum
  }
  sum = overflow ? '1' + sum : sum
  return sum
}
console.log(add('11111111101234567', '77777777707654321'))
console.log(add('911111111101234567', '77777777707654321'))

其他思路:

  1. 转为数组,然后倒序,遍历
  2. 使用队列,使用 while 循环

12.2 两数之和

const numbers = [2, 7, 11, 15]
const target = 9
const twoSum = (numbers, target) => {
  const map = {}
  for (let i = 0; i < numbers.length; i++) {
    const number = target - numbers[i]
    if (number in map) {
      const numberIndex = map[number]
      return [i, numberIndex]
    } else {
      map[numbers[i]] = i
    }
  }
  return []
}
console.log(twoSum(numbers, target))

12.3 无重复最长子串的长度

如题

var lengthOfLongestSubString = s => {
  if (s.length <= 1) return s.length
  let max = 0
  let p1 = 0
  let p2 = 1
  while (p2 < s.length) {
    let sameIndex = -1
    for (let i = p1; i < p2; i++) {
      if (s[i] === s[p2]) {
        sameIndex = i
        break
      }
    }
    let tempMax
    if (sameIndex >= 0) {
      tempMax = p2 - p1
      p1 = sameIndex + 1
    } else {
      tempMax = p2 - p1 + 1
    }
    if (tempMax > max) {
      max = tempMax
    }
    p2 += 1
  }
  return max
}

使用 map 加速

var lengthOfLongestSubstring = function (s) {
  if (s.length <= 1) return s.length
  let max = 0
  let p1 = 0
  let p2 = 1
  const map = {}
  map[s[p1]] = 0
  while (p2 < s.length) {
    let hasSame = false
    if (s[p2] in map) {
      hasSame = true
      if (map[s[p2]] >= p1) {
        p1 = map[s[p2]] + 1
      }
    }
    map[s[p2]] = p2
    let tempMax = p2 - p1 + 1
    if (tempMax > max) max = tempMax
    p2 += 1
  }
  return max
}

改用 new Map() 试试

var lengthOfLongestSubstring = function (s) {
  if (s.length <= 1) return s.length
  let max = 0
  let p1 = 0
  let p2 = 1
  const map = new Map()
  map.set(s[p1], 0)
  while (p2 < s.length) {
    let hasSame = false
    if (map.has(s[p2])) {
      hasSame = true
      if (map.get(s[p2]) >= p1) {
        p1 = map.get(s[p2]) + 1
      }
    }
    map.set(s[p2], p2)
    let tempMax = p2 - p1 + 1
    if (tempMax > max) max = tempMax
    p2 += 1
  }
  return max
}

13. 刁钻题

13.1 [1,2,3].map(parseInt)

[1,2,3].map(parseInt)
// 展开
parseInt(1, 0, arr) => parseInt(1) => 1
parseInt(2, 1, arr) => NaN
parseInt(3, 2, arr) => NaN

// 正确写法
[1, 2, 3].map(number => parseInt(number))

13.2 a.x = a = {}

var  a = {x: 1}
var b = a;
a.x = a = {x: 2};
console.log(a.x)
console.log(b.x)

13.3 if true / function a / a = 2

var a = 0
if (true) {
  a = 1
  function a() {
    return 3
  }
  a = 2
  console.log(a) // 2
}
console.log(a) // 1

这题属于未定义行为(见 MDN),答案并不唯一。

Chrome/Edge 运行结果:2 1

Safari 运行结果:2 2

Firefox 运行结果:2 1