阅读 15220

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

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

前言:

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

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

好的,下面进入正文,对于这篇文章所探讨的所有ES6知识,我预先用脑图做了以下整理:

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

由于文章内容较长,并且探讨的技术点之间基本互不依赖,所以我建议您可以挑选感兴趣的部分来看。

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

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

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

在理清楚行文思路之后,下面我们就进入第一部分,探讨两个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的attr1、attr2、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的attr1、attr2、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开发时,经常会有实现对象之间的一对一、一对多、多对多(桥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 = valueReflect.set(target, propertyKey, value[, receiver])
属性读取target.propertyKeyReflect.get(target, propertyKey[, receiver])
属性删除delete target.propertyKeyReflect.deleteProperty(target, propertyKey)
属性包含propertyKey in targetReflect.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)
复制代码

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

文章分类
前端
文章标签