美文网首页
ECMAScript6 核心特性(二)

ECMAScript6 核心特性(二)

作者: 摘叶先生 | 来源:发表于2020-10-23 17:36 被阅读0次

    六、数组的扩展

    1、Array.from()

    将伪数组对象或可遍历对象转换为真数组。典型的伪数组有函数的arguments对象,以及大多数DOM元素集,还有字符串,也包括 ES6新增的数据结构 Set 和 Map。

    let arrayLike = {
        '0': 'a',
        '1': 'b',
        '2': 'c',
        length: 3
    };
    
    // ES5的写法
    var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
    
    // ES6的写法
    let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
    

    DOM 操作返回的NodeList 集合:

    ...
    <button>测试1</button>
    <br>
    <button>测试2</button>
    <br>
    <button>测试3</button>
    <br>
    <script type="text/javascript">
    let btns = document.getElementsByTagName("button")
    console.log("btns",btns);//得到一个伪数组
    btns.forEach(item=>console.log(item)) 
    //Uncaught TypeError: btns.forEach is not a function
    
    Array.from(btns).forEach(item=>console.log(item))将伪数组转换为数组
    </script>
    

    arguments对象:

    function foo() {
      var args = Array.from(arguments);
      // ...
    }
    

    只要是部署了 可遍历(Iterator) 接口的数据结构,Array.from都能将其转为数组。

    Array.from('hello')
    // ['h', 'e', 'l', 'l', 'o']
    
    let namesSet = new Set(['a', 'b'])
    Array.from(namesSet) // ['a', 'b']
    

    2、Array.of()

    将一系列值转换为数组。

    当调用 new Array( )构造器时,根据传入参数的类型与数量的不同,实际上会导致一些不同的结果, 例如:

    let items = new Array(2) ;
    console.log(items.length) ; // 2
    console.log(items[0]) ; // undefined
    console.log(items[1]) ; // undefined
    
    let items = new Array(1, 2) ;
    console.log(items.length) ; // 2
    console.log(items[0]) ; // 1
    console.log(items[1]) ; // 2
    

    当使用单个数值参数来调用 Array 构造器时,数组的长度属性会被设置为该参数。 如果使用多个参数(无论是否为数值类型)来调用,这些参数也会成为目标数组的项。数组的这种行为既混乱又有风险,因为有时可能不会留意所传参数的类型。
    ES6中 Array.of()解决了这个问题。该方法总会创建一个包含所有传入参数的数组,而不管参数的数量与类型:

    let items = Array.of(1, 2);
    console.log(items.length); // 2
    console.log(items[0]); // 1
    console.log(items[1]); // 2
    
    items = Array.of(2);
    console.log(items.length); // 1
    console.log(items[0]); // 2
    

    Array.of基本上可以用来替代Array()或newArray(),并且不存在由于参数不同而导致的重载,而且他们的行为非常统一。

    Array.of方法可以用下面的代码模拟实现。

    function ArrayOf(){
      return [].slice.call(arguments);
    }
    

    3、数组实例的 find() 和 findIndex()

    数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。

    [2, 4, -6, 8].find((n) => n < 0) // -6
    

    数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

    [2, 4, -6, 8].findIndex((value, index, arr)=> {
      return value < 0;
    }) // 2
    

    4、数组实例的 entries(),keys() 和 values()

    ES6 提供entries(),keys()和values(),用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

    for (let index of ['a', 'b'].keys()) {
      console.log(index);
    }
    // 0
    // 1
    
    for (let elem of ['a', 'b'].values()) {
      console.log(elem);
    }
    // 'a'
    // 'b'
    
    for (let [index, elem] of ['a', 'b'].entries()) {
      console.log(index, elem);
    }
    // 0 "a"
    // 1 "b"
    

    5、数组实例的 includes()

    Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值。该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

    [1, 2, 3].includes(2)   // true
    [1, 2, 3].includes(3, -1); // true
    [1, 2, 3, 5, 1].includes(1, 2); // true
    

    没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。indexOf方法有两个缺点:
    一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。
    二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。

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

    下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。

    const contains = (() =>
      Array.prototype.includes
        ? (arr, value) => arr.includes(value)
        : (arr, value) => arr.some(el => el === value)
    )();
    contains(['foo', 'bar'], 'baz'); // => false
    

    6、数组实例的 flat(),flatMap()

    数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

    [1, 2, [3, 4]].flat()
    // [1, 2, 3, 4]
    

    flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为1。如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

    [1, 2, [3, [4, 5]]].flat()
    // [1, 2, 3, [4, 5]]
    
    [1, 2, [3, [4, 5]]].flat(2)
    // [1, 2, 3, 4, 5]
    
    [1, [2, [3]]].flat(Infinity)
    // [1, 2, 3]
    

    如果原数组有空位,flat()方法会跳过空位。

    [1, 2, , 4, 5].flat()
    // [1, 2, 4, 5]
    

    flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

    // 相当于 [[2, 4], [3, 6], [4, 8]].flat()
    [2, 3, 4].flatMap((x) => [x, x * 2])
    // [2, 4, 3, 6, 4, 8]
    

    flatMap()只能展开一层数组。

    // 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
    [1, 2, 3, 4].flatMap(x => [[x * 2]])
    // [[2], [4], [6], [8]]
    

    七、箭头函数

    ES6 允许使用“箭头”(=>)定义函数。它主要有两个作用:缩减代码和改变this指向,接下来我们详细介绍:
    1、缩减代码

    const double1 = function(number){
       return number * 2;   //ES5写法
    }
    const double2 = (number) => {
     return number * 2;    //ES6写法
    }
    const double3 = number => number * 2; //可以进一步简化
    

    多个参数必须加括号

    const double4 = (number,number2) => number + number2;
    

    如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

     const double5 = (number,number2) => {
       sum = number + number2 
       return sum;
     }
    

    由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

    // 报错
    let getTempItem = id => { id: id, name: "Temp" };
    // 不报
    let getTempItem = id => ({ id: id, name: "Temp" });
    

    此外还有个好处就是简化回调函数

    // 正常函数写法
    [1,2,3].map(function (x) {
      return x * x;
    });
    // 箭头函数写法
    [1,2,3].map(x => x * x);//[1, 4, 9]
    

    2、改变this指向
    JavaScript 语言的this对象一直是一个令人头痛的问题,在对象方法中使用this,必须非常小心。箭头函数”绑定”this,很大程度上解决了这个困扰。先看一个例子:

    const team = {
      members:["Henry","Elyse"],
      teamName:"es6",
      teamSummary:function(){
        return this.members.map(function(member){
          return `${member}隶属于${this.teamName}小组`;    // this不知道该指向谁了
        })
      }
    }
    console.log(team.teamSummary());//["Henry隶属于undefined小组", "Elyse隶属于undefined小组"]
    

    teamSummary函数里面又嵌了个函数,这导致内部的this的指向发生了错乱。
    做一下修改:
    方法一、 let self = this

    const team = {
      members:["Henry","Elyse"],
      teamName:"es6",
      teamSummary:function(){
        let self = this;
        return this.members.map(function(member){
          return `${member}隶属于${self.teamName}小组`;
        })
      }
    }
    console.log(team.teamSummary());//["Henry隶属于es6小组", "Elyse隶属于es6小组"]
    

    方法二、bind函数

    const team = {
      members:["Henry","Elyse"],
      teamName:"es6",
      teamSummary:function(){
        return this.members.map(function(member){
          // this不知道该指向谁了
          return `${member}隶属于${this.teamName}小组`;
        }.bind(this))
      }
    }
    console.log(team.teamSummary());//["Henry隶属于es6小组", "Elyse隶属于es6小组"]
    

    方法三、 箭头函数

    const team = {
      members:["Henry","Elyse"],
      teamName:"es6",
      teamSummary:function(){
        return this.members.map((member) => {
          // this指向的就是team对象
          return `${member}隶属于${this.teamName}小组`;
        })
      }
    }
    console.log(team.teamSummary());//["Henry隶属于es6小组", "Elyse隶属于es6小组"]
    

    3、使用注意点
    (1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
    (2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
    (3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
    (4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

    八、Proxy、Reflect、Set和Map、Symbol

    Proxy

    Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

    Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

    var obj = new Proxy({}, {
      get: function (target, propKey, receiver) {
        console.log(`getting ${propKey}!`);
        return Reflect.get(target, propKey, receiver);
      },
      set: function (target, propKey, value, receiver) {
        console.log(`setting ${propKey}!`);
        return Reflect.set(target, propKey, value, receiver);
      }
    });
    

    ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。如下面这种形式,不同的只是handler参数的写法。target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

    var proxy = new Proxy(target, handler);
    

    handler共有十三种劫持方式,比如deleteProperty就是用于劫持域删除。
    deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

    var handler = {
      deleteProperty (target, key) {
        invariant(key, 'delete');
        delete target[key];
        return true;
      }
    };
    function invariant (key, action) {
      if (key[0] === '_') {
        throw new Error(`Invalid attempt to ${action} private "${key}" property`);
      }
    }
    
    var target = { _prop: 'foo' };
    var proxy = new Proxy(target, handler);
    delete proxy._prop
    // Error: Invalid attempt to delete private "_prop" property
    

    Reflect

    为操作对象而提供的新API。设计目的有以下几个:
    (1)将Object对象的属于语言内部的方法放到Reflect对象上,即从Reflect对象上拿Object对象内部方法。
    (2)将用 老Object方法 报错的情况,改为返回false

    // 老写法
    try {
      Object.defineProperty(target, property, attributes);
      // success
    } catch (e) {
      // failure
    }
    
    // 新写法
    if (Reflect.defineProperty(target, property, attributes)) {
      // success
    } else {
      // failure
    }
    

    (3)让Object操作都变成函数行为

    // 老写法
    'assign' in Object // true
    
    // 新写法
    Reflect.has(Object, 'assign') // true
    

    (4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。

    Proxy(target, {
      set: function(target, name, value, receiver) {
        var success = Reflect.set(target, name, value, receiver);
        if (success) {
          console.log('property ' + name + ' on ' + target + ' set to ' + value);
        }
        return success;
      }
    });
    

    上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。

    Set和Map

    1、Set

    ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
    Set本身是一个构造函数,用来生成 Set 数据结构。

    const s = new Set();
    
    [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
    
    for (let i of s) {
      console.log(i);
    }
    // 2 3 5 4
    

    Set函数可以接受一个数组,来达到去重的目的。

    const set = new Set([1, 2, 3, 4, 4]);
    [...set]// [1, 2, 3, 4]
    

    也可去除字符串里面的重复字符。

    [...new Set('ababbc')].join('')
    

    Set 结构的实例有两个属性:
    (1)Set.prototype.constructor:构造函数,默认就是Set函数。
    (2)Set.prototype.size:返回Set实例的成员总数。
    Set 实例的操作方法:
    (1)Set.prototype.add(value):添加某个值,返回 Set 结构本身。
    (2)Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
    (3)Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
    (4)Set.prototype.clear():清除所有成员,没有返回值。

    s.add(1).add(2).add(2);
    // 注意2被加入了两次
    
    s.size // 2
    
    s.has(1) // true
    s.has(2) // true
    s.has(3) // false
    
    s.delete(2);
    s.has(2) // false
    

    Array.from方法可以将 Set 结构转为数组.

    const items = new Set([1, 2, 3, 4, 5]);
    const array = Array.from(items);
    

    去除数组重复的另一种方法。

    function dedupe(array) {
      return Array.from(new Set(array));
    }
    
    dedupe([1, 1, 2, 3]) // [1, 2, 3]
    

    Set 结构的实例有四个遍历方法:
    (1)Set.prototype.keys():返回键名的遍历器
    (2)Set.prototype.values():返回键值的遍历器
    (3)Set.prototype.entries():返回键值对的遍历器
    (4)Set.prototype.forEach():使用回调函数遍历每个成员

    let set = new Set(['red', 'green', 'blue']);
    
    for (let item of set.keys()) {
      console.log(item);
    }
    // red
    // green
    // blue
    
    for (let item of set.values()) {
      console.log(item);
    }
    // red
    // green
    // blue
    
    for (let item of set.entries()) {
      console.log(item);
    }
    // ["red", "red"]
    // ["green", "green"]
    // ["blue", "blue"]
    
    let set = new Set([1, 4, 9]);
    set.forEach((value, key) => console.log(key + ' : ' + value))
    // 1 : 1
    // 4 : 4
    // 9 : 9
    
    2、Map

    ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

    const m = new Map();
    const o = {p: 'Hello World'};
    
    m.set(o, 'content')
    m.get(o) // "content"
    
    m.has(o) // true
    m.delete(o) // true
    m.has(o) // false
    

    上面的例子展示了如何向 Map 添加成员。作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。

    const map = new Map([
      ['name', '张三'],
      ['title', 'Author']
    ]);
    
    map.size // 2
    map.has('name') // true
    map.get('name') // "张三"
    map.has('title') // true
    map.get('title') // "Author"
    

    Symbol

    ES6之前,Javascript的六种类型分别是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)
    ES6新加了第七种类型:Symbol,代表独一无二的值,以保证不会与其他属性名产生冲突。

    let s = Symbol();
    
    typeof s
    // "symbol"
    

    注意:Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

    Symbol函数可以接受一个字符串作为参数

    let s1 = Symbol('foo');
    let s2 = Symbol('bar');
    
    s1 // Symbol(foo)
    s2 // Symbol(bar)
    
    s1.toString() // "Symbol(foo)"
    s2.toString() // "Symbol(bar)"
    

    如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。

    const obj = {
      toString() {
        return 'abc';
      }
    };
    const sym = Symbol(obj);
    sym // Symbol(abc)
    

    注意:Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。

    // 没有参数的情况
    let s1 = Symbol();
    let s2 = Symbol();
    
    s1 === s2 // false
    
    // 有参数的情况
    let s1 = Symbol('foo');
    let s2 = Symbol('foo');
    
    s1 === s2 // false
    

    作为属性名的Symbol

    const mySymbol = Symbol();
    const a = {};
    
    a.mySymbol = 'Hello!';
    a[mySymbol] // undefined
    a['mySymbol'] // "Hello!"
    

    九、类与继承

    从概念上讲,在 ES6 之前的 JS 中并没有和其他面向对象语言那样的“类”的概念。长时间里,人们把使用 new 关键字通过函数(也叫构造器)构造对象当做“类”来使用。由于 JS 不支持原生的类,而只是通过原型来模拟,各种模拟类的方式相对于传统的面向对象方式来说非常混乱,尤其是处理当子类继承父类、子类要调用父类的方法等等需求时。

    ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。但是类只是基于原型的面向对象模式的语法糖。
    传统构造函数实现类:

    //传统构造函数
    function MathHandle(x,y){
      this.x=x;
      this.y=y;
    }
    MathHandle.prototype.add =function(){
      return this.x+this.y;
    };
    var m=new MathHandle(1,2);
    console.log(m.add())
    

    ES6 中Class实现类:

    //class语法
    class MathHandle {
     constructor(x,y){
      this.x=x;
      this.y=y;
    }
     add(){
       return this.x+this.y;
      }
    }
    const m=new MathHandle(1,2);
    console.log(m.add())
    

    两者看似不同,其实本质是一样的,只不过是语法糖写法上有区别。所谓语法糖是指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。比如这里class语法糖让程序更加简洁,有更高的可读性。

    typeof MathHandle //"function"
    MathHandle===MathHandle.prototype.constructor //true
    

    传统构造函数实现继承:

    //传统构造函数继承
    function Animal() {
        this.eat = function () {
            alert('Animal eat')
        }
    }
    function Dog() {
        this.bark = function () {
            alert('Dog bark')
        }
    }
    Dog.prototype = new Animal()// 绑定原型,实现继承
    var hashiqi = new Dog()
    hashiqi.bark()//Dog bark
    hashiqi.eat()//Animal eat
    

    ES6 中Class实现继承:

    //ES6继承
    class Animal {
        constructor(name) {
            this.name = name
        }
        eat() {
            alert(this.name + ' eat')
        }
    }
    class Dog extends Animal {
        constructor(name) {
            super(name) // 有extend就必须要有super,它代表父类的构造函数,即Animal中的constructor
            this.name = name
        }
        say() {
            alert(this.name + ' say')
        }
    }
    const dog = new Dog('哈士奇')
    dog.say()//哈士奇 say
    dog.eat()//哈士奇 eat
    

    Class之间可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。
    Class 和传统构造函数有何区别
    1)Class 在语法上更加贴合面向对象的写法
    2)Class 实现继承更加易读、易理解,对初学者更加友好
    3)本质还是语法糖,使用prototype

    十、Iterator 和 for...of 循环

    JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就需要一种统一的接口机制,来处理所有不同的数据结构。遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。
    任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

    1、Interator的作用:

    1)为各种数据结构,提供一个统一的、简便的访问接口;
    2)使得数据结构的成员能够按某种次序排列。
    3)ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。

    2、原生具备iterator接口的数据(可用for of遍历)

    Array
    set容器
    map容器
    String
    函数的 arguments 对象
    NodeList 对象

    //Array
    let arr3 = [1, 2, 'kobe', true];
    for(let i of arr3){
       console.log(i); // 1 2 kobe true
    }
    
    //String
    let str = 'abcd';
    for(let item of str){
       console.log(item); // a b c d
    } 
    
    var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
    for (var e of engines) {
      console.log(e);// Gecko Trident  Webkit
    }
    

    3、比较几种遍历方式

    1)for of 循环不仅支持数组、大多数伪数组对象,也支持字符串遍历,此外还支持 Map 和 Set 对象遍历。
    2)for in循环可以遍历字符串、对象、数组,不能遍历Set/Map。
    3)forEach 循环不能遍历字符串、对象,可以遍历Set/Map。

    参考文章
    ECMAScript 6 入门
    ES6 核心特性

    相关文章

      网友评论

          本文标题:ECMAScript6 核心特性(二)

          本文链接:https://www.haomeiwen.com/subject/udoumktx.html