學習資料:拉勾課程《大前端高薪訓練營》
閱讀建議:文章較長,搭配文章的側邊欄目錄進行食用,體驗會更佳哦!
內容說明:本文不作知識點的搬運工,文中只記錄我的對該技術點的認識和理解以及該技術在平常開發中的使用場景
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)
本文結束,謝謝觀看。 如若承認,一鍵三連。