学习资料:拉勾课程《大前端高薪训练营》
阅读建议:文章较长,搭配文章的侧边栏目录进行食用,体验会更佳哦!
内容说明:本文不作知识点的搬运工,文中只记录我的对该技术点的认识和理解以及该技术在平常开发中的使用场景
javascript
一:面向对象:类class
面向对象三大特性之封装
封装是面向对象的重要原则,它在代码中的体现主要是如下两点:css
- 封装总体:把对象的属性和行为封装为一个总体,其中内部成员能够分为静态成员(也叫类成员)和实例成员,成员之间又可细分为属性和方法。
- 访问控制:外部对对象内部属性和行为的访问权限,简单来分时就是私有和公有两种权限。
如下是基本封装示例:html
class Animal{ constructor(name) { this.name = name;// 实例属性 } cry() { // 实例方法 console.log('cry'); } static getNum(){ // 静态方法 return AnimalES6.num } } Animal.num = 42;// 静态属性
面向对象三大特性之继承
继承是面向对象最显著的一个特性,它在代码中的体现主要是如下两点:前端
- 子类对象具备父类对象的属性和行为
- 子类对象能够有它本身的属性和行为
如下是定义一个Cat类并对上述Animal类的继承示例:java
class Cat extends Animal{ constructor(name, type) { super(name);// 必须先构造父类空间 this.type = type; } cry() { console.log('miao miao');// 方法重写 } }
面向对象三大特性之多态
多态指容许不一样的对象对同一消息作出不一样响应,在Java中,实现多态有如下三个条件:编程
- 继承
- 重写
- 父类引用指向子类对象
因为JavaScript是弱类型语言,因此JavaScript实现多态,不存在父类引用指向子类对象的问题。api
如下再定义一个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是一种新的原始数据类型,用来表示独一无二的值。此外,它也是对象属性名的第二种数据类型(另外一种是字符串)。promise
接下来列举几个在平常开发中可能会用到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):实现对象之间的一对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标签的内容,当用户输入一个<script>标签等任意HTML标签时,若是咱们直接把它交给p标签,那么浏览器就会把它当成inneHTML进行解析后执行其中的脚本或者渲染HTML,这确定是不被指望且有风险的。因此咱们在把用户的输入交给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)
本文结束,谢谢观看。 如若承认,一键三连。