熟悉ES6,这些内容起码得懂个七八十吧

摸鱼酱的文章声明:内容保证原创,纯技术干货分享交流,不打广告不吹牛逼。javascript

前言:前端

记得我第一次接触ES6,仍是在大学写JavaEE的时候。当时因为须要作个后端管理系统,因此临时找了一些培训视频资源学了一下前端基础和vue框架。经过那个视频资料,我学会了一些简单的ES6知识,好比用let和const声明变量,解构赋值、模板字符串、可变参数等等。vue

以后因为一些故事性的情节,春招临时转行前端,简历上实在没啥前端技能可写。仗着会那么一点点ES6,我居然在技能一栏,厚颜无耻的写上了熟练运用ES6(…(⊙_⊙;)…)。结果可想而知,个人前端面途坎坷,啪啪不断(巴掌与脸亲密接触所发出的声音)。不过幸运的是,个人脸不但没有被打烂,反而越打脸皮越厚。java

好的,下面进入正文,对于这篇文章所探讨的全部ES6知识,我预先用脑图作了如下整理:node

对于接下来的行文,我都会围绕这副脑图展开。若是您有兴趣继续往下看下去,我但愿您能在这幅图上停留多一些时间。编程

因为文章内容较长,而且探讨的技术点之间基本互不依赖,因此我建议您能够挑选感兴趣的部分来看。后端

好的,按照上述脑图中的逻辑,接下来我会分红如下几个部分来展开探讨本文。api

  • 基本语法
  • 数据类型
  • 数据结构
  • 面向对象
  • 内置对象

对于缺失的异步编程部分,以后会单独总结成一篇文章。数组

在理清楚行文思路以后,下面咱们就进入第一部分,探讨两个ES6新添加的基本语法。浏览器

一:基本语法

ES6提供了不少的基本语法,这其中包括了用于声明变量的let、const,与形参相关的默认参数、可变参数,以及一些解构赋值等等基本语法。在本文中,我就再也不花费大量笔墨去探讨这些路人皆知的ES6基础中的基础知识。下面咱们主要介绍两个ES6基本语法,即:

  • 迭代器与for...of
  • 模板字符串与标签函数

OK,下面我就进入迭代器与for...of语法的探讨。

1.迭代器与for...of

遍历器的概念我就不瞎掰了,下面是我从阮一峰ES6文档中搬过来的概念。迭代器(Iterator)就是这样一种机制。它是一种接口,为各类不一样的数据结构提供统一的访问机制

对于for...of循环,它则是ES6 创造出的一种新的用于遍历序列结构的语法。它能够配合迭代器使用,只要实现了Iterator接口,任意对象均可以使用for...of循环遍历

值得一提的是,JavaScript常见的数据结构如Array、Set、Map、伪数组arguments等等,在它们的原型上都有Symbol.iterator标识,而且有默认的Iterator实现,因此它们的实例对象均可以使用for of语法遍历。

然而,普通对象是没有这个接口标识以及iterator的实现的,可是咱们能够手动为普通对象添加这个标识以及对应的iterator实现,让它支持for...of循环遍历,下面咱们作个demo,以便阁下更好地理解。

假设咱们对一个序列结构数据变量有如下几个需求:

  • 可以做为一个对象来管理(能够把对数据的操做也封装进来)
  • 可以像有序结构同样直接遍历
  • 更给力的是,做为对象管理后可以对数据进行更细粒度的管理

下面这个代码段,就是可以同时知足以上三个需求的示例:

舒适提示:一般需求下,咱们不会把对象做为一种序列结构来遍历,因此这个示例能够学习但请不要滥用。

// 1.为对象添加Symbol.iterator属性
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
        }
      }
    }
  }
  // others object method
}

// 2.用for...of遍历对象
for (const item of todos) {
  console.log(item)
}
复制代码

这个示例很经典,建议阁下可以从需求到实现,多品悟几下。

OK,下面咱们进入下一部分,模板字符串与标签函数的探讨。

2.模板字符串与标签函数

模板字符串你们都很熟悉,可是标签函数可能知道的人比较少,下面我先对标签函数作个认知分享。

1)理解标签函数

标签函数在定义时和普通函数没有区别。区别在函数的调用上,主要体如今如下两点:

  • 标签函数以模板字符串做为参数输入
  • 标签函数具有独特的形实参匹配规则

在有了以上对标签函数的基本认识以后,下面咱们作个简单demo,以便帮助阁下加深理解。

这里我把demo演示和探讨分为如下步骤:

  • 定义标签函数
  • 使用标签函数
  • 运行结果
  • 回顾总结

好的,理清思路后,下面进入demo。

step1:定义标签函数:

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;
};
复制代码

step2:使用标签函数:

const name = '张三';
const age = 18;
const result = fn`姓名:${ name },年龄:${ age }`;
复制代码

step3:运行结果

step4:回顾总结

前面说到,我认为标签函数与普通函数在调用上的区别分为两点,即标签函数以模板字符串做为输入,标签函数有独特的形实参匹配规则。第一点没什么好说的,下面咱们以示例做为依据,从标签函数的两个形参literals与...values来总结一下标签函数的形实参匹配规则。

下面这个规则纯属我的思考总结,没有官方依据,阁下只要理解了形参literals与...values的含义便可。

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

通过上面的探讨,我相信阁下就已经理解并掌握了标签函数的基本使用。那么标签函数具体有什么应用场景呢?下面咱们就探讨几个应用场景,以求加深阁下对标签函数的实践理解。

下面我会分红如下几个场景来展开探讨:

  • 模板字符串的转义
  • 模板字符串的国际化(i18n)
  • 其它场景

2)应用场景:模板字符串的转义

在平常开发中,咱们极可能会碰到这么一个需求:

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

咱们极可能的作法是直接把用户的输入做为p标签的内容,可是这样会有不少的潜在错误和风险,下面咱们分析一下。

潜在错误和风险:因为用户的输入直接做为了p标签的内容,因此当用户输入一个<script>标签等任意HTML标签时,咱们直接把它交给p标签。在渲染过程当中,浏览器会把它当成inneHTML进行解析并执行其中的脚本或者将字符串以HTML标签形式渲染,这确定是不被指望且有风险的。因此咱们在把用户的输入交给p标签展现以前,应该对其中的一些特殊字符进行转义,防止被浏览器解析为标签执行,出现错误或者风险。

接下来咱们演示用标签函数解决这个问题的示例,示例以下:

  • 定义标签函数
function SaferHTML(templateData) {// 这里使用隐式参数arguments来访问模板字符串中的全部变量
  let s = templateData[0];
  for (let i = 1; i < arguments.length; i++) {
    let arg = String(arguments[i]);

    s += arg.replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");
    s += templateData[i];
  }
  return s;
}
复制代码
  • 调用标签函数
let sender = '<script>alert("abc")</script>'; 		// 1.得到用户输入
const safeSender = SaferHTML`${sender}`;		// 2.转义用户输入
document.genElementByid("p").innerHtml = safeSender	// 3.使用转义后的用户输入
复制代码

如此作法以后,即可以解决咱们的问题,杜绝由用户直接输入代码而致使的潜在错误和风险。

好的,下面咱们就进入另一个应用场景,模板字符串的国际化。

3)应用场景:模板字符串的国际化(i18n)

在咱们的项目中支持国际化(i18n)的逻辑自己很是简单,只须要界面中的全部字符串变量化,然后这些变量自动根据项目的当前语音渲染出该语言下的字符串便可。咱们使用函数式编程的思想来分析,能够获得如下结果:

  • 输入:须要翻译的字符串键
  • 映射关系:根据输入得到输出,具体映射逻辑与当前语言与语言包有关
  • 输出:翻译后的字符串

下面咱们就经过模板字符串和标签函数,实现一个简单的i18n标签函数,用于自动将语言键翻译获得当前语言环境下的语言字符串,步骤以下:

  • 准备语言包:resource.js
export const enUS = {
	'Welcome to': 'Welcome to',
	'you are visitor number': 'you are visitor number'
}
export const zhCN = {
	'Welcome to': '你好',
	'you are visitor number': '你的访问号码'
}
复制代码
  • 编写i18标签函数:i18n.js
export function i18nInit(language, zhCNResource, enUSResource) {
  return (literals, ...values) => {
    let output = "";
    let index;
    let resource;
    switch (language) {    // 根据当前语言得到语言包
      case 'zh-CN':
        resource = zhCNResource;
        break;
      case 'en-US':
        resource = enUSResource;
        break;
    }
    for (index = 0; index < values.length; index++) {
      output += resource[literals[index]] + values[index]; // 把字面量做为键获得语言包中对应的翻译
    }
    output += resource[literals[index]]
    return output;
  }
}
复制代码
  • 使用i18n标签函数:page.js
import { i18nInit } from './i18n.js';
import { enUS, zhCN } from './resource.js';

let currentLanguage = 'zh-CN';
const i18n = i18nInit(currentLanguage, zhCN, enUS );
i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`
复制代码

如此操做以后,便可实现一个咱们本身的i18n标签函数,简单吧。

下面咱们在简单说说其它场景。

4)其它场景

通过上面的探讨,我相信有模板引擎使用经验的人就很容易就发现他们的共性。是的,我认为,在模板字符串与标签函数配合使用以后,就能够实现模板引擎的功能,可用于定义内部语言如jsx。在取得了这个认识以后,咱们就能够看到目标字符串与标签函数其实有不少的应用场景能够开发,同时也是个造轮子利器。

引导至此,其它的我就很少说了。下面进入另外一个部分,ES6新增数据类型的探讨。

二:数据类型

1.Symbol

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

有些对它感到陌生的朋友可能会以为它高大上,可是理解一点,symbol只不过是一种原始数据类型,就和number、string这些同样,没什么大不了的。为了消除你们因为对他陌生而产生的畏惧感,下面咱们会直接进入几个应用场景的探讨,加深对symbol这种原始数据类型的实践理解。

好的,接下来我会按照如下顺序来展开探讨几种应用场景:

  • 消除魔法字符串
  • 实现对象的保护成员 / 私有成员
  • 实现类的保护成员、私有成员

OK,下面依次进入这些场景的探讨分析。

写的好累了,原谅我下面的知识点之间就再也不多情的编写大量的承上启下文字了。

1)消除魔法字符串

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

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

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

const type = 'type2';
fn1(type)
fn2(type)
复制代码

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

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

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

const Type = {
  type1: Symbol(),
  type2: Symbol(),
}

function fn1(type) {
	if (type === Type.type1) {
	    // xxx
	} else if (type === Type.type2) {
		// xxx
	}
}
function fn2(type) {
	if (type === Type.type1) {
	    // xxx
	} else if (type === Type.type2) {
		// xxx
	}
}

const type = Type.type2;
fn1(type)
fn2(type)
复制代码

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

假设咱们对一个对象须要作以下的访问控制:

  • 公有成员attr1:外部能够访问
  • 保护成员attr2:外部受限访问,须要引入键attr2才能访问数据
  • 私有成员attr3:外部不能访问,仅支持当前模块文件内部访问

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

  • export.js:暴露数据的文件
const obj = {
  attr1: 'public Attr1',// 公有
  attr2: 'protect Attr2',// 保护
  attr3: 'private Attr3',// 私有
}

// code ... 模块内任意访问obj的attr一、attr二、attr3属性

export default = obj
复制代码
  • import.js:引入数据的文件
import obj from './export.js'

const attr1 = obj.attr1;	// 外部模块对公有数据直接访问,访问控制成功
const attr2 = obj.attr2;	// 外部模块对保护数据直接访问,访问控制失败
const attr3 = obj.attr3;	// 外部模块对私有数据直接访问,访问控制失败
复制代码

这是没有实现访问控制的代码,没有知足咱们的需求。为了实现访问控制,接下来咱们使用Symbol对上述代码改造。

借由Symbol实现访问控制:

  • protectKey.js:维护受限访问键的常量模块
export const attr2 = Symbol('attr2');
复制代码
  • export.js:暴露数据的模块
import { attr2 } from './protectKey.js';

const attr3 = Symbol('attr3');

export const = obj {
  attr1: 'public Attr1',// 公有
  [attr2]: 'protect Attr2',// 保护
  [attr3]: 'private Attr3',// 私有,一般也不会暴露私有变量出去,由于没有意义
}

// code ... 模块内任意访问obj的attr一、attr二、attr3属性

export default = obj
复制代码
  • import.js:引入数据的模块
import obj from './export.js';
import { attr2 } from './protectKey.js';

const attr1 = obj.attr1;	// 外部模块对公有数据直接访问,访问控制成功
const attr2 = obj[attr2];	// 外部模块对需额外从protectKey中引入attr属性,访问控制成功
// const attr3 = error!		// 外部模块对拿不到私有数据,访问控制成功
复制代码

如上代码就实现了对咱们所须要的访问控制,相对于使用字符串类型做为键,这里以Symbol类型值做为对象的属性键,可以使得模块外部彻底没法感知到这些不能访问的成员的存在(外部对这些不能访问的成员不但没法读写,并且也不能遍历和序列化)。

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

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

以下示例,咱们封装一个集合类Collection,模块外部只能使用add方法而不能访问内部私有属性arr和私有方法logAdd(ps:实现保护成员的方式和上一点一致,这里就再也不举例了):

  • export.js:暴露一个实现了含有保护成员和私有成员的类
const arr = Symbol('size');
const logAdd = Symbol('logAdd');

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

  [logAdd](item) {	// 私有方法
	console.log( `${item} add success`)
  }

  add(item) {
    this[arr].push(item);	// 模块内能够访问私有属性
    this[logAdd](item);		// 模块内能够访问私有方法
  }
  
}

export default = Collection
复制代码
  • import.js:使用class建立实例并访问属性和方法
import Collection from './export.js'

const col = new Collection()

col.add('zhangsan')	// 外部访问Collection中的公有方法add
// col.arr/logAdd error // 外部没法访问Collection中的私有属性和方法
复制代码

舒适提示: 滥用保护数据和私有数据在大多数状况只会下降业务代码的阅读性哦!

三:数据结构

1.Set

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

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

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

// 数组去重
let arr = [1,1,2,3];
arr = Array.from(new Set(arr));// 通过性能比较测试,表现优秀
// 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}
复制代码

2.Map

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

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

通过上面的对比分析能够得出结论,不到必须使用引用类型做为键的状况下,咱们都用Object对象字面量的方式来定义并存储键值对会更好一些。

对于Map的应用场景,我还真没想到合适的demo。在Java开发时,常常会有实现对象之间的一对1、一对多、多对多(桥Map方式)的关系这种类别的需求,因此我认为至少在nodejs中也会有一些这种应用场景存在,就好比在用nodejs作后端时,须要在多个service之间进行复杂的数据传递。

总的来讲,Map结构的出现告诉了咱们这些JavaScript开发者,此后在JavaScript中咱们也能够很简单的实现对象之间的映射关系

四:面向对象

1.class

JavaScript大量借鉴了Java面向对象的思想和语法,下面咱们就以学习Java面向对象时所要掌握的面向对象三大特性(即封装、继承、多态)为行文思路,展开探讨JavaScript如何优雅的实现面向对象。

1)面向对象三大特性之封装

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

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

如下是基本封装示例:

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

Animal.num = 42;// 静态属性
复制代码

2)面向对象三大特性之继承

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

  • 子类对象具备父类对象的属性和行为
  • 子类对象能够有它本身的属性和行为

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

class Cat extends Animal{
    constructor(name, type) {
        super(name);// 必须先构造父类空间
        this.type = type;
    }
	
	cry() {
		console.log('miao miao');// 方法重写
	}
}
复制代码

3)面向对象三大特性之多态

多态指容许不一样的对象对同一消息作出不一样响应,在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
复制代码

舒适提示:面向对象的灵魂从不在于何种实现语法(如class),而在于面向对象编程这个编程思想自己。

五:内置对象

1.Reflect

Refelect是JavaScript的一个新内置对象(非函数类型对象),与Math对象上挂载了不少用于数学处理方面的方法同样,Refelect对象身上挂在了一套用于操做对象的方法

下表总结列举了Refelect对象上的13个操做对象的静态方法的做用,以及在Reflect出现以前的实现方案:

做用 不用Reflect实现 用Reflect闪现
属性写入 target.propertyKey = value Reflect.set(target, propertyKey, value[, receiver])
属性读取 target.propertyKey Reflect.get(target, propertyKey[, receiver])
属性删除 delete target.propertyKey Reflect.deleteProperty(target, propertyKey)
属性包含 propertyKey in target Reflect.has(target, propertyKey)
属性遍历 Object.keys(target) Reflect.ownKeys(target)
属性描述定义属性 Object.defineProperty(target, propertyKey, attributes) Reflect.defineProperty(target, propertyKey, attributes)
属性描述读取 Object.getOwnPropertyDescriptor(target, propertyKey) Reflect.getOwnPropertyDescriptor(target, propertyKey)
原型读取 target.prototype / Object.getPrototypeOf(target) Reflect.getPrototypeOf(target)
原型写入 target.prototype = prototype / Object.setPrototypeOf(target, prototype) Reflect.setPrototypeOf(target, prototype)
获取对象可扩展标记 Object.isExtensible(target) Reflect.isExtensible(target)
设置对象不可扩展 Object.preventExtensions(target) Reflect.preventExtensions(target)
函数对象调用 target(...argumentsList) / target.apply(this, argumentsList) Reflect.apply(target, thisArgument, argumentsList)
构造函数对象调用 new target(...args) Reflect.construct(target, argumentsList[, newTarget])

由上面刚刚总结出的表格内容能够得知,Reflect在对象层面以及属性层面的Api都有相应的实现,而且比单独的Object原型更加全面。那么咱们在平常开发中如何选择呢,出于代码的运行性能、可读性以及统一操做思想考虑,我的是这么选择的,平常简洁的属性读写、函数对象调用操做不用Reflect,其它都统一使用Reflect对象操做(也就是不用操做符delete、in以及重叠的Object原型上的方法)。

这里有一个故事:我的在业务代码曾经因大量使用Reflect而致使被同事群体批斗o(╥﹏╥)o,以后乖乖所有改回了Object。

总的来讲,在业务代码中,出于兼顾同事技术栈以及不下降业务代码阅读性的须要,仍是忘了Reflect,乖乖用Object吧。固然,其它场景好比造轮子时,我依然推荐使用Reflect。

2.Proxy

Proxy是JavaScript的一个新内置对象(函数类型对象),它的实例对象用于定义对象基本操做的自定义行为(如属性查找、赋值、枚举、函数调用等)。

在上述Reflect的介绍中,咱们发如今平常开发中,咱们能够也常常对对象进行对象层面和属性层面的不少操做,既然是操做,那么咱们就但愿可以具有对这些操做进行切面处理的能力,也即实现代理操做,那么应该怎么作呢?

ES5提供了存取器属性get、set,这让咱们具有了代理一个对象的属性读写操做以进行切面处理的能力。可是这时候对于其它对对象操做行为的代理方案仍然没有官方的实现方案。直到ES6的Proxy出现,咱们才具有了对这些各类类型的对象操做进行代理以进行切面处理的能力(上述Reflect的13个静态方法对应的对象操做所有均可以AOP处理)。

既然Object.defineProperty和Reflect均可以代理对象操做,那么咱们对比一下二者的代理原理和优缺点以备日后甄选方案:

  • 代理原理:Object.defineProperty的原理是经过将数据属性转变为存取器属性的方式实现的属性读写代理。而Proxy则是由于这个内置的Proxy对象内部有一套监听机制,在传入handler对象做为参数构造代理对象后,一旦代理对象的某个操做触发,就会进入handler中对应注册的处理函数,此时咱们就能够 有选择的使用Reflect将操做转发被代理对象上
  • 代理局限性:Object.defineProperty始终仍是局限于属性层面的读写代理,对于对象层面以及属性的其它操做代理它都没法实现。鉴于此,因为数组对象push、pop等方法的存在,它对于数组元素的读写代理实现的并不彻底。而使用Proxy则能够很方便的监视数组操做。
  • 自我代理:Object.defineProperty方式能够代理到自身(代理以后使用对象自己便可),也能够代理到别的对象身上(代理以后须要使用代理对象)。Proxy方式只能代理到Proxy实例对象上。这一点在其它说法中是Proxy对象不须要侵入对象就能够实现代理,实际上Object.defineProperty方式也能够不侵入。

接下来叙述在平常开发中咱们可能会见到或者用到Proxy代理的场景:

1)实现属性读写AOP

const person = {
  name: 'zce',
  age: 20
}

const personProxy = new Proxy(person, {
  get (target, property) {
    return property in target ? target[property] : 'default'
  },

  set (target, property, value) {
    if (property === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError(`${value} is not an int`)
      }
    }
    target[property] = value
  }
})

personProxy.age = 100
personProxy.gender = true
console.log(personProxy.name)
console.log(personProxy.xxx)
复制代码

2)实现数组操做的监视

const list = []

const listProxy = new Proxy(list, {
  set (target, property, value) {
    console.log('set', property, value)
    target[property] = value
    return true // 表示设置成功
  }
})

listProxy.push(100)
listProxy.push(100)
复制代码

行文结束,分享不易,点赞关注,么么哒(^-^)。

相关文章
相关标签/搜索