ES6新特性总结和自己的一些思考(掌握后可以超过90%的前端程序员)

104 阅读14分钟

1.let const

都具有块级作用域:任何一对花括号中的语句集都属于一个块,在这之中定义的所有变量在代码块外都是不可见的.

存在暂时性死区: 区块中存在 const/let 就不能在声明之前使用该变量

var tmp = 123
if(true){
    tmp = 456 //报错 ReferenceError
    let tmp;
}

都不具备变量提升 不允许重复声明

let 申明变量,const 申明常量,除非定义的是对象,否则不能修改

2.模板字符串

${变量}不用写之前的++符号

使用反引号`123${变量}456` ==> 123变量456

3.变量解构赋值

  • 使用[]和{}对数组和对象进行解构

    //数组 按照对应位置 只要某种数据具有Iterator接口都可以采用数组形式的解构赋值
    const foods = ['辣椒','肉','其他']
    const [food1,food2,food3] = foods
    ​
    //对象 按照键名
    const person = {name:'xx',age:24}
    const {name,age} = person ==> {name:name,age:age} = person
    以上代码说明对象的解构赋值的内部机制,是先找到同名属性,再赋给对应的变量.真正被赋值的是后者,而不是前者
    
  • 字符串/数值/布尔值解构赋值

    只要等号右边的值不是对象或数组,都会将其转为对象 undefinednull无法转为对象会报错
    //字符串 字符串被转换成了一个类似数组的对象,所以具有length属性,可以进行解构赋值
    const [a,b,c] = '123' // a == 1,b == 2,c == 3
    //数值和布尔值 数值和布尔值得包装对象都有toString属性,因此toString修饰下的s1,s2可取得值
    let {toString:s1} = 123
    let {toString:s2} = true
    s1 === Number.prototype.toString //true
    s2 === Boolean.prototype.toString //true
    
  • 函数参数的解构赋值 类似于赋默认参数

  • 常见用途

    交换变量值  [x,y] = [y,x]
    从函数返回多个值 let {foo,bar} = example()
    函数参数定义
    提取JSON数据 let {id,status,data} = res
    遍历Map结构 for(let [key,value] of map){...}
    获取模块指定方法 const {SourceNode} = require('source-map')
    

4.函数

参数默认值

function print(text = 'default'){...}
JavaScript 中函数的参数默认是undefined
新特性允许在没有值或undefined被传入时使用默认形参,有了默认参数,我们不需要再在函数体内做不必要的检查

尾调用和尾递归

尾调用:函数式编程的一个概念 指某个函数最后一步是调用一个函数 目前只有Safari支持尾调用优化
function f(x){return g(x)} //需要return一个函数 不然等同于函数操作后return undefined
​
函数调用会在内存形成一个"调用记录",又称调用帧(call frame),保存调用位置和内部变量信息,
如果A函数调用B,B再调用C,...循环往复所有的调用帧就会形成一个调用栈(call stack)
​
尾调用因为是函数内部最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置和变量信息都用不到了,
所以只需保留内层函数的调用帧,可以节省很大内存.
​
注意!闭包的情况下无法进行尾调用优化
​
尾递归:函数调用自身为递归;尾调用自身,就成为尾递归
尾递归不会发生栈溢出的情况,这里使用到了尾调用优化
​
function dg(n){if(n===1) return 1; return n*dg(n-1)} //递归 复杂度O(n)
function wdg(m,total=1){if(m===1) return total; return wdg(m-1,m*total)}
//尾递归 复杂度O(1)

箭头函数

不需要function关键字,不需要进行return关键字
箭头函数没有不可以使用arguments对象,该对象不存在于函数体内,可以用rest代替
this指向函数声明时的上下文作用域,所以是固定的定义时上层作用域中的this
需要获取动态this的时候,不要用箭头函数定义!
​
js传统的this指向情况有很多种,有时间的话会单独写一篇文章谈谈自己的体会

5.Spread/Rest 操作符

也被称作"三点运算符" 具体是 Spread 还是 Rest 需要结合上下文语境

当被用于迭代器中时,是一个Spread 扩展操作符:
迭代器(Iterator)是按照一定的顺序对一个或多个容器中的元素进行遍历的一种机制
​
function foo(x,y,z){...}
let arr = [1,2,3]
foo(...arr) //1,2,3 替代foo.apply(null,arr)这种写法
用途:
    复制数组
    合并数组
    与解构赋值结合生成数组
    具有迭代器接口的对象转为真正的数组(包含Map,Set结构,Generator函数)
    对没有Iterator接口的对象使用扩展运算符会报错
​
当用于函数传参/解构赋值时,为Rest 剩余集合操作符:可以获取多余参数到数组,替代arguments对象
function foo(...args){...}
foo(1,2,3,4) //[1,2,3,4]

6.新增的对象 Symbol

首先是新的一种基本数据类型

为了避免对象属性名的冲突,Symbol类型的值通过Symbol('描述值')函数生成,该类型具有静态属性和静态方法;
不是严格意义上的构造函数,因为它不支持语法:"new Symbol()"
​
每个从Symbol()返回的symbol值都是唯一的。一个symbol值能作为对象属性的标识符;这是该数据类型仅有的目的。
Symbol.for(名称): 接收字符串为参数,可以使用此方式使两个Symbol相等 具有"全局登记"机制

7.for...of

  • for...of 是遍历键值对中的值

  • for...in 遍历键值对中的键名

    for...of和迭代器适用的其他类型...
    

8.class 类

支持 class 语法,不是新的对象继承模型,可以看做是原型链继承的语法糖

原型链继承也是js的核心部分,也会单独出一篇文章记录一下自己的理解。

9.import 和 export

ECMA 官方支持 export default 和 import 进行模块导出和导入,之前使用的是 require 模块引入的方法

10.Promise

异步编程的解决方案 比传统的--回调函数和事件--更合理和强大

简单来说就是一个容器 里面保存着未来才会结束的事件的结果.对象的状态不受外界影响;一旦状态改变就不会再变

注意: 如果某些事件不断地重复发生,一般来说,使用 Stream 模式比部署 Promise 更好

Promise是一个构造函数,接收一个函数作为参数,该函数有两个参数resolve,reject,它们是两个函数,由js部署.
​
1、基本逻辑:
有三个状态pending,fulfilled,rejected(只有异步操作的结果可以决定这个状态是哪一种)
pending--->resolved/rejected 这个过程是不可逆的
resolve将Promise状态由pending变为fulfilled,异步操作成功时调用,将异步操作结果以参数返回(res)
reject将pending变为rejected,是处理失败的回调,将异步操作报出错误以参数形式传递出去(error)
​
2、常用方法:
.then方法返回一个新的Promise实例且具有链式(.then.then...)传递结果的能力.接收两个回调函数作为参数.
第一个回调是Promise对象状态变为resolve时调用,第二个回调是Promise状态变为rejected时调用,
两个回调都接受Promise对象传出的值作为参数
.catch用于指定发生错误时的回调函数,Promise对象的错误具有"冒泡"性质,会一直向后传递,知道被捕获为止.
所以错误总是会被下一个catch语句捕获
​
3Promise实例方法:
    finally():不管Promise对象最后状态如何都会执行的操作
    all():将多个Promise实例包装成一个新的Promise实例 必须多个实例状态都为fulfilled,
    返回的才会是fulfilled
    race():将多个Promise实例包装成一个新的Promise实例 只要有一个实例率先改变状态,新的实例状态就跟着改变
    
4、常见应用:
    加载图片:
            将图片加载写成一个Promise,一旦加载完成,Promise状态发生变化.多见于绘图库(Konva)的使用
            const preloadImage = function (path) {
                  return new Promise(function (resolve, reject) {
                    const image = new Image();
                    image.onload  = resolve;
                    image.onerror = reject;
                    image.src = path;
                  });
              };
    Generator函数与Promise结合:
            ...待续

11.async/await

Promise 不能很好的解决一个请求依赖于另一个请求的情况,容易形成地狱回调

async/await 能将异步请求同步化

12.Set/Map 集合

  • Set 集合: 存储任何类型的"唯一"值,集合中的元素是不会重复的 常用来数组去重

    let arr = [1,1,2,3]
    let singleArr = new Set(arr) // 1,2,3
    singleArr是类数组结构,要转化成数组需要Array.from(singleArr)
    ​
    Set本身是一个构造函数,用来生成Set数据结构
    方法: Set.prototype === new Set().__proto__ 指向同一个原型对象
        实例方法: has() 用来查找值 Set.prototype.has(value)
                delete(value) 删除某个值
                add(value) 添加某个值
                clear() 删除所有成员
        常用方法:[...new Set([+1,-1,NaN,NaN,2,2])] //[1,-1,NaN,2] 去除数组重复成员
                [...new Set('ababbc')].join('') //"abc"
    
  • Map 集合: 各种类型的值(包括对象)都可以当做键的集合(一种更完善的 Hash 结构实现)

    1、特性:
        ES6之后,如果需要"键值对"的数据结构,MapObject更合适!
        注意点: 只有对同一个对象的引用,Map结构才将其视为同一个键!! 
        比如如下情况:['a']是两个不同的数组实例,内存地址不一样
        const map = new Map();
        map.set(['a'], 555);
        map.get(['a']) // undefined
        即Map的键实际上跟内存地址绑定,只要内存地址不一样,就视为两个键
    ​
        !Map 中的 key 是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。
        !Map 的键值对个数可以轻易地通过size 属性获取
        !Map 是 iterable 的,所以可以直接被迭代。
        !在频繁增删键值对的场景下表现更好。
        !骚年,用什么Object.create(null)啊? 直接new Map()走起...
    2、方法:
        Map对象方法:  has() 用来查找键名 Map.prototype.has(key)
                    set(key,value) 设置键值对
                    get(key) ...
                    has(key) ...
                    delete(key) ...
                    clear() ...
        常用方法:
            Map转为数组: [...new Map()]
            数组转为Map: new Map([[true,7],[{foo:3},['a,b,c']]])
            Map转为对象: 利用Object.create(null) 再进行键值对赋值
            对象转为Map: new Map(Object.entries(obj))
    

13.Proxy 和 Reflect

非常重要的新特性,vue3对于数据响应式处理就是基于Proxy和Reflect,代替了vue2的Object.defineProperty

Proxy

Proxy用于修改某些操作的默认行为,等于在语言层面做出修改,属于对编程语言进行编程
行为:在目标对象外层设置了"拦截",外界对该对象的访问都必须先通过这层拦截,
通常对外界访问进行过滤和改写,"代理"某些操作
操作:var proxy = new Proxy({},{get(){return...},set(){...}})
实例方法:
    get(target,propKey,receiver):目标对象 属性名和实例本身(严格说是操作行为针对对象,可选);
    如果一个属性不可配置且不可写,则Proxy不能修改该属性,否则通过Proxy对象访问该属性会报错
    set(target,propKey,value,receiver):拦截某个属性的赋值操作,应该返回布尔值表明是否修改值成功
    apply(target,ctx,args):目标对象,目标对象的上下文this,目标对象的参数数组 拦截某个函数
    has(target,key):拦截HasProperty操作 典型的操作就是in运算符
    constructor(target,args,newTarget):用来拦截new命令 必须返回一个对象
    deleteProperty(target,key):拦截delete操作
    defineProperty(target,key,descriptor):拦截Object.defineProperty()操作
    ...
this问题:
    Proxy代理的情况下,目标对象内部的this关键字会指向Proxy代理

Reflect?

ReflectProxy对象一样,是ES6为了操作对象而提供的
(1)将Object对象一些明显属于语言内部的方法,放在Reflect对象上(比如Object.defineProperty) 
会逐步迁移到Reflect上
(2)修改某些Object方法的返回结果
(3)让Object操作都变成函数行为 Reflect.has(Object,'assign') === 'assign' in Object
(4)Reflect对象的方法与Proxy对象的方法一一对应,
只要是Proxy对象的方法,都可以在Reflect对象上找到对应的方法(很重要)

14 新增方法

字符串:
    实例方法 includes/startsWith/endsWith: 返回boolean值,是否含有/在头部/在尾部
    实例方法 repeat: 返回新字符串,将原字符串重复n次
    实例方法 padStart/padEnd: 头部补全/尾部补全
    实例方法 trim/trimStart/trimEnd: 去除全部/头部/尾部空格
数值:
    Number对象 isFinite/isNaN: 是否有限/是否为数值类型
    Number对象 parseInt/parseFloat: 去除小数点后的数字/转成浮点数
    Number对象 isInteger: 判断一个数值是否为整数
函数:
    Function原型链 toString:返回和函数一模一样的原始代码的字符串
    try...catch...:catch后面不必带有参数error
数组:
    Array.from(): 类数组对象(具有length属性)/可遍历对象转为数组
    Array.of(): 将一组值转换为数组 只有一个参数时是指定数组长度
    实例方法 find/findIndex((value,index,arr)=>{},obj): 接收第二个参数obj,绑定回调函数的this对象;
             都可以发现NaN,弥补indexOf方法的不足
    实例方法 fill:使用给定值 填充一个数组
    实例方法 entries():返回一个遍历器对象 是对键值对的遍历;
             遍历器对象新增next()方法可以代替for..of进行手动遍历
    实例方法 keys()/values():返回一个遍历器对象 对键名/键值的遍历
    实例方法 includes: 判断数组是否包含给定的值
    实例方法 flat()/flatMap(fn,this): 转换为一维数组/对数组成员执行一个函数返回一个数组,
             然后对数组执行flat方法,但是默认只能展开一层
    实例方法 at(): arr.at(-2) 获取到数组倒数第二位的值
    实例方法 sort(): 排序算法 排序稳定性是排序算法的重要属性,即排序关键字相同的项目,排序前后的顺序不会变化.
             现在sort和插入排序/合并排序/冒泡排序都是稳定的;堆排序/快速排序是不稳定的.
对象:
    遍历对象属性的方法:
                for...in 循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)
                Object.keys 返回数组,包括对象自身(不含继承)所有可枚举属性(不含Symbol)
                Object.getOwnPropertyNames 返回数组,包括不可枚举属性的键名
                Object.getOwnPropertySymbols 返回数组,包含对象自身所有Symbol属性的键名
                Reflect.ownKeys 返回数组,包含对象所有键名
    克隆对象的方法:
                Json.parse(Json.stringify) //字符串类型的值储存在栈内存,可以达到深克隆效果
                Object.assign({},obj) //obj的属性是基本类型的话,就是深克隆
                {...obj} //浅克隆
                for...in循环 逐个进行赋值
                自己构建深克隆函数
    Object对象方法 is(): 按照SameValue算法比较两个值是否相等 +0 !== -0 NaN===NaN 其他与严格相等运算符一致
    Object对象方法 assign():将源对象可枚举属性复制到目标对象,后者会覆盖前者 无法正确拷贝get/set属性
    Object对象方法 getOwnPropertyDescriptors:返回所有自身属性的描述对象,可以正确拷贝get/set属性
    实例对象方法 __proto__: 这个属性时es5中对象的属性
    首先 __proto__ === Object.prototype.__proto__
    其次 obj.__proto__ === Object.create(obj) 读取或设置当前对象的原型对象,
        __proto__只在浏览器环境下必须进行支持,从兼容性的角度,需要使用ES6中的方法进行此属性的替代
    Object.getPrototypeOf(): 读取原型对象操作
    Object.setPrototypeOf(): 设置原型对象操作
    Object.create(): 生成原型对象操作
    Object.entries():返回自身(不含继承)所有可遍历属性的键值对数组
    Object.keys():返回所有可遍历属性的键名的数组
    Object.values():返回可遍历属性的键值的数组,会过滤属性名为Symbol值得属性

n.ES6 转 ES5

目前通常使用 babel 转码器并配置.babelrc 文件

但是 Babel 默认只转换新的 js 句法(syntax),不转换新的 API,比如 Iterator Generator Set Map Proxy Reflect Symbol Promise 等全局对象 以及一些定义在全局对象上的方法 Object.assign()都不会进行转码

所以还需要为当前环境提供一个垫片: polyfill