ES 新特性与 TypeScript、JS 性能优化

es2015

模板字符串

带标签的模板字符串

定义模板字符串之前可以添加一个标签,该标签就是一个函数,添加这个标签就是调用这个函数

let str = console.log`hello` // [ 'hello' ]
function func (string, str1, str2) { 
    console.log(string)
    console.log(str1, str2) // 可以接收到模板字符串中表达式的返回值
}
let a = 'tom'
let b = true
let result = func`${a} is man, ${b}` // [ '', ' is man, ', '' ]
let result1 = func`asd ${a} is man, ${b}asd` // [ 'asd ', ' is man, ', 'asd' ]

在模板字符串中会有嵌入的表达式,所以最后输出的结果就是按照表达式分割过后那些静态的内容,相当于对传入使用split方法分割

字符串的扩展方法

let message = 'Error: foo is not defined'
// 判断字符串是否以指定字符串开头
console.log(message.startsWith('Error')) // true
// 判断字符串是否以指定字符串结尾
console.log(message.endsWith('.')) // false
// 判断字符串是否包含指定字符串
console.log(message.includes('Error')) // true

Object扩展方法

Object.assign

将多个源对象中的属性复制到另一个目标对象中

let obj1 = {
    a: 123,
    b: 456
}
let obj2 = {
    a: 111,
    c: 789
}
let newObj = Object.assign(obj1, obj2)
console.log(newObj) // { a: 111, b: 456, c: 789 }
console.log(obj1) // { a: 111, b: 456, c: 789 }
console.log(obj1 === newObj) // true

Object.is

用来比较两个值是否严格相等

// === 全等运算符比较+0 和 -0时的表现是相等的, NaN和NaN比较是不等的
// Object.is比较+0 和 -0是不等的,NaN和NaN是相等的

Proxy

const p = new Proxy(target, handle)
// target: 要使用Proxy包装的目标对象,可以是任何类型的对象,包括原生数组,函数,甚至另一个代理
// handle: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

Proxy和Object.defineProperty()的区别:

  • Object.defineProperty只能监听到对象属性的读取和写入,Proxy能监听到更多对象操作例如delete操作,对象方法的调用等

Proxy监听可监听的对象操作

handle方法 触发方式
get 读取某个属性
set 写入某个属性
has in操作符
deleteProperty delete操作符
getPrototypeOf Object.getPrototypeOf()
setPrototypeOf Object.setPrototypeOf()
isExtensible Object.isExtensible()
preventExtensions Object.preventExtensions()
getOwnPropertyDescriptor Object.getOwnPropertyDescriptor()
defineProperty Object.defineProperty()
ownKeys Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()
apply 调用一个函数
construct 用new调用一个函数
  • Proxy更好的支持数组对象的监视
    原始的Object.defineProperty()对数组的监视最常见的就是重写数组的方法,这也是vue.js中的操作方式,大体思路就是通过自定义方法覆盖数组原有的方法
  • Proxy是以非侵入的方式监管了对象的读写(对已定义好的对象无需对对象本身进行任何操作就可以监视到对象的读写)

Reflect

Reflect属于一个静态类,不能通过new操作符构建一个实例,只能调用该静态类中的静态方法(Reflect.get()).
Reflect内部封装了一系列对对象的底层操作,Reflect成员方法就是Proxy处理对象的默认实现,Reflect统一提供了一套用于操作对象的API

class

class类中,static静态方法中的this指向的事当前的类而非实例

class A {
    constructor() {
        this.name = 1
    }
    static say() {
        console.log(this)
    }
}
A.say() // class A

extends继承

super关键字指向父类

Set数据结构

Set内部的成员都是唯一的不允许重复的

let s = new Set()
s.add(1).add(2) // add方法往Set集合中添加成员且返回集合对象本身,所以可以链式调用
s.size // size获取集合长度
s.has(1) // 判断集合中是否存在某个特定的值
s.delete(1) // 删除集合中某个特定的值
s.clear() // 清除当前集合中全部的值

Map数据结构

原始对象只能使用字符串作为对象的键,如果使用非字符串作为键则内部会调用toString()方法将其转换为字符串,
es6中也可以使用Symbol作为键
Map数据结构解决了这一问题,是严格意义上的键值对集合,用来映射两个任意数据类型之前的对应关系
Map数据结构和对象最大的区别就是对象只能使用字符串作为键,Map集合可以使用任何类型作为键

// 原始对象
let obj = {}
obj[123] = false
obj['name'] = false
obj[true] = false
obj[{a: 1}] = false // [object Object]
for (let k in obj) {
    console.log(typeof k) // string
}

// Map
let m = new Map()
let key = {a: 1}
m.set(key, false) // 设置属性
m.get(key)  // 获取指定键对应的值
m.has(key) // 判断指定键是否存在
m.clear() // 清空Map集合

Symbol

Symbol是一种新的原始数据类型,表示一个独一无二的数据
最主要的作用就是为对象添加独一无二的属性名

    // 每次用过Symbol函数创建的值都是唯一的一个值,无论传入的描述文本是否相同
    console.log(Symbol('foo') === Symbol('foo')) // false
    // 如果想复用一个相同的Symbol值,可以使用变量接收或Symbol提供的静态方法for
    // for方法会根据给的值从Symbol注册表中找到对应的ymbol,如果找到了,则返回它,否则,新建一个与该键关联的Symbol
    let s1 = Symbol.for('foo') // 
    let s2 = Symbol.for('foo')
    console.log(s1 === s2) // true
    // for维护的是字符串与Symbol值对应的关系,如果传入非字符串类型,会转换成字符串
    console.log(Symbol.for(true) === Symbol.for('true')) // true
    console.log(Symbol.for('[object Object]') === Symbol.for({b: 1}))

Symbol.toStringTag 是一个内置Symbol,它通常作为对象的属性键使用,对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签

let obj = {}
console.log(obj.toString()) // [object Object]
let obj1 = {
    [Symbol.toStringTag]: 'asd'
}
console.log(obj1.toString()) // [object asd]

用Symbol值作为对象的属性名,使用for...in循环和Object.keys()是无法拿到的,使用JSON.stringify将对象转换成字符串时Symbol值也会被忽略掉,所以Symbol适合作为对象的私有属性

let obj = {
    [Symbol()]: 1,
    a: 2
}
for (let k in obj) {
    console.log(k) // a
}
console.log(Object.keys(obj)) // [ 'a' ]
console.log(JSON.stringify(obj)) // {"a":2}
// 可以通过Object.getOwnPropertySymbol(obj) 获取到对象中Symbol的属性名

for...of

for...of方法可以遍历任意可迭代对象包括 Array,Map,Set,String,TypedArray,arguments 对象等等,所以可以被for...of循环直接遍历的数据结构必须实现了Iterable接口
for...of方法可以使用break关键词终止循环,forEach循环除了抛出异常以外不能终止
for...of遍历Map数据结构返回键值对数组

let m = new Map()
m.set('a', 1)
m.set('b', 2)
for (let value of m) {
    console.log(value) // [ 'a', 1 ]
                       // [ 'b', 2 ]
}

可迭代对象Iterable

// 实现可迭代接口
let obj = {
    store: ['asd', 'qwe', 'zxc', 'fgh'],
    [Symbol.iterator]: function () {
        let index = 0
        let self = this
        return {
            next: function () {
                let result = {
                    value: self.store[index],
                    done: index >= self.store.length
                }
                index++
                return result
            }
        }
    }
}
for (let i of obj) {
    console.log(i)
}

迭代器模式

核心就是对外部提供统一的遍历接口,让外部不用关心数据内部的结构

let obj = {
    life: ['生活', '吃饭', '睡觉'],
    work: ['上班', '下班'],
    learn: ['js', 'java', 'python'],
    [Symbol.iterator]: function () {
        let all = [...this.life, ...this.work, ...this.learn]
        let index = 0
        return {
            next: function () {
                return {
                    value: all[index],
                    done: index++ >= all.length
                }
            }
        }
    }
}
for (let i of obj) {
    console.log(i)
}

Generator生成器

Generator的是避免异步编程中毁掉嵌套过深的问题而提供的更好的异步编程解决方案,Generator内部也实现了迭代器接口。
yield关键词并不会结束掉当前方法的执行,而是暂停执行
Generator会返回一个生成器对象,调用生成器对象的next方法才会让函数体开始执行,遇到yield关键词会暂停后续代码的执行,yield后面的值会作为next的结果返回

es2016

Array.prototype.includes

indexOf方法不能查找NaN,includes可以

console.log([NaN].indexOf(NaN));  // -1
console.log([NaN].includes(NaN)); // true

指数运算符

// es5
console.log(Math.pow(2, 10)) // 2底数 10指数
// es6
console.log(2 ** 10) // 2底数 10指数

es2017

Object.values

// 返回对象值的数组
let obj = {
    a: 1,
    b: 2
}
console.log(Object.values(obj)) // [ 1, 2 ]

Object.entries

// 返回以数组的方式返回键值对
let obj = {
    a: 1,
    b: 2
}
console.log(Object.entries(obj)) // [ [ 'a', 1 ], [ 'b', 2 ] ]

Object.getOwnPropertyDescriptors

// 获得对象的完整描述信息
let obj = {
    a: 1,
    b: 2
}
console.log(Object.getOwnPropertyDescriptors(obj))
/*
{
  a: { value: 1, writable: true, enumerable: true, configurable: true },
  b: { value: 2, writable: true, enumerable: true, configurable: true }
}
*/

String.prototype.padStart、String.prototype.padEnd

用给定字符串去填充目标字符串开始或结束的位置,直到达到指定长度为止

性能优化

所有提高运行效率,降低运行开销的行为,都可以称作性能优化
如请求资源时用到的网络,数据的传输方式,框架等

内存管理

开发者人为的去操作一片空间的申请、使用和释放

内存

由可读写单元组成,表示一片可操作空间

// 申请一片内存空间
let obj = {}
// 使用内存
obj.name = 'abc'
// 释放内存
obj = null

垃圾回收

JavaScript中内存管理是自动的,当对象不再被引用或者不能从根上访问到时,就会被看作垃圾,垃圾回收机制就会把它们占用的空间进行回收

可达对象

可以访问到的对象就是可达对象(引用、作用域链)

GC算法

常见的GC算法包括引用计数、标记清楚、标记整理、分代回收

引用计数算法

设置引用数,判断当前引用数是否为0,当引用关系发生改变时引用计数器就会修改引用数字,引用数字为0时立即回收
优点:

  • 发现垃圾立即回收
  • 最大程度较少程序暂停 ()

缺点:

  • 无法回收循环引用的对象
function fn () {
    let obj1 = {}
    let obj2 = {}
    obj1.name = obj2
    obj2.name = obj1
}
  • 时间开销大,引用计数需要维护一个数值的变化,所以时刻监控着当前对象的引用数值是否需要修改,本身数值修改就需要消耗时间,如果内存中有更多的对象需要修改

标记清除算法

遍历所有对象找到活动对象进行标记,然后遍历所有对象将**没有标记的对象清除,把回收的空间放到空间链表中,方便后续程序直接申请使用,同时抹去所有标记以便下一轮GC正常工作
优点:

  • 可以解决循环引用对象的回收的问题

缺点:

  • 空间碎片化
  • 不会立即回收垃圾对象

标记整理算法

标记整理可以看做是标记清除的增强,标记阶段和标记清除一致,清理阶段会先执行整理,移动对象的位置让它们在地址上连续

V8引擎

V8采用即时编译,内存设有上线,64位1.5G,32位800M

V8垃圾回收

采用分代回收,将内存分为新生代和老生代,针对不同对象采用不同算法, V8中常用的GC算法包括分代回收,空间复制,标记清除,标记整理,标记增量

回收新生代

V8将内存空间一分为二,小空间用于存储新生代对象(64位32M,32位16M),新生代指的是存活时间较短的对象
回收过程采用复制算法 + 标记整理,新生代内存分为两个等大小空间,使用空间为From,空闲空间为To,活动对象存储于From空间,标记整理后将活动对象拷贝至To, from与to交换空间,完成释放
拷贝过程中可能出现晋升,就是将新生代对象移动至老生代, 一轮GC还存活的新生代需要晋升,当To空间的使用率超过25%也需要晋升

回收老生代

老生代指的是存活时间较长的对象,至64位1.4G, 32位700M
主要采用标记清除、标记整理、增量标记算法
首先使用标记清除完成垃圾空间回收, 采用标记整理进行空间优化

你可能感兴趣的:(ES 新特性与 TypeScript、JS 性能优化)