前端知识体系(持续更新)

512 阅读38分钟

数据类型

基础数据类型

  • null
    • typeof 返回'object',这是一个遗留的 bug: 在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头的是对象,null 是全 0,所以将 null 误判为 object
  • undefined
  • Boolean
  • String
    • 字符串的内部格式始终是  UTF-16,即:每个字符都有对应的数字代码。可以用 charCodeAt 查看
    • UTF-16 编码中,一个英文字母字符或一个汉字字符存储都需要2 个字节(2*8bit)(Unicode 扩展区的一些汉字存储需要 4 个字节
  • Number
    • 双精度 IEEE 754 64 位浮点,8 个字节
    • 整数类型表示范围:-2^532^53(16个数字),超过范围无法精确表示
  • BigInt
    • 任意精度数字类型,已经进入 stage3 规范
    • 整数字面量后面加  n,或者调用 BigInt
    • 支持+*-**%/  操作符会向零取整
    • BigInt 和 Number 不是严格相等的,但是宽松相等的。
    • typeof 返回'bigint'
    • toString 方法返回以指定基数(base)表示指定数字的字符串
    • 不能被 JSON.stringify 序列化
    • 当使用  BigInt  时,带小数的运算会被取整,如 5n / 2n 结果为 2n
  • Symbol
    • typeof 返回 'symbol'
    • 表示唯一值
    • 用来作为对象属性的标识符,获取 symbol 属性:Object.getOwnPropertySymbols()、Reflect.ownKeys()
    • for...in、Object.getOwnPropertyNames 不可枚举对象的 symbol 属性

cloud.tencent.com/developer/a…

计算变量所占内存大小

使用 object-sizeof 计算: github.com/miktam/size…

包装对象

当基本字符串、布尔值等基本值需要调用一个对应对象才有的方法或者查询值的时候(基本字符串是没有这些方法的),JavaScript 会自动将基本值转化为对象并且调用相应的方法或者执行查询,转化后的对象就是包装对象。

parseInt

parseInt() 函数解析一个字符串参数,并返回一个指定基数的整数 (数学系统的基础)。

const intValue = parseInt(string[, radix]);
  • string 要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。
  • radix 一个介于 2 和 36 之间的整数(数学系统的基础),表示上述字符串的基数。默认为 10。
  • 返回值 返回一个整数或 NaN

注意: 在 radix 为 undefined,或者 radix 为 0 或者没有指定的情况下,JavaScript 作如下处理:

  • 如果字符串 string 以"0x"或者"0X"开头, 则基数是 16 (16 进制).
  • 如果字符串 string 以"0"开头, 基数是 8(八进制)或者 10(十进制),那么具体是哪个基数由实现环境决定。ECMAScript 5 规定使用 10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出 radix 参数的值。
  • 如果字符串 string 以其它任何值开头,则基数是 10 (十进制)。

位运算

&AND如果两位都是 1 则设置每位为 1
|OR如果两位之一为 1 则设置每位为 1
XOR如果两位只有一位为 1 则设置每位为 1
~NOT反转所有位
<<零填充左位移通过从右推入零向左位移,并使最左边的位脱落。
>>有符号右位移通过从左推入最左位的拷贝来向右位移,并使最右边的位脱落。
>>>零填充右位移通过从左推入零来向右位移,并使最右边的位脱落。
异或运算
  • 交换律:a ^ b ^ c  <=> a ^ c ^ b
  • 任何数于0异或为任何数 0 ^ n => n
  • 相同的数异或为0: n ^ n => 0

应用:

  1. 利用异或运算可以找出数组中奇个数的项(相同的数异或为0)
  2. 使某些特定的位翻转,例如对数10100001的第2位和第3位翻转,则可以将该数与00000110进行按位异或运算。
  3. 实现两个值的交换,而不必使用临时变量。
  4. 快速判断两个值是否相等,(a ^ b) === 0
JavaScript 使用 32 位按位运算数

JavaScript 将数字存储为 64 位浮点数,但所有按位运算都以 32 位二进制数执行。

在执行位运算之前,JavaScript 将数字转换为 32 位有符号整数。

执行按位操作后,结果将转换回 64 位 JavaScript 数。

取整:~~num

对象

对象的属性

ECMAScript 将对象的属性分为两种:数据属性和访问器属性。 然后根据具体的上下文环境的不同,我们又可以将属性分为:原型属性和实例属性。

数据属性(Data Properties)

数据属性有 4 个描述其行为的特性:

  • [[Configurable]]: 表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
  • [[Enumerable]]: 表示能否通过 for-in 循环返回属性。
  • [[Writable]]: 表示能否修改属性的值。
  • [[Value]]: 包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值时,把新值保存在这个位置。默认值是 undefined。

注意: 直接在对象上定义属性, [[Configurable]]、[[Enumerable]] 和 [[Writable]] 特性默认都被设置为 true,而 [[Value]] 特性被设置为指定的值。 而直接通过 Object.defineProperty() 为对象添加属性以及值,这种情况下,这个对象的这个属性的另外 3 个特性的默认值都是 false。

访问器属性(Accessor Properties)

访问器属性有如下 4 个特性:

  • [[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。
  • [[Enumerable]]:表示能否通过 for-in 循环返回属性。
  • [[Get]]:在读取属性时调用的函数。默认值为 undefined。
  • [[Set]]:在写入属性时调用的函数。默认值为 undefined。

访问器属性不能直接定义,必须使用 Object.defineProperty() 来定义。

Object.keys 、for...in、 Object.getOwnPropertyNames

  • for...in 语句以任意顺序遍历一个对象的除 Symbol 以外的可枚举属性,包括继承的可枚举属性。

    • 其实 for...in 操作的主要目的就是遍历对象的属性,如果只需要获取对象的实例属性,可以使用 hasOwnProperty()进行过滤。
    • 遍历顺序:事实上,它不一定根据定义时的顺数输出,所有浏览器的最新版本现在都按 chrome 执行,先把当中的非负整数键提出来,排序好输出,然后将剩下的定义时的顺序输出。
  • Object.keys()用于获取对象自身所有的可枚举的属性值,但不包括原型中的属性,然后返回一个由属性名组成的数组。注意它同 for..in 一样不能保证属性按对象原来的顺序输出。

  • Object.getOwnPropertyNames()方法返回对象的所有自身属性的属性名(包括不可枚举的属性)组成的数组,但不会获取原型链上的属性。

  • Object.getOwnPropertySymbols, 遍历原型链获取 Symbol 属性: Object.getOwnPropertySymbols(Object.getPrototypeOf(obj))

注意:

  • 若扩展了原生的 Array,for...in 遍历数组会输出扩展属性

  • for..in 遍历数组它自动过滤掉了不存在的元素,但是对于存在的元素且值为 undefined 或者'null'仍然会有效输出

var colors = ['red', 'green', 'blue'];
// 将数组长度变为10
colors.length = 10;
// 再添加一个元素的数组末尾
colors.push('yellow');

for (var i in colors) {
  console.log(i); // 0 1 2 10
}

for in 遍历顺序

标准参考 根据 ECMA-262(ECMAScript)第三版中描述,for-in 语句的属性遍历的顺序是由对象定义时属性的书写顺序决定的。

在现有最新的 ECMA-262(ECMAScript)第五版规范中,对 for-in 语句的遍历机制又做了调整,属性遍历的顺序是没有被规定的。

新版本中的属性遍历顺序说明与早期版本不同,这将导致遵循 ECMA-262 第三版规范内容实现的 JavaScript 解析引擎在处理 for-in 语句时,与遵循第五版规范实现的解析引擎,对属性的遍历顺序存在不一致的问题。

Chrome Opera 中使用 for-in 语句遍历对象属性时会遵循一个规律,它们会先提取所有 key 的 parseFloat 值为非负整数的属性, 然后根据数字顺序对属性排序首先遍历出来,然后按照对象定义的顺序遍历余下的所有属性。其它浏览器则完全按照对象定义的顺序遍历属性。

参考: www.cnblogs.com/wujie520303… javascript.info/object#orde…

for of

for...of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句 for in 语句以任意顺序迭代对象的可枚举属性。

for...of 语句遍历可迭代对象定义要迭代的数据。

定义迭代对象:

let iterable = {
  [Symbol.iterator]() {
    return {
      i: 0,
      next() {
        if (this.i < 3) {
          return { value: this.i++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
};

new

new 的作用:

  • 创建一个新对象 obj
  • 把 obj 的proto指向 构造函数原型 实现继承
  • 执行构造函数,传递参数,改变 this 指向
  • 返回值,执行构造函数返回的结果是对象则返回执行结果,否则返回新对象
function _new() {
  let constructor = Array.prototype.shift.call(arguments);
  let args = arguments;
  const obj = new Object();
  obj.__proto__ = constructor.prototype;
  let res = constructor.call(obj, ...args);
  return res&& typeof res ==='object'? res : obj;
}

Map Set

Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

WeakSet

WeakSet 的成员只能是对象,而不能是其他类型的值。 WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

WeakSet 结构有以下三个方法。

  • WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。

Map

  1. Object的键只能是字符串或者Symbols,Map的键可以是任何类型。
  2. Map中的键值遵循FIFO原则,即有序的。而Object添加的键则不是。
  3. Map中的键值对可以通过size来计算,Object需要我们手动计算。
  4. Object 都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。

Map 结构原生提供三个遍历器生成函数和一个遍历方法。

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

何时使用map、object

  • 当所要存储的是简单数据类型,并且 key 都为字符串或者整数或者 Symbol 的时候,优先使用 Object ,因为Object可以使用 字符变量 的方式创建,更加高效。
  • JSON 直接支持 Object,但不支持 Map
  • Map 会按照插入顺序保持元素的顺序,而Object做不到。
  • Map 在存储大量元素的时候性能表现更好,特别是在代码执行时不能确定 key 的类型的情况。

WeakMap

  • WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名
  • WeakMap 的键名所指向的对象,不计入垃圾回收机制, 键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
  • WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
let myElement = { key: 1 };
let wm = new WeakMap();

wm.set(myElement, { timesClicked: 0 });
myElement = null;
// 强制垃圾回收后 wm为空
console.log(wm);

总结 Set

  • 成员唯一、无序且不重复
  • [value, value],键值与键名是一致的(或者说只有键值,没有键名)
  • 可以遍历,方法有:add、delete、has

WeakSet

  • 成员都是对象
  • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏
  • 不能遍历,方法有 add、delete、has

Map

  • 本质上是键值对的集合,类似集合,键类型可以是任意的
  • 可以遍历,方法很多可以跟各种数据格式转换

WeakMap

  • 只接受对象作为键名(null 除外),不接受其他类型的值作为键名
  • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
  • 不能遍历,方法有 get、set、has、delete

WeakRef

WeakSet 和 WeakMap 是基于弱引用的数据结构,ES2021 更进一步,提供了 WeakRef 对象,用于直接创建对象的弱引用。 es6.ruanyifeng.com/#docs/set-m…

FinalizationRegistry

ES2021 引入了清理器注册表功能 FinalizationRegistry,用来指定目标对象被垃圾回收机制清除以后,所要执行的回调函数。

函数

执行栈

js 的运行有三种环境:

  • Global Code, JavaScript 代码开始运行的默认环境
  • Function Code, 代码进入一个 JavaScript 函数
  • Eval Code, 使用 eval()执行代码

为了表示不同的运行环境,JavaScript 中有一个执行上下文(Execution context,EC)的概念。也就是说,当 JavaScript 代码执行的时候,会进入不同的执行上下文,这些执行上下文就构成了一个执行上下文栈(Execution context stack,ECS)。

执行上下文有三个重要的属性:

  • 变量对象(Variable object,VO),进入一个执行上下文时被激活(Activation object,AO)
  • 作用域链(Scope chain)
  • this

解释器执行代码的伪逻辑

  1. 查找调用函数的代码
  2. 执行代码之前,先进入创建上下文阶段
    • 分析形参
    • 扫描上下文的函数声明
      • 为发现的每一个函数,在变量对象上创建一个属性——确切的说是函数的名字——其有一个指向函数在内存中的引用
      • 如果函数的名字已经存在,引用指针将被重写
    • 扫描上下文的变量声明
      • 为发现的每个变量声明,在变量对象上创建一个属性——就是变量的名字,并且将变量的值初始化为 undefined
      • 如果变量的名字已经在变量对象里存在,将不会进行任何操作并继续扫描
    • 求出上下文内部“this”的值。
  3. 执行代码阶段
    • 在当前上下文上运行/解释函数代码,并随着代码一行行执行指派变量的值。

AO

VO/AO 代表局部作用域, VO 对应伪逻辑第二阶段,AO 对应第三阶段。

GO

GO 代表全局作用域

作用域链 [[Scopes]]

[[Scopes]] 是一个链式数组结构:[AO1,AO2,...,GO],最前端是当前函数的活动对象 AO。

对于自由变量,即当前作用域中没有定义的变量,需要向父级作用域寻找(AO2), 如果父级中没有找到,则再一层一层向上查找,直到全局作用域。这种一层一层间的关系,就是作用域链。

注意:自由变量的查找依据的是函数定义时的作用域,而不是执行时的作用域,例如闭包。

词法环境

在 JavaScript 中,每个运行的函数,代码块  {...} (包括 eval、with、catch)以及整个脚本,都有一个被称为  词法环境(Lexical Environment)   的内部(隐藏)的关联对象。

词法环境对象由两部分组成:

  • 1:环境记录(Environment Record) ,这个就是真正登记变量的地方。

    • 1.1:声明式环境记录(Declarative Environment Record) :用来记录直接有标识符定义的元素,比如变量、常量、let、class、module、import 以及函数声明。
    • 1.2:对象式环境记录(Object Environment Record) :主要用于 with 和 global 的词法环境。
  • 2:对外部词法环境的引用(outer) ,它是作用域链能够连起来的关键。

一个“变量”只是  环境记录  这个特殊的内部对象的一个属性。“获取或修改变量”意味着“获取或修改词法环境的一个属性”。

内部词法环境和 外部词法环境

在一个函数运行时,在调用刚开始时,会自动创建一个新的词法环境以存储这个调用的局部变量和参数。 在这个函数调用期间,我们有两个词法环境:内部一个(用于函数调用)和外部一个(全局):

  • 内部词法环境与   函数的当前执行相对应。
  • 外部词法环境是全局词法环境。

代码执行流程

引擎在执行代码的步骤如下:

  • 1:创建一个新的执行上下文(Execution Context)
  • 2:创建一个新的词法环境(Lexical Environment)
  • 3:把LexicalEnvironmentVariableEnvironment指向新创建的词法环境
  • 4:把这个执行上下文压入执行栈并成为正在运行的执行上下文
  • 5:执行代码
  • 6:执行结束后,把这个执行上下文弹出执行栈

闭包

闭包的解释:

  • 闭包是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回了之后。
  • 闭包就是指:执行完的执行上下文被弹出执行栈,它的词法环境处于失联状态,后续的执行上下文没办法直接访问这个失联的词法环境。在闭包这种情况下还保留了对那个词法环境的引用,从而可以通过这个引用去访问失联的词法环境,这个引用就是闭包。

原型

在 Javascript 中,每个函数都有一个原型属性 prototype 指向自身的原型,而由这个函数创建的对象也有一个proto属性指向这个原型,而函数的原型是一个对象,所以这个对象也会有一个proto指向自己的原型,这样逐层深入直到 Object 对象的原型,这样就形成了原型链。

  • 每个函数都有一个特殊的属性叫作原型(prototype)
    • prototype的constructor指向构造函数
  • 每个通过构造函数创建出来的实例对象,其本身有个属性__proto__,指构造函数原型对象
    • 生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型
补充
  • 所有的原型对象__proto__属性都是指向function Object原型对象,即null
  • 每个函数都是 Function 函数创建的对象,所以每个函数也有一个proto属性指向 Function 函数的原型。该原型对象为JS中所有函数的原型对象,而其__proto__属性也还是指向了function Object原型对象

原型链

当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会通过它的__proto__隐式属性,找到它的构造函数原型对象,如果还没有找到就会再在其构造函数prototype__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链

原型继承

function Father() {
    this.name = "father";
}
Father.prototype.lastName = 'lastName'
function Child(name) {
    this.name = name;
}
Child.prototype = new Father();
let obj = new Child("child");
console.log(obj.lastName);

子类的原型指向了父类的实例,所以子类的实例可以通过原型链访问到父类的实例,然后通过原型链向上可以访问到父类原型,就可以实现子类实例可以继承和访问父类的属性和方法。

借用构造函数继承

function Father(name) {
    this.name = name
}
Father.prototype.lastName = 'lastName'
function Child(name) {
     Father.call(this, "cat")
}
//Child.prototype = new Father();
let obj = new Child("child");
console.log(obj);

在子类的构造函数中通过call()调用父类的构造函数,但是不能访问父类原型中的属性和方法。

原型链+借用构造函数的组合继承

通过调用父类构造函数,继承父类的属性并且可以向父类传递参数,然后再通过将父类实例作为子类原型,实现函数复用。

function Father(name) {
    this.name = name;
}
Father.prototype.lastName = 'lastName'
function Child(name) {
    Father.call(this, name)
}
Child.prototype = new Father();
let obj1 = new Child("child1");
let obj2 = new Child("child2");
obj1.lastName ="new lastName "
console.log(obj1,obj2);

优点:

  • 可以继承父类的属性和方法,也可以继承父类原型的属性和方法
  • 不存在引用属性共享问题
  • 可以传参给父类构造函数
  • 函数可以复用

缺点:

  • 调用了两次构造函数,生成了两份实例

instanceof

  • instanceof  的内部机制是通过判断对象的原型链中是不是能找到类型的  prototype
  • instanceof  只能用来判断对象类型,原始类型不可以。并且所有对象类型 instanceof Object 都是 true。
  • instanceof 是判断类型的 prototype 是否出现在对象的原型链中,但是对象的原型可以随意修改,所以这种判断并不准确。
[]  instanceof Array; // true
[]  instanceof Object; // true
const obj = {}
Object.setPrototypeOf(obj, Array.prototype)
obj instanceof Array // true

箭头函数和普通函数的区别

引入箭头函数有两个方面的作用:更简短的函数并且不绑定this。箭头函数与普通函数不同之处有:

  1. 箭头函数没有 this,无法使用 apply / call / bind 进行绑定 this 值;
  2. 没有 arguments;
  3. 不绑定 super 和 new.target(指向构造函数的引用);
  4. 没有 prototype 属性,即指向 undefined;
  5. 无法使用 new 实例化对象,因为普通构造函数通过 new 实例化对象时 this 指向实例对象,而箭头函数没有 this 值。

内存

内存的生命周期

  • 内存分配:声明变量、函数、对象的时候,js 会自动分配内存。
  • 内存的使用:调用的时候,使用的时候
  • 内存的回收

垃圾回收机制

基本概念:

  • collector指的就是垃圾收集器
  • mutator 除了垃圾收集器之外的部分,如应用程序本身。mutator的职责一般是NEW(分配内存),READ(从内存中读取内容),WRITE(将内容写入内存)
  • mutator roots(mutator根对象),mutator根对象一般指的是分配在堆内存之外,可以直接被mutator直接访问到的对象,一般是指静态/全局变量以及Thread-Local变量
标记清除算法(mark-sweep)

定义:清除无法达到的对象

  1. 在运行的时候给存储在内存的所有变量加上标记;
  2. 在标记阶段,从根部出发,能触及的对象,把标记清除,将其记录为可达对象;
  3. 在清除阶段,对堆内存从头到尾进行线性的遍历,有标记的变量被视为即将要删除的变量

缺点

  • Collector在进行标记和清除阶段时会将整个应用程序暂停(mutator),等待标记清除结束后才会恢复应用程序的运行,这也是Stop-The-World这个单词的来历。
  • 垃圾收集后有可能会造成大量的内存碎片

www.jianshu.com/p/b0f5d21fe…

引用计数

定义:清除无引用的对象。

算法原理

引用计数算法很简单,它实际上是通过在对象头中分配一个空间来保存该对象被引用的次数。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。

引用计数法的优点

垃圾收集的开销被分摊到整个应用程序的运行当中,而不需要在进行垃圾收集时,要挂起整个应用的运行,直到对堆中所有对象的处理都结束。其他垃圾收集都是在为新对象分配内存空间时由于内存空间不足而触发的,而且是针对整个堆中的所有对象进行的。而引用计数垃圾收集机制不一样,它只是在引用计数变化为0时即刻发生,而且只针对某一个对象以及它所依赖的其它对象。

注意,当某个对象的引用计数减为0时,collector需要递归遍历它所指向的所有域,将它所有域所指向的对象的引用计数都减一,然后才能回收当前对象。在递归过程中,引用计数为0的对象也都将被回收

引用计数法的缺点

对于循环引用的对象,无法回收。

参考: www.jianshu.com/p/1d5fa7f60…

内存泄漏场景
  1. 全局变量
  2. 定时器和回调函数
  3. 闭包
  4. dom 引用
const elements={
    image:document.getElementById('image')
}
// 移除dom后elements.image的引用还存在内存当中
document.body.removeChild(document.getElementById('image'))
// clear
elements.image=null
计算变量占用内存的大小
// 1. key、value都需要计算
// 2. 同一引用不重复计算
const seen = new WeakSet();
function sizeOfObject(object){
    if(object===null) return 0
    let bytes=0
    const properties=Object.keys(object)
    for(let i=0;i<properties.length;i++){
        const key =properties[i]
        if(typeof object[key] === 'object' && object[key] !===null){
            if(seen.has(object[key])) continue;
            seen.add(object[key])
        }
        bytes+=calculator(key)
        bytes+=calculator(object[key])
    }
    return bytes;
}
// 每个字符占2byte number:8byte boolean:4byte
function calculator(object){
    const objectType = typeof object
    switch(objectType){
        case 'string':
            return object.length*2
        case 'number':
            return 8; // 64位,8字节
        case 'boolean':
            return 4
        case 'object':{
            if(Array.isArray(object)){
                return object.map(calculator).reduce((res,val)=>res+val),0)
            }else{
                return sizeOfObject(object)
            }
        }
        default:
            return 0;
    }
}

V8 引擎内存管理

  • 新生代内存空间(64 位 OS 下大小限制为 64MB)
    • semi space Form
    • semi space To
  • 老生代内存空间(64 位 OS 下大小限制为 1400MB)

内存大小

和操作系统有关 64 位 OS 下,内存大小限制为 1.4GB, 新生代内存空间大小限制为 64MB, 老生代内存空间大小限制为 1400MB

由于以下原因,js 内存设计不会太大:

  • 内存不会持久化,执行后进行回收;
  • 回收内存时,js 暂停执行, 回收一次 100mb,需要暂停 6ms

内存大小调整

V8 提供选择来调整内存大小的配置,需要在初始化时候配置生效,遇到 Node 无法分配足够内存给 JS 对象的情况,可以用如下办法来放宽 V8 默认内存限制。避免执行过程内存用的过多导致崩溃

node --max-old-space-size=1700 index.js
node --max-new-space-size=1024 index.js

V8 垃圾回收算法

在 V8 中,主要将内存分为新生代和老生代,新生代的对象为存活时间较短的对象,老生代的对象为存活时间较长或常驻内存的对象,

  1. 新生代-半区复制法

新生代内存空间用来存储新产生的变量,变量小,存在时间短

  • 变量先存储在 From 空间里,满足一定条件后发生回收
  • 将 From 空间中活着的变量复制到 To 空间中
  • 然后清空 From 空间
  • 下一次发生回收时 From 和 To 对调

这种复制算法即 Cheney 算法:

Cheney 算法是一种采用复制的方式实现的垃圾回收算法。它将堆内存一分为二,每一部分空间称为 semispace。在这两个 semispace 空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的 semispace 空间称为 From 空间,处于闲置状态的空间称为 To 空间。当我们分配对象时,先是在 From 空间中进行分配。当开始进行垃圾回收时,会检查 From 空间中的存活对象,这些存活对象将被复制到 To 空间中,而非存活对象占用的空间将会被释放。完成复制后,From 空间和 To 空间的角色发生兑换。简而言之,在垃圾回收过程中,就是通过将存活对象在两个 semispace 空间之间进行复制。

型的牺牲空间换取时间的算法,内存中有一半空间始终是空闲的,新生代中对象的生命周期较短,恰恰适合这个算法。

www.jianshu.com/p/74659de07…

  1. 老生代-标记清除+压缩
  • 标记未使用的空间
  • 删除标记的空间
  • 删除后内存空间会产生空隙,需要进行整理使空间连续(数组需要连续的空间存储)

Mark Sweep: Mark Sweep 是将需要被回收的对象进行标记,在垃圾回收运行时直接释放相应的地址空间

Mark Compact: Mark Compact 的思想有点像新生代垃圾回收时采取的 Cheney 算法:将存活的对象移动到一边,将需要被回收的对象移动到另一边,然后对需要被回收的对象区域进行整体的垃圾回收。

www.jianshu.com/p/698eb5e1c…

新生代晋升到老生带

实际使用的堆内存是新生代的两个 semispace 空间大小和老生代所用内存大小之和。当一个对象经过多次复制依然存活时,它将会被认为是生命周期较长的对象。这种较长生命周期的对象随后会被移动到老生代中,采用新的算法进行管理。对象从新生代中移动到老生代中的过程称为 晋升

对象晋升的条件主要有两个,一个是对象是否经历过 Scavenge 回收,一个是 To 空间的内存占用比超过限制。

  1. 变量是否经历过回收?
  • 是,进入老生代
  • 否,进入 To 空间
  1. To 空间是否已经使用 25%(8MB)?
  • 是,进入老生代
  • 否,进入 To 空间

使用 node 查看内存使用情况

  • process.memoryUsage()
// 单位 byte
{
   rss: 23273472,
  heapTotal: 9682944,
  heapUsed: 5391304,
  external: 8874
}

www.jianshu.com/p/698eb5e1c…

V8 中的变量

  • 内存主要就是存储变量等数据的

JS 声明变量并赋值时,所使用对象的内存就分配在堆中。如果已申请的堆空闲内存不够分配新的对象,将继续申请堆内存,直到对的大小超过 V8 的限制为止。

变量分为局部变量和全局变量,这两者的回收方式有差异

  • 局部变量当程序执行结束,且没有引用的时候就会随着消失
    • 局部变量会回收,只是说可以回收,并不是用完立即回收
  • 全局对象会始终存活到程序运行结束

内存溢出例子

const size = 100 * 1024 * 1024;

let arrobj = {};

for (let i = 0; i < 10; i++) {
  arrobj[i] = new Array(size);
}

console.log('done');

参考: github.com/zqjflash/no…

www.jianshu.com/p/455d0b9ef…

异步编程

浏览器事件循环

  1. 一般来说首次执行的宏任务就是解析页面 html 和引入的 js 脚本。

  2. 整体流程:

  • 从宏队列中队首任务推入主执行栈。
  • 宏任务执行过程中遇到微任务,则需要将微任务添加到微队列中。
  • 主执行栈中宏任务执行完毕后,微队列中的所有任务会推入到主执行栈中,依次执行。
  • 主执行栈中的所有微任务执行完成后,开始检查渲染,渲染线程接管资源,js 主线程挂起
  • 渲染完毕后,渲染线程挂起,js 主线程接管资源,再次重复整个过程。
  1. 一次 tick 只能执行一个宏任务,但是可以执行多个微任务,这样可以避免不必要的 UI 重绘

常见的 macro task 有: setTimeout、MessageChannel、postMessage、setImmediate、requestAnimationFrame; 常见的 micro task 有: MutationObsever 和 Promise.then。

参考: zhuanlan.zhihu.com/p/96958260

Node事件循环

node 的事件循环比浏览器复杂很多。由 6 个宏任务队列+6 个微任务队列组成。 node宏任务优先级从高到低依次是:

  1. Timers,setTimeout setInterval
  2. I/O callbacks
  3. Idle,prepare
  4. Poll,获取新的 I/O 事件, 例如操作读取文件等;
  5. Check setImmediate
  6. Close callback

其执行规律是:

  1. 在一个宏任务队列全部执行完毕后,去清空一次微任务队列,然后到下一个等级的宏任务队列,以此往复。
  2. 一个宏任务队列搭配一个微任务队列。六个等级的宏任务全部执行完成,才是一轮循环。

除此之外,node 端微任务也有优先级先后:

  1. process.nextTick;
  2. promise.then 等; 清空微任务队列时,会先执行 process.nextTick,然后才是微任务队列中的其他。 参考: mp.weixin.qq.com/s/-cHJa8k3M…

Promise

Promise 简单实现

Promise A+ 规范: promisesaplus.cn/

let PADDING = 'PADDING',
  FULFILLED = 'FULFILLED',
  REJECTED = 'REJECTED';
class Promise {
  static status;
  result;
  resolvedCallbacks = [];
  rejectedCallbacks = [];
  constructor(fn) {
    this.status = PADDING;

    try {
      fn(this.resolve.bind(this), this.reject.bind(this));
    } catch (e) {
      this.reject(e);
    }
  }
  // 当promise处于pending时 promise可以转为fulfilled或rejected状态
  resolve(res) {
    setTimeout(() => {
      if (this.status === PADDING) {
        this.status = FULFILLED;
        this.result = res;
        this.resolvedCallbacks.forEach((fn) => {
          fn(res);
        });
      }
    });
  }
  reject(err) {
    setTimeout(() => {
      if (this.status === PADDING) {
        this.status = REJECTED;
        this.result = err;
        this.rejectedCallbacks.forEach((fn) => {
          fn(err);
        });
      }
    });
  }
  then(onFulfilled, onRejected) {
    return new Promise((resolve, reject) => {
      if (this.status === PADDING) {
        this.resolvedCallbacks.push(onFulfilled);
        this.rejectedCallbacks.push(onRejected);
      } else if (this.status === FULFILLED) {
        setTimeout(() => {
          try {
            this.result = onFulfilled();
          } catch (e) {
            onRejected(e);
          }
        });
      } else if (this.status === REJECTED) {
        setTimeout(() => {
          onRejected(this.result);
        });
      }
    });
  }
}
let p = new Promise((resolve, reject) => {
  console.log('created promise');
  setTimeout(() => {
    resolve('1');
    console.log('setTimeout called');
  });
});
p.then(
  (res) => {
    console.log('then resolved', res);
  },
  (err) => {
    console.log('then rejected', err);
  }
);
console.log('executed');
Promise 顺序执行

for 循环、reduce 等可以实现 p.then().then()...从而完成 Promise 顺序执行

Promise 限制并发
// func promise函数列表
// limit 并发数
function limitTask(...tasks,limit){
    const sequence=[...tasks]
    let promises=sequence.splice(0,limit).map((task,index)=>task().then(()=>index))
    let p =Promise.race(promises)
    // for实现p.then().then()...
    for(let i=0;i<sequence.length;i++){
        p=p.then(index=>{
            promise[index]=sequence[i].then(()=>index)
            return Promise.race(promises)
        })
    }

}

async 函数

async 是 Generator 函数的语法糖 和 Generator 相比,async 函数的有点:

  • 内置执行器。
  • 更好的语义
  • 更广的适用性,async 函数的 await 命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
  • 返回值是 Promise
返回 Promise 对象

async 函数返回一个 Promise 对象。

async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。

Promise 对象的状态变化

async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有 async 函数内部的异步操作执行完,才会执行 then 方法指定的回调函数。

await 命令

正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。 另一种情况是,await 命令后面是一个 thenable 对象(即定义了 then 方法的对象),那么 await 会将其等同于 Promise 对象。

注意

async 函数可以保留运行堆栈

const a = async () => {
  await b();
  c();
};

上面代码中,b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦 b()或 c()报错,错误堆栈将包括 a()。

微任务-MutationObserver

MutationObserver 接口提供了监视对 DOM 树所做更改的能力。它被设计为旧的 Mutation Events 功能的替代品,该功能是 DOM3 Events 规范的一部分。

function microFn(callback) {
  // 创建一个观察器实例并传入回调函数
  let observer = new MutationObserver((mutationsList, observer) => {
    //console.log('MutationObserver callback',mutationsList,observer)
    callback && callback();
    observer.disconnect();
  });
  let targetNode = document.createElement('div');
  // 以上述配置开始观察目标节点
  observer.observe(targetNode, { attributes: true, childList: true, subtree: true });
  targetNode.innerText = '1';
  targetNode = null;
}
microFn(() => {
  console.log('microFn callback');
});
console.log('microFn end');

模块

AMD

[AMD] 是 “Asynchronous Module Definition” 的缩写,意思就是“异步模块定义”。它采用异步加载方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。最有代表性的实现则是 requirejs。

CommonJS

Node.js 应用由模块组成,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。每个模块内部有两个变量可以使用,require  和  module

  • require  用来加载某个模块;
    • 第一次加载某个模块时,Node.js 会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的  module.exports  属性返回了
    • CommonJS 模块的加载机制是,require  的是被导出的值的拷贝。也就是说,一旦导出一个值,模块内部的变化就影响不到这个值 。
  • module  代表当前模块,是一个对象,保存了当前模块的信息。exports  是  module  上的一个属性,保存了当前模块要导出的接口或者变量。

exports 与 module.exports

exports  是模块内的私有局部变量,它只是指向了  module.exports,所以直接对  exports  赋值是无效的,这样只是让  exports  不再指向  module.exports了而已

CommonJS 实现

向一个立即执行函数提供  require 、 exports 、 module  三个参数,模块代码放在这个立即执行函数里面。模块的导出值放在  module.exports  中

  • installedModules CommonJS 规范有说明,加载过的模块会被缓存,所以需要一个对象来缓存已经加载过的模块
  • require 定义  require  函数来加载模块,返回模块导出的值 module.exports
  • module require  函数中,要生成一个  module,供模块使用
  • exports module  上 要有一个  exports,用来接收模块导出的内容
// bundle.js
(function (modules) {
  // 模块管理的实现
  var installedModules = {};
  /**
   * 加载模块的业务逻辑实现
   * @param {String} moduleName 要加载的模块名
   */
  var require = function (moduleName) {
    // 如果已经加载过,就直接返回
    if (installedModules[moduleName]) return installedModules[moduleName].exports;
    // 如果没有加载,就生成一个 module,并放到 installedModules
    var module = (installedModules[moduleName] = {
      moduleName: moduleName,
      exports: {}
    });
    // 执行要加载的模块
    modules[moduleName].call(module.exports, module, module.exports, require);
    return module.exports;
  };
  return require('index.js');
})({
  'a.js': function (module, exports, require) {
    // a.js 文件内容
  },
  'b.js': function (module, exports, require) {
    // b.js 文件内容
  },
  'index.js': function (module, exports, require) {
    // index.js 文件内容
  }
});

参考: zhuanlan.zhihu.com/p/113009496

浏览器

浏览器帧(Frame)

51423451-4a5f1f80-1bfb-11e9-8c0a-597f0d52f4c0.png 浏览器一帧内的工作:

  • 处理用户的交互
  • JS 解析执行
  • 帧开始。窗口尺寸变更,页面滚去等的处理
  • rAF
  • 布局
  • 绘制

requestIdleCallback

上面六个步骤完成后没超过 16 ms,说明时间有富余,此时就会执行 requestIdleCallback 里注册的任务。 假如浏览器一直处于非常忙碌的状态,requestIdleCallback 注册的任务有可能永远不会执行。此时可通过设置 timeout (见下面 API 介绍)来保证执行。

API

var handle = window.requestIdleCallback(callback[, options])
  • callback: ():回调即空闲时需要执行的任务,接收一个 IdleDeadline 对象作为入参。其中 IdleDeadline 对象包含:
    • didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用。
    • timeRemaining(),表示当前闲置周期的预估剩余毫秒数。
  • options:
    • timeout 。回调在timeout毫秒过后还没有被调用,那么回调任务将放入事件循环中排队执行。

示例

requestIdleCallback(myNonEssentialWork, { timeout: 2000 });
function myNonEssentialWork (deadline) {
  // 如果帧内有富余的时间,或者超时
  while ((deadline.timeRemaining() > 0 || deadline.didTimeout) &&
         tasks.length > 0)
    doWorkIfNeeded();
  if (tasks.length > 0)
    requestIdleCallback(myNonEssentialWork);
}

一些低优先级的任务可使用 requestIdleCallback 等浏览器不忙的时候来执行,同时因为时间有限,它所执行的任务应该尽量是能够量化,细分的微任务(micro task)。

因为它发生在一帧的最后,此时页面布局已经完成,所以不建议在 requestIdleCallback 里再操作 DOM,这样会导致页面再次重绘。

参考 www.cnblogs.com/Wayou/p/req…

浏览器渲染机制

  • 浏览器采用流式布局模型(Flow Based Layout
  • 浏览器会把HTML解析成DOM,把CSS解析成CSSOM
    • CSSOM 不包含未打印在页面上的元素,如 link、script
  • DOMCSSOM合并就产生了渲染树(Render Tree)。
    • RenderTree 不包含 display:none 和尺寸为 0 的元素
    • RenderTree 是将 DOM 和对应样式结合起来但是不包含位置信息和大小信息
  • 有了RenderTree,我们就知道了所有节点的样式,然后计算他们在页面上的大小和位置,最后把节点绘制到页面上。
    • Layout (回流) 计算位置和大小
    • Paint (重绘),填充元素可见属性的各个像素 由于浏览器使用流式布局,对Render Tree的计算通常只需要遍历一次就可以完成,table及其内部元素除外,他们可能需要多次计算,通常要花 3 倍于同等元素的时间,这也是为什么要避免使用table布局的原因之一
    • GPU分层渲染

预加载扫描器

浏览器构建DOM树时,这个过程占用了主线程。当这种情况发生时,预加载扫描仪将解析可用的内容并请求高优先级资源,如CSS、JavaScript和web字体。多亏了预加载扫描器,我们不必等到解析器找到对外部资源的引用来请求它。它将在后台检索资源,以便在主HTML解析器到达请求的资源时,它们可能已经在运行,或者已经被下载。预加载扫描仪提供的优化减少了阻塞。

渲染阻塞

js阻塞

JS 阻塞 DOM 解析。加载 js 文件的时会阻塞 dom 的解析、并且阻塞其它资源(如 css,js 或图片资源)的加载, 直到 js 下载、解析、执行完毕后, 才开始继续并行下载其他资源并呈现内容。

将 script 标签放在 body 结束标签之前, 虽然也会阻塞页面的呈现,但不会阻塞资源下载,还可以避免因页面没有加载完成而导致获取元素失败的情况

css阻塞
  1. css加载不会阻塞DOM树解析(异步加载时DOM照常构建)

浏览器是解析DOM生成DOM Tree,结合CSS生成的CSS Tree,最终组成render tree,再渲染页面,两者分别解析不会互相影响,因而不会阻塞DOM解析

  1. css阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)

  2. 等待获取CSS不会阻塞HTML的解析或者下载,但是它的确阻塞JavaScript,因为JavaScript经常用于查询元素的CSS属性。

重绘

由于节点的几何属性发生改变或者由于样式发生改变而不会影响布局的,称为重绘,例如outlinevisibilitycolorbackground-color等,重绘的代价是高昂的,因为浏览器必须验证 DOM 树上其他节点元素的可见性。

回流

回流是布局或者几何属性需要改变就称为回流。回流是影响浏览器性能的关键因素,因为其变化涉及到部分页面(或是整个页面)的布局更新。一个元素的回流可能会导致了其所有子元素以及 DOM 中紧随其后的节点、祖先节点元素的随后的回流。 回流必定会发生重绘,重绘不一定会引发回流。

交互

一旦主线程绘制页面完成,你会认为我们已经“准备好了”,但事实并非如此。如果加载包含JavaScript(并且延迟到onload事件激发后执行),则主线程可能很忙,无法用于滚动、触摸和其他交互。

”Time to Interactive“(TTI)是测量从第一个请求导致DNS查找和SSL连接到页面可交互时所用的时间——可交互是”First Contentful Paint“之后的时间点,页面在50ms内响应用户的交互。如果主线程正在解析、编译和执行JavaScript,则它不可用,因此无法及时(小于50ms)响应用户交互。

比如index.js文件可能是2 MB,而且用户的网络连接很慢。在这种情况下,用户可以非常快地看到页面,但是在下载、解析和执行脚本之前,就无法滚动。这不是一个好的用户体验。应当避免占用主线程。

参考:

developer.mozilla.org/zh-CN/docs/…

浏览器优化

现代浏览器大多都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即 16.6ms)才会清空队列,但当你获取布局信息的时候,队列中可能有会影响这些属性或方法返回值的操作,即使没有,浏览器也会强制清空队列,触发回流与重绘来确保返回正确的值

主要包括以下属性或方法:

  • offsetTopoffsetLeftoffsetWidthoffsetHeight
  • scrollTopscrollLeftscrollWidthscrollHeight
  • clientTopclientLeftclientWidthclientHeight
  • widthheight
  • getComputedStyle()
  • getBoundingClientRect()

所以,我们应该避免频繁的使用上述的属性,他们都会强制渲染刷新队列。

如何减少重绘和回流

CSS

  • 使用  transform  替代  top
  • 避免使用table布局,可能很小的一个小改动会造成整个  table  的重新布局。
  • 将动画效果应用到position属性为absolutefixed的元素上,避免影响其他元素的布局,这样只是一个重绘,而不是回流。
  • 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点,例如will-changevideoiframe等标签,浏览器会自动将该节点变为图层。
  • CSS3 硬件加速(GPU 加速) ,使用 css3 硬件加速,可以让transformopacityfilters这些动画不会引起回流重绘 。 JavaScript
  • 避免频繁操作样式,最好一次性重写style属性,或者将样式列表定义为class并一次性更改class属性。
  • 避免频繁操作DOM,创建一个documentFragment,在它上面应用所有DOM操作,最后再把它添加到文档中。
  • 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来。
  • 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。

参考: github.com/Advanced-Fr…

defer 和async

script: 浏览器会立即加载JS文件并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行

async: 加载JS文档和渲染文档可以同时进行(异步),当JS加载完成,JS代码立即执行,会阻塞HTML渲染

defer: 加载后续文档元素的过程将和 script.js 的加载并行进行(异步),当HTML渲染完成,才会执行JS代码

渲染阻塞的原因:

由于JavaScript 是可操纵 DOM 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。因此为了防止渲染出现不可预期的结果,浏览器设置 GUI 渲染线程与 JavaScript 引擎为互斥的关系。当浏览器在执行 JavaScript 程序的时候,GUI 渲染线程会被保存在一个队列中,直到 JS 程序执行完成,才会接着执行。如果 JS 执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉

浏览器缓存

HTTP 缓存

强缓存

强缓存:不会向服务器发送请求,直接从缓存中读取资源,在 chrome 控制台的 Network 选项中可以看到该请求返回 200 的状态码,并且 Size 显示 from disk cache 或 from memory cache。强缓存可以通过设置两种 HTTP Header 实现:Expires 和 Cache-Control。

1.Expires

缓存过期时间,用来指定资源到期的时间,是服务器端的具体的时间点。也就是说,Expires=max-age + 请求时间,需要和 Last-modified 结合使用。Expires 是 Web 服务器响应消息头字段,在响应 http 请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求。

Expires 是 HTTP/1 的产物,受限于本地时间,如果修改了本地时间,可能会造成缓存失效Expires: Wed, 22 Oct 2018 08:41:00 GMT表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。

2.Cache-Control

在 HTTP/1.1 中,Cache-Control 是最重要的规则,主要用于控制网页缓存。比如当Cache-Control:max-age=300时,则代表在这个请求正确返回时间(浏览器也会记录下来)的 5 分钟内再次加载资源,就会命中强缓存。

Cache-Control 指令:

public所有内容都将被缓存(客户端和代理服务器都可缓存) 。具体来说响应可被任何中间节点缓存.

private所有内容只有客户端可以缓存,Cache-Control 的默认取值,中间节点不允许缓存.

no-cache:客户端缓存内容,是否使用缓存则需要经过协商缓存来验证决定。表示不使用 Cache-Control 的缓存控制方式做前置验证,而是使用 Etag 或者 Last-Modified 字段来控制缓存。

no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存

max-age:max-age=xxx (xxx is numeric)表示缓存内容将在 xxx 秒后失效

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程,主要有以下两种情况:

  • 协商缓存生效,返回 304 和 Not Modified
  • 协商缓存失效,返回 200 和请求结果

协商缓存可以通过设置两种 HTTP Header 实现:Last-Modified 和 ETag 。

1.Last-Modified 和 If-Modified-Since

浏览器在第一次访问资源时,服务器返回资源的同时,在 response header 中添加 Last-Modified 的 header,值是这个资源在服务器上的最后修改时间,浏览器接收后缓存文件和 header; 浏览器下一次请求这个资源,浏览器检测到有 Last-Modified 这个 header,于是添加 If-Modified-Since 这个 header,值就是 Last-Modified 中的值;服务器再次收到这个资源请求,会根据 If-Modified-Since 中的值与服务器中这个资源的最后修改时间对比

但是 Last-Modified 存在一些弊端:

  • 本地打开缓存文件,即使没有对文件进行修改,但还是会造成 Last-Modified 被修改
  • Last-Modified 只能以秒计时

** 2.ETag 和 If-None-Match**

Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),只要资源有变化,Etag 就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到 request header 里的 If-None-Match 里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好地判断资源相对客户端而言是否被修改过了

两者之间对比:

  • 首先在精确度上,Etag要优于Last-Modified。

Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。

  • 第二在性能上,Etag要逊于Last-Modified,毕竟Last-Modified只需要记录时间,而Etag需要服务器通过算法来计算出一个hash值。
  • 第三在优先级上,服务器校验优先考虑Etag

参考: www.jianshu.com/p/54cc04190…

Observer API

  • Intersection Observer,交叉观察者。
  • Mutation Observer,变动观察者。
  • Resize Observer,视图观察者。
  • Performance Observer,性能观察者
PerformanceObserver:性能观察者

这是一个浏览器和Node.js 里都存在的API,采用相同W3CPerformance Timeline规范

  • 在浏览器中,我们可以使用 window 对象取得window.performancewindow.PerformanceObserver
  • 而在 Node.js 程序中需要perf_hooks 取得性能对象

juejin.cn/post/684490…

Cookie

developer.mozilla.org/zh-CN/docs/…

  • Expires/Max-Age 默认会话期,可开启持久性
  • HttpOnly JavaScript 无法访问 Document.cookie API
  • Secure 只应通过被 HTTPS 协议加密过的请求发送给服务端
  • Domain ,默认为 origin,不包含子域名
  • Path
  • SameSite
    • None,跨站请求下继续发送 cookie
    • Strict。浏览器将只在访问相同站点时发送 cookie。
    • Lax。与 Strict 类似,但用户从外部站点导航至URL时(例如通过链接)除外

以前,如果 SameSite 属性没有设置,或者没有得到运行浏览器的支持,那么它的行为等同于 None,Cookies 会被包含在任何请求中——包括跨站请求。 大多数主流浏览器正在将 SameSite 的默认值迁移至 Lax。

ES Module

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,可以利用此特点做tree shaking

  • 自动采用严格模式,顶层的this指向undefined

模块功能主要由两个命令构成:exportimport

export:

  • 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。
  • export输出的变量就是本来的名字,但是可以使用as关键字重命名。
  • export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。
  • export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
  • export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,import命令也是如此。这是因为处于条件代码块之中,无法做静态优化。

import:

  • import只能作为模块顶层的语句出现

  • import命令输入的变量都是只读的,因为它的本质是输入接口。immutable binding ,引入的模块不能再进行修改。

  • import语句会执行所加载的模块,如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次

  • import语句是 Singleton 模式。多个 import 语句引用的是同一个模块

  • import在静态解析阶段执行,所以它是一个模块之中最早执行的

  • 其他语法

    • export default: 使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。 为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
    • export *: 表示再输出对应模块的所有属性和方法,可以用来实现模块的继承 export 与 import 的复合写法:
//`foo``bar`实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用`foo``bar`。
export { foo, bar } from 'my_module';

import()函数: importexport命令只能在模块的顶层,不能在代码块之中,导致无法在运行时加载模块. ES2020 提案  引入import()函数,支持动态加载模块。

ES6 模块与 CommonJS 模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
  • require 是动态引入模块,import 在静态解析阶段执行。 参考: juejin.cn/user/281521…

循环依赖

循环依赖指的是,a 模块的执行依赖 b 模块,而 b 模块的执行又依赖 a 模块。循环依赖可能导致递归加载,处理不好的话可能使得程序无法执行。

CommonJS 中循环依赖的解法

在 CommonJS 规范中,当遇到 require() 语句时,会执行 require 模块中的代码,并缓存执行的结果,当下次再次加载时不会重复执行,而是直接取缓存的结果。正因为此,出现循环依赖时才不会出现无限循环调用的情况。

ES6 中循环依赖的解法

ES6 中,import 静态执行,import 命令会被 JavaScript 引擎静态分析,优先于模块内的其他内容执行。也就是说, import 具有提升效果, import 命令的位置并不影响程序的输出。 import 是在编译阶段执行的,这样就使得程序在编译时就能确定模块的依赖关系,一旦发现循环依赖,ES6 本身就不会再去执行依赖的那个模块了,所以程序可以正常结束。这也说明了 ES6 本身就支持循环依赖,保证程序不会因为循环依赖陷入无限调用。

其他解决方法

1、引用抽离 就是把有循环引用的地方抽离到另一单独文件里,换句话说就是不使用循环引用。

2、导出函数 把之前默认的导出对象改成导出函数的形式,从函数返回值里取导出结果。 由于每一个函数都会形成一个单独的局部作用域,不同的作用域有着不同的数据引用地址。 这样每次引入的结果都是一个新的引用,不会冲突,这种情况webpack也能正常的处理。

正则表达式

分组

  1. 匿名捕获分组: 正则表达式通过使用括号将表达式分为不同的分组,识别的方法是通过从左至右搜寻左半括号, 遇到第一个左半括号时,则该左半括号与对应的右半括号所包含的内容即为第一分组,以此类推 。 例如,在表达式((A)(B(C))),有四个这样的组:((A)(B(C)))、(A)、(B(C))、(C)
/* “日-月-年”互换“月-日-年” */
function toLocalDate(date) {
  return date.replace(/(\d{2})-(\d{2})-(\d{4})/, '$2-$1-$3');
}
  1. 命名捕获分组: 命名捕获分组自身的语法是 (?<name>...),比普通的分组多了一个 ? 字样
function toLocalDate(date) {
  return date.replace(/(?<month>\d{2})-(?<day>\d{2})-(?<year>\d{4})/, '$<day>-$<month>-$<year>');
}

反向引用一个命名分组的语法是 \k,注意命名分组同样可以通过数字索引来反向引用,比如:

/(?<foo>a)\k<foo>\1/.test('aaa'); // true
'abc'.replace(/(?<foo>a)/, '$<foo>-'); // "a-bc",同样 $1 仍然可用

命名分组相关的有三种语法,分别是 ?、\k、$,相同点是都用尖括号包裹着分组名。

参考: tc39.es/proposal-re…

方法

  1. RegExp.prototype.exec exec() 方法在一个指定字符串中执行一个搜索匹配。返回一个结果数组或 null。

返回值:

  • 如果匹配成功,exec() 方法返回一个数组,并更新正则表达式对象的属性。返回的数组将完全匹配成功的文本作为第一项,将正则括号里匹配成功的作为数组填充到后面。

  • 如果匹配失败,exec() 方法返回 null。

var regex1 = RegExp('foo*', 'g');
var str1 = 'table football, foosball';
var array1;

while ((array1 = regex1.exec(str1)) !== null) {
  console.log(`Found ${array1[0]}. Next starts at ${regex1.lastIndex}.`);
  // expected output: "Found foo. Next starts at 9."
  // expected output: "Found foo. Next starts at 19."
}
  1. String.prototype.match()

match() 方法检索返回一个字符串匹配正则表达式的的结果。 返回值:

  • 如果使用 g 标志,则将返回与完整正则表达式匹配的所有结果(Array),但不会返回捕获组,或者未匹配 null。
  • 如果未使用 g 标志,则仅返回第一个完整匹配及其相关的捕获组(Array)。 在这种情况下,返回的项目将具有如下所述的其他属性,或者未匹配 null。
    • groups: 一个捕获组数组 或 undefined(如果没有定义命名捕获组)。
    • index: 匹配的结果的开始位置
    • input: 搜索的字符串.

如果正则表达式不包含 g 标志,str.match() 将返回与 RegExp.exec(). 相同的结果。

例: 匹配 div 标签内的任意字符

str = `
<div>
    <div>hello</div>
</div>
`;
str.match(/<div[^>]*>([\s\S]*)<\/div>/i);

位置类元数据

即像^、$、\b、\B 这样的元字符,是用来表示一个位置。作为一个判断条件,匹配的字符需要满足这样的位置信息,但最终匹配的字符串中并不会包含这个样的位置信息。

零宽断言

\b,^,$那样用于指定一个位置,这个位置应该满足一定的条件(即断言),因此它们也被称为零宽断言。

  • (exp) :目标字符串需要匹配exp,并将该分组匹配的子文本保存到自动命名的组里;

  • (?:exp) :目标字符串需要匹配exp, 该括号所包括的内容不会被作为一个分组对待。

  • (?=exp) :定义目标字符串结束位置要求,即紧随目标字符串后面出现的字符串需要匹配上exp表达式,该字符串不会被计入目标字符串,表达中出现的括号也不会被视作一个分组;

    • 比如\b\w+(?=ing\b),匹配以 ing 结尾的单词的前面部分(除了 ing 以外的部分)
  • (?<=exp):定义目标字符串起始位置要求,即紧邻目标字符串前面出现的字符串需要匹配上exp表达式,该字符串不会被计入目标字符串,表达中出现的括号也不会被视作一个分组;

    • 比如(?<=\bre)\w+\b 会匹配以 re 开头的单词的后半部分(除了 re 以外的部分)
  • (?!exp):定义目标字符串结束位置要求,即紧随目标字符串后面出现的字符串不能匹配上exp表达式,该字符串不会被计入目标字符串,表达中出现的括号也不会被视作一个分组;效果上与(?=exp) 表示的情况刚好相反;

  • (?<!exp):定义目标字符串起始位置要求,即紧邻目标字符串前面出现的字符串不能匹配上exp表达式,该字符串不会被计入目标字符串,表达中出现的括号也不会被视作一个分组;效果上与(?<=exp)表示的情况刚好相反;

反向引用

对一个正则表达式模式或部分模式两边添加圆括号将导致相关匹配存储到一个临时缓冲区中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 \n 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。

可以使用非捕获元字符 ?:、?= 或 ?! 来重写捕获,忽略对相关匹配的保存。

反向引用的最简单的、最有用的应用之一,是提供查找文本中两个相同的相邻单词的匹配项的能力。以下面的句子为例:

var str = 'Is is the cost of of gasoline going up up';
var patt1 = /\b([a-z]+) \1\b/gi;
console.log(str.match(patt1));
//  ["Is is", "of of", "up up"]

贪婪与懒惰

当正则表达式中包含能接受重复的限定符时,通常的行为是(在使整个表达式能得到匹配的前提下)匹配尽可能多的字符。 以这个表达式为例:a.*b,它将会匹配最长的以 a 开始,以 b 结束的字符串。被称为贪婪匹配。

有时,我们更需要懒惰匹配,也就是匹配尽可能少的字符。要在它后面加上一个问号?。 这样.*?就意味着匹配任意数量的重复,但是在能使整个匹配成功的前提下使用最少的重复。

例子:

let str = `
<p>第一个</p>
<pre><code>console.log(1);</code></pre>
<p>第二个</p>
<pre><code>console.log(2);</code></pre>`;

str.match(/(?<=<pre><code>)[\s\S]*?(?=<\/code><\/pre>)/gi);  // 获得,/somePattern*?/是懒惰匹配。

str.replace(/(?<=<pre><code>)[\s\S]*?(?=<\/code><\/pre>)/gi, 'asdf');  // 替换

性能优化

mp.weixin.qq.com/s/wJxj5QbOH…

优化关键渲染路径

developer.mozilla.org/zh-CN/docs/…

浏览器加载数据

  1. 减少HTTP请求,只请求当前页面需要的资源
  • 异步加载
  • 懒加载
  • polyfill
  1. 使用HTTP2.0,需要配置一个支持h2的web服务器,并下载安装一张TLS证书,让浏览器与服务器通过h2链接

    • 对消息头采用Hpack进行压缩传输,能够节省消息头占用的网络流量,1.1每次请求,都会携带大量冗余的头信息,浪费了很多宽带资源
    • 采用二进制格式传输数据,支持多路复用
  2. 设置浏览器缓存策略

  3. 预加载、预解析 prefetch,prerender,preload dns 预解析 图片预加载

HTML优化

  • 语义化HTML,代码简洁清晰,利于SEO,便于开发维护。
  • 减少HTML嵌套关系,减少DOM节点数量。
  • 提前声明字符编码,让浏览器快速确定如何渲染网页内容<html lang="en"> <meta charset="UTF-8">
  • 删除多余空格、空行、注释、无用属性
  • 减少iframe,子iframe会阻塞父级的onload事件。可以使用js动态给iframe赋值,就能解决这个问题。
  • 避免table布局,table 渲染需要多次计算,通常要花 3 倍于同等元素的时间

CSS优化

  • 合理使用选择器,减少嵌套
  • 减少重绘和重排
    • 用transform 来做形变和位移
    • 通过绝对定位的方式来脱离当前层叠上下文,形成新的布局
  • 修改元素的样式时,修改 class 是性能最高的方法
  • 文件压缩- optimize-css-assets-webpack-plugin
  • 异步加载文件
  • 减少@import的使用,因为它使用串行加载
  • contain属性,控制是否根据子元素的改变回流;向浏览器解释网页的布局,让浏览器可以据此做一些性能优化。
    • layout: 向浏览器说明元素内和元素外的布局互不侵犯,BFC+containing block
  • link media属性加载非阻塞的css
  • will-change 告知浏览器该元素会有哪些变化的方法,这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作
  • font-display 允许文本在字体加载或加载失败时显示回退字体
  • 避免使用标签选择器,搜索效率低

参考: # CSS performance optimization

JS代码:

  1. 慎用全局变量
1.  全局变量定义在全局执行上下文,是所有作用域链的顶端。局部找不到就会一直往上找,影响性能
1.  全局执行上下文一直存在于上下文执行栈,直到程序退出,不利于GC回收
1.  命名污染

2. 缓存全局变量 将使用中无法避免的全局变量缓存到局部

  1. 减少重绘回流 回流:当元素的规模尺寸,布局,隐藏等改变的时候,render dom需要重新构建,这就称为回流
    重绘:元素只更新外观,风格,而不会影响布局的,叫重绘

强制同步布局问题:获取元素的位置时会触发强制同步布局,影响渲染性能

  1. 节流、防抖

  2. 少用闭包、减少内存泄漏

  3. 减少数据读取次数 如for循环优化,减少length读取次数

```
let arr = [1,2,3]
for (var i = 0; i < arr.length; i++) {
  console.log(arr[i])
}
// 优化
for (var i = 0, len = arr.length; i < len; i++) {
  console.log(arr[i])
}

```

减少读取强制同步布局的属性,如scrollTop,读取逻辑可以放在循环外

7. 不直接操作真实DOM,可以先修改,然后一次性应用到DOM上; 文档碎片优化节点添加 dom:document.createDocumentFragment()

  1. 减少判断层级 减少html层级、语义化标签

计算

其他

  • rAF

  • GPU硬件加速

  • 时序优化,并发请求,promise.all

webpack优化(额外)

  • 减小代码体积
  • 按需加载
  • 提取第三库代码
  • webpack dll优化

参考:

juejin.cn/post/702997…

mp.weixin.qq.com/s/wJxj5QbOH…

资源

  1. 缩减资源体积
  • 打包压缩 webpack
  • gzip
  • 图片优化 webp,根据屏幕分辩率展示不同图片 imgset
  • 控制 cookie 的大小,request header
  1. CDN

    • cdn 预热:资源分发到各站上,大流量请求先做 cdn 预热

    • cdn 刷新:强制拉取原站资源

    • 静态资源单独域名

    • 浏览器请求并发限制(同一域名(包括二级域名)在同一时间支持的并发请求数量的限制)

    • cookie传输,单独域名,不会携带cookie

    • 方便分流和缓存(动静分离,有利于静态资源做cdn缓存)

    • http2 多路复用加载图片

  2. gzip压缩
    使资源体积更小
    服务端配置,如nginx可配置支持gzip压缩资源传输的方式
    如果浏览器支持gzip解析,服务器就会推送gzip的资源,在http的相应头里可以看到显示Content-Encoding:gzip

  3. 做服务端渲染(SSR) 现在主流框的react、vue导致的一个痛点,就是页面构建交给了客户端来渲染,构建的过程无疑是排在了请求到html/js资源后,也就是至少两次http请求后才开始构建,这无疑是导致白屏的关键点之一,所以做ssr页面的话,能够直接返回页面,减少了不少首屏渲染时间

  4. 将CSS放在文件头部,JavaScript文件放在底部
    单线程js可能会阻滞文档加载

  5. 图片

  • 优先使用iconfont css替换
  • 雪碧图
  • base64,可以使用webpack url-loader处理
  • png webp
  • 图片懒加载,使用IntersectionObserver实现

React 的性能优化

zh-hans.reactjs.org/docs/optimi…

zh-hans.reactjs.org/docs/react-…

zhuanlan.zhihu.com/p/425635864

性能优化指标

First Paint(FP)

从开始加载到浏览器首次绘制像素到屏幕上的时间,也就是页面在屏幕上首次发生视觉变化的时间。
计算方式:performance.getEntriesByType('paint')

First Contentful Paint(FCP)

浏览器首次绘制来自 DOM 的内容的时间,内容必须是文本、图片(包含背景图)、非白色的 canvas 或 SVG,也包括带有正在加载中的 Web 字体的文本。
计算方式:performance.getEntriesByType('paint')

First Meaningful Paint(FMP)

页面的主要内容绘制到屏幕上的时间,这是一个更好的衡量用户感知加载体验的指标,但仍然不理想在 Lighthouse 6.0 中已不推荐使用 FMP,建议使用 Largest Contentful Paint代替。

First Screen Paint(FSP) 页面从开始加载到首屏内容全部绘制完成的时间,用户可以看到首屏的全部内容。

Largest Contentful Paint(LCP)

try {
  const po = new PerformanceObserver((entryList) => {
    const entries = entryList.getEntries();
    const lastEntry = entries[entries.length - 1];

    // 优先取 renderTime,如果没有则取 loadTime
    let lcp = lastEntry.renderTime || lastEntry.loadTime;
    window.perfData.push({
      'LCP', lcp
    });
  });
  po.observe({type: 'largest-contentful-paint'});
} catch (e) {
  // Do nothing 
}

可视区域中最大的内容元素呈现到屏幕上的时间,用以估算页面的主要内容对用户可见时间。

Time to Interactive(TTI) 表示网页第一次 完全达到可交互状态 的时间点,浏览器已经可以持续性的响应用户的输入。

常用的性能测量API
DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
网络请求耗时 (TTFB): responseStart - requestStart
数据传输耗时: responseEnd - responseStart
DOM 解析耗时: domInteractive - responseEnd
资源加载耗时: loadEventStart - domContentLoadedEventEnd
First Byte时间: responseStart - domainLookupStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
DOM Ready 时间: domContentLoadEventEnd - fetchStart
页面完全加载时间: loadEventStart - fetchStart
http 头部大小: transferSize - encodedBodySize
重定向次数:performance.navigation.redirectCount
重定向耗时: redirectEnd - redirectStart

URI

TODO

juejin.cn/post/706383…