ES6理解进阶【大前端高薪训练营】

Stella981
• 阅读 562

学习资料:拉勾课程《大前端高薪训练营》
阅读建议:文章较长,搭配文章的侧边栏目录进行食用,体验会更佳哦!
内容说明:本文不做知识点的搬运工,文中只记录个人对该技术点的认识和理解以及该技术在日常开发中的使用场景

一:面向对象:类class

面向对象三大特性之封装

封装是面向对象的重要原则,它在代码中的体现主要是以下两点:

  • 封装整体:把对象的属性和行为封装为一个整体,其中内部成员可以分为静态成员(也叫类成员)和实例成员,成员之间又可细分为属性和方法。
  • 访问控制:外部对对象内部属性和行为的访问权限,简单来分时就是私有和公有两种权限。

以下是基本封装示例:

class Animal{
   
   
    constructor(name) {
   
   
        this.name = name;// 实例属性
    }
    
    cry() {
   
   // 实例方法
        console.log('cry');
    }
    
    static getNum(){
   
   // 静态方法
        return AnimalES6.num
    }
}

Animal.num = 42;// 静态属性
面向对象三大特性之继承

继承是面向对象最显著的一个特性,它在代码中的体现主要是以下两点:

  • 子类对象具有父类对象的属性和行为
  • 子类对象可以有它自己的属性和行为

以下是定义一个Cat类并对上述Animal类的继承示例:

class Cat extends Animal{
   
   
    constructor(name, type) {
   
   
        super(name);// 必须先构造父类空间
        this.type = type;
    }
    
    cry() {
   
   
        console.log('miao miao');// 方法重写
    }
}
面向对象三大特性之多态

多态指允许不同的对象对同一消息做出不同响应,在Java中,实现多态有以下三个条件:

  • 继承
  • 重写
  • 父类引用指向子类对象

由于JavaScript是弱类型语言,所以JavaScript实现多态,不存在父类引用指向子类对象的问题。

以下再定义一个Dog类,实现Animal实例对象、Cat实例对象和Dog实例对象对同样的cry调用做出不同的响应示例:

class Dog extends Animal{
   
   
    constructor(name, type) {
   
   
        super(name);
        this.type = type;
    }
    
    cry() {
   
   
        console.log('wang wang');
    }
}

const ani = new Animal('不知名动物');
const cat = new Cat('小白', '美短');
const dog= new Dog('大黑', '二哈');
ani.cry();// 输出 cry
cat.cry();// 输出 miao miao
dog.cry();// 输出 wang wang

二:数据类型Symbol

Symbol是一种新的原始数据类型,用来表示独一无二的值。此外,它也是对象属性名的第二种数据类型(另一种是字符串)。

接下来列举几个在日常开发中可能会用到Symbol数据类型的场景:

1):消除魔法字符串

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。 —阮一峰

如下含有魔法字符串的代码示例:

const obj = {
   
   type: 'type2'};
function fn1() {
   
   
    if (obj.type === 'type1') {
   
   
        // xxx
    } else if (obj.type ==='type2') {
   
   
        // xxx
    }
}
function fn2() {
   
   
    if (obj.type === 'type1') {
   
   
        // xxx
    } else if (obj.type ==='type2') {
   
   
        // xxx
    }
}
// ...其它对obj.type的判断

在上述代码中,大量出现的type1与type2字符串就是魔法字符串。我们分析这样大量使用魔法字符串可能会出现的问题:

  • 添加逻辑时,我们每次判断obj的类型都需要输入该魔法字符串,这时不但没有输入提示需要一个一个字符输入,而且一旦字符少输、多输或者输入错误,都会导致代码运行错误。
  • 修改逻辑时,如果type1变成了type3,那么就需要把代码里所有的type1找到并替换成type3。

接下来使用Symbol对上述代码改造:

const obj = {
   
   type: 'type2'};
const objType = {
   
   
  type1: Symbol(),
  type2: Symbol(),
}
function fn1() {
   
   
    if (obj.type === objType.type1) {
   
   
        // xxx
    } else if (obj.type === objType.type2) {
   
   
        // xxx
    }
}
function fn2() {
   
   
    if (obj.type === objType.type1) {
   
   
        // xxx
    } else if (obj.type === objType.type2) {
   
   
        // xxx
    }
}

2):实现对象的保护成员 / 私有成员

假设我们对一个对象需要做如下的访问控制:

  • attr1和attr2公有成员:外部可以访问
  • attr3和attr4保护成员:外部受限访问,需要引入键attr3和attr4才能访问
  • attr5和attr6私有成员:外部不能访问,仅支持当前模块文件内部访问

以下是没有实现访问控制的代码:

// index.js
export const Obj = {
   
   
  attr1: 'public Attr1',// 公有
  attr2: 'public Attr2',// 公有
  attr3: 'protect Attr3',// 保护
  attr4: 'protect Attr4',// 保护
  attr5: 'private Attr5',// 私有
  attr6: 'private Attr6',// 私有
}

接下来使用Symbol对上述代码改造:

// protectKey.js
export const attr3 = Symbol('attr3');
export const attr4 = Symbol('attr4');

// index.js
import {
   
    attr3, attr4 } from './protect.js';

const attr5 = Symbol('attr5');
const attr6 = Symbol('attr6');

export const Obj = {
   
   
  attr1: 'public Attr1',// 公有
  attr2: 'public Attr2',// 公有
  [attr3]: 'protect Attr3',// 保护
  [attr4]: 'protect Attr4',// 保护
  [attr5]: 'private Attr5',// 私有
  [attr6]: 'private Attr6',// 私有
}

如上代码就实现了对我们所需要的访问控制,外部对不能访问的成员是无法感知的,因为外部对这些不能访问的成员不但不支持读写,甚至也不能遍历和序列号操作。

在我们以往的日常开发中,我们基本上对对象的访问控制都是设置为公有的,很少设置为私有,设置为保护的就更是没见过。但少归少,至少说明了ES6引入的Symbol能帮助我们实现类似Java中保护和私有成员的访问控制

3):实现类的保护成员、私有成员

如下示例,封装一个集合Collection,它对模块外部具有私有属性size与私有方法logAdd:

const size = Symbol('size');
const logAdd = Symbol('logAdd');

export class Collection {
   
   
  constructor() {
   
   
    this[size] = 0;// 私有属性
  }

  add(item) {
   
   
    this[this[size]] = item;
    this[size]++;
    this[logAdd]();
  }

  [logAdd]() {
   
   // 私有方法
    console.log( 'now size' + this[size])
  }
}

三:数据结构Set

Set对于JavaScript而言是一种新的数据结构,相对于数组用于存储有序、可重复的元素集合,Set用于存储有序、不可重复的元素集合。

接下来列举几个在日常开发中可能会用到Set数据结构的场景:

1):数组去重、字符串去重等任何可迭代类型的去重

// 数组去重
let arr = [1,1,2,3];
arr = Array.from(new Set());// 经过性能比较测试,表现优秀
// arr = [1,2,3]

// 字符串去重
let str = 'aaabbsf';
let newStr = '';
new Set(str).forEach(item) => {
   
   newStr += item});
// newStr absf

2):集合间操作:交集、并集、差集

下面截取阮一峰ES6对Set的说明案例:

let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

四:数据结构Map

Map对于JavaScript而言也是一种新的数据结构,用于存储键值对形式的字典 / 双列集合。在Map对象出现之前,我们通常使用Object对象来做键值对的存储,下面对比一下Map对象实现键值对存储与普通对象存储键值对的区别:

  • 功能角度:Object对象只能使用字符串或者Symbol类型作为键,而Map对象可以使用任何数据类型作为键。Map对象使用引用类型作为键时,以内存地址是否一致来作为判断两个键是否相同的标准
  • 构造与读写角度:Object对象字面量构造并存储键值对的方式比Map方便,其读写操作也比Map需要调用get、set方法而言性能更好(性能分析工具初步对比分析)。
  • 常用Api角度:Object对象的原型为Object.protoype,而Map对象的原型为Map.prototype,两者对常用的键值对操作都有相应的api可以调用,不过Map原型上定义的Api更加纯粹一些。
  • 序列化角度:Object对象存储键值时支持序列化,而Map对象不支持。

经过上面的对比分析可以得出结论,不到必须使用引用类型作为键的情况下,我们都用Object对象字面量的方式来定义并存储键值对会更好一些。

接下来叙述在日常开发中可能会用到Map数据结构的场景:

1):实现对象之间的一对一、一对多、多对多(桥Map方式)的关系

经验尚浅,日常开发示例暂时没想到,有机会补上。但是Map结构的出现告诉了我们这些JavaScript开发者,此后在JavaScript中我们也可以很简单的实现对象之间的映射关系

五:迭代器Iterator和for of

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。而for…of循环是ES6 创造出的一种新的遍历命令,它可以配合迭代器使用,只要实现了Iterator接口的任意对象就可以使用for…of循环遍历。

在JavaScript常见的数据结构如Array、Set、Map、伪数组arguments等等一系列对象的原型上都有Symbol.iterator标识,并且有默认的Iterator实现。普通对象是没有这个接口标识以及iterator的实现的,但是我们可以手动为普通对象添加这个标识以及对应的iterator实现,示例代码如下:

// test1.js:封装者封装
const todos = {
   
   
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  work: ['喝茶'],

  // 添加Symbol.iterator标识接口以及iterator实现
  [Symbol.iterator]: function () {
   
   
    const all = [...this.life, ...this.learn, ...this.work]
    let index = 0
    return {
   
   
      next: function () {
   
   
        return {
   
   
          value: all[index],
          done: index++ >= all.length
        }
      }
    }
  }
}

// test2.js:调用者遍历
for (const item of todos) {
   
   
  console.log(item)
}

上述代码的优点是封装者在对外界遍历没有影响的情况下,对数据进行了更细粒度的管理。是一种解耦合的代码优化操作!

六:promise、generator和Async

这三者都与异步编程有关,之后会单独拎出来写在另一篇博客当中,在此文中就不做赘述了。

七:模板字符串和标签函数

模板字符串就不做介绍了,标签函数在定义时和普通函数没什么区别。区别在调用上,标签函数以模板字符串作为参数输入,并且有独特的规则完成形实参匹配。接下来看一个简单的例子:

// 标签函数定义
const fn = (literals, ...values) => {
   
   
  console.log('字面量数组', literals);
  console.log('变量数组', values);
  console.log('字面量数组是否比变量数组多一个元素', literals.length -1 === values.length);// true
  let output = "";
  let index; // 不能放在for里,因为index在块级作用域之外还有访问
  for (index = 0; index < values.length; index++) {
   
   
    output += literals[index] + values[index];
  }
  output += literals[index]
  return output;
};

// 标签函数调用
const name = '张三';
const age = 18;
const result = fn`姓名:${
     
      name },年龄:${
     
      age }`;

上述示例运行结果:
ES6理解进阶【大前端高薪训练营】
经过上述例子我们可以大概得知标签函数的形实参匹配规则:

  • 模板中字面量数组的形实参匹配:模板字符串以类似/${[^}]+}/g 的正则规则进行split 得到其内所有字面量组成的数组,而后作为实参匹配标签函数的第一个形参literals
  • 模板中所有变量的形实参匹配:模板字符串以 /${[^}]+}/g 的正则规则进行match找到所有的JS变量数组,解析得到其值后,按顺序作为实参匹配标签函数剩下的形参,上例代码中用rest剩余参数作为形参接收所有实参。

通过上面的例子和解析,我们认识了标签函数调用的执行规则。根据标签函数和模板字符串的配合机制,我们很容易就想到这种机制可以实现模板引擎甚至是定义内部语言的功能

接下来叙述在日常开发中我们可能会用到标签函数的场景:

1):把可能作为innerHtml的string中的特殊字符转义,使它不被解析为HTML标签

在日常开发中,我们很可能会碰到这么一个需求:

  • 一个input输入框接收用户的输入
  • 另一个p标签用来展示这个用户的输入

先分析一下这样做的风险:由于用户的输入直接作为了p标签的内容,当用户输入一个