在ES2015規範敲定而且Node.js增添了大量的函數式子集的背景下,咱們終於能夠拍着胸脯說:將來就在眼前。javascript
… 我早就想這樣說了html
但這是真的。V8引擎將很快實現規範,並且Node已經添加了大量可用於生產環境的ES2015特性。下面要列出的是一些我認爲頗有必要的特性,並且這些特性是不使用須要像Babel或者Traceur這樣的翻譯器就能夠直接使用的。java
這篇文章將會講到三個至關流行的ES2015特性,而且已經在Node中支持了了:git
用let和const聲明塊級做用域;github
箭頭函數;flask
簡寫屬性和方法。設計模式
讓咱們立刻開始。數組
做用域是你程序中變量可見的區域。換句話說就是一系列的規則,它們決定了你聲明的變量在哪裏是可使用的。promise
你們應該都聽過 ,在JavaScript中只有在函數內部纔會創造新的做用域。然而你建立的98%的做用域事實上都是函數做用域,其實在JavaScript中有三種建立新做用域的方法。你能夠這樣:緩存
建立一個函數。你應該已經知道這種方式。
建立一個catch塊。 我絕對沒喲開玩笑.
建立一個代碼塊。若是你用的是ES2015,在一段代碼塊中用let或者const聲明的變量會限制它們只在這個塊中可見。這叫作塊級做用域。
一個代碼塊就是你用花括號包起來的部分。 { 像這樣 }。在if/else聲明和try/catch/finally塊中常常出現。若是你想利用塊做用域的優點,你能夠用花括號包裹任意的代碼來建立一個代碼塊
考慮下面的代碼片斷。
// 在 Node 中你須要使用 strict 模式嘗試這個 "use strict"; var foo = "foo"; function baz() { if (foo) { var bar = "bar"; let foobar = foo + bar; } // foo 和 bar 這裏均可見 console.log("This situation is " + foo + bar + ". I'm going home."); try { console.log("This log statement is " + foobar + "! It threw a ReferenceError at me!"); } catch (err) { console.log("You got a " + err + "; no dice."); } try { console.log("Just to prove to you that " + err + " doesn't exit outside of the above `catch` block."); } catch (err) { console.log("Told you so."); } } baz(); try { console.log(invisible); } catch (err) { console.log("invisible hasn't been declared, yet, so we get a " + err); } let invisible = "You can't see me, yet"; // let 聲明的變量在聲明前是不可訪問的
還有些要強調的
注意foobar在if塊以外是不可見的,由於咱們沒有用let聲明;
咱們能夠在任何地方使用foo ,由於咱們用var定義它爲全局做用域可見;
咱們能夠在baz內部任何地方使用bar, 由於var-聲明的變量是在定義的整個做用域內均可見。
用let or const聲明的變量不能在定義前調用。換句話說,它不會像var變量同樣被編譯器提高到做用域的開始處。
const 與 let 相似,但有兩點不一樣。
必須給聲明爲const的變量在聲明時賦值。不能夠先聲明後賦值。
不能改變const變量的值,只有在建立它時能夠給它賦值。若是你試圖改變它的值,會獲得一個TyepError。
咱們已經用var將就了二十多年了,你可能在想咱們真的須要新的類型聲明關鍵字嗎?(這裏做者應該是想表達這個意思)
問的好,簡單的回答就是–不, 並不真正須要。但在能夠用let和const的地方使用它們頗有好處的。
let和const聲明變量時都不會被提高到做用域開始的地方,這樣可使代碼可讀性更強,製造儘量少的迷惑。
它會盡量的約束變量的做用域,有助於減小使人迷惑的命名衝突。
這樣可讓程序只有在必須從新分配變量的狀況下從新分配變量。 const 能夠增強常量的引用。
另外一個例子就是 let 在 for 循環中的使用:
"use strict"; var languages = ['Danish', 'Norwegian', 'Swedish']; //會污染全局變量! for (var i = 0; i < languages.length; i += 1) { console.log(`${languages[i]} is a Scandinavian language.`); } console.log(i); // 4 for (let j = 0; j < languages.length; j += 1) { console.log(`${languages[j]} is a Scandinavian language.`); } try { console.log(j); // Reference error } catch (err) { console.log(`You got a ${err}; no dice.`); }
在for循環中使用var聲明的計數器並不會真正把計數器的值限制在本次循環中。 而let能夠。
let在每次迭代時從新綁定循環變量有很大的優點,這樣每一個循環中拷貝自身 , 而不是共享全局範圍內的變量。
"use strict"; // 簡潔明瞭 for (let i = 1; i < 6; i += 1) { setTimeout(function() { console.log("I've waited " + i + " seconds!"); }, 1000 * i); } // 功能徹底混亂 for (var j = 0; j < 6; j += 1) { setTimeout(function() { console.log("I've waited " + j + " seconds for this!"); }, 1000 * j); }
第一層循環會和你想象的同樣工做。而下面的會每秒輸出 「I’ve waited 6 seconds!」。
好吧,我選擇狗帶。
JavaScript的this關鍵字由於老是不按套路出牌而臭名昭著。
事實上,它的規則至關簡單。無論怎麼說,this在有些情形下會致使奇怪的用法
"use strict"; const polyglot = { name : "Michel Thomas", languages : ["Spanish", "French", "Italian", "German", "Polish"], introduce : function () { // this.name is "Michel Thomas" const self = this; this.languages.forEach(function(language) { // this.name is undefined, so we have to use our saved "self" variable console.log("My name is " + self.name + ", and I speak " + language + "."); }); } } polyglot.introduce();
在introduce裏, this.name是undefined。在回調函數外面,也就是forEach中, 它指向了polyglot對象。在這種情形下咱們老是但願在函數內部this和函數外部的this指向同一個對象。
問題是在JavaScript中函數會根據肯定性四原則在調用時定義本身的this變量。這就是著名的動態this 機制。
這些規則中沒有一個是關於查找this所描述的「附近做用域」的;也就是說並無一個確切的方法可讓JavaScript引擎可以基於包裹做用域來定義this的含義。
這就意味着當引擎查找this的值時,能夠找到值,但卻和回調函數以外的不是同一個值。有兩種傳統的方案能夠解決這個問題。
在函數外面把this保存到一個變量中,一般取名self,並在內部函數中使用;
或者在內部函數中調用bind阻止對this的賦值。
以上兩種辦法都可生效,但會產生反作用。
另外一方面,若是內部函數沒有設置它本身的this值,JavaScript會像查找其它變量那樣查找this的值:經過遍歷父做用域直到找到同名的變量。這樣會讓咱們使用附近做用域代碼中的this值,這就是著名的詞法this。
若是有樣的特性,咱們的代碼將會更加的清晰,不是嗎?
在 ES2015 中,咱們有了這一特性。箭頭函數不會綁定this值,容許咱們利用詞法綁定this關鍵字。這樣咱們就能夠像這樣重構上面的代碼了:
"use strict"; let polyglot = { name : "Michel Thomas", languages : ["Spanish", "French", "Italian", "German", "Polish"], introduce : function () { this.languages.forEach((language) => { console.log("My name is " + this.name + ", and I speak " + language + "."); }); } }
… 這樣就會按照咱們想的那樣工做了。
箭頭函數有一些新的語法。
"use strict"; let languages = ["Spanish", "French", "Italian", "German", "Polish"]; // 多行箭頭函數必須使用花括號, // 必須明確包含返回值語句 let languages_lower = languages.map((language) => { return language.toLowerCase() }); // 單行箭頭函數,花括號是可省的, // 函數默認返回最後一個表達式的值 // 你能夠指明返回語句,這是可選的。 let languages_lower = languages.map((language) => language.toLowerCase()); // 若是你的箭頭函數只有一個參數,能夠省略括號 let languages_lower = languages.map(language => language.toLowerCase()); // 若是箭頭函數有多個參數,必須用圓括號包裹 let languages_lower = languages.map((language, unused_param) => language.toLowerCase()); console.log(languages_lower); // ["spanish", "french", "italian", "german", "polish"] // 最後,若是你的函數沒有參數,你必須在箭頭前加上空的括號。 (() => alert("Hello!"))();
MDN關於箭頭函數的文檔解釋的很好。
ES2015提供了在對象上定義屬性和方法的一些新方式。
在 JavaScript 中, method 是對象的一個有函數值的屬性:
"use strict"; const myObject = { const foo = function () { console.log('bar'); }, }
在ES2015中,咱們能夠這樣簡寫:
"use strict"; const myObject = { foo () { console.log('bar'); }, * range (from, to) { while (from < to) { if (from === to) return ++from; else yield from ++; } } }
注意你也可使用生成器去定義方法。只須要在函數名前面加一個星號(*)。
這些叫作 方法定義 。和傳統的函數做爲屬性很像,但有一些不一樣:
只能在方法定義處調用super;
不容許用new調用方法定義。
我會在隨後的幾篇文章中講到super關鍵字。若是你等不及了, Exploring ES6中有關於它的乾貨。
ES6還引入了簡寫和推導屬性 。
若是對象的鍵值和變量名是一致的,那麼你能夠僅用變量名來初始化你的對象,而不是定義冗餘的鍵值對。
"use strict"; const foo = 'foo'; const bar = 'bar'; // 舊語法 const myObject = { foo : foo, bar : bar }; // 新語法 const myObject = { foo, bar }
兩中語法都以foo和bar鍵值指向foo and bar變量。後面的方式語義上更加一致;這只是個語法糖。
當用揭示模塊模式來定義一些簡潔的公共 API 的定義,我經常利用簡寫屬性的優點。
"use strict"; function Module () { function foo () { return 'foo'; } function bar () { return 'bar'; } // 這樣寫: const publicAPI = { foo, bar } /* 不要這樣寫: const publicAPI = { foo : foo, bar : bar } */ return publicAPI; };
這裏咱們建立並返回了一個publicAPI對象,鍵值foo指向foo方法,鍵值bar指向bar方法。
這是不常見的例子,但ES6容許你用表達式作屬性名。
"use strict"; const myObj = { // 設置屬性名爲 foo 函數的返回值 [foo ()] () { return 'foo'; } }; function foo () { return 'foo'; } console.log(myObj.foo() ); // 'foo'
根據Dr. Raushmayer在Exploring ES6中講的,這種特性最主要的用途是設置屬性名與Symbol值同樣。
最後,我想提一下get和set方法,它們在ES5中就已經支持了。
"use strict"; // 例子採用的是 MDN's 上關於 getter 的內容 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get const speakingObj = { // 記錄 「speak」 方法調用過多少次 words : [], speak (word) { this.words.push(word); console.log('speakingObj says ' + word + '!'); }, get called () { // 返回最新的單詞 const words = this.words; if (!words.length) return 'speakingObj hasn\'t spoken, yet.'; else return words[words.length - 1]; } }; console.log(speakingObj.called); // 'speakingObj hasn't spoken, yet.' speakingObj.speak('blargh'); // 'speakingObj says blargh!' console.log(speakingObj.called); // 'blargh'
使用getters時要記得下面這些:
Getters不接受參數;
屬性名不能夠和getter函數重名;
能夠用Object.defineProperty(OBJECT, "property name", { get : function () { . . . } }) 動態建立 getter
做爲最後這點的例子,咱們能夠這樣定義上面的 getter 方法:
"use strict"; const speakingObj = { // 記錄 「speak」 方法調用過多少次 words : [], speak (word) { this.words.push(word); console.log('speakingObj says ' + word + '!'); } }; // 這只是爲了證實觀點。我是絕對不會這樣寫的 function called () { // 返回新的單詞 const words = this.words; if (!words.length) return 'speakingObj hasn\'t spoken, yet.'; else return words[words.length - 1]; }; Object.defineProperty(speakingObj, "called", get : getCalled ) 除了 getters,還有 setters。像日常同樣,它們經過自定義的邏輯給對象設置屬性。 "use strict"; // 建立一個新的 globetrotter(環球者)! const globetrotter = { // globetrotter 如今所處國家所說的語言 const current_lang = undefined, // globetrotter 已近環遊過的國家 let countries = 0, // 查看環遊過哪些國家了 get countryCount () { return this.countries; }, // 不論 globe trotter 飛到哪裏,都從新設置他的語言 set languages (language) { // 增長環遊過的城市數 countries += 1; // 重置當前語言 this.current_lang = language; }; }; globetrotter.language = 'Japanese'; globetrotter.countryCount(); // 1 globetrotter.language = 'Spanish'; globetrotter.countryCount(); // 2
上面講的關於getters的也一樣適用於setters,但有一點不一樣:
getter不接受參數,setters必須接受正好一個參數。
破壞這些規則中的任意一個都會拋出一個錯誤。
既然 Angular 2 正在引入TypeCript而且把class帶到了臺前,我但願get and set可以流行起來… 但還有點但願它們不要流行起來。
將來的JavaScript正在變成現實,是時候把它提供的東西都用起來了。這篇文章裏,咱們瀏覽了 ES2015的三個很流行的特性:
let和const帶來的塊級做用域;
箭頭函數帶來的this的詞法做用域;
簡寫屬性和方法,以及getter和setter函數的回顧。
在本文的開始,咱們要說明一件事:
從本質上說,ES6的classes主要是給建立老式構造函數提供了一種更加方便的語法,並非什麼新魔法 —— Axel Rauschmayer,Exploring ES6做者
從功能上來說,class聲明就是一個語法糖,它只是比咱們以前一直使用的基於原型的行爲委託功能更強大一點。本文將重新語法與原型的關係入手,仔細研究ES2015的class關鍵字。文中將說起如下內容:
定義與實例化類;
使用extends建立子類;
子類中super語句的調用;
以及重要的標記方法(symbol method)的例子。
在此過程當中,咱們將特別注意 class 聲明語法從本質上是如何映射到基於原型代碼的。
讓咱們從頭開始提及。
JavaScript的『類』與Java、Python或者其餘你可能用過的面嚮對象語言中的類不一樣。其實後者可能稱做面向『類』的語言更爲準確一些。
在傳統的面向類的語言中,咱們建立的類是對象的模板。須要一個新對象時,咱們實例化這個類,這一步操做告訴語言引擎將這個類的方法和屬性複製到一個新實體上,這個實體稱做實例。實例是咱們本身的對象,且在實例化以後與父類毫無內在聯繫。
而JavaScript沒有這樣的複製機制。在JavaScript中『實例化』一個類建立了一個新對象,但這個新對象卻不獨立於它的父類。
正相反,它建立了一個與原型相鏈接的對象。即便是在實例化以後,對於原型的修改也會傳遞到實例化的新對象去。
原型自己就是一個無比強大的設計模式。有許多使用了原型的技術模仿了傳統類的機制,class便爲這些技術提供了簡潔的語法。
總而言之:
JavaScript不存在Java和其餘面嚮對象語言中的類概念;
JavaScript 的class很大程度上只是原型繼承的語法糖,與傳統的類繼承有很大的不一樣。
搞清楚這些以後,讓咱們先看一下class。
咱們使用class 關鍵字建立類,關鍵字以後是變量標識符,最後是一個稱做類主體的代碼塊。這種寫法稱做類的聲明。沒有使用extends關鍵字的類聲明被稱做基類:
"use strict"; // Food 是一個基類 class Food { constructor (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F` } print () { console.log( this.toString() ); } } const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); chicken_breast.print(); // 'Chicken Breast | 26g P :: 0g C :: 3.5g F' console.log(chicken_breast.protein); // 26 (LINE A)
須要注意到如下事情:
類只能包含方法定義,不能有數據屬性;
定義方法時,可使用簡寫方法定義;
與建立對象不一樣,咱們不能在類主體中使用逗號分隔方法定義;
咱們能夠在實例化對象上直接引用類的屬性(如 LINE A)。
類有一個獨有的特性,就是 contructor 構造方法。在構造方法中咱們能夠初始化對象的屬性。
構造方法的定義並非必須的。若是不寫構造方法,引擎會爲咱們插入一個空的構造方法:
"use strict"; class NoConstructor { /* JavaScript 會插入這樣的代碼: constructor () { } */ } const nemo = new NoConstructor(); // 能工做,但沒啥意思
將一個類賦值給一個變量的形式叫類表達式,這種寫法能夠替代上面的語法形式:
"use strict"; // 這是一個匿名類表達式,在類主體中咱們不能經過名稱引用它 const Food = class { // 和上面同樣的類定義…… } // 這是一個命名類表達式,在類主體中咱們能夠經過名稱引用它 const Food = class FoodClass { // 和上面同樣的類定義…… // 添加一個新方法,證實咱們能夠經過內部名稱引用 FoodClass…… printMacronutrients () { console.log(`${FoodClass.name} | ${FoodClass.protein} g P :: ${FoodClass.carbs} g C :: ${FoodClass.fat} g F`) } } const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); chicken_breast.printMacronutrients(); // 'Chicken Breast | 26g P :: 0g C :: 3.5g F' // 可是不能在外部引用 try { console.log(FoodClass.protein); // 引用錯誤 } catch (err) { // pass }
這一行爲與匿名函數與命名函數表達式很相似。
使用extends建立的類被稱做子類,或派生類。這一用法簡單明瞭,咱們直接在上面的例子中構建:
"use strict"; // FatFreeFood 是一個派生類 class FatFreeFood extends Food { constructor (name, protein, carbs) { super(name, protein, carbs, 0); } print () { super.print(); console.log(`Would you look at that -- ${this.name} has no fat!`); } } const fat_free_yogurt = new FatFreeFood('Greek Yogurt', 16, 12); fat_free_yogurt.print(); // 'Greek Yogurt | 26g P :: 16g C :: 0g F / Would you look at that -- Greek Yogurt has no fat!'
派生類擁有咱們上文討論的一切有關基類的特性,另外還有以下幾點新特色:
子類使用class關鍵字聲明,以後緊跟一個標識符,而後使用extend關鍵字,最後寫一個任意表達式。這個表達式一般來說就是個標識符,但理論上也能夠是函數。
若是你的派生類須要引用它的父類,可使用super關鍵字。
一個派生類不能有一個空的構造函數。即便這個構造函數就是調用了一下super(),你也得把它顯式的寫出來。但派生類卻能夠沒有構造函數。
在派生類的構造函數中,必須先調用super,才能使用this關鍵字(譯者注:僅在構造函數中是這樣,在其餘方法中能夠直接使用this)。
在JavaScript中僅有兩個super關鍵字的使用場景:
在子類構造函數中調用。若是初始化派生類是須要使用父類的構造函數,咱們能夠在子類的構造函數中調用super(parentConstructorParams),傳遞任意須要的參數。
引用父類的方法。在常規方法定義中,派生類可使用點運算符來引用父類的方法:super.methodName。
咱們的 FatFreeFood 演示了這兩種狀況:
在構造函數中,咱們簡單的調用了super,並將脂肪的量傳入爲0。
在咱們的print方法中,咱們先調用了super.print,以後才添加了其餘的邏輯。
無論你信不信,我反正是信了以上說的已涵蓋了有關class的基礎語法,這就是你開始實驗須要掌握的所有內容。
如今咱們開始關注class是怎麼映射到JavaScript內部的原型機制的。咱們會關注如下幾點:
使用構造調用建立對象;
原型鏈接的本質;
屬性和方法委託;
使用原型模擬類。
使用構造調用建立對象
構造函數不是什麼新鮮玩意兒。使用new關鍵字調用任意函數會使其返回一個對象 —— 這一步稱做建立了一個構造調用,這種函數一般被稱做構造器:
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } // 使用 'new' 關鍵字調用 Food 方法,就是構造調用,該操做會返回一個對象 const chicken_breast = new Food('Chicken Breast', 26, 0, 3.5); console.log(chicken_breast.protein) // 26 // 不用 'new' 調用 Food 方法,會返回 'undefined' const fish = Food('Halibut', 26, 0, 2); console.log(fish); // 'undefined'
當咱們使用new關鍵字調用函數時,JS內部執行了下面四個步驟:
建立一個新對象(這裏稱它爲O);
給O賦予一個鏈接到其餘對象的連接,稱爲原型;
將函數的this引用指向O;
函數隱式返回O。
在第三步和第四步之間,引擎會執行你函數中的具體邏輯。
知道了這一點,咱們就能夠重寫Food方法,使之不用new關鍵字也能工做:
"use strict"; // 演示示例:消除對 'new' 關鍵字的依賴 function Food (name, protein, carbs, fat) { // 第一步:建立新對象 const obj = { }; // 第二步:連接原型——咱們在下文會更加具體地探究原型的概念 Object.setPrototypeOf(obj, Food.prototype); // 第三步:設置 'this' 指向咱們的新對象 // 盡然咱們不能再運行的執行上下文中重置 `this` // 咱們在使用 'obj' 取代 'this' 來模擬第三步 obj.name = name; obj.protein = protein; obj.carbs = carbs; obj.fat = fat; // 第四步:返回新建立的對象 return obj; } const fish = Food('Halibut', 26, 0, 2); console.log(fish.protein); // 26
四步中的三步都是簡單明瞭的。建立一個對象、賦值屬性、而後寫一個return聲明,這些操做對大多數開發者來講沒有理解上的問題——然而這就是難倒衆人的黑魔法原型。
在一般狀況下,JavaScript中的包括函數在內的全部對象都會連接到另外一個對象上,這就是原型。
若是咱們訪問一個對象自己沒有的屬性,JavaScript就會在對象的原型上檢查該屬性。換句話說,若是你對一個對象請求它沒有的屬性,它會對你說:『這個我不知道,問個人原型吧』。
在另外一個對象上查找不存在屬性的過程稱做委託。
"use strict"; // joe 沒有 toString 方法…… const joe = { name : 'Joe' }, sara = { name : 'Sara' }; Object.hasOwnProperty(joe, toString); // false Object.hasOwnProperty(sara, toString); // false // ……但咱們仍是能夠調用它! joe.toString(); // '[object Object]',而不是引用錯誤! sara.toString(); // '[object Object]',而不是引用錯誤!
儘管咱們的 toString 的輸出徹底沒啥用,但請注意:這段代碼沒有引發任何的ReferenceError!這是由於儘管joe和sara沒有toString的屬性,但他們的原型有啊。
當咱們尋找sara.toString()方法時,sara說:『我沒有toString屬性,找個人原型吧』。正如上文所說,JavaScript會親切的詢問Object.prototype 是否含有toString屬性。因爲原型上有這一屬性,JS 就會把Object.prototype上的toString返回給咱們程序並執行。
sara自己沒有屬性不要緊——咱們會把查找操做委託到原型上。
換言之,咱們就能夠訪問到對象上並不存在的屬性,只要其的原型上有這些屬性。咱們能夠利用這一點將屬性和方法賦值到對象的原型上,而後咱們就能夠調用這些屬性,好像它們真的存在在那個對象上同樣。
更給力的是,若是幾個對象共享相同的原型——正如上面的joe和sara的例子同樣——當咱們給原型賦值屬性以後,它們就均可以訪問了,無需將這些屬性單獨拷貝到每個對象上。
這就是爲什麼你們把它稱做原型繼承——若是個人對象沒有,但對象的原型有,那個人對象也能繼承這個屬性。
事實上,這裏並無發生什麼『繼承』。在面向類的語言裏,繼承指從父類複製屬性到子類的行爲。在JavaScript裏,沒發生這種複製的操做,事實上這就是原型繼承與類繼承相比的一個主要優點。
在咱們探究原型到底是怎麼來的以前,咱們先作一個簡要回顧:
joe和sara沒有『繼承』一個toString的屬性;
joe和sara實際上根本沒有從Object.prototype上『繼承』;
joe和sara是連接到了Object.prototype上;
joe和sara連接到了同一個Object.prototype上。
若是想找到一個對象的(咱們稱它做O)原型,咱們可使用 Object.getPrototypeof(O)。
而後咱們再強調一遍:對象沒有『繼承自』他們的原型。他們只是委託到原型上。
以上。
接下來讓咱們深刻一下。
咱們已瞭解到基本上每一個對象(下文以O指代)都有原型(下文以P指代),而後當咱們查找O上沒有的屬性,JavaScript引擎就會在P上尋找這個屬性。
至此咱們有兩個問題:
以上狀況函數怎麼玩?
這些原型是從哪裏來的?
名爲Object的函數
在JavaScript引擎執行程序以前,它會建立一個環境讓程序在內部執行,在執行環境中會建立一個函數,叫作Object, 以及一個關聯對象,叫作Object.prototype。
換句話說,Object和Object.prototype在任意執行中的JavaScript程序中永遠存在。
這個Object乍一看好像和其餘函數沒什麼區別,但特別之處在於它是一個構造器——在調用它時返回一個新對象:
"use strict"; typeof new Object(); // "object" typeof Object(); // 這個 Object 函數的特色是不須要使用 new 關鍵字調用
這個Object.prototype對象是個……對象。正如其餘對象同樣,它有屬性。
關於Object和Object.prototype你須要知道如下幾點:
Object函數有一個叫作.prototype的屬性,指向一個對象(Object.prototype);
Object.prototype對象有一個叫作.constructor的屬性,指向一個函數(Object)。
實際上,這個整體方案對於JavaScript中的全部函數都是適用的。當咱們建立一個函數——下文稱做 someFunction——這個函數就會有一個屬性.prototype,指向一個叫作someFunction.prototype 的對象。
與之相反,someFunction.prototype對象會有一個叫作.contructor的屬性,它的引用指回函數someFunction。
"use strict"; function foo () { console.log('Foo!'); } console.log(foo.prototype); // 指向一個叫 'foo' 的對象 console.log(foo.prototype.constructor); // 指向 'foo' 函數 foo.prototype.constructor(); // 輸出 'Foo!' —— 僅爲證實確實有 'foo.prototype.constructor' 這麼個方法且指向原函數
須要記住如下幾個要點:
全部的函數都有一個屬性,叫作 .prototype,它指向這個函數的關聯對象。
全部函數的原型都有一個屬性,叫作 .constructor,它指向這個函數自己。
一個函數原型的 .constructor 並不是必須指向建立這個函數原型的函數……有點繞,咱們等下會深刻探討一下。
設置函數的原型有一些規則,在開始以前,咱們先歸納設置對象原型的三個規則:
『默認』規則;
使用new隱式設置原型;
使用Object.create顯式設置原型。
考慮下這段代碼:
"use strict"; const foo = { status : 'foobar' };
十分簡單,咱們作的事兒就是建立一個叫foo的對象,而後給他一個叫status的屬性。
而後JavaScript在幕後多作了點工做。當咱們在字面上建立一個對象時,JavaScript將對象的原型指向Object.prototype並設置其原型的.constructor指向Object:
"use strict"; const foo = { status : 'foobar' }; Object.getPrototypeOf(foo) === Object.prototype; // true foo.constructor === Object; // true
讓咱們再看下以前調整過的 Food 例子。
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; }
如今咱們知道函數Food將會與一個叫作Food.prototype的對象關聯。
當咱們使用new關鍵字建立一個對象,JavaScript將會:
設置這個對象的原型指向咱們使用new調用的函數的.prototype屬性;
設置這個對象的.constructor指向咱們使用new調用到的構造函數。
const tootsie_roll = new Food('Tootsie Roll', 0, 26, 0); Object.getPrototypeOf(tootsie_roll) === Food.prototype; // true tootsie_roll.constructor === Food; // true
這就可讓咱們搞出下面這樣的黑魔法:
"use strict"; Food.prototype.cook = function cook () { console.log(`${this.name} is cooking!`); }; const dinner = new Food('Lamb Chops', 52, 8, 32); dinner.cook(); // 'Lamb Chops are cooking!'
最後咱們可使用Object.create方法手工設置對象的原型引用。
"use strict"; const foo = { speak () { console.log('Foo!'); } }; const bar = Object.create(foo); bar.speak(); // 'Foo!' Object.getPrototypeOf(bar) === foo; // true
還記得使用new調用函數的時候,JavaScript在幕後幹了哪四件事兒嗎?Object.create就幹了這三件事兒:
建立一個新對象;
設置它的原型引用;
返回這個新對象。
你能夠本身去看下MDN上寫的那個polyfill。
(譯者注:polyfill就是給老代碼實現現有新功能的補丁代碼,這裏就是指老版本JS沒有Object.create函數,MDN上有手工擼的一個替代方案)
直接使用原型來模擬面向類的行爲須要一些技巧。
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } Food.prototype.toString = function () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; }; function FatFreeFood (name, protein, carbs) { Food.call(this, name, protein, carbs, 0); } // 設置 "subclass" 關係 // ===================== // LINE A :: 使用 Object.create 手動設置 FatFreeFood's 『父類』. FatFreeFood.prototype = Object.create(Food.prototype); // LINE B :: 手工重置 constructor 的引用 Object.defineProperty(FatFreeFood.constructor, "constructor", { enumerable : false, writeable : true, value : FatFreeFood });
在Line A,咱們須要設置FatFreeFood.prototype使之等於一個新對象,這個新對象的原型引用是Food.prototype。若是沒這麼搞,咱們的子類就不能訪問『超類』的方法。
不幸的是,這個致使了至關詭異的結果:FatFreeFood.constructor是Function,而不是FatFreeFood。爲了保證一切正常,咱們須要在Line B手工設置FatFreeFood.constructor。
讓開發者從使用原型對類行爲笨拙的模仿中脫離苦海是class關鍵字的產生動機之一。它確實也提供了避免原型語法常見陷阱的解決方案。
如今咱們已經探究了太多關於JavaScript的原型機制,你應該更容易理解class關鍵字讓一切變得多麼簡單了吧!
如今咱們已瞭解到JavaScript原型系統的必要性,咱們將深刻探究一下類支持的三種方法,以及一種特殊狀況,以結束本文的討論。
構造器;
靜態方法;
原型方法;
一種原型方法的特殊狀況:『標記方法』。
並不是我提出的這三組方法,這要歸功於Rauschmayer博士在探索ES6一書中的定義。
一個類的constructor方法用於關注咱們的初始化邏輯,constructor方法有如下幾個特殊點:
只有在構造方法裏,咱們才能夠調用父類的構造器;
它在背後處理了全部設置原型鏈的工做;
它被用做類的定義。
第二點就是在JavaScript中使用class的一個主要好處,咱們來引用一下《探索 ES6》書裏的15.2.3.1 的標題:
子類的原型就是超類
正如咱們所見,手工設置很是繁瑣且容易出錯。若是咱們使用class關鍵字,JavaScript在內部會負責搞定這些設置,這一點也是使用class的優點。
第三點有點意思。在JavaScript中類僅僅是個函數——它等同於與類中的constructor方法。
"use strict"; class Food { // 和以前同樣的類定義…… } typeof Food; // 'function'
與通常把函數做爲構造器的方式不一樣,咱們不能不用new關鍵字而直接調用類構造器:
const burrito = Food('Heaven', 100, 100, 25); // 類型錯誤
這就引起了另外一個問題:當咱們不用new調用函數構造器的時候發生了什麼?
簡短的回答是:對於任何沒有顯式返回的函數來講都是返回undefined。咱們只須要相信用咱們構造函數的用戶都會使用構造調用。這就是社區爲什麼約定構造方法的首字母大寫:提醒使用者要用new來調用。
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } const fish = Food('Halibut', 26, 0, 2); // D'oh . . . console.log(fish); // 'undefined'
長一點的回答是:返回undefined,除非你手工檢測是否使用被new調用,而後進行本身的處理。
ES2015引入了一個屬性使得這種檢測變得簡單: new.target.
new.target是一個定義在全部使用new調用的函數上的屬性,包括類構造器。 當咱們使用new關鍵字調用函數時,函數體內的new.target的值就是這個函數自己。若是函數沒有被new調用,這個值就是undefined。
"use strict"; // 強行構造調用 function Food (name, protein, carbs, fat) { // 若是用戶忘了手工調用一下 if (!new.target) return new Food(name, protein, carbs, fat); this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } const fish = Food('Halibut', 26, 0, 2); // 糟了,不過不要緊! fish; // 'Food {name: "Halibut", protein: 20, carbs: 5, fat: 0}'
在ES5裏用起來也還行:
"use strict"; function Food (name, protein, carbs, fat) { if (!(this instanceof Food)) return new Food(name, protein, carbs, fat); this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; }
MDN文檔講述了new.target的更多細節,並且給有興趣者配上了ES2015規範做爲參考。規範裏有關 [[Construct]] 的描述頗有啓發性。
靜態方法是構造方法本身的方法,不能被類的實例化對象調用。咱們使用static關鍵字定義靜態方法。
"use strict"; class Food { // 和以前同樣…… // 添加靜態方法 static describe () { console.log('"Food" 是一種存儲了養分信息的數據類型'); } } Food.describe(); // '"Food" 是一種存儲了養分信息的數據類型'
靜態方法與老式構造函數中直接屬性賦值類似:
"use strict"; function Food (name, protein, carbs, fat) { Food.count += 1; this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } Food.count = 0; Food.describe = function count () { console.log(`你建立了 ${Food.count} 個 food`); }; const dummy = new Food(); Food.describe(); // "你建立了 1 個 food"
任何不是構造方法和靜態方法的方法都是原型方法。之因此叫原型方法,是由於咱們以前經過給構造函數的原型上附加方法的方式來實現這一功能。
"use strict"; // 使用 ES6: class Food { constructor (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; } print () { console.log( this.toString() ); } } // 在 ES5 裏: function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } // 『原型方法』的命名大概來自咱們以前經過給構造函數的原型上附加方法的方式來實現這一功能。 Food.prototype.toString = function toString () { return `${this.name} | ${this.protein}g P :: ${this.carbs}g C :: ${this.fat}g F`; }; Food.prototype.print = function print () { console.log( this.toString() ); };
應該說明,在方法定義時徹底可使用生成器。
"use strict"; class Range { constructor(from, to) { this.from = from; this.to = to; } * generate () { let counter = this.from, to = this.to; while (counter < to) { if (counter == to) return counter++; else yield counter++; } } } const range = new Range(0, 3); const gen = range.generate(); for (let val of range.generate()) { console.log(`Generator 的值是 ${ val }. `); // Prints: // Generator 的值是 0. // Generator 的值是 1. // Generator 的值是 2. }
最後咱們說說標誌方法。這是一些名爲Symbol值的方法,當咱們在自定義對象中使用內置構造器時,JavaScript引擎能夠識別並使用這些方法。
MDN文檔提供了一個Symbol是什麼的簡要概覽:
Symbol是一個惟一且不變的數據類型,能夠做爲一個對象的屬性標示符。
建立一個新的symbol,會給咱們提供一個被認爲是程序裏的惟一標識的值。這一點對於命名對象的屬性十分有用:咱們能夠確保不會不當心覆蓋任何屬性。使用Symbol作鍵值也不是無數的,因此他們很大程度上對外界是不可見的(也不徹底是,能夠經過Reflect.ownKeys得到)
"use strict"; const secureObject = { // 這個鍵能夠看做是惟一的 [new Symbol("name")] : 'Dr. Secure A. F.' }; console.log( Object.getKeys(superSecureObject) ); // [] -- 標誌屬性不太好獲取 console.log( Reflect.ownKeys(secureObject) ); // [Symbol("name")] -- 但也不是徹底隱藏的
對咱們來說更有意思的是,這給咱們提供了一種方式來告訴 JavaScript 引擎使用特定方法來達到特定的目的。
所謂的『衆所周知的Symbol』是一些特定對象的鍵,當你在定義對象中使用時他們時,JavaScript引擎會觸發一些特定方法。
這對於JavaScript來講有點怪異,咱們仍是看個例子吧:
"use strict"; // 繼承 Array 可讓咱們直觀的使用 'length' // 同時可讓咱們訪問到內置方法,如 // map、filter、reduce、push、pop 等 class FoodSet extends Array { // foods 把傳遞的任意參數收集爲一個數組 // 參見:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator constructor(...foods) { super(); this.foods = []; foods.forEach((food) => this.foods.push(food)) } // 自定義迭代器行爲,請注意,這不是多麼好用的迭代器,可是個不錯的例子 // 鍵名前必須寫星號 * [Symbol.iterator] () { let position = 0; while (position < this.foods.length) { if (position === this.foods.length) { return "Done!" } else { yield `${this.foods[ position++ ]} is the food item at position ${position}`; } } } // 當咱們的用戶使用內置的數組方法,返回一個數組類型對象 // 而不是 FoodSet 類型的。這使得咱們的 FoodSet 能夠被一些 // 指望操做數組的代碼操做 static get [Symbol.species] () { return Array; } } const foodset = new FoodSet(new Food('Fish', 26, 0, 16), new Food('Hamburger', 26, 48, 24)); // 當咱們使用 for ... of 操做 FoodSet 時,JavaScript 將會使用 // 咱們以前用 [Symbol.iterator] 作鍵值的方法 for (let food of foodset) { // 打印所有 food console.log( food ); } // 當咱們執行數組的 `filter` 方法時,JavaScript 建立並返回一個新對象 // 咱們在什麼對象上執行 `filter` 方法,新對象就使用這個對象做爲默認構造器來建立 // 然而大部分代碼都但願 filter 返回一個數組,因而咱們經過重寫 [Symbol.species] // 的方式告訴 JavaScript 使用數組的構造器 const healthy_foods = foodset.filter((food) => food.name !== 'Hamburger'); console.log( healthy_foods instanceof FoodSet ); // console.log( healthy_foods instanceof Array );
當你使用for...of遍歷一個對象時,JavaScript將會嘗試執行對象的迭代器方法,這一方法就是該對象 Symbol.iterator屬性上關聯的方法。若是咱們提供了本身的方法定義,JavaScript就會使用咱們自定義的。若是沒有本身制定的話,若是有默認的實現就用默認的,沒有的話就不執行。
Symbo.species更奇異了。在自定義的類中,默認的Symbol.species函數就是類的構造函數。當咱們的子類有內置的集合(例如Array和Set)時,咱們一般但願在使用父類的實例時也能使用子類。
經過方法返回父類的實例而不是派生類的實例,使咱們更能確保咱們子類在大多數代碼裏的可用性。而Symbol.species能夠實現這一功能。
若是不怎麼須要這個功能就別費力去搞了。Symbol的這種用法——或者說有關Symbol的所有用法——都還比較罕見。這些例子只是爲了演示:
咱們能夠在自定義類中使用JavaScript內置的特定構造器;
用兩個普通的例子展現了怎麼實現這一點。
ES2015的class關鍵字沒有帶給咱們 Java 裏或是SmallTalk裏那種『真正的類』。寧肯說它只是提供了一種更加方便的語法來建立經過原型關聯的對象,本質上沒有什麼新東西。
ES2015發生了一些重大變革,像promises和generators. 但並不是新標準的一切都遙不可及。 – 至關一部分新特性能夠快速上手。
在這篇文章裏,咱們來看下新特性帶來的好處:
新的集合: map,weakmap,set, weakset
大部分的new String methods
模板字符串。
咱們開始這個系列的最後一章吧。
模板字符串 解決了三個痛點,容許你作以下操做:
定義在字符串內部的表達式,稱爲 字符串插值。
寫多行字符串無須用換行符 (n) 拼接。
使用「raw」字符串 – 在反斜槓內的字符串不會被轉義,視爲常量。
「use strict」; /* 三個模板字符串的例子: 字符串插值,多行字符串,raw 字符串。 ================================= */ // ================================== // 1. 字符串插值 :: 解析任何一個字符串中的表達式。 console.log(1 + 1 = ${1 + 1}); // ================================== // 2. 多行字符串 :: 這樣寫: let childe_roland = I saw them and I knew them all. And yet <br> Dauntless the slug-horn to my lips I set, <br> And blew 「Childe Roland to the Dark Tower came.」 // … 代替下面的寫法: child_roland = ‘I saw them and I knew them all. And yet\n’ + ‘Dauntless the slug-horn to my lips I set,\n’ + ‘And blew 「Childe Roland to the Dark Tower came.」’; // ================================== // 3. raw 字符串 :: 在字符串前加 raw 前綴,javascript 會忽略轉義字符。 // 依然會解析包在 ${} 的表達式 const unescaped = String.rawThis ${string()} doesn't contain a newline!\n function string () { return 「string」; } console.log(unescaped); // ‘This string doesn’t contain a newline!\n’ – 注意 \n 會被原樣輸出 // 你能夠像 React 使用 JSX 同樣,用模板字符串建立 HTML 模板 const template = ` Example I’m a pure JS & HTML template! ` function getClass () { // Check application state, calculate a class based on that state return 「some-stateful-class」; } console.log(template); // 這樣使用略顯笨,本身試試吧! // 另外一個經常使用的例子是打印變量名: const user = { name : ‘Joe’ }; console.log(「User’s name is 」 + user.name + 「.」); // 有點冗長 console.log(User's name is ${user.name}.); // 這樣稍好一些
使用字符串插值,用反引號代替引號包裹字符串,並把咱們想要的表達式嵌入在${}中。
對於多行字符串,只須要把你要寫的字符串包裹在反引號裏,在要換行的地方直接換行。 JavaScript 會在換行處插入新行。
使用原生字符串,在模板字符串前加前綴String.raw,仍然使用反引號包裹字符串。
模板字符串或許只不過是一種語法糖 … 但它比語法糖略勝一籌。
ES2015也給String新增了一些方法。他們主要歸爲兩類:
通用的便捷方法
擴充 Unicode 支持的方法。
在本文裏咱們只講第一類,同時unicode特定方法也有至關好的用例 。若是你感興趣的話,這是地址在MDN的文檔裏,有一個關於字符串新方法的完整列表。
對新手而言,咱們有String.prototype.startsWith。 它對任何字符串都有效,它須要兩個參數:
一個是 search string 還有
整形的位置參數 n。這是可選的。
String.prototype.startsWith方法會檢查以nth位起的字符串是否以search string開始。若是沒有位置參數,則默認從頭開始。
若是字符串以要搜索的字符串開頭返回 true,不然返回 false。
"use strict"; const contrived_example = "This is one impressively contrived example!"; // 這個字符串是以 "This is one" 開頭嗎? console.log(contrived_example.startsWith("This is one")); // true // 這個字符串的第四個字符以 "is" 開頭? console.log(contrived_example.startsWith("is", 4)); // false // 這個字符串的第五個字符以 "is" 開始? console.log(contrived_example.startsWith("is", 5)); // true
String.prototype.endsWith和startswith類似: 它也須要兩個參數:一個是要搜索的字符串,一個是位置。
然而String.prototype.endsWith位置參數會告訴函數要搜索的字符串在原始字符串中被當作結尾處理。
換句話說,它會切掉nth後的全部字符串,並檢查是否以要搜索的字符結尾。
"use strict"; const contrived_example = "This is one impressively contrived example!"; console.log(contrived_example.endsWith("contrived example!")); // true console.log(contrived_example.slice(0, 11)); // "This is one" console.log(contrived_example.endsWith("one", 11)); // true // 一般狀況下,傳一個位置參數向下面這樣: function substringEndsWith (string, search_string, position) { // Chop off the end of the string const substring = string.slice(0, position); // 檢查被截取的字符串是否已 search_string 結尾 return substring.endsWith(search_string); }
ES2015也添加了String.prototype.includes。 你須要用字符串調用它,而且要傳遞一個搜索項。若是字符串包含搜索項會返回true,反之返回false。
"use strict"; const contrived_example = "This is one impressively contrived example!"; // 這個字符串是否包含單詞 impressively ? contrived_example.includes("impressively"); // true
ES2015以前,咱們只能這樣:
"use strict"; contrived_example.indexOf("impressively") !== -1 // true
不算太壞。可是,String.prototype.includes是 一個改善,它屏蔽了任意整數返回值爲true的漏洞。
還有String.prototype.repeat。能夠對任意字符串使用,像includes同樣,它會或多或少地完成函數名指示的工做。
它只須要一個參數: 一個整型的count。使用案例說明一切,上代碼:
const na = "na"; console.log(na.repeat(5) + ", Batman!"); // 'nanananana, Batman!'
最後,咱們有String.raw,咱們在上面簡單介紹過。
一個模板字符串以 String.raw 爲前綴,它將不會在字符串中轉義:
/* 單右斜線要轉義,咱們須要雙右斜線才能打印一個右斜線,\n 在普通字符串裏會被解析爲換行 * */ console.log('This string \\ has fewer \\ backslashes \\ and \n breaks the line.'); // 不想這樣寫的話用 raw 字符串 String.raw`This string \\ has too many \\ backslashes \\ and \n doesn't break the line.`
雖然咱們不涉及剩餘的 string 方法,可是若是我不告訴你去這個主題的必讀部分就會顯得我疏忽。
Dr Rauschmayer對於Unicode in JavaScript的介紹
他關於ES2015’s Unicode Support in Exploring ES6和The Absolute Minimum Every Software Developer Needs to Know About Unicode 的討論。
不管如何我不得不跳過它的最後一部分。雖然有些老可是仍是有優勢的。
這裏是文檔中缺失的字符串方法,這樣你會知道缺哪些東西了。
String.fromCodePoint & String.prototype.codePointAt;
String.prototype.normalize;
Unicode point escapes.
ES2015新增了一些集合類型:
Map和WeakMap
Set和WeakSet。
合適的Map和Set類型十分方便使用,還有弱變量是一個使人興奮的改動,雖然它對Javascript來講像舶來品同樣。
map就是簡單的鍵值對。最簡單的理解方式就是和object相似,一個鍵對應一個值。
"use strict"; // 咱們能夠把 foo 當鍵,bar 當值 const obj = { foo : 'bar' }; // 對象鍵爲 foo 的值爲 bar obj.foo === 'bar'; // true
新的Map類型在概念上是類似的,可是可使用任意的數據類型做爲鍵 – 不止strings和symbols–還有除了pitfalls associated with trying to use an objects a map的一些東西。
下面的片斷例舉了 Map 的 API.
"use strict"; // 構造器 let scotch_inventory = new Map(); // BASIC API METHODS // Map.prototype.set (K, V) :: 建立一個鍵 K,並設置它的值爲 V。 scotch_inventory.set('Lagavulin 18', 2); scotch_inventory.set('The Dalmore', 1); // 你能夠建立一個 map 裏面包含一個有兩個元素的數組 scotch_inventory = new Map([['Lagavulin 18', 2], ['The Dalmore', 1]]); // 全部的 map 都有 size 屬性,這個屬性會告訴你 map 裏有多少個鍵值對。 // 用 Map 或 Set 的時候,必定要使用 size ,不能使用 length console.log(scotch_inventory.size); // 2 // Map.prototype.get(K) :: 返回鍵相關的值。若是鍵不存在返回 undefined console.log(scotch_inventory.get('The Dalmore')); // 1 console.log(scotch_inventory.get('Glenfiddich 18')); // undefined // Map.prototype.has(K) :: 若是 map 裏包含鍵 K 返回true,不然返回 false console.log(scotch_inventory.has('The Dalmore')); // true console.log(scotch_inventory.has('Glenfiddich 18')); // false // Map.prototype.delete(K) :: 從 map 裏刪除鍵 K。成功返回true,不存在返回 false console.log(scotch_inventory.delete('The Dalmore')); // true -- breaks my heart // Map.prototype.clear() :: 清楚 map 中的全部鍵值對 scotch_inventory.clear(); console.log( scotch_inventory ); // Map {} -- long night // 遍歷方法 // Map 提供了多種方法遍歷鍵值。 // 重置值,繼續探索 scotch_inventory.set('Lagavulin 18', 1); scotch_inventory.set('Glenfiddich 18', 1); /* Map.prototype.forEach(callback[, thisArg]) :: 對 map 裏的每一個鍵值對執行一個回調函數 * 你能夠在回調函數內部設置 'this' 的值,經過傳遞一個 thisArg 參數,那是可選的並且沒有太大必要那樣作 * 最後,注意回調函數已經被傳了鍵和值 */ scotch_inventory.forEach(function (quantity, scotch) { console.log(`Excuse me while I sip this ${scotch}.`); }); // Map.prototype.keys() :: 返回一個 map 中的全部鍵 const scotch_names = scotch_inventory.keys(); for (let name of scotch_names) { console.log(`We've got ${name} in the cellar.`); } // Map.prototype.values() :: 返回 map 中的全部值 const quantities = scotch_inventory.values(); for (let quantity of quantities) { console.log(`I just drank ${quantity} of . . . Uh . . . I forget`); } // Map.prototype.entries() :: 返回 map 的全部鍵值對,提供一個包含兩個元素的數組 // 之後會常常看到 map 裏的鍵值對和 "entries" 關聯 const entries = scotch_inventory.entries(); for (let entry of entries) { console.log(`I remember! I drank ${entry[1]} bottle of ${entry[0]}!`); }
可是Object在保存鍵值對的時候仍然有用。 若是符合下面的所有條件,你可能仍是想用Object:
當你寫代碼的時候,你知道你的鍵值對。
你知道你可能不會去增長或刪除你的鍵值對。
你使用的鍵全都是 string 或 symbol。
另外一方面,若是符合如下任意條件,你可能會想使用一個 map。
你須要遍歷整個map – 然而這對 object 來講是難以置信的.
當你寫代碼的時候不須要知道鍵的名字或數量。
你須要複雜的鍵,像 Object 或 別的 Map (!).
像遍歷一個map同樣遍歷一個object是可行的,但奇妙的是–還會有一些坑潛伏在暗處。 Map更容易使用,而且增長了一些可集成的優點。然而object是以隨機順序遍歷的,map是以插入的順序遍歷的。
添加隨意動態鍵名的鍵值對給一個object是可行的。但奇妙的是: 好比說若是你曾經遍歷過一個僞 map,你須要記住手動更新條目數。
最後一條,若是你要設置的鍵名不是string或symbol,你除了選擇Map別無選擇。
上面的這些只是一些指導性的意見,並非最好的規則。
你可能據說過一個特別棒的特性垃圾回收器,它會按期地檢查再也不使用的對象並清除。
To quote Dr Rauschmayer:
WeakMap 不會阻止它的鍵值被垃圾回收。那意味着你能夠把數據和對象關聯起來不用擔憂內存泄漏。
換句換說,就是你的程序丟掉了WeakMap鍵的全部外部引用,他能自動垃圾回收他們的值。
儘管大大簡化了用例,考慮到SPA(單頁面應用) 就是用來展現用戶但願展現的東西,像一些物品描述和一張圖片,咱們能夠理解爲API返回的JSON。
理論上來講咱們能夠經過緩存響應結果來減小請求服務器的次數。咱們能夠這樣用Map :
"use strict"; const cache = new Map(); function put (element, result) { cache.set(element, result); } function retrieve (element) { return cache.get(element); }
… 這是行得通的,可是有內存泄漏的危險。
由於這是一個SPA,用戶或許想離開這個視圖,這樣的話咱們的 「視圖」object就會失效,會被垃圾回收。
不幸的是,若是你使用的是正常的Map ,當這些object不使用時,你必須自行清除。
使用WeakMap替代就能夠解決上面的問題:
"use strict"; const cache = new WeakMap(); // 不會再有內存泄露了 // 剩下的都同樣
這樣當應用失去不須要的元素的引用時,垃圾回收系統能夠自動重用那些元素。
WeakMap的API和Map類似,但有以下幾點不一樣:
在WeakMap裏你可使用object做爲鍵。 這意味着不能以String和Symbol作鍵。
WeakMap只有set,get,has,和delete方法 – 那意味着你不能遍歷weak map.
WeakMaps沒有size屬性。
不能遍歷或檢查WeakMap的長度的緣由是,在遍歷過程當中可能會遇到垃圾回收系統的運行: 這一瞬間是滿的,下一秒就沒了。
這種不可預測的行爲須要謹慎對待,TC39(ECMA第39屆技術委員會)曾試圖避免禁止WeakMap的遍歷和長度檢測。
其餘的案例,能夠在這裏找到Use Cases for WeakMap,來自Exploring ES6.
Set就是隻包含一個值的集合。換句換說,每一個set的元素只會出現一次。
這是一個有用的數據類型,若是你要追蹤惟一而且固定的object ,好比說聊天室的當前用戶。
Set和Map有徹底相同的API。主要的不一樣是Set沒有set方法,由於它不能存儲鍵值對。剩下的幾乎相同。
"use strict"; // 構造器 let scotch_collection = new Set(); // 基本的 API 方法 // Set.prototype.add (O) :: 和 set 同樣,添加一個對象 scotch_collection.add('Lagavulin 18'); scotch_collection.add('The Dalmore'); // 你也能夠用數組構造一個 set scotch_collection = new Set(['Lagavulin 18', 'The Dalmore']); // 全部的 set 都有一個 length 屬性。這個屬性會告訴你 set 裏有多少對象 // 用 set 或 map 的時候,必定記住用 size,不用 length console.log(scotch_collection.size); // 2 // Set.prototype.has(O) :: 包含對象 O 返回 true 不然返回 false console.log(scotch_collection.has('The Dalmore')); // true console.log(scotch_collection.has('Glenfiddich 18')); // false // Set.prototype.delete(O) :: 刪除 set 中的 O 對象,成功返回 true,不存在返回 false scotch_collection.delete('The Dalmore'); // true -- break my heart // Set.prototype.clear() :: 刪除 set 中的全部對象 scotch_collection.clear(); console.log( scotch_collection ); // Set {} -- long night. /* 迭代方法 * Set 提供了多種方法遍歷 * 從新設置值,繼續探索 */ scotch_collection.add('Lagavulin 18'); scotch_collection.add('Glenfiddich 18'); /* Set.prototype.forEach(callback[, thisArg]) :: 執行一個函數,回調函數 * set 裏在每一個的鍵值對。 You can set the value of 'this' inside * the callback by passing a thisArg, but that's optional and seldom necessary. */ scotch_collection.forEach(function (scotch) { console.log(`Excuse me while I sip this ${scotch}.`); }); // Set.prototype.values() :: 返回 set 中的全部值 let scotch_names = scotch_collection.values(); for (let name of scotch_names) { console.log(`I just drank ${name} . . . I think.`); } // Set.prototype.keys() :: 對 set 來講,和 Set.prototype.values() 方法一致 scotch_names = scotch_collection.keys(); for (let name of scotch_names) { console.log(`I just drank ${name} . . . I think.`); } /* Set.prototype.entries() :: 返回 map 的全部鍵值對,提供一個包含兩個元素的數組 * 這有點多餘,可是這種方法能夠保留 map API 的可操做性 * */ const entries = scotch_collection.entries(); for (let entry of entries) { console.log(`I got some ${entry[0]} in my cup and more ${entry[1]} in my flask!`); }
WeakSet相對於Set就像WeakMap相對於 Map :
在WeakSet裏object的引用是弱類型的。
WeakSet沒有property屬性。
不能遍歷WeakSet。
Weak set的用例並很少,可是這兒有一些Domenic Denicola稱呼它們爲「perfect for branding」 – 意思就是標記一個對象以知足其餘需求。
這兒是他給的例子:
/* 下面這個例子來自 Weakset 使用案例的郵件討論 * 郵件的內容和討論的其他部分在這兒: * https://mail.mozilla.org/pipermail/es-discuss/2015-June/043027.html */ const foos = new WeakSet(); class Foo { constructor() { foos.add(this); } method() { if (!foos.has(this)) { throw new TypeError("Foo.prototype.method called on an incompatible object!"); } } }
這是一個輕量科學的方法防止你們在一個沒有被Foo構造出的object上使用method。
使用的WeakSet的優點是容許foo裏的object使用完後被垃圾回收。
這篇文章裏,咱們已經瞭解了ES2015帶來的一些好處,從string的便捷方法和模板變量到適當的Map和Set實現。
String方法和模板字符串易於上手。同時你很快也就不用處處用weak set了,我認爲你很快就會喜歡上Set和Map。