摸鱼酱的文章声明:内容保证原创,纯技术干货分享交流,不打广告不吹牛逼。javascript
前言:前端
记得我第一次接触ES6,仍是在大学写JavaEE的时候。当时因为须要作个后端管理系统,因此临时找了一些培训视频资源学了一下前端基础和vue框架。经过那个视频资料,我学会了一些简单的ES6知识,好比用let和const声明变量,解构赋值、模板字符串、可变参数等等。vue
以后因为一些故事性的情节,春招临时转行前端,简历上实在没啥前端技能可写。仗着会那么一点点ES6,我居然在技能一栏,厚颜无耻的写上了熟练运用ES6(…(⊙_⊙;)…)。结果可想而知,个人前端面途坎坷,啪啪不断(巴掌与脸亲密接触所发出的声音)。不过幸运的是,个人脸不但没有被打烂,反而越打脸皮越厚。java
好的,下面进入正文,对于这篇文章所探讨的全部ES6知识,我预先用脑图作了如下整理:node
对于接下来的行文,我都会围绕这副脑图展开。若是您有兴趣继续往下看下去,我但愿您能在这幅图上停留多一些时间。编程
因为文章内容较长,而且探讨的技术点之间基本互不依赖,因此我建议您能够挑选感兴趣的部分来看。后端
好的,按照上述脑图中的逻辑,接下来我会分红如下几个部分来展开探讨本文。api
对于缺失的异步编程部分,以后会单独总结成一篇文章。数组
在理清楚行文思路以后,下面咱们就进入第一部分,探讨两个ES6新添加的基本语法。浏览器
ES6提供了不少的基本语法,这其中包括了用于声明变量的let、const,与形参相关的默认参数、可变参数,以及一些解构赋值等等基本语法。在本文中,我就再也不花费大量笔墨去探讨这些路人皆知的ES6基础中的基础知识。下面咱们主要介绍两个ES6基本语法,即:
OK,下面我就进入迭代器与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,下面咱们进入下一部分,模板字符串与标签函数的探讨。
模板字符串你们都很熟悉,可是标签函数可能知道的人比较少,下面我先对标签函数作个认知分享。
标签函数在定义时和普通函数没有区别。区别在函数的调用上,主要体如今如下两点:
在有了以上对标签函数的基本认识以后,下面咱们作个简单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的含义便可。
通过上面的探讨,我相信阁下就已经理解并掌握了标签函数的基本使用。那么标签函数具体有什么应用场景呢?下面咱们就探讨几个应用场景,以求加深阁下对标签函数的实践理解。
下面我会分红如下几个场景来展开探讨:
在平常开发中,咱们极可能会碰到这么一个需求:
咱们极可能的作法是直接把用户的输入做为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, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
s += templateData[i];
}
return s;
}
复制代码
let sender = '<script>alert("abc")</script>'; // 1.得到用户输入
const safeSender = SaferHTML`${sender}`; // 2.转义用户输入
document.genElementByid("p").innerHtml = safeSender // 3.使用转义后的用户输入
复制代码
如此作法以后,即可以解决咱们的问题,杜绝由用户直接输入代码而致使的潜在错误和风险。
好的,下面咱们就进入另一个应用场景,模板字符串的国际化。
在咱们的项目中支持国际化(i18n)的逻辑自己很是简单,只须要界面中的全部字符串变量化,然后这些变量自动根据项目的当前语音渲染出该语言下的字符串便可。咱们使用函数式编程的思想来分析,能够获得如下结果:
下面咱们就经过模板字符串和标签函数,实现一个简单的i18n标签函数,用于自动将语言键翻译获得当前语言环境下的语言字符串,步骤以下:
export const enUS = {
'Welcome to': 'Welcome to',
'you are visitor number': 'you are visitor number'
}
export const zhCN = {
'Welcome to': '你好',
'you are visitor number': '你的访问号码'
}
复制代码
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;
}
}
复制代码
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标签函数,简单吧。
下面咱们在简单说说其它场景。
通过上面的探讨,我相信有模板引擎使用经验的人就很容易就发现他们的共性。是的,我认为,在模板字符串与标签函数配合使用以后,就能够实现模板引擎的功能,可用于定义内部语言如jsx。在取得了这个认识以后,咱们就能够看到目标字符串与标签函数其实有不少的应用场景能够开发,同时也是个造轮子利器。
引导至此,其它的我就很少说了。下面进入另外一个部分,ES6新增数据类型的探讨。
Symbol是ES6提供的一种新的原始数据类型,能够用来表示独一无二的值。此外,它也是对象属性名的第二种数据类型(另外一种是字符串)。
有些对它感到陌生的朋友可能会以为它高大上,可是理解一点,symbol只不过是一种原始数据类型,就和number、string这些同样,没什么大不了的。为了消除你们因为对他陌生而产生的畏惧感,下面咱们会直接进入几个应用场景的探讨,加深对symbol这种原始数据类型的实践理解。
好的,接下来我会按照如下顺序来展开探讨几种应用场景:
OK,下面依次进入这些场景的探讨分析。
写的好累了,原谅我下面的知识点之间就再也不多情的编写大量的承上启下文字了。
魔术字符串指的是,在代码之中屡次出现、与代码造成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽可能消除魔术字符串,改由含义清晰的变量代替。 ---阮一峰
以下含有魔法字符串的代码示例:
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字符串就是魔法字符串。咱们分析这样大量使用魔法字符串可能会出现的问题:
接下来使用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)
复制代码
假设咱们对一个对象须要作以下的访问控制:
如下是没有实现访问控制的代码:
const obj = {
attr1: 'public Attr1',// 公有
attr2: 'protect Attr2',// 保护
attr3: 'private Attr3',// 私有
}
// code ... 模块内任意访问obj的attr一、attr二、attr3属性
export default = obj
复制代码
import obj from './export.js'
const attr1 = obj.attr1; // 外部模块对公有数据直接访问,访问控制成功
const attr2 = obj.attr2; // 外部模块对保护数据直接访问,访问控制失败
const attr3 = obj.attr3; // 外部模块对私有数据直接访问,访问控制失败
复制代码
这是没有实现访问控制的代码,没有知足咱们的需求。为了实现访问控制,接下来咱们使用Symbol对上述代码改造。
借由Symbol实现访问控制:
export const attr2 = Symbol('attr2');
复制代码
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 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中保护和私有成员的访问控制。
以下示例,咱们封装一个集合类Collection,模块外部只能使用add方法而不能访问内部私有属性arr和私有方法logAdd(ps:实现保护成员的方式和上一点一致,这里就再也不举例了):
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 Collection from './export.js'
const col = new Collection()
col.add('zhangsan') // 外部访问Collection中的公有方法add
// col.arr/logAdd error // 外部没法访问Collection中的私有属性和方法
复制代码
舒适提示: 滥用保护数据和私有数据在大多数状况只会下降业务代码的阅读性哦!
Set对于JavaScript而言是一种新的数据结构,相对于数组用于存储有序、可重复的元素集合,Set用于存储有序、不可重复的元素集合。
接下来列举几个在平常开发中可能会用到Set数据结构的场景:
// 数组去重
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
复制代码
下面截取阮一峰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对于JavaScript而言也是一种新的数据结构,用于存储键值对形式的字典 / 双列集合。在Map对象出现以前,咱们一般会使用Object对象来作键值对的存储,下面对比一下Map对象实现键值对存储与普通对象存储键值对的区别:
通过上面的对比分析能够得出结论,不到必须使用引用类型做为键的状况下,咱们都用Object对象字面量的方式来定义并存储键值对会更好一些。
对于Map的应用场景,我还真没想到合适的demo。在Java开发时,常常会有实现对象之间的一对1、一对多、多对多(桥Map方式)的关系这种类别的需求,因此我认为至少在nodejs中也会有一些这种应用场景存在,就好比在用nodejs作后端时,须要在多个service之间进行复杂的数据传递。
总的来讲,Map结构的出现告诉了咱们这些JavaScript开发者,此后在JavaScript中咱们也能够很简单的实现对象之间的映射关系。
JavaScript大量借鉴了Java面向对象的思想和语法,下面咱们就以学习Java面向对象时所要掌握的面向对象三大特性(即封装、继承、多态)为行文思路,展开探讨JavaScript如何优雅的实现面向对象。
封装是面向对象的重要原则,它在代码中的体现主要是如下两点:
如下是基本封装示例:
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
复制代码
舒适提示:面向对象的灵魂从不在于何种实现语法(如class),而在于面向对象编程这个编程思想自己。
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。
Proxy是JavaScript的一个新内置对象(函数类型对象),它的实例对象用于定义对象基本操做的自定义行为(如属性查找、赋值、枚举、函数调用等)。
在上述Reflect的介绍中,咱们发如今平常开发中,咱们能够也常常对对象进行对象层面和属性层面的不少操做,既然是操做,那么咱们就但愿可以具有对这些操做进行切面处理的能力,也即实现代理操做,那么应该怎么作呢?
ES5提供了存取器属性get、set,这让咱们具有了代理一个对象的属性读写操做以进行切面处理的能力。可是这时候对于其它对对象操做行为的代理方案仍然没有官方的实现方案。直到ES6的Proxy出现,咱们才具有了对这些各类类型的对象操做进行代理以进行切面处理的能力(上述Reflect的13个静态方法对应的对象操做所有均可以AOP处理)。
既然Object.defineProperty和Reflect均可以代理对象操做,那么咱们对比一下二者的代理原理和优缺点以备日后甄选方案:
接下来叙述在平常开发中咱们可能会见到或者用到Proxy代理的场景:
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)
复制代码
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)
复制代码
行文结束,分享不易,点赞关注,么么哒(^-^)。