学习资料:拉勾课程《大前端高薪训练营》
阅读建议:文章较长,搭配文章的侧边栏目录进行食用,体验会更佳哦!
内容说明:本文不做知识点的搬运工,文中只记录个人对该技术点的认识和理解以及该技术在日常开发中的使用场景
一:面向对象:类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 }`;
上述示例运行结果:
经过上述例子我们可以大概得知标签函数的形实参匹配规则:
- 模板中字面量数组的形实参匹配:模板字符串以类似/${[^}]+}/g 的正则规则进行split 得到其内所有字面量组成的数组,而后作为实参匹配标签函数的第一个形参literals
- 模板中所有变量的形实参匹配:模板字符串以 /${[^}]+}/g 的正则规则进行match找到所有的JS变量数组,解析得到其值后,按顺序作为实参匹配标签函数剩下的形参,上例代码中用rest剩余参数作为形参接收所有实参。
通过上面的例子和解析,我们认识了标签函数调用的执行规则。根据标签函数和模板字符串的配合机制,我们很容易就想到这种机制可以实现模板引擎甚至是定义内部语言的功能。
接下来叙述在日常开发中我们可能会用到标签函数的场景:
1):把可能作为innerHtml的string中的特殊字符转义,使它不被解析为HTML标签
在日常开发中,我们很可能会碰到这么一个需求:
- 一个input输入框接收用户的输入
- 另一个p标签用来展示这个用户的输入
先分析一下这样做的风险:由于用户的输入直接作为了p标签的内容,当用户输入一个
// 0.标签函数 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; } // 1.用户输入 let sender = '<script>alert("abc")</script>'; // 2.转义后的用户输入 const safeSender = SaferHTML`${ sender}`; // 3.把safeSender渲染到标签元素中 // xxx
2):i18n 国际化
在我们的项目中支持国际化(i18n)的逻辑本身非常简单,只需要界面中的所有字符串变量化,而后这些变量自动根据项目的当前语音渲染出该语言下的字符串。使用函数式编程的思想来实现的基本思路如下:
-
输入:需要翻译的字符串键
-
映射关系:根据输入获得输出,具体映射逻辑与当前语言与语言包有关
-
输出:翻译后的字符串
// 语言包resource const enUS = {
'Welcome to': 'Welcome to', 'you are visitor number': 'you are visitor number'} const zhCN = {
'Welcome to': '你好', 'you are visitor number': '你的访问号码'}
// 根据当前语言和语言包得到i18n标签函数 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;} }
// 翻译语言 let currentLanguage = 'zh-CN'; const i18n = i18nInit(currentLanguage, zhCN, enUS ); i18n`Welcome to ${
siteName}, you are visitor number ${ visitorNumber}!`
3):定义语言,如 jsx
jsx标签函数,实现了将一个含有html、css、js的模板字符串解析为一个React 对象的功能。它的模板解析功能很强大,以至于我们把它称之为一门语言。思想和原理大概如此,由于博主暂未看过jsx源码,下文对此不再赘述。
八:内置对象Refelect
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原型上的方法)。
九:内置对象Proxy
Proxy是JavaScript的一个新内置对象(函数类型对象),它的实例对象用于定义对象基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
在上述Reflect的介绍中,我们发现在日常开发中,我们可以也经常对对象进行对象层面和属性层面的很多操作,既然是操作,那么我们就希望能够具备对这些操作进行AOP处理的能力,也即实现代理操作,那么应该怎么做呢?ES5提供了存取器属性get、set,这让我们具备了代理一个对象的属性读写操作以进行AOP处理的能力。但是这时候对于其它对对象操作行为的代理方案仍然没有官方的实现方案。直到ES6的Proxy出现,这才让我们具备了对这些各种类型的对象操作进行代理以进行AOP处理的能力(上述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)
本文结束,谢谢观看。
如若认可,一键三连。
