ES6 常见特性讲解

237 阅读17分钟

前言

近期在查阅ES6的相关资料后,发现好多常见的新特性还没有深入了解,趁着这深入学习的过程中,将查到的资料进行总结并记录es6的部分常见特性,供大家参考学习,若有不正确之处,烦请指教。

在查资料的过程中看到ES6体系的知识结构图,先看一下有个大致的了解,也方便后续深入学习。

声明和作用域

let 声明变量,const 声明常量
作用域 :作用于代码块内,如局部代码块 {} 或者 函数代码块function () {}
注意:
(1)const 声明常量后必须立刻赋值, let 声明变量后可以在使用时赋值

    let b; // 输出 undefined 
    const a; // 输出 Uncaught SyntaxError: Missing initializer in const declaration

(2)未定义就使用会报错,constlet不存在变量提升,而 var 存在变量提升

    a=1;
    let a; // 输出:Uncaught ReferenceError: Cannot access 'a' before initialization,
    // 在初始化之前无法访问 a
    
    b =2;
    var b; //输出 2

变量提升

什么是变量提升:
var==1来说,javascript引擎将变量声明var a=1分两个阶段执行,第一个是编译阶段务,而第二个则是执行阶段的。也就是说,先声明变量 var a ,再赋值a =1。这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理,这个过程将所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

举几个栗子说一下:
(1)如果变量声明在函数里面,则将变量声明提升到函数的开头

    var a=1;
    function test(){
        console.log(a);  
         var a=1;
    }
    test();
    
// 输出结果:  undefined
// 解析:在函数 test 内,变量a 提升至函数的开头,此时变量a 只定义了,但未赋值。

以上函数 等价于以下函数

  var a=1;
  function test(){
      var a;
      console.log(a);  
      a=1;
  }
  test();

(2)如果变量声明是一个全局变量,则将变量声明提升到全局作用域的开头
 例子:

    b=1; 
    function test(){
      window.b=3;
      console.log('第一个',b);
      var b;
      console.log('第二个',window.b);
    }
    test();
    // 输出结果: 第一个:undefined   第二个:3

以上函数等价于以下函数

    b=1; 
    function test(){
     window.b=3;
     var b; // 声明局部变量
     console.log('第一个',b);
     //此处的b 是局部变量,已提升到函数开头声明,局部声明的变量未赋值,打印结果是undefined
     console.log('第二个',window.b); // window 是当前js 环境的顶级作用域
    }
    test();

(3) 声明会提升到作用域顶端。

   a = 1;
   var a;
   console.log(a); //  输出:1
   
   // 以上函数等于以下函数
   
   var a;
   a = 1;
   console.log(a)

解构赋值

  解构赋值是一种打破数据结构,将其拆分为更小部分,并将属性(值) 从对象(数组)中取出赋值给其他变量的过程。

字符串解构

字符串有length属性,可知字符串具有Iterator接口,那么在解构时,字符串会转换成 类数组的对象

const [a,b,c] = 'jscript'
const [,d] = 'es6'
console.log(a) // j
console.log(b) // s
console.log(c) // undefined   等号左边多余的变量视为 undefined,等号右边多余的会忽略
console.log(d) // s   等号左边会跳过逗号,d赋值为s

对象解构

对象字面量的语法形式是在一个赋值操作符左边放置一个对象字面量

 let example = {
    type: "object",
    name: "foo"
 };
 let { type, name } = example;
 console.log(type); // "object"
 console.log(name); // "foo"
 // example.type的值 赋值给type这个变量, example.name的值 赋值给name这个变量。

JS引擎将一对开放的花括号视为一个代码块。语法规定,代码块语句不允许出现在赋值语句左侧,添加小括号后可以将块语句转化为一个表达式,从而实现整个解构赋值过程 也就是上述可以写成这样:

 let example = {
    type: "object",
    name: "foo"
 };
 ({ type, name } = example); //用小括号包裹作为一个表达式
 console.log(type); // "object"
 console.log(name); // "foo"

变量添加默认值解构

当指定的属性不存在时,可以随意定义一个默认值,在属性名称后添加一个等号(=)和相应的默认值即可

let example = {
    type: "object",
    name: "foo"
 };
 ({ type, name,value='test' } = example);
 console.log(type); // "object"
 console.log(name); // "foo"
 console.log(value) // 'test' 只有当example上没有该属性或者该属性值为undefined时该值才生效

非同名局部变量解构赋值

let example = {
    type: "object",
    name: "foo"
};
let { type: newType, name: newName } = example;
console.log(newType); // "object"
console.log(newName); // "foo"
// example.type属性值赋值给newType, example.name属性值赋值给newName 

嵌套对象解构

let examle = {
   type: "object",
   name: "foo",
   childs: {
     son : {
         age: 5,
         name: 'jeson'
     },
 }
};
let { childs: { son:newson }} = examle;
console.log(newson.age); // 5
console.log(newson.name); // jeson

以上拆分解构

let examle = {
 type: "object",
 name: "foo",
 childs: {
   son : {
       age: 5,
       name: 'jeson'
   },
}
};
let { childs} = examle;
console.log(childs); //  {son:{age:5,name:'jeson'}}
let {son:newson} = childs
console.log(son); // {age:5,name:'jeson'}

数组解构

在数组解构语法中,通过值在数组中的位置进行选取,且可以将其存储在任意变量中,未显式声明的元素都会直接被忽略

let FOO = [ "ONE", "TWO", "THREE" ];
let [firsT, second ] = FOO;
let [, ,last] = FOO;
console.log(firsT); // "ONE"
console.log(second); // "TWO"
cosnole.log(last) // "THREEE"  last 选取 fOO第三个值进行赋值

交换变量

// 在 ES6 中互换值
let a = 1,b = 2;
[ a, b ] = [ b, a ];
console.log(a); // 2
console.log(b); // 1
// 互换值不需要中间变量

不定元素解构

当解构一个数组时,可以使用剩余模式,将数组剩余部分赋值给一个变量,语法格式是...

let words = ['a','b','c','d','e','f']
let [wd1,...wd2] = words
console.log(wd1)  //a
console.log(wd2)  // ['b','c','d','e','f']

注意:如果剩余元素右侧有逗号,会抛出 SyntaxError,因为剩余元素必须是数组的最后一个元素。

var [a, ...b,] = [1, 2, 3];  // 抛出错误
//Uncaught SyntaxError: Rest element must be last element
//Rest元素必须是最后一个元素

... 语法还可以用来克隆数组

// 在 ES5 中克隆数组
    var colors = [ "red", "green", "blue" ];
    var clonedColors = colors.concat();
    console.log(clonedColors);  //"[red,green,blue]"
//concat()方法是连接两个数组,如果调用时不传递参数就会返回当前函数的副本

// 在 ES6 中克隆数组
    let colors = [ "red", "green", "blue" ];
    let [ ...clonedColors ] = colors;
    console.log(clonedColors);  //"[red,green,blue]"

Set

set 是es6中新的数据结构,它类似于数组的数据结构,成员值都是唯一且没有重复的值
基本的声明语句是 const set = new Set() 接受 具有Iterator接口的数据结构作为参数

关于Iterator
  遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。
在ES6中,原生具备Iterator接口有:
数组某些类似数组的对象Set结构Map结构字符串

Set 实例的属性和方法

属性
   Set.prototype.constructor:构造函数,默认就是Set函数。
   Set.prototype.size:返回Set实例的成员总数。

方法-用于操作数据
   Set.prototype.add(value):添加某个值,返回 Set 结构本身。
   Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
   Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
   Set.prototype.clear():清除所有成员,没有返回值。
方法-用于遍历成员
   Set.prototype.keys():返回键名的遍历器
   Set.prototype.values():返回键值的遍历器
   Set.prototype.entries():返回键值对的遍历器
   Set.prototype.forEach():使用回调函数遍历每个成员

应用场景

字符串去重

 语法: [...new Set(str)].join("")
 (1) new Set(str) 生成类似数组的数据,内容为不重复的值
 (2) 扩展命令[...arr],将类数组数据转换为数组,
 (3) 再用join方法把数组中所有元素放入一个字符串。返回的是一个新的字符串

  let str ='qwrqr1234'
  let temp=[...new Set(str)].join("") //join参数为-指定要使用的分隔符。如果省略该参数,则使用逗号作为分隔符。
  console.log(temp) // qwr1234

数组去重

 语法: [...new Set(arr)]或Array.from(new Set(arr))
 (1) [...new Set(arr)] set 去重,解构赋值重组数组,详见上述解构赋值
 (2)Array.from(new Set(arr))
  new Set()的结果是一个类数组的结构,需要array.from()方法将其转化为一个真正的数组。
  Array.from()方法从具有length属性或可迭代对象的任何对象返回新的Array对象。
  Array.from(object, mapFunction, thisValue)参数
   object:接受一个类数组或可迭代对象。必选
   mapFunction:新数组每个元素都会执行的回调函数
   thisValue:指定第二个参数的this对象
  例子

    var myArr = Array.from("ABCDEFG");
    console.log(myArr) //["A", "B", "C", "D", "E", "F", "G"]
    // 在 解构赋值部分讲到 字符串具有length 属性,因此from方法可将字符串转换为数组

 (3)集合取交/并/差/集
 并集:new Set([...a, ...b])

let a = new Set([1,2,3]);
let b = new Set([4,3,2]);
let _union = new Set([...a, ...b]);
console.log(_union); //Set(4) {1, 2, 3, 4}

 交集:new Set([...a].filter(v => b.has(v)))

let a = new Set([1,2,3]);
let b = new Set([4,3,2]);
let _intersect = new Set([...a].filter(x => b.has(x)));
console.log(_intersect); //Set(2) {2, 3}

 差集:new Set([...a].filter(v => !b.has(v)))

let a = new Set([1,2,3]);
let b = new Set([4,3,2]);
let _difference = new Set([...a].filter(x => !b.has(x)));
console.log(_difference); //Set(1) {1}

 (4)数据的遍历处理/过滤处理

let _init = new Set([1,2,3]);
var _map = new Set([..._init].map(x => x * 2));
console.log(_map); //Set(3) {2, 4, 6}

var _filter = new Set([..._map].filter(x => (x %3)==0);
console.log(_filter);  // Set(1) {6}

Map

map对象是类似于对象的数据结构,成员键可以是任何类型的值。声明:const set = new Map(arr),接受具有Iterator接口且每个成员都是一个双元素数组的数据结构作为参数。 Object结构提供了“字符串—值”的对应,而Map结构提供了“值—值”的对应,也就是说Map对象的键/值,也可以是对象,函数,NaN,跟Object有很大区别

Map 实例的属性和方法

属性
  map.prototype.constructor:构造函数。
  map.prototype.size:返回map实例的成员总数。

方法
  get():返回键值对
  set():添加键值对,返回实例
  delete():删除键值对,返回布尔值
  has():检查键值对,返回布尔值
  clear():清除所有成员
  keys():返回以键为遍历器的对象
  values():返回以值为遍历器的对象
  entries():返回以键和值为遍历器的对象
  forEach():使用回调函数遍历每个成员

使用示例

 (1) set()方法 如果对同一个键多次赋值,后面的值将覆盖前面的值。

   let newMap = new Map();
   newMap.set('var1','test1');
   console.log(newMap); // Map(1) {"var1" => "test1"}
   newMap.set('var1','test2');
   console.log(newMap); // Map(1) {"var1" => "test2"}
   newMap.set('var1','test3');
   console.log(newMap); // Map(1) {"var1" => "test3"}
   console.log(newMap.get("var1")) //test3

注意的是,只有对同一个对象的引用,Map结构才将其视为同一个键。对同样值的两个实例,被视为两个键。Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。

  let newMap = new Map();
  const t1= ['a']
  newMap.set(t1,'var1'); 
  console.log(newMap);  // Map(1) {Array(1) => "var1"}
  const t2= ['a']
  newMap.set(t2,'var2');
  console.log(newMap);  // Map(2) {Array(1) => "var1", Array(1) => "var2"}
  console.log(newMap.get(t1)) // var1
  console.log(newMap.get(t2))  // var2

 (2) Map对象遍历方法
  (a) keys()获取 键

    let map= new Map();
    map.set("{name:'test'}","testname")
    map.set("['aa']","bb")
    console.log(map) // Map(2) {"{name:'test'}" => "testname", "['aa']" => "bb"}
    for(let key of map.keys()) {
        console.log(key)
    }
//  for..of 结果:  {name:'test'}   ['aa']

  (b) values()获取 键

    let map= new Map();
    map.set("{name:'test'}","testname")
    map.set("['aa']","bb")
    for(let key of map.values()) {
        console.log(key)
    }
//  for..of 结果: testname  bb

  (c) entries()获取 键和值

    let map= new Map();
    map.set("{name:'test'}","testname")
    map.set("['aa']","bb")
    for(let key of map.entries()) {
        console.log(key)
    }
    // 输出结果:  ["{name:'test'}", "testname"]  ["['aa']", "bb"]

    // 以下 默认等同于 上述 entries()
    for (let [key, value] of map) {
     console.log(key, value);
    }

  (d) forEach()迭代映射获取 键和值

    let map= new Map();
    map.set("{name:'test'}","testname")
    map.set("['aa']","bb")
    map.forEach((val,key)=>{
        console.log(val+" => "+key)
    })
    // 输出   testname => {name:'test'}  bb => ['aa']

如何判断Set和Map等类型

核心是使用Object.prototype.toString.call()方法输出数据的类型,根据数据类型进行判断
  在开始之前呢,先讲一下 [object object],很多时候,在打印某个函数或者后台返回数据的时候,没留意数据格式,导致打印出来的数据是 [object object],那这个东西表示什么意思呢? 其实就是[object 数据类型],形如 [object String][object Array][object Function]等。因为在JavaScript中对象就是拥有属性和方法的数据。
对象的数据类型包括:Undefined、Null、Boolean、Number、String这些基本数据类型以及复杂数据类型 数组、对象。
所以,万物皆object,那就输出数据类型的 [obejct 数据类型] 这个值来作为判断的依据。 object.prototype.toString.call() 方法就可以精确的判断js对象的数据类型。

let  obj = {}
let  bolen = true
let str = '测试字符串'
let  arr = ['1']
let  fun = function() {}
console.log(Object.prototype.toString.call(obj))  //[object Object]
console.log(Object.prototype.toString.call(bolen)) //[object Boolean]
console.log(Object.prototype.toString.call(str))  //[object String]
console.log(Object.prototype.toString.call(arr))  //[object Array]
console.log(Object.prototype.toString.call(fun))  //[object Function]

有人会问,typeof也可以查看 数据类型,为啥不用这个呢,因为 typeof 只能输出基本数据类型:numberstringundefinedbooleanobject。 其他复杂一点的就判断不出来了,像 对象、数组、函数,时间类型都输出 object
作为对比,例子如下:

let  obj = new Date()
console.log(typeof(obj))  //object
let  obj1 = ['1']
console.log(typeof(obj1)) //object

上一步获取到[object 数据类型] 结果后,把 数据类型拿出来进行判断就是了。这里,我们用match() +正则匹配一下。

let  str = '[object String]'
let rs = str.match(/^\[object (.*)\]$/)
console.log(rs)
// 输出结果:
// ["[object String]", "String", index: 0, input: "[object String]", groups: undefined]

//---------------这里说一下match方法返回值  -------------------
  .match方法在有匹配结果的时候返回值是一个数组。
(1)数组第一个元素是match方法首次匹配到的子字符串
(2)index属性值返回首次匹配到子字符串的位置。
(3)input属性值是原字符串"[object String]"。
(4).groups属性当前并不被支持,暂时不做介绍。

有了上面的基础知识后,写个小函数进一步判断是Set 还是Map

function getType(obj) {
    var type = Object.prototype.toString.call(obj).match(/^\[object (.*)\]$/)[1].toLowerCase();
    if (obj === null) return 'null'; 
    if (obj === undefined) return 'undefined'; 
    return type;
}
getType(new Map()) // "map"
getType(new Set()) // "set"

Proxy

proxy 它主要用于改变某些操作的默认行为,在目标对象的外层搭建了一层拦截,外界对目标对象的某些操作,必须通过这层拦截。

语法及其相关操作

语法let proxy = new Proxy(target,handler)
target参数:要拦截的目标对象,handler参数:配置对象,定义需要拦截的操作

拦截操作

  • get():拦截对象属性读取

  • set():拦截对象属性设置,返回布尔值

  • has():拦截对象属性检查,返回布尔值

  • deleteProperty():拦截对象属性删除, delete obj[k]返回布尔值

  • ownKeys():拦截对象属性遍历,for-inObject.keys()Object.getOwnPropertyNames()Object.getOwnPropertySymbols(),返回数组

  • getOwnPropertyDescriptor():拦截对象属性描述读取,Object.getOwnPropertyDescriptor(),返回对象

  • getPrototypeOf():拦截对象原型读取,instanceof``、Object.getPrototypeOf()Object.prototype.__proto__Object.prototype.isPrototypeOf()Reflect.getPrototypeOf(),返回对象

  • setPrototypeOf():拦截对象原型设置,Object.setPrototypeOf(),返回布尔值

  • isExtensible():拦截对象是否可扩展读取,Object.isExtensible(),返回布尔值

  • apply():拦截Proxy实例作为函数调用,proxy()proxy.apply()proxy.call()

  • construct():拦截Proxy实例作为构造函数调用new proxy()

应用场景

dom的数据双向绑定

 有如下dom
  <input type="text" id="inputdemo" value="" />
  dom的数据对象如下:

// #input输入框的内部状态
const inputDemo = {
  id: 'inputdemo',
  value:''
};

当我们在input输入框输入值的时候,实时获取输入框的值,常规的,我们用js给input 绑定input时间监听来实现,方法如下: 这可以通过绑定onchange事件实现:

 inputChange(inputDemo);
    function inputChange(object) {
        if (!object || !object.id) return;
        const input = document.getElementById(object.id);
        input.addEventListener('input', function (e) {
            inputDemo.value = input.value;
            console.log('change改变inputDemo对象', inputDemo)
        });
    }

那么我们改变input绑定的数据对象来更新视图,可以用proxy来实现

const inputHandler = {
    set: function (target, key, newValue) {
        // target目标对象,key属性名,newValue属性值
        if (key == 'value' && target.id) {
             target[key] = newValue;  // 更新对象属性
            document.getElementById(target.id).value = newValue; // 更新输入框值
             return true;
        } else return false;
    }
}
    // 创建代理
const inputProxy = new Proxy(inputDemo, inputHandler);
console.log('初始默认值: ', inputDemo.value); // 初始默认值:  defalut
// 设置新值
inputProxy.value = 'this is new data';
console.log('设置新值结果: ', inputDemo.value); // 设置新值结果:this is new data
console.log('dom验证:', document.getElementById('inputdemo').value); // dom验证:this is new data

通过以上的方法,将dom节点的数据绑定到定义的数据对象上,改变数据对象时,又能将dom视图进行改变,这样一个简单的数据-视图双向绑定就完成了。 结果截图如下:

数据校验

let book = {
    name: '这是书',
    price: 20
}
let handler = {
    set (target, key, value, receiver) {
    // receiver 参数 Proxy或者继承Proxy的对象,
      //通俗的理解就是,当前操作作用域限定于receiver指代实例,类比于this 指代的实例。默认指代是当前这个proxy实例
      if (key === 'name' && typeof value !== 'string') {
        throw new Error('书名必须是字符串类型')
      }
      if (key === 'price' && typeof value !== 'number') {
        throw new Error('书的价格必须是数字类型')
      }
      return Reflect.set(target, key, value, receiver)
    }
}
let newbook = new Proxy(book, handler)
newbook.name = '这是一本新的书' // 
newbook.price = '23' // 报错  用户年龄必须是数字类型

// ------------关于上述方法里的Reflect---------
// Reflect 保持Object方法的默认行为,将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。也就是说 reflect的一些方法能代替 object 对象的一些原有属性/方法,
//并且 proxy的方法在reflect中也有相应的方法,一般的Proxy和Reflect联合使用
    Reflect.set(target, key, value, receiver) 和下面的方法是一样的
    new Proxy(object,{
        set:function(target,key,newval){
            if(target[key]){
                target[key] = newval
            }
        }
    }) 

Reflect

Reflect对象与Proxy对象一样,为了操作对象而提供的新API,设计目的有如下几种:

  • Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。
  • 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false
  • 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。
  • Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为

相关方法

get():返回对象属性
set():设置对象属性,返回布尔值
has():检查对象属性,返回布尔值
deleteProperty():删除对象属性,返回布尔值
defineProperty():定义对象属性,返回布尔值
ownKeys():遍历对象属性,返回数组(Object.getOwnPropertyNames()+Object.getOwnPropertySymbols()
getOwnPropertyDescriptor():返回对象属性描述,返回对象
getPrototypeOf():返回对象原型,返回对象
setPrototypeOf():设置对象原型,返回布尔值
isExtensible():返回对象是否可扩展,返回布尔值
preventExtensions():设置对象不可扩展,返回布尔值
apply():绑定this后执行指定函数
construct():调用构造函数创建实例

使用示例

(1)Reflect.set方法设置target对象的name属性等于value。如果name属性设置了赋值函数,则赋值函数的this绑定receiver。

let myObject = {
  foo: 4,
  set name(value) {
    return this.foo = value;
  },
};
let myReceiverObject = {
  foo: 0,
};
Reflect.set(myObject, 'name', 1, myReceiverObject);
myObject.foo // 4
myReceiverObject.foo // 1,Reflect.set第四个参数默认指代myObject,但这里指定了操作的对象是myReceiverObject

(2)Reflect.construct方法等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法。

function preson(name) {
  this.name = name;
}
// new 的写法
const instance = new preson('张三');

// Reflect.construct 的写法
const instance = Reflect.construct(preson, ['张三']);

(3)观察者模式(Observer mode),函数自动观察数据对象,一旦对象有变化,函数就会自动执行。

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn); //所有观察者放入一个集合中
const observable = obj => new Proxy(obj, {set});//observable函数返回原始对象的代理,拦截赋值操作
// 拦截函数set之中,会自动执行所有观察者。
function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer()); 
  return result;
}
const person = observable({ age: 25, name: "李四" }); // 需要观察的对象,初始值
const print = () =>console.log(`${person.name} 已经 ${person.age} 岁了`); // 观察者的观察到变化后执行的函数
observe(print); // 将观察函数添加到观察者里
person.name = "王五";  // 变更数据后,等待观察者观察到数据的变化
// 观察结果: 王五已经25岁了

根据上述观察者并结合Proxy模块的数据校验应用,可以动态的组装数据校验条件