一、ES6新特性
1. 块级作用域
- ES6新增了块级作用域的概念,即使用
{}
包裹的代码块即为块级作用域。 - ES6之前并没有块级作用域的概念,
var
关键字声明的变量也不会受{}
的限制。ES6也新增了两个生命变量的关键字,let
和const
。 let
和const
声明的变量只在其所在的{}
内起作用,而且变量声明不存在声明提升,必须先声明后使用。let
声明的变量在赋值之后仍可以再次修改,而const
则是用来定义常量,变量赋值之后就不可再修改。
2.模板字符串
-
模板字符串使用 `` 来包裹,字符串内的内容会原样输出。
const template = `hello 你好, 李磊, 哈哈 `; console.log(template);
运行结果:
-
模板字符串内可以使用
${}
来使用js表达式,表达式的值将会 替换${}
的位置。const name = 'LiLei'; const template = `hello, my name is ${name}`; // hello, my name is LiLei
-
带标签的模板字符串,这里的标签指得是标签函数。带标签的模板字符串的结果为标签函数返回的结果。
const name = 'tom'; const sex = 1; const r1 = console.log`${name} is a ${sex}`; console.log(r1);
运行结果:
-
标签函数接收一个按
${}
分割模板字符串得到的数组,和模板字符串中${}
包裹的参数,并返回处理之后的结果作为带标签模板字符串的值。const name = 'tom'; const sex = 1; function tempString(arr, name, sex) { const sexName = sex ? 'man' : 'woman' return name + arr[1] + sexName; } const r = tempString`${name} is a ${sex}`; console.log(r);
运行结果:
3.箭头函数
-
ES6新增了一种函数的声明方式,箭头函数。
const print = msg => { console.log(msg); }
-
箭头函数和普通函数的区别:箭头函数内部没有
this
,其访问的this
变量是其定义时上层作用域的this
。
如下面两段代码:var a = 10; var obj = { a : 20, fn() { setTimeout(() => { console.log(this.a); }) } } obj.fn(); // 20
上面的代码中
setTimeout
内部的执行函数为箭头函数,其内部的this
指向的是fn
函数的this
,而调用fn
函数时是通过obj
调用的,所以this
指向obj
。var a = 10; var obj = { a : 20, fn: () => { setTimeout(() => { console.log(this.a); }) } } obj.fn();// 10
上面的代码中
setTimeout
内部的执行函数为箭头函数,其内部的this
指向的是fn
函数的this
,而fn
函数也是一个箭头函数,其this
指向的是外层作用域,及全局对象,所以this
指向obj
。
4.Object.is 方法
-
Object.is
方法判断两个变量是否相等,和===
功能类似。 -
Object.is
和===
的区别,Object.is
可以判断+0
和-0
不相等,NaN
和NaN
相等。console.log(Object.is(+0, -0)); // false console.log(Object.is(NaN, NaN)); // true console.log(Object.is(0, false)); // false console.log(+0 === -0); // true console.log(NaN === NaN); // false
5.Proxy代理对象
-
Proxy
代理对象,为对象添加统一的操作监听方法,如set
,get
,delete
等。const obj = { a: 342, b: 123, }; const person = new Proxy(obj, { // target === obj; receiver === person get: function (target, property, receiver) { console.log(target, property, receiver); return property in target ? target[property] : 'undefined' }, // target === obj; receiver === person set: function(target, property, value, receiver) { console.log(target, property, value, receiver); target[property] = value; }, deleteProperty(target, property) { console.log(target, property); delete target[property]; } }); person.a = 123; console.log(person.b);
运行结果:
-
Object.defineProperty
对对象的单个属性设置访问代理,但只针对set
和get
,不能对delete
等操作进行监控。const obj = { a: 342, b: 123, }; Object.defineProperty(obj, 'a', { get() { console.log(arguments); return obj._a; }, set(val) { obj._a = val; } }); obj.a = 123; console.log(obj.a);
运行结果:
-
Proxy
和Object.defineProperty
的区别:Proxy
功能更强大,不仅可以对set
和get
进行监控,还可以对delete
等操作进行监控,Object.defineProperty
只能对set
和get
进行监控Proxy
操作更简单,只需要定义一次监控函数就可以对所有属性进行监控,而Object.definedProperty
需要对每个属性定义监控函数。Proxy
也可以对数组进行监控,Object.defineProperty
对数组监控比较麻烦。Object.defineProperty
对源对象进行监控,而Proxy
是对代理对象进行监控。
6.Reflect
Reflect
是一个内置对象,和Math
类似,不可构造。Reflect
提供了一些方法和操作符的函数实现,为对象操作提供了统一的形式。其内的一些方法也是Proxy
监听函数的默认操作方法。
运行结果:const person = new Proxy(obj, { get: function (target, property, receiver) { console.log(target, property, receiver); return Reflect.get(target, property) }, set: function(target, property, value, receiver) { console.log(target, property, value, receiver); Reflect.set(target, property) }, deleteProperty(target, property) { console.log(target, property); Reflect.deleteProperty(target, property) } }); person.a = 123; console.log(person.b);
7.类
-
类的声明通过
class
来实现,通过new
来创建类的实例对象。class Person { constructor(name) { this.name = name; } say() { console.log(`this is ${this.name}`); } } const tom = new Person('tom'); tom.say(); // this is tom
-
类的静态成员通过
static
来定义,静态成员只能通过Class.
的方式来访问,静态方法中的this
指向类本身。class Person { constructor(name) { this.name = name; } say() { console.log(`this is ${this.name}`); } static create(name) { console.log(this); //[class Person] return new Person(name) } } const tom = Person.create('tom'); tom.say(); // this is tom
8.Set
-
Set
是ES2015 提供的一种新的数据结构,与数组类似,但是其中的元素不可重复。 -
Set
的创建可以通过 new 关键字来创建,可以选择传入一个初始数组。const arr = [1, 2, 3, 4, 5, 2, 1]; const s = new Set(arr); const s1 = new Set();
-
Set
操作方法:- 添加元素:
add
,接收一个要添加的值,返回一个新的Set
,所以支持链式调用 。s.add(11).add('a').add(32).add(34);
- 遍历:
forEach
和for...of
循环。s.forEach(i => console.log(i)); for (let i of s) { console.log(i); }
- 获取长度:
set.size
。console.log(s.size);
- 判断是否包含某个值:
set.has(val)
s.has(1)
- 删除某个值:
set.delete(val)
,返回删除的元素console.log(s.delete(1)); // true console.log(s.delete(1231));// false
- 清空:
set.clear()
。s.clear();
- 添加元素:
-
与数组的相互转换
- 数组转化为
Set
,通过new Set(array)
来实现const arr = [1, 2, 3, 4, 5, 2, 1]; const s = new Set(arr);
Set
转化为数组-
通过
Array.from(Set)
方法const array = Array.from(s);
-
通过扩展运算符
[...Set]
const array = [...s]
-
- 数组转化为
9.Map
-
存储任意类型的键值对,接收任意类型的数据作为键。
const m = new Map(); const tom = { name: 'tom' }; m.set(tom, 99); // 添加键值对 m.forEach((v, k) => console.log(v, k)); console.log(m); console.log(m.get(tom)); // 获取某个key对应的value console.log(m.has(tom)); // 判断是否包含某个key console.log(m.delete(tom));// 删除map中的某个key m.clear(); // 清空所有键值
输出结果:
10.Symbol
-
Symbol
是ES2015新增的基本类型,表示唯一。const symbol = Symbol('ffff');
-
Symbol([string])
:每次都会创建新的symbol
类型,即使传入相同的描述。Symbol
函数只能接受字符串,如果传入非字符串,会将其先转换成字符串。Symbol(true) ⇔ Symbol('true')
。console.log(Symbol('foo') === Symbol('foo')); // false
-
Symbol.for(key)
:根据key
从symbol
注册表中查找Symbol
,如果找到则返回,若没有找到,则创建一个新的Symbol
, 并放入全局的Symbol
注册表中。注册表内部维护了一个字符串到Symbol
的映射console.log(Symbol.for('foo') === Symbol.for('foo')); // true
-
Symbol
还包含一些内置常量,如Symbol.toStringTag
用于定义toString
打印的对象标签const obj = { a: '123', [Symbol.toStringTag]: 'xswqe', }; console.log(obj.toString()); // [object xswqe] 添加Symbol之前打印 [object Object]
-
Object
对象无法使用for...of
循环遍历,Symbol.iterator
定义for...of
的Iterator
接口const obj = { a: 12, b: 2, c: 3, [Symbol.iterator]() {// Iterable 接口 const all = [this.a, this.b, this.c]; let index = 0; return { // Iterator 接口 next() { return { // IteratorResult 接口 value: all[index], done: index++ >= all.length } } } } }; for(const o of obj) { console.log(o); }
11.for…of循环
-
所有实现
Iterator
接口的对象,都可以使用for...of
循环进行遍历,为集合提供统一的遍历方式 -
实现了
Iterator
接口的对象都有一个[Symbol.iterator]
方法
-
该方法返回一个具有
next
方法的Iterator
对象,调用next
方法将返回一个包含value
和done
的对象,表示迭代器内部实现了一个指针,指向当前访问的值
const s = new Set(); s.add(1).add(2).add(3); const setIterator = s[Symbol.iterator](); console.log(setIterator.next()); console.log(setIterator.next()); console.log(setIterator.next()); console.log(setIterator.next());
-
Object
对象无法使用for...of
进行遍历,因为缺乏Iterable
接口,所有要想使用for...of
进行循环,需要自己定义Iterable
接口const obj = { a: 12, b: 2, c: 3, [Symbol.iterator]() {// Iterable 接口 const all = [this.a, this.b, this.c]; let index = 0; return { // Iterator 接口 next() { return { // IteratorResult 接口 value: all[index], done: index++ >= all.length } } } } }; for(const o of obj) { console.log(o); }
12.ES2016
-
array.includes
:与array.indexOf
相比,可以判断数组中的NaN
const arr = [1, 2, 3, NaN, 4]; console.log(arr.indexOf(1)); // 0 console.log(arr.indexOf(NaN)); // -1 console.log(arr.includes(1)); // true console.log(arr.includes(NaN)); // true
-
指数运算符:
**
console.log(2 ** 9); // 512
13.ES2017
-
Object.values
:返回值组成的数组 -
Object.entries
:返回键值对组成的数组const obj = { a: 123, b: 'c' }; for(const [k, v] of Object.entries(obj)) { console.log(k, v); } console.log(Object.entries(obj)); //[ [ 'a', 123 ], [ 'b', 'c' ] ] console.log(new Map(Object.entries(obj)));
运行结果:
-
Object.getOwnPropertyDescriptors
:获取所有对象属性的完整描述-
Object.assign()
不能完全复制对象的setter
和getter
属性。assign()
会把这两个属性作为普通属性进行复制,所以得到下面的结果const obj = { firstName: 'Wu', lastName: 'ShaoQing', get fullName() { console.log(this) return (`${this.firstName} ${this.lastName}`) }, } const p = Object.assign({}, obj); p.firstName = 'Zhang'; console.log(p, p.fullName) // WuShaoqing
因为此时
fullName
只是p1
中的一个普通属性,它的值是固定的。
-
利用
Object.getOwnPropertyDescriptors
可以完成setter
和getter
属性的完美复制const p = { firstName: 'Lei', lastName: 'Wang', get fullName() { return `${this.firstName} ${this.lastName}`; } }; const descriptors = Object.getOwnPropertyDescriptors(p); const p1 = Object.defineProperties({}, descriptors); p1.firstName = 'Wu'; console.log(p1.fullName); // Wu Wang
-
二、TypeScript
1.语言类型分类
- 按类型系统划分:静态类型和动态类型
- 静态类型:变量声明时类型就确定了,除了做强制类型转换,类型不可再更改
- 动态类型:运行阶段才能确定变量类型,变量类型可以随时变化
- 按类型安全划分:强类型和弱类型
- 强类型:不允许任意的隐式类型转换,在编译期间就会报错
- 弱类型:允许任意的隐式类型转换
JS是弱类型的动态语言
2.Flow
Flow
是一种静态类型转换器,通过类型注解,在编译阶段检查代码,查找代码使用的异常。Flow
的使用-
npm i flow-bin -d
安装flow-bin
检查工具\ -
编辑文件,为要检查的文件添加
@flow
注释,并为代码添加类型注解
-
执行
yarn flow init
生成flow
配置文件 -
执行
yarn flow
,注意项目的文件路径中不能有中文
-
- 移除
Flow
类型注解- 使用
flow-remove-types
插件- 安装
flow-remove-types
插件 - 运行命令
yarn flow-remove-types . -d dist
命令,将移除类型注解之后的文件放在dist目录中。其中.
表示当前目录,如果是其他目录就换成目录名
- 安装
- 使用
babel
- 安装
@babel/core @babel/cli @babel/preset-flow
- 新建
.babelrc
文件,并添加presets: ['@babel/preset-flow']
- 运行命令
yarn babel . -d src
命令,将移除类型注解之后的文件放在dist目录中。其中.
表示当前目录,如果是其他目录就换成目录名
- 安装
- 使用
Flow
特性- 类型推断:
flow
能够根据代码推断类型 - 类型注解:通过变量后面加上
:[类型]
对类型进行注解。推荐使用类型注解 - 原始数据类型:
string
number:100/NaN/Infinity
boolean: true/false
null: null
undefined: undefined
Symbol:Symbol()
void: undefined
- 数组:
Array<T>
、T[]
、[number, string](元组)
- 对象:两种定义方法
const obj: {foo?: string, bar: string} = {foo: ‘string’, bar: ‘string’ }
const obj:{[string]: string} = {}
表示可以添加任意个字符串键值对
- 函数类型:
- 函数定义
function(a: stirng): string { return ‘string’; }
- 函数作为参数:
function(callback:(string, number) => void){}
- 函数定义
- 特殊类型:
-
字面量类型:
const str: ‘f00’ = ‘f00';
变量的值只能是变量后面指定的值 -
联合类型(或类型):
const a: ‘success’ | ‘danger’ | ‘warning’ = ‘warning'; const b: string | mumber = 100;
只能指定值/类型中的一个 -
自定义类型: 使用
type
定义自定义类型type stringOrNumber = string | number; const a:stringOrNumber = 120;
-
maybe类型:
const a: ?number = undefined/null/100;
通过在类型前面添加?来定义,表示类型或undefined
或null
。 -
mixed
和any
类型:表示任意类型。mixed
是强类型,在确定变量类型之前,不能使用类型相关的方法,比如toString
等any
是弱类型,可以使用类型相关方法。
-
- 类型推断:
3.Typescript(TS)
-
TS
是JavaScript
的超集,包含JavaScript
、ES6
和 类型系统 -
TS
的使用npm i typescript -d
安装TypeScript
- 编写
.ts
文件 yarn tsc filename;
编译指定文件,会产生同名的js
文件,js
文件中会移除类型注解。
-
配置文件
- 运行命令
yarn tsc --init
生成配置文件 - 配置文件只会在执行
yarn tsc
对项目中所有文件进行编译时生效,如果通过yarn tsc filename
编译指定文件时是不会生效的
- 运行命令
-
原始数据类型
string
: 非严格模式下可以是null/undefined
,严格模式下不能。严格模式和非严格模式可以通过配置文件中的strict
属性控制number
:100/NaN/Infinity
,非严格模式下可以是null/undefined
,严格模式下不能boolean: true/false
,非严格模式下可以是null/undefined
,严格模式下不能null: null
undefined: undefined
void: undefined
, 非严格模式下还可以是nullsymbol: Symbol()
,Symbol
是es2015
特性,如果转化成ES5
,则无法转化。配置文件中target
-
错误消息提示中文:
yarn tsc --locale zh-CN
-
变量重名问题解决:
- 自执行函数包裹
- 添加
export {}
开启模块化
-
object类型:除了原始类型之外的数据类型,包括
object
,function
,array
const obj:object = function () {};// [] //{} const o: {} = {};
-
数组:
const arr1: Array<number> = [1, 2, 3]; const arr2: number[] = [1, 2, 3];
-
元组:固定长度固定类型的数组
const tuple: [string, number] = ['a', 1];
-
枚举类型:通过
enum
声明-
默认值为从
0
开始累加 -
如果没有给定值,则将从第一个给定值的地方开始累加
enum postStatus { Draft = 0, // 0 Unpublished, // 1 Published = 'aa', // aa }
-
如果存在字符串,则字符串之后的所有元素需要给定值。
enum postStatus { Draft = 0, // 0 Unpublished, // 1 Published = 'aa', // aa Private = 'bb' }
-
使用:和
Object
中属性的使用一致。
-
-
函数
// 函数声明 function fn(a: string, b?: number, ...rest: number[]): void {} // 字面量 const func: (a:string, b:number) => void = function (a: string, b:number): void { };
-
任意类型:
any
, 动态类型 -
隐式类型推断:根据变量的使用推断变量的类型。尽可能为每个变量添加类型
-
类型断言(
assertion
):由代码编写者确定的类型,只在编译时起作用。使用方法:-
通过
as
关键字 -
在变量前使用
<>
,但是在jsx
语法中不能使用,因为jsx
中存在<>
表示标签const nums = [111, 2, 333]; const res = nums.find(i => i > 0); const num1 = res as number; const num2 = <number> res;
-
-
接口:为有结构的数据做类型约束
-
可选成员、只读成员、动态成员
interface Post { title: string desc: string // subTitle?: string // 可选成员 readonly summary: string //只读成员 [prop: string ]: string // 动态成员,允许添加指定类型的任意数量的成员 } function printInfo(post: Post) { console.log(`${post.title} ${post.desc}`); } const post:Post = { title: 'book', desc: 'this is a book', summary: 'book store' }; printInfo(post);
-
可选属性和动态成员同时存在时,会报错。因为动态成员的
key
是string/number
类型,但是可选成员key
则是undefined | string
类型
-
-
类的用法与ES6的不同:
-
类的成员属性需要在类中声明,不能只在
constructor
中动态定义 -
类成员必须指定默认值,在声明时或
constructor
中指定均可。class Person { name: string age: number constructor(name:string, age: number) { this.name = name; this.age = age; } sayHi(msg: string):void { console.log(`hello, this is ${this.name}`, msg); } }
-
类的成员可以使用访问修饰符修饰:
public
:默认 ,无特殊限制private
:不可继承,只能在类内部访问。private
如果修饰constructor
构造函数,则此类不可通过new
关键字创建类的实例,需要提供方法,在类的内部创建实例并返回。protect
: 可以继承,只能在类及其子类内部访问。
-
只读属性
readonly
:可以和访问修饰符一起使用,放在访问修饰符后面class Person { private name: string age: number protected readonly gender: number = 1 constructor(name:string, age: number) { this.name = name; this.age = age; } sayHi(msg: string):void { console.log(`hello, this is ${this.name}`, msg); } }
-
-
接口:interface,接口功能应该尽量单一,然后使用implements 来让类实现多个接口
interface Eat{ eat(food: string): void } interface Run { run(distance: number): void } class Person implements Eat, Run{ eat(food: string): void { console.log(`person eat ${food}`); } run(distance: number): void { console.log(`person run ${distance}`); } } class Animal implements Eat, Run{ eat(food: string): void { console.log(`animal eat ${food}`); } run(distance: number): void { console.log(`animal run ${distance}`); } } export {}
-
抽象类:可以包含抽象方法,抽象方法和接口中的方法类似,只有方法声明,没有方法的具体实现。抽象类不可实例化,只能由子类继承,并实现其中的抽象方法。 抽象方法和抽象类都要用
abstract
修饰。抽象方法也可以包含普通方法abstract class Animal { abstract eat(food: string): void; say(msg: string) { console.log(msg); } } class Dog extends Animal{ eat(food: string): void { console.log(`Dog eat ${food}`); } } new Dog().say('Hello')
-
泛型:在定义时不指定类型,调用时传入具体的类型。这样是为了最大程度复用代码。泛型的实现就是将类型作为参数,函数后面添加
<T>
,T
就是类型参数,使用时将类型传入到类型参数中。function createArray<T> (length: number, value: T): T[] { return Array<T>(length).fill(value) } const res = createArray<number>(3, 11);
-
类型声明:
declare
,在使用函数时,没有定义定义类型声明,可以使用declare
声明类型。-
使用第三方库时,如果没有类型声明文件时,可以先尝试导入类型声明文件,如果
lodash
的类型声明库@types/lodash
-
如果没有类型声明库,则可以使用
declare
来声明import { camelCase } from 'lodash' declare function camelCase(val: string): string; const res = camelCase('wangliwei'); export {}
-
js性能优化
1.内存管理:内存是可读写单元,表示可操作的空间。内存管理就是开发者主动申请、使用、释放内存空间。js的内存管理是自动的。
// 申请内存空间
let obj = {};
// 使用内存空间
obj.a = 123;
// 无法直接释放内存空间,通过这种方式释放内存空间
obj = null;
2.js
垃圾回收(GC)机制:GC
的目的是为了实现内存空间的良性循环
- 可达对象: 可以访问到的对象,从根出发可以被找到的对象,也称活动对象
- 根: 程序执行的起始环境,在
js
中即被认为是global
全局变量对象 - 垃圾对象: 无法从根上被访问的变量。
- 程序中不在需要使用的对象
- 程序中访问不到的变量对象
- 垃圾回收(GC): 变量从可达对象变为垃圾对象,并被回收机制回收的过程
3.GC
算法
GC
是一种机制,垃圾回收器完成具体的工作- 工作的内容:查找垃圾、释放空间、回收空间
- 算法就是工作时查找和回收时遵循的规则
- 常见的
GC
算法:-
引用计数
- 实现原理:为对象设置引用数,通过判断引用数是否为
0
,来决定是否要回收对象空间- 引用计数器:维护引用数
- 计数改变:在引用关系发生改变时改变,对象被其他对象引用时,引用加
1
,如果删除对该对象的引用,引用计数就减1
- 优点:
- 发现垃圾立即回收; 时刻监听引用计数的变化,如果为
0
,立即回收 - 最大限度减少程序暂停:垃圾回收分摊到每时每刻,所以几乎感知不到程序暂停。
- 发现垃圾立即回收; 时刻监听引用计数的变化,如果为
- 缺点:
- 无法回收循环引用的对象,因为计数始终为
1
- 时间和空间开销大,需要监控引用计数的变化,修改引用计数需要花费额外的时间
- 无法回收循环引用的对象,因为计数始终为
- 实现原理:为对象设置引用数,通过判断引用数是否为
-
标记清除
- 实现原理:分为标记和清除两个阶段
- 标记:遍历所有对象,找到活动对象(可达对象)并标记,会递归进行深层次的查找并标记。
- 清除:遍历所有对象,清除所有没有标记的对象,并回收对象空间到空闲链表中,下次有对象要使用内存空间时会从空闲链表中取出使用
- 优点:可以回收循环引用的对象。
- 当回收的内存空间不连续时,而新定义的定义的对象所使用的空间大小不能匹配空闲链表中的空间碎片大小时,空间的使用效率会降低。 因为使用大的碎片空间会造成空间浪费,而使用小的碎片空间有不够。
- 实现原理:分为标记和清除两个阶段
-
标记整理: 可以看做标记清除的增强,也分标记和清除两个阶段
- 标记阶段和标记清除一致
- 清除阶段在清除没有标记的对象之前,会先移动活动对象的位置,使地址连续,然后再清除没有标记的对象,回收的内存空间地址将连续,之后的使用将最大程度的使用内存空间
-
分代回收
-
4.V8引擎:最主流的js
执行引擎,js
的高效运转
- 支撑
js
高效运转的基础- 优秀的内存管理
- 内存设限:
64bit
下不超过1.5G
32bit
下不超过800M
- 内存设限原因:
- V8是专为浏览器设置的,限制内的内存足够浏览器应用使用
- 内部的垃圾回收机制决定限制是合理的。Google官方实验:当垃圾内存达到
1.5G
时,采用增量标记的算法进行垃圾回收需要50ms
,而采用非增量标记的算法需要1s
。
- 内存设限:
- 即时编译
- 其他
js
执行引擎在执行js
代码时需要先将代码转成字节码才能执行 - V8则是直接将
js
代码翻译成可以直接执行的机器码,所以速度非常快
- 其他
- 优秀的内存管理
- V8垃圾回收策略:垃圾回收主要是针对对象数据,原始数据由语言本身管理
- 采用分代回收的思想,将内存分为新生代和老生代
-
分代回收将内存分为两块,小的(
64bit 32M | 32bit 16M
)那块为新生代存储区,大的那块为老生代存储区 -
新生代:存活时间较短的对象,新生代空间分为带下相等的两块
from
和to
,from
是使用空间,to
是空闲空间- 回收过程采用复制算法和标记整理算法
- 回收过程:
- 活动对象存储于
from
空间 - 当
from
空间使用超过一定程度后,对from
空间的活动对象进行整理 - 将整理完的
from
空间的活动对象复制到to
空间,并释放
- 活动对象存储于
- 回收过程细节
- 回收过程中可能出现 晋升
- 晋升是指从将新生代移至老生代
- 一轮GC之后还存活的对象需要晋升
to
空间的使用率超出25%
之后,会将其中的对象都移动至老生代。
- 回收过程中可能出现 晋升
-
老生代:存活时间较长的对象,如全局变量、闭包等。 大小(
64bit 1.4G | 32bit 700M
),垃圾回收主要采用标记清除、标记整理、增量标记算法- 首先使用标记清除完成垃圾空间的回收
- 当老生代中的空间碎片不足以供晋升使用时,会触发标记整理,对碎片空间进行优化。
- 采用增量标记提升效率。
- 垃圾回收将会阻塞js的执行
- 标记增量:就是将整个垃圾回收拆分成多个小段组合执行。程序执行和垃圾回收交替进行
-
新老生代回收细节对比:
- 新生代采用复制算法速度快,但总有一半的空间闲置,是使用空间换时间。
- 老生代不适合复制算法,因为空间比较大,如果使用复制算法,太浪费了。
-
- 针对不同的对象采用不同的算法。
- 常用GC算法:
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 增量标记:为了提高效率
- 采用分代回收的思想,将内存分为新生代和老生代
5.内存监控:performance
工具
- 内存问题的外在表现:
- 页面出现延迟加载或暂停
- 糟糕的性能,经常卡顿(内存膨胀)
- 流畅度越来越差(内存泄漏)
- 界定内存问题的标准:
- 内存泄漏:内存使用持续升高
- 内存膨胀:应用本身消耗内存较大,有的设备可能满足不了
- 频繁的垃圾回收:通过内存变化图进行分析
- 监控内存的几种方式:
-
浏览器任务管理器:
shift + esc
打开 -
Timeline
时序图:浏览器工具performance
,监控内存变化 -
堆快照查找分离
DOM
- 分离DOM: 界面元素是在
DOM
树上存在的,从DOM树上脱离,但是在JS
代码中被引用的DOM
对象 - 垃圾对象的
DOM
节点:从DOM
树上脱离,没有被引用的DOM
节点,将会被GC
回收 - 分离
DOM
,会造成内存泄漏。可以使用堆快照查找,detached
开头的就是分离DOM
, 浏览器工具内存
中
- 分离DOM: 界面元素是在
-
判断是否存在频繁的垃圾回收
- Timline 中内存的频繁的上升下降
- 任务管理器中频繁的数据变化
-
6.js性能优化
-
JSBench的使用:https://jsbench.me/ 在线js性能测试
-
慎用全局变量
- 全局变量处于作用域链的顶端,变量搜索时会自底向上搜索,耗时比较长
- 全局变量存在于全局的执行上下文中,直到程序退出才会释放空间,不方便
GC
工作。 - 同名的局部变量会污染全局变量
-
缓存全局变量:在局部作用域中使用全局变量时,使用局部变量缓存全局变量更有效率。缩短了查找过程,但增加了内存消耗。
-
通过原型对象添加方法:把附加方法添加在原型对象中,比在方法内部添加方法 执行效率更好
const fn1 = function () { this.foo = function () { console.log(111); } }; const f1 = new fn1(); f1.foo(); const fn2 = function () { }; fn2.prototype.foo = function () { console.log(222); }; const f2 = new fn2(); f2.foo();
-
闭包陷阱:即内存泄漏。
-
避免属性访问方法的使用,直接访问属性效率更高
js
中属性都是可以在外部访问的,没必要通过属性访问方法达到私有化的控制- 使用属性访问方法会增加一层方法定义
-
for循环的优化:缓存数组的长度。
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; for (let i = 0, len = arr.length; i < len; i++) { console.log(arr[i]); }
-
节点操作的优化:
- 对于多个节点的添加,创建好节点之后一次添加,而不是每次都添加
- 对于多次新增节点,可以先创建节点,然后克隆节点再添加。
-
堆栈中js的执行过程:
- 创建执行栈,并创建全局的执行上下文环境开始执行
- 变量定义
- 如果遇到基础类型定义,则直接在栈中开辟空间存放变量的值
- 如果遇到函数(引用类型)定义,则在堆区开辟空间存放函数定义信息,并将引用地址赋值给变量
- 函数执行:创建新的执行上下文,并开始执行函数内部代码,执行逻辑和全局的执行逻辑一致。过程:
- 确定
this
指向 - 初始化作用域链
- 声明变量
- 执行逻辑代码
- 确定
-
js 性能优化之代码优化
-
减少判断嵌套层级,先判断终止条件
var doSomething = function (part, level) { const parts = ['A', 'B', 'C', 'D']; if (part){ if (parts.includes(part)) { console.log('属于当前课程'); if(level > 5) { console.log('请提供 VIP 身份'); } } } else { console.log('请确认模块信息'); } }
优化后:
var doSomething = function (part, level) { const parts = ['A', 'B', 'C', 'D']; if (!part){ console.log('请确认模块信息'); return; } if (!parts.includes(part)) return; console.log('属于当前课程'); if(level > 5) { console.log('请提供 VIP 身份'); } }
-
减少作用域链的查找层级:变量的查找是自底向上查找的,采用局部变量缓存上层作用域链的变量,可以缩短查找时间。空间换时间
-
字面量和构造式:字面量形式是直接开辟空间存储变量对应的值,而构造式则是调用函数,中间多出了很多过程,所以花费的时间要长很多
-
惰性函数:对于一些要根据平台才能判断使用的函数,应该使用变量来缓存,而不是每次调用函数的时候判断。
let Request = function () { if(window.XMLHttpRequest) { return window.XMLHttpRequest; } else if(window.ActiveXObject) { return window.ActiveXObject; } };
优化后:
let Request = function () { if(window.XMLHttpRequest) { getRequest = window.XMLHttpRequest; } else if(window.ActiveXObject) { getRequest = window.ActiveXObject; } return getRequest; };
-
事件代理:利用冒泡机制,在父元素中统一绑定事件而不是在每个子元素中绑定事件
-