摸魚醬的文章聲明:內容保證原創,純技術乾貨分享交流,不打廣告不吹牛逼。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)
複製代碼
行文結束,分享不易,點贊關注,麼麼噠(^-^)。