面向对象

抽象 封装 继承 多态

工厂模式

1
2
3
4
5
6
7
8
9
10
11
12
function Factory() {
// 原料
let obj = {}
// 加工原料
obj.prop = ''
obj.fn = function() {}
// 出厂
return obj
}

let productA = Factory()
let productB = Factory()
  • 工厂模式解决了代码复用的问题
  • 但是却没有解决对象识别的问题
    • 创建的实例都是 Object 类型, 不清楚是哪个工厂的实例
  • 公共方法和属性会随着工厂方法的调用而重复占用内存

构造函数

1
2
3
4
5
6
7
8
9
10
function Factory() {
this.prop = ''
}
Factory.prototype = {
constructor: Factory,
fn: function() {}
}

let productA = new Factory()
let productB = new Factory()

原型 原型链

原型链是指对象在访问属性或方法时的查找方式

  • 当访问一个对象的属性或方法时,会先在对象自身上查找属性或方法是否存在,如果存在就使用对象自身的属性或方法;如果不存在就去创建对象的构造函数的原型对象中查找,依此类推,直到找到为止。如果到顶层对象中还找不到,则返回 undefined
  • 原型链最顶层为 Object 构造函数的 prototype 原型对象,给 Object.prototype 添加属性或方法可以被除 null 和 undefined 之外的所有数据类型对象使用
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 Animal() {}

const animal = new Animal

animal.__proto__ === Animal.prototype
Animal.prototype.constructor === Animal
animal.constructor === Animal

console.log(Animal.prototype.isPrototypeOf(animal))
console.log(animal instanceof Animal)

// 属性或原型
console.log('name' in animal)

// 仅属性
console.log(animal.hasOwnProperty('name'))

Animal.__proto__ === Function.prototype
Function.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null

Function.prototype.constructor === Function
Object.prototype.constructor === Object

Function.__proto__ === Function.prototype
Object.__proto__ === Function.prototype

原型链

继承

  • constructor 指向问题
  • 属性共享问题
  • 子类不能定义自己的参数
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
function inheritPrototype(Child, Parent) {
const protoType = Object.create(Parent.prototype, {
constructor: {
configurable: true,
enumerable: false,
writable: true,
value: Child
}
})
/* const Temp = function() {}
Temp.prototype = Parent.prototype
Child.prototype = new Temp()
Child.prototype.constructor = Child */

// protoType.constructor = Child
Child.prototype = protoType
}

function Plane(color) {
this.color = color
}
Plane.prototype.fly = function() {
console.log('flying')
}

function Fighter(color) {
Plane.call(this, color)
this.bullets = []
}
inheritPrototype(Fighter, Plane)

Fighter.prototype.shoot = function() {
console.log('biu biu biu')
}

const fighter = new Fighter('黑色')


// Class 还继承了静态属性
if (const staticAttribute in Parent) {
if (Parent.hasOwnProperty(staticAttribute) && !(staticAttribute in Child)) {
Child[staticAttribute] = Parent[staticAttribute]
}
}

类型判断

instanceof

通过原型链的方式来判断是否为构造函数的实例

1
2
3
4
5
6
7
8
9
10
11
12
let str = 'hello world'
str instanceof String // false

let str1 = new String('hello world')
str1 instanceof String // true

class PrimitiveString {
static [Symbol.hasInstance](str) {
return typeof str === 'string'
}
}
console.log('hello world' instanceof PrimitiveString) // true

constructor

1
[].constructor === Array

Object.prototype.toString

常用于判断浏览器内置对象

1
2
Object.prototype.toString.call(null) === '[object Null]' // 👍
Object.prototype.toString.call(null).slice(8, -1).toLowerCase() // null

is API

1
2
Array.isArray([])
Number.isNaN('')

class 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Parent {
constructor(value) {
this.val = value
}
getValue() {
console.log(this.val)
}
}

class Child extends Parent {
constructor(value) {
super(value) // Parent.call(this, value)
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

Parent instanceof Function // class 只是语法糖,本质上还是函数

作用域 闭包

  • this执行时作用域决定
  • 闭包声明时作用域决定
  • 作用域变量或函数能够被访问的范围

this

this 的指向 是根据调用时上下文动态决定的

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
/* 普通函数 箭头函数 的 this */
const obj = {
f1() {
const fn = () => { console.log('f1', this) }
fn()
fn.call(window)
},
f2: () => {
function fn() { console.log('f2', this) }
fn()
fn.call(this)
}
}
obj.f1() // obj obj
obj.f2() // window window

/* class 相关的 this */
class Foo {
f1() { console.log('f1', this) }
f2 = () => { console.log('f2', this) }
static f3() { console.log('f3', this) }
}
const f = new Foo()
f.f1() // f
f.f2() // f
Foo.f3() // Foo
  1. 在简单调用时,this 默认指向 window(浏览器)/global(node)/undefined(严格模式)
  2. 对象调用时,绑定在对象上
  3. 使用 bind call apply 时,绑定在指定参数上
  4. 使用 new 关键字时,绑定到新创建的对象上
  5. 使用箭头函数时,根据外层的规则决定

优先级: new > bind/call/apply > 对象调用

自由变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let n = 10
function f1 () {
n++
function f2() {
function f3() { n++ }
let n = 20
f3()
n++
}
f2()
n++
}
f1()
console.log(n) // 12

Closure 闭包

  • 函数嵌套函数
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
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i)
})
}

for (var i = 0; i < 10; i++) {
((i) => {
setTimeout(() => {
console.log(i)
})
})(i)
}

/* 函数作为返回值 */
function F1() {
let a = 1
return function() {
console.log(a)
}
}
let f1 = F1()
let a = 2
f1() // 1

/* 函数作为参数传递 */
function F2(fn) {
let a = 3
fn()
}
F2(f1) // 1

/* 有权访问另一个函数作用域中的变量的函数 */
function foo() {
let i = 0
function bar() {
console.log(i++)
}
return bar
}
let bar = foo()
bar() // 0
bar() // 1
bar() // 2

/* 私有变量 收敛权限 */
function isFirstLoad() {
let _cache = []
return (id) => {
if (_cache.includes(id)) {
return false
} else {
_cache.push(id)
return true
}
}
}
let firstLoad = isFirstLoad()
firstLoad(1) // true
firstLoad(1) // false
firstLoad(2) // true

ES6

let const

1
2
3
4
5
6
7
8
9
10
11
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i)
})
}

for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i)
})
}

var

  • 全局/函数作用域
  • 全局作用域下声明变量会挂载在 window 上
  • 变量提升

let

  • 块级作用域
  • 不能重复声明
  • 不会被预解析(暂时性死区不能在声明前使用)

const

  • 常量不能重新赋值
  • 块级作用域
  • 不能重复声明
  • 不会被预解析(暂时性死区不能在声明前使用)

箭头函数

  • 没有 arguments 参数
  • 无法通过 call apply bind 改变 this
  • 简写的函数会变得难以阅读
  • 不适用箭头函数的场景
    1. 对象方法
    2. 扩展对象原型(包括构造函数的原型)
    3. 构造函数
    4. 动态上下文中的回调函数addEventListener
    5. Vue 生命周期和方法
    6. class 中使用箭头函数没有问题,所以在 React 中可以使用箭头函数
      • Vue 组件是一个对象,而 React 组件是一个 class(如果不考虑 Composition API 和 Hooks)

Set Map

  • 数组

    1. 有序结构,可排序的
    2. 中间插入删除比较慢
  • Set

    1. 有序结构,不可排序,没有 index
    2. 去重
  • 对象

    1. 无序结构
    2. key 两种类型 string symbol
  • Map

    1. 有序结构forEach
    2. key 任意类型

Iterator 和 for…of

迭代器

1
2
3
4
5
6
7
8
9
10
obj[Symbol.iterator] = function () {
return {
next() {
return {
value,
done: true
}
}
}
}

迭代对象

实现了[Symbol.iterator]方法

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
let obj = {
a: 1,
b: 2,
c: 3
}
obj[Symbol.iterator] = function() {
let keys = Object.keys(obj)
let index = 0
return {
next() {
if (index >= keys.length) {
return {
done: true
}
} else {
return {
done: false,
value: {
key: keys[index],
value: obj[keys[index++]]
}
}
}
}
}
}
for (let val of obj) {
console.log(val)
}

迭代语句

for…in 👎
  • 以原始插入的顺序遍历对象的可枚举属性
    • obj.hasOwnProperty(key)
  • 用于可枚举数据 enumerable
    • 对象字符串 数组
    • Object.getOwnPropertyDescriptors( { a: 1 } )
  • 得到 key 值
  • 主要是为遍历对象而设计的,不适用于遍历数组
    • 某些情况下,循环会以任意顺序遍历键名
for…of 👍
  • 根据迭代器具体实现遍历对象
  • 用于可迭代数据 next
    • 数组字符串 Set Map 类数组 Generator对象
    • [Symbol.iterator]()
  • 得到 value 值
  • 可以使用breakcontinue
  • 提供了遍历所有数据结构的统一操作接口
for await…of

用于遍历异步请求的可迭代对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function createTimeoutPromise(val) {
return new Promise(resolve => {
setTimeout(() => {
resolve(val)
}, 1000)
})
}

(async function () {
const list = [
createTimeoutPromise(10),
createTimeoutPromise(20)
]
/* Promise.all(list).then(res => console.log(res)) */
for await (const p of list) {
console.log(p)
}
// for (let n of [100, 200]) {
// const v = await createTimeoutPromise(n)
// console.log(v)
// }
})()

forEach 数组 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function forEach(obj, fn) {
if (obj instanceof Array) {
obj.forEach((item, index) => {
fn(index, item)
})
} else {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
fn(key, obj[key])
}
}
}
}

let arr = [1, 2, 3]
forEach(arr, (index, item) => {
console.log(index, item)
})

let obj = {x: 3, y: 4, z: 5}
forEach(obj, (key, value) => {
console.log(key, value)
})

Generator 函数

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
function* gen() {
yield new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 500)
})
yield 2
yield new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3)
}, 500)
})
}

co(gen)
function co(gen) {
let fn = gen(),
result
(function iterate() {
result = fn.next()
if (!result.done) {
if ('then' in Object(result.value)) {
result.value.then(iterate)
} else {
iterate()
}
}
})()
}

模块化

  • 解决命名冲突
  • 代码可复用性
  • 代码可维护性

IIFE 立即执行函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const iifeModule = ((window, jQuery, undefined, dependencyModule1) => {
// ... 声明各种变量、函数都不会污染全局作用域
let count = 0
return {
increase() {
++count
},
reset: () => (count = 0)
}
})(window, jQuery, undefined, dependencyModule1)

// window 1.全局作用域换成局部作用域,提升效率 2.编译时变量名压缩
// jQuery 1.独立进行改动挂载,保障稳定 2.防止全局污染
// undefined 1.防止重写

AMD 异步模块 RequireJS

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
define('amdModule', ['dependencyModule1', 'dependencyModule2'], (dependencyModule1, dependencyModule2) => {
let count = 0
const increase = () => ++count
const reset = () => {
count = 0

// dependencyModule1.do() ...
}
})

require(['amdModule'], amdModule => {
amdModule.reset()
})

/**
* CommonJS 迁移 AMD
*/
define('amdModule', [], require => {
const dependencyModule1 = require('./dependencyModule1')
const dependencyModule2 = require('./dependencyModule2')

let count = 0
const increase = () => ++count
const reset = () => {
count = 0

// dependencyModule1.do() ...
}

export.increase = increase
export.reset = reset

return {
increase,
reset
}
})

CMD 通用模块 SeaJS

1
2
3
4
5
6
7
8
9
10
11
12
13
define('cmdModule', (require, exports, module) => {
// 按需加载 依赖就近
const $ = require('jquery')
$('selector')

})

/*
1.依赖于打包
2.增加模块体积
3.难以模块静态分析
*/

CommonJS

  • 动态语法,运行时加载,可以写在判断里
  • 同步导入
  • 单值导出,对模块的浅拷⻉
  • this 指向当前模块
  • 应用于 Node、小程序、Webpack
1
2
3
4
5
6
7
8
9
10
// let exports = module.exports = {} 不能直接对 exports 赋值

module.exports = {
data: 'data'
}
/* or */
exports.data = 'data'

let cjsModule = require('./cjsModule')
console.log(cjsModule.data)

ES6 Module

  • 静态语法,编译时输出,必须放在文件开头
  • 异步导入
  • 多值导出,对模块的引用
  • this 等于 undefined
  • 编译成 require / exports 执行
1
2
3
export default esModule
/* 等同于 */
export { esModule as default }

ESM 的模块化通过 Babel 编译 就是 UMD 模块规范, 它是一个兼容 CMD 和 CJS 的模块化规范, 同时还支持老式的”全局”变量规范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
/* AMD */
define(['jquery'], factory)
} else if (typeof exports === 'object') {
/* CommonJS */
module.exports = factory(require('jquery'))
} else {
/* 浏览器全局变量 root 即 window */
root.returnExports = factory(root.jQuery)
}
}(this, function($) {
function myFunc() { }
// 暴露公共方法
return myFunc
}))

ES11

1
import('./esModule.js').then(dynamicEsModule => {})

手写代码

  1. 代码规范性
    • ESLint 统一格式
    • 代码可读: 命名语义化 函数抽离 注释
  2. 功能完整性
  3. 健壮性
    • 边界
    • Typescript 类型约束
    • 单元测试

new

  • 创建一个空对象, 继承构造函数的原型
  • this 指向这个对象
  • 执行构造函数, 对 this 赋值
  • 隐式返回 this
1
2
3
4
5
function customNew<T>(Con: Function, ...args: any[]): T {
let obj = Object.create(Con.prototype)
let result = Con.apply(obj, args)
return result instanceof Object ? result : obj
}

Object.create

1
2
3
4
5
6
7
8
9
10
// 指定原型 创建空对象
function create(proto) {
function F() {}
F.prototype = proto
return new F()
}

// 字面量 {} 相当于 Object.create(Object.prototype)
const animal = new Animal() = Object.create(Animal.prototype)

instanceof

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myInstanceof(L, R) {
if (L == null) returnn false
const type = typeof L
if (type !== 'object' || type !== 'function') return

// L 表示左表达式,R 表示右表达式
let O = R.prototype // 取 R 的显示原型
while (true) {
L = L.__proto__ // 取 L 的隐式原型
if (L === null) return false
if (O === L) return true
}
/* while (L = L.__proto__) {
if (L === O) return true
}
return false */
}

bind

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.prototype.bind = function(context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('Type error.')
}

if (context == null) context = globalThis
if (typeof context !== 'object') context = new Object(context)

const _this = this

return function F() {
if (this instanceof F) {
/* new F */
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}

// fn.bind(a, 'a').bind(b, 'b') 等同于
let fn2 = function fn1() {
return function() {
return fn.apply(a, ['a', 'b', 'c'])
}.apply(b, ['b', 'c'])
}
fn2('c')
// 无论给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定

call

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Function.prototype.call = function(context, ...args) {
if (typeof this !== 'function') {
throw new TypeError('Type error.')
}

if (context == null) context = globalThis
if (typeof context !== 'object') context = new Object(context)

const fnKey = Symbol()
context[fnKey] = this

// Array.from(arguments)
// Array.prototype.slice.call(arguments, 1)

// Array.prototype.shift.call(arguments)
// const args = [...arguments].slice(1)

const result = context[fnKey](...args)
delete context[fnKey]
return result
}

apply

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.apply = function(context, args) {
if (typeof this !== 'function') {
throw new TypeError('Type error.')
}

if (context == null) context = globalThis
if (typeof context !== 'object') context = new Object(context)

const fnKey = Symbol()
context[fnKey] = this

const result = context[fnKey](...args)
delete context[fnKey]
return result
}

throttle 节流 第一个人说了算

1
2
3
4
5
6
7
8
9
10
function throttle(fn, interval = 50) {
let last = 0
return function(...args) {
let now = +new Date()
if (now - last > interval) {
last = now
fn.apply(this, args)
}
}
}

debounce 防抖 最后一个人说了算

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

heartbeat 心跳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function sleep(duration) {
return new Promise((resolve) => {
setTimeout(resolve, duration)
})
}

async function* heartbeat() {
let i = 0
while (true) {
await sleep(1000)
yield i++
}
}

for await(let i of heartbeat())
console.log(i)

deepClone 深拷贝

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
let map = new WeakMap()
function deepClone(obj) {
if (obj instanceof Object) {
if (map.has(obj)) {
return map.get(obj)
}
let newObj
if (obj instanceof Array) {
newObj = []
} else if (obj instanceof Function) {
newObj = function() {
return obj.apply(this, arguments)
}
} else if (obj instanceof RegExp) {
newobj = new RegExp(obj.source, obj.flags)
} else if (obj instanceof Date) {
newobj = new Date(obj)
} else {
newObj = {}
}
let desc = Object.getOwnPropertyDescriptors(obj)
let clone = Object.create(Object.getPrototypeOf(obj), desc)
map.set(obj, clone)
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = deepClone(obj[key])
}
}
return newObj
}
return obj
}

LRU 缓存

Least Recently Used 最近最少使用

  1. Map
    • 哈希表
    • 有序
  2. Object + Array
  3. Object(查) + 双向链表(增 删 移动)
  4. 第三方 lib
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
/* Map */
class LRUCache {
private length: number
private data: Map<any, any> = new Map()

constructor(length: number) {
if (length < 1) throw new Error('Invalid length')
this.length = length
}

set(key: any, value: any) {
const data = this.data

if (data.has(key)) {
data.delete(key)
}
data.set(key, value)

if (data.size > this.length) {
const delKey = data.keys().next().value
data.delete(delKey)
}
}

get(key: any): any {
const data = this.data

if (!data.has(key)) return

const value = data.get(key)

data.delete(key)
data.set(key, value)

return value
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* Object + Array */

// 执行 lru.set('a', 1) lru.set('b', 2) lru.set('c', 3) 后的数据
const obj1 = { key: 'a', value: 1 }
const obj2 = { key: 'b', value: 2 }
const obj3 = { key: 'c', value: 3 }

const data = [obj1, obj2, obj3] /* 有序 */

const map = { 'a': obj1, 'b': obj2, 'c': obj3 }

/**
* 模拟 get set 操作时,会发现来自于数组的两个问题
*
* 超出 cache 容量时,要移除最早的元素,数组 shift 效率低
* 每次 get set 时都要把当前元素移动到最新的位置,数组 splice 效率低
*/
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
/* Array 改为双向链表 */
/* 对象(查) + 双向链表(增 删 移动) */

interface IListNode {
value: any
key: string
prev?: IListNode
next?: IListNode
}

export default class LRUCache {
private length: number
private data: { [key: string]: IListNode } = {}
private dataLength: number = 0
private listHead: IListNode | null = null
private listTail: IListNode | null = null

constructor(length: number) {
if (length < 1) throw new Error('Invalid length')
this.length = length
}

private moveToTail(currNode: IListNode) {
const tail = this.listTail
if (tail === currNode) return

const prevNode = currNode.prev
const nextNode = currNode.next
if (prevNode) {
if (nextNode) {
prevNode.next = nextNode
} else {
delete prevNode.next
}
}
if (nextNode) {
if (prevNode) {
nextNode.prev = prevNode
} else {
delete nextNode.prev
}

if (this.listHead === currNode) this.listHead = nextNode
}

delete currNode.prev
delete currNode.next

if (tail) {
tail.next = currNode
currNode.prev = tail
}
this.listTail = currNode
}

private tryClean() {
while (this.dataLength > this.length) {
const head = this.listHead
const headNext = head.next

delete headNext.prev
delete head.next

this.listHead = headNext

delete this.data[head.key]
this.dataLength--
}
}

get(key: string): any {
const data = this.data
const currNode = data[key]

if (currNode == null) return

if (this.listTail === currNode) {
// 最新鲜的位置
return currNode.value
}

// 移动到末尾
this.moveToTail(currNode)

return currNode.value
}

set(key: string, value: any) {
const data = this.data
const currNode = data[key]

if (currNode == null) {
// 新增数据
const newNode: IListNode = { key, value }

data[key] = newNode
this.dataLength++

if (this.dataLength === 1) {
this.listHead = newNode
this.listTail = newNode
}

// 移动到末尾
this.moveToTail(newNode)
} else {
// 修改数据
currNode.value = value
// 移动到末尾
this.moveToTail(currNode)
}

// 尝试清理长度
this.tryClean()
}
}

DOM-To-VNode render函数

1
2
3
4
5
<div id="div" style="border: 1px solid #ccc; padding: 10px;">
<p>一行文字<a href="abc.html" target="_blank">链接</a></p>
<img src="loading.png" alt="加载中..." class="image"/>
<button click="clickHandler">点击</button>
</div>
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
const vnode = {
tag: 'div',
props: {
id: 'div',
style: {
'border': '1px solid #ccc',
'padding': '10px'
}
},
children: [
{
tag: 'p',
props: {},
children: [
'一行文字',
{
tag: 'a',
props: {
href: 'abc.html',
target: '_blank'
},
children: ['链接']
}
]
},
{
tag: 'img',
props: {
className: 'image',
src: 'loading.png',
alt: '加载中...'
}
},
{
tag: 'button',
props: {
events: {
click: clickHandler
}
},
children: ['点击']
}
]
}

Array-To-Tree 数组转树

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
interface IArrayItem {
id: number
name: string
parentId: number
}

interface ITreeNode {
id: number
name: string
children?: ITreeNode[]
}

function convert(arr: IArrayItem[]): ITreeNode | null {
const idToTreeNode: Map<number, ITreeNode> = new Map()

let root = null

arr.forEach(item => {
const { id, name, parentId } = item

const treeNode: ITreeNode = { id, name }
idToTreeNode.set(id, treeNode)

const parentNode = idToTreeNode.get(parentId)
if (parentNode) {
parentNode.children = parentNode.children || []
parentNode.children.push(treeNode)
}

if (parentId === 0) root = treeNode // 根节点
})

return root
}

function convert(arr) {
const res = []

const map = arr.reduce((res, item) => {
res[item.id] = item
return res
}, {})

for (const item of arr) {
if (item.parentId === 0) {
res.push(item)
continue
}
if (item.parentId in map) {
const parent = map[item.parentId]
parent.children = parent.children || []
parent.children.push(item)
}
}

return res
}

const arr = [
{ id: 1, name: '部门 A', parentId: 0 },
{ id: 2, name: '部门 B', parentId: 1 },
{ id: 3, name: '部门 C', parentId: 1 },
{ id: 4, name: '部门 D', parentId: 2 },
{ id: 5, name: '部门 E', parentId: 2 },
{ id: 6, name: '部门 F', parentId: 3 }
]
const tree = convert(arr)
console.log(tree)

Tree-To-Array 树转数组

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
interface IArrayItem {
id: number
name: string
parentId: number
}

interface ITreeNode {
id: number
name: string
children?: ITreeNode[]
}

function convert(root: ITreeNode): IArrayItem[] {
const nodeToParent: Map<ITreeNode, ITreeNode> = new Map()

const arr: IArrayItem[] = []

/* 广度优先遍历 */
const queue: ITreeNode[] = []
queue.unshift(root)

while (queue.length) {
const currNode = queue.pop()

const { id, name, children = [] } = currNode

const parentNode = nodeToParent.get(currNode)
const parentId = parentNode?.id || 0
const item = { id, name, parentId }
arr.push(item)

children.forEach(child => {
nodeToParent.set(child, currNode)
queue.unshift(child)
})
}

return arr
}

const tree = {
id: 1,
name: '部门 A',
children: [
{
id: 2,
name: '部门 B',
children: [
{ id: 4, name: '部门 D' },
{ id: 5, name: '部门 E' }
]
},
{
id: 3,
name: '部门 C',
children: [
{ id: 6, name: '部门 F' }
]
}
]
}
const arr = convert(tree)
console.log(arr)

统计 SDK

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
const PV_URL_SET = new Set()

class MyStatistic {
constructor(productId) {
this.productId = productId

this.initPerformance() /* 性能统计 */
this.initError() /* 错误监控 */
}

send(url, params = {}) {
params.productId = productId

const paramArr = []
for (let key in params) {
const value = params[key]
paramArr.push(`${key}=${value}`)
}

// 兼容性好 可跨域
const img = document.createElement('img')
img.src = `${url}?${paramArr.join('&')}`
}

initPerformance() {
const url = ''
this.send(url, performance.timing)
}

initError() {
window.addEventListener('error', event => {
const { error, lineno, colno } = event
this.error(error, { lineno, colno })
})
// Promise 未 catch 住的报错
window.addEventListener('unhandledrejection', event => {
this.error(new Error(event.reason), { type: 'unhandledrejection' })
})
}

pv() {
const href = location.href
if (PV_URL_SET.get(href)) return

this.event('pv', {})

PV_URL_SET.add(href)
}

/* 埋点 */
event(key, val) {
const url = ''
this.send(url, {key, val})
}

error(err, info = {}) {
const url = ''
const { message, stack } = err
this.send(url, { message, stack, ...info })
}
}

图片懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* <img src="loading.gif" data-src="" /> */
function imgLazyLoad() {
const images = document.querySelectorAll('img[data-src]')

images.forEach(img => {
const rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src
img.removeAttribute('data-src')
}
})
}

window.addEventListener('scroll', throttle(() => {
imgLazyLoad()
}))

imgLazyLoad()

UUID

1
2
3
4
5
6
7
8
9
10
11
12
function generateUUID() {
let timestamp = new Date().getTime()
if (window.performance && typeof window.performance.now === 'function') {
timestamp += performance.now() // use high-precision timer if available
}
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(item) {
let random = (timestamp + Math.random() * 16) % 16 | 0
timestamp = Math.floor(timestamp / 16)
return (item == 'x' ? random : (random & 0x3) | 0x8).toString(16)
})
return uuid
}

内存泄漏

垃圾回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function foo() {
globalVariable = '' // 全局变量
this.bigData = {}
}

function fn() {
const obj = {
v: 100
}
window.obj = obj // 全局变量
}

function genDataFn() {
const data = {} // 闭包
return {
get(key) {
return data[key]
},
set(key, val) {
data[key] = val
}
}
}
const { get, set } = genDataFn()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const bigData = {
js: {

},
css: {},
html: {}
}

const _obj = bigData

// 暂未GC
bigData = 'other'

const _class = _obj.js

// 暂未GC
_obj = 'other'

// GC
_class = null

垃圾回收机制

引用计数 ❌

早期的垃圾回收算法,以数据是否被引用用来判断要不要回收,这个算法有一个缺陷循环引用

1
2
3
4
5
6
7
function fn() {
const obj1 = {}
const obj2 = {}
obj1.a = obj2
obj2.b = obj1 // 循环引用
}
fn()

早期 IE6、7 使用引用计数算法进行垃圾回收,常常因为循环引用导致 DOM 对象无法进行垃圾回收

1
2
3
4
5
6
7
let elem
window.onload = function () {
elem = document.getElementById('elem')
elem.customAttribute = elem
elem.someBigData = { ... } /* 一个很大的数据 */
}
// 即便界面上删除了 elem,但在 JS 内存中它仍然存在,包括它的所有属性。现代浏览器已经解决了这个问题

不希望它存在的,它却仍然存在,这是不符合预期的,这就是内存泄漏

标记清除 👍

基于上面的问题,现代浏览器使用标记清除算法,根据数据是否可获得来判断是否回收

内存泄漏场景

Vue 组件中全局变量、定时器、注册全局事件、自定义事件,组件销毁时要记得清空解绑

闭包,变量销毁不了,是内存泄漏吗?
不一定 闭包它是符合开发者预期的,即本身就这么设计的;而内存泄漏是非预期的
但是,也有会认为不可被垃圾回收就是内存泄漏🤷

可使用 Chrome devTools Performance 检测内存变化

早期前端不太关注内存泄漏,因为不会像服务端一样 7*24 运行
而随着现在富客户端系统不断出现,内存泄漏也在慢慢地被重视

WeakMap WeakSet

弱引用,不会影响垃圾回收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const wMap = new WeakMap()
function fn() {
const obj = {
v: 100
}
// WeakMap 专门做弱引用的,因此 WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名。其他的无意义
wMap.set(obj, 100)
}
fn()

const wSet = new WeakSet()
function fn() {
const obj = {
v: 100
}
wSet.add(obj)
}
fn()

Event Loop 事件循环

  • Call Stack 清空
  • 执行微任务
  • 尝试 DOM 渲染
  • 触发 Event Loop
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
Promise.resolve().then(() => {
console.log(0)
return Promise.resolve(4)
}).then((res) => {
console.log(res)
}).then(() =>{
console.log(6)
})

Promise.resolve().then(() => {
console.log(1)
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(5)
}).then(() =>{
console.log(7)
})

Promise.resolve().then(() => {
console.log('第一拍')
const p = Promise.resolve(4)
Promise.resolve().then(() => {
console.log('第二拍')
p.then(res => {
console.log(res, '慢两拍')
}).then(() => {
console.log(6, '慢两拍')
})
})
})
/**
* 1. then 交替执行
* 2. then 返回 promise 对象时“慢两拍”
* - promise 状态由 pending 变为 fulfilled
* - then 函数挂载到 microTaskQueue
*/

异步 单线程

  • JS 是单线程的,浏览器中 JS 和 DOM 渲染线程互斥
  • 解决方案: 异步
  • 实现原理: Event Loop

宏任务 微任务

在 ES6 规范中,microtask 称为 jobs,macrotask 称为 task。

  • 宏任务(ES6语法规定的)Callback Queue
    1. script
    2. setTimeout、setInterval
    3. setImmediate
    4. requestAnimationFrame、requestIdleCallback
    5. UI渲染、 网络请求、I/O、DOM事件
  • 微任务(浏览器规定的)MicroTask Queue
    1. promise.then、async/await
    2. queueMicrotask
    3. Object.observe
    4. MutationObserver
    5. process.nextTick(Nodejs)

微任务在 DOM 渲染前执行,而宏任务在 DOM 显示后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const p = document.createElement('p')
p.innerHTML = 'new paragraph'
document.body.appendChild(p)

const list = document.getElementsByTagName('p')
console.log(list.length)

console.log('start')
setTimeout(() => {
console.log('timeout ', list.length)
alert('timeout 阻塞')
})
Promise.resolve().then(() => {
console.log('promise.then ', list.length)
alert('promise.then 阻塞')
})
console.log('end')

Nodejs

浏览器的 Event-Loop 由各个浏览器自己实现;而 Node 的 Event-Loop 由 libuv 来实现

  1. 浏览器
    • 一个 macro, 一队 micro
  2. Node
    • 执行异步任务都是以批量的形式,一队一队地执行
    • 循环形式为:宏任务队列 -> 微任务队列 -> 宏任务队列 -> 微任务队列 … 这样交替进行
    • Node 11 事件循环已与浏览器事件循环机制趋同
  • 宏任务
    • 按优先级顺序执行
  • 微任务
    1. process.nextTick优先级高
    2. promise.then 和 async/await
  • process.nextTick是在当前帧结束后立即执行,会阻断 IO 并且有最大数量限制
  • setImmediate不会阻断 IO ,更像是setTimeout(fun, 0),推荐使用 👍

Hybrid App

Hybrid App 兼具Native App 良好用户交互体验Web App 跨平台开发的优势

自定义 Schema 协议

类似weixin://

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const sdk = {
invoke(url, data, success, err) {
const iframe = document.createElement('iframe')
iframe.style.visibility = 'hidden'
document.body.appendChild(iframe)

iframe.onload = () => {
const content = iframe.contentWindow.document.body.innerHTML
success(JSON.parse(content))
iframe.remove()
}
iframe.onerror = () => {
err()
iframe.remove()
}
iframe.src = `my-app-name://${url}?data=${JSON.string(data)}`
}
}

JSBridge

单点登录

如果业务系统都在同一主域名下,比如 wenku.baidu.com tieba.baidu.com
可以把 cookie domain 设置为主域名 .baidu.com

SSO

复杂一点的,滴滴 同时拥有 didichuxing.com xiaojukeji.com didiglobal.com 等域名,则需要使用 SSO 技术方案

单点登录

OAuth2

SSO 是 oAuth 的实际案例,其他常见的还有微信登录、github 登录等。当涉及到第三方用户登录校验时,都会使用 OAuth2.0 标准

  • http 请求是无状态的,每次请求之后都会断开连接
  • 所以,每次请求时,都可以携带一段信息发送到服务端,以表明客户端的用户身份。服务端也可以通过 set-cookie 向客户端设置 cookie 内容
  • 由于每次请求都携带 cookie,所以 cookie 大小限制 4kb 以内

本地存储

cookie 作为本地存储,并不完全合适

  1. localStorage
  2. sessionStorage

跨域限制

浏览器存储 cookie 是按照域名区分的,浏览器无法通过 JS document.cookie 获取到其他域名的 cookie

http 请求传递 cookie 默认有跨域限制,如果想要开启,需要客户端和服务器同时设置允许

  • 客户端:使用 fetch 和 XMLHttpRequest 或者 axios 需要配置 withCredentials
  • 服务端:需要配置 header Access-Control-Allow-Credentials

现代浏览器都开始禁用第三方 cookie(第三方 js 设置 cookie),打击第三方广告,保护用户个人隐私

例如一个电商网站 A 引用了淘宝广告的 js

  • 当访问 A 时,淘宝 js 设置 cookie,记录下商品信息
  • 你再次访问淘宝时,淘宝即可获取这个 cookie 内容
  • 并和你的个人信息一起发送到服务端,方便精准推荐

登录校验

  • 前端输入用户名密码,传给后端
  • 后端验证成功,返回信息时 set-cookie
  • 客户端 cookie 存储 sessionId,不含用户敏感信息
  • 用户信息存储在 session 中,session 是服务端的一个 hash 表

JWT

JSON Web Token

  • cookie http 规范;默认存储;有跨域限制;配合 session 实现登录;只支持浏览器;有 CSRF 风险
  • token 自定义标准;自定义存储;无跨域限制;可用于 JWT 登录;还支持 App;无 CSRF 风险
  • Session
    1. 占用服务端内存,有硬件成本
    2. 多服务器 Redis 同步成本高
    3. 跨域传递 cookie,需要特殊配置
    4. 用户信息存储在服务端,可以快速封禁用户
  • JWT
    1. 不占用服务器内存
    2. 多进程、多服务器,不受影响
    3. 不受跨域限制
    4. 无法快速封禁登录用户

虚拟列表

URL URLSearchParams

1
2
new URLSearchParams(location.search).get('token')
new URL(location.href).searchParams.get('language')