摘要:但在可以用和的地方使用它們很有好處的。它會(huì)盡可能的約束變量的作用域,有助于減少令人迷惑的命名沖突。在回調(diào)函數(shù)外面,也就是中,它指向了對象。這就意味著當(dāng)引擎查找的值時(shí),可以找到值,但卻和回調(diào)函數(shù)之外的不是同一個(gè)值。
使用 ES6 寫更好的 JavaScript part I:廣受歡迎新特性 介紹
在ES2015規(guī)范敲定并且Node.js增添了大量的函數(shù)式子集的背景下,我們終于可以拍著胸脯說:未來就在眼前。
… 我早就想這樣說了
但這是真的。V8引擎將很快實(shí)現(xiàn)規(guī)范,而且Node已經(jīng)添加了大量可用于生產(chǎn)環(huán)境的ES2015特性。下面要列出的是一些我認(rèn)為很有必要的特性,而且這些特性是不使用需要像Babel或者Traceur這樣的翻譯器就可以直接使用的。
這篇文章將會(huì)講到三個(gè)相當(dāng)流行的ES2015特性,并且已經(jīng)在Node中支持了了:
用let和const聲明塊級作用域;
箭頭函數(shù);
簡寫屬性和方法。
讓我們馬上開始。
let和const聲明塊級作用域作用域是你程序中變量可見的區(qū)域。換句話說就是一系列的規(guī)則,它們決定了你聲明的變量在哪里是可以使用的。
大家應(yīng)該都聽過 ,在JavaScript中只有在函數(shù)內(nèi)部才會(huì)創(chuàng)造新的作用域。然而你創(chuàng)建的98%的作用域事實(shí)上都是函數(shù)作用域,其實(shí)在JavaScript中有三種創(chuàng)建新作用域的方法。你可以這樣:
創(chuàng)建一個(gè)函數(shù)。你應(yīng)該已經(jīng)知道這種方式。
創(chuàng)建一個(gè)catch塊。 我絕對沒喲開玩笑.
創(chuàng)建一個(gè)代碼塊。如果你用的是ES2015,在一段代碼塊中用let或者const聲明的變量會(huì)限制它們只在這個(gè)塊中可見。這叫做塊級作用域。
一個(gè)代碼塊就是你用花括號(hào)包起來的部分。 { 像這樣 }。在if/else聲明和try/catch/finally塊中經(jīng)常出現(xiàn)。如果你想利用塊作用域的優(yōu)勢,你可以用花括號(hào)包裹任意的代碼來創(chuàng)建一個(gè)代碼塊
考慮下面的代碼片段。
// 在 Node 中你需要使用 strict 模式嘗試這個(gè) "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 聲明的變量在聲明前是不可訪問的
還有些要強(qiáng)調(diào)的
注意foobar在if塊之外是不可見的,因?yàn)槲覀儧]有用let聲明;
我們可以在任何地方使用foo ,因?yàn)槲覀冇胿ar定義它為全局作用域可見;
我們可以在baz內(nèi)部任何地方使用bar, 因?yàn)関ar-聲明的變量是在定義的整個(gè)作用域內(nèi)都可見。
用let or const聲明的變量不能在定義前調(diào)用。換句話說,它不會(huì)像var變量一樣被編譯器提升到作用域的開始處。
const 與 let 類似,但有兩點(diǎn)不同。
必須給聲明為const的變量在聲明時(shí)賦值。不可以先聲明后賦值。
不能改變const變量的值,只有在創(chuàng)建它時(shí)可以給它賦值。如果你試圖改變它的值,會(huì)得到一個(gè)TyepError。
let & const: Who Cares?我們已經(jīng)用var將就了二十多年了,你可能在想我們真的需要新的類型聲明關(guān)鍵字嗎?(這里作者應(yīng)該是想表達(dá)這個(gè)意思)
問的好,簡單的回答就是–不, 并不真正需要。但在可以用let和const的地方使用它們很有好處的。
let和const聲明變量時(shí)都不會(huì)被提升到作用域開始的地方,這樣可以使代碼可讀性更強(qiáng),制造盡可能少的迷惑。
它會(huì)盡可能的約束變量的作用域,有助于減少令人迷惑的命名沖突。
這樣可以讓程序只有在必須重新分配變量的情況下重新分配變量。 const 可以加強(qiáng)常量的引用。
另一個(gè)例子就是 let 在 for 循環(huán)中的使用:
"use strict"; var languages = ["Danish", "Norwegian", "Swedish"]; //會(huì)污染全局變量! 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循環(huán)中使用var聲明的計(jì)數(shù)器并不會(huì)真正把計(jì)數(shù)器的值限制在本次循環(huán)中。 而let可以。
let在每次迭代時(shí)重新綁定循環(huán)變量有很大的優(yōu)勢,這樣每個(gè)循環(huán)中拷貝自身 , 而不是共享全局范圍內(nèi)的變量。
"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); }
第一層循環(huán)會(huì)和你想象的一樣工作。而下面的會(huì)每秒輸出 “I’ve waited 6 seconds!”。
好吧,我選擇狗帶。
動(dòng)態(tài)this關(guān)鍵字的怪異JavaScript的this關(guān)鍵字因?yàn)榭偸遣话刺茁烦雠贫裘阎?/p>
事實(shí)上,它的規(guī)則相當(dāng)簡單。不管怎么說,this在有些情形下會(huì)導(dǎo)致奇怪的用法
"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。在回調(diào)函數(shù)外面,也就是forEach中, 它指向了polyglot對象。在這種情形下我們總是希望在函數(shù)內(nèi)部this和函數(shù)外部的this指向同一個(gè)對象。
問題是在JavaScript中函數(shù)會(huì)根據(jù)確定性四原則在調(diào)用時(shí)定義自己的this變量。這就是著名的動(dòng)態(tài)this 機(jī)制。
這些規(guī)則中沒有一個(gè)是關(guān)于查找this所描述的“附近作用域”的;也就是說并沒有一個(gè)確切的方法可以讓JavaScript引擎能夠基于包裹作用域來定義this的含義。
這就意味著當(dāng)引擎查找this的值時(shí),可以找到值,但卻和回調(diào)函數(shù)之外的不是同一個(gè)值。有兩種傳統(tǒng)的方案可以解決這個(gè)問題。
在函數(shù)外面把this保存到一個(gè)變量中,通常取名self,并在內(nèi)部函數(shù)中使用;
或者在內(nèi)部函數(shù)中調(diào)用bind阻止對this的賦值。
以上兩種辦法均可生效,但會(huì)產(chǎn)生副作用。
另一方面,如果內(nèi)部函數(shù)沒有設(shè)置它自己的this值,JavaScript會(huì)像查找其它變量那樣查找this的值:通過遍歷父作用域直到找到同名的變量。這樣會(huì)讓我們使用附近作用域代碼中的this值,這就是著名的詞法this。
如果有樣的特性,我們的代碼將會(huì)更加的清晰,不是嗎?
箭頭函數(shù)中的詞法this在 ES2015 中,我們有了這一特性。箭頭函數(shù)不會(huì)綁定this值,允許我們利用詞法綁定this關(guān)鍵字。這樣我們就可以像這樣重構(gòu)上面的代碼了:
"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 + "."); }); } }
… 這樣就會(huì)按照我們想的那樣工作了。
箭頭函數(shù)有一些新的語法。
"use strict"; let languages = ["Spanish", "French", "Italian", "German", "Polish"]; // 多行箭頭函數(shù)必須使用花括號(hào), // 必須明確包含返回值語句 let languages_lower = languages.map((language) => { return language.toLowerCase() }); // 單行箭頭函數(shù),花括號(hào)是可省的, // 函數(shù)默認(rèn)返回最后一個(gè)表達(dá)式的值 // 你可以指明返回語句,這是可選的。 let languages_lower = languages.map((language) => language.toLowerCase()); // 如果你的箭頭函數(shù)只有一個(gè)參數(shù),可以省略括號(hào) let languages_lower = languages.map(language => language.toLowerCase()); // 如果箭頭函數(shù)有多個(gè)參數(shù),必須用圓括號(hào)包裹 let languages_lower = languages.map((language, unused_param) => language.toLowerCase()); console.log(languages_lower); // ["spanish", "french", "italian", "german", "polish"] // 最后,如果你的函數(shù)沒有參數(shù),你必須在箭頭前加上空的括號(hào)。 (() => alert("Hello!"))();
MDN關(guān)于箭頭函數(shù)的文檔解釋的很好。
簡寫屬性和方法ES2015提供了在對象上定義屬性和方法的一些新方式。
簡寫方法在 JavaScript 中, method 是對象的一個(gè)有函數(shù)值的屬性:
"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 ++; } } }
注意你也可以使用生成器去定義方法。只需要在函數(shù)名前面加一個(gè)星號(hào)(*)。
這些叫做 方法定義 。和傳統(tǒng)的函數(shù)作為屬性很像,但有一些不同:
只能在方法定義處調(diào)用super;
不允許用new調(diào)用方法定義。
我會(huì)在隨后的幾篇文章中講到super關(guān)鍵字。如果你等不及了, Exploring ES6中有關(guān)于它的干貨。
簡寫和推導(dǎo)屬性ES6還引入了簡寫和推導(dǎo)屬性 。
如果對象的鍵值和變量名是一致的,那么你可以僅用變量名來初始化你的對象,而不是定義冗余的鍵值對。
"use strict"; const foo = "foo"; const bar = "bar"; // 舊語法 const myObject = { foo : foo, bar : bar }; // 新語法 const myObject = { foo, bar }
兩中語法都以foo和bar鍵值指向foo and bar變量。后面的方式語義上更加一致;這只是個(gè)語法糖。
當(dāng)用揭示模塊模式來定義一些簡潔的公共 API 的定義,我常常利用簡寫屬性的優(yōu)勢。
"use strict"; function Module () { function foo () { return "foo"; } function bar () { return "bar"; } // 這樣寫: const publicAPI = { foo, bar } /* 不要這樣寫: const publicAPI = { foo : foo, bar : bar } */ return publicAPI; };
這里我們創(chuàng)建并返回了一個(gè)publicAPI對象,鍵值foo指向foo方法,鍵值bar指向bar方法。
推導(dǎo)屬性名這是不常見的例子,但ES6允許你用表達(dá)式做屬性名。
"use strict"; const myObj = { // 設(shè)置屬性名為 foo 函數(shù)的返回值 [foo ()] () { return "foo"; } }; function foo () { return "foo"; } console.log(myObj.foo() ); // "foo"
根據(jù)Dr. Raushmayer在Exploring ES6中講的,這種特性最主要的用途是設(shè)置屬性名與Symbol值一樣。
Getter 和 Setter 方法最后,我想提一下get和set方法,它們在ES5中就已經(jīng)支持了。
"use strict"; // 例子采用的是 MDN"s 上關(guān)于 getter 的內(nèi)容 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get const speakingObj = { // 記錄 “speak” 方法調(diào)用過多少次 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時(shí)要記得下面這些:
Getters不接受參數(shù);
屬性名不可以和getter函數(shù)重名;
可以用Object.defineProperty(OBJECT, "property name", { get : function () { . . . } }) 動(dòng)態(tài)創(chuàng)建 getter
作為最后這點(diǎn)的例子,我們可以這樣定義上面的 getter 方法:
"use strict"; const speakingObj = { // 記錄 “speak” 方法調(diào)用過多少次 words : [], speak (word) { this.words.push(word); console.log("speakingObj says " + word + "!"); } }; // 這只是為了證明觀點(diǎn)。我是絕對不會(huì)這樣寫的 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。像平常一樣,它們通過自定義的邏輯給對象設(shè)置屬性。 "use strict"; // 創(chuàng)建一個(gè)新的 globetrotter(環(huán)球者)! const globetrotter = { // globetrotter 現(xiàn)在所處國家所說的語言 const current_lang = undefined, // globetrotter 已近環(huán)游過的國家 let countries = 0, // 查看環(huán)游過哪些國家了 get countryCount () { return this.countries; }, // 不論 globe trotter 飛到哪里,都重新設(shè)置他的語言 set languages (language) { // 增加環(huán)游過的城市數(shù) countries += 1; // 重置當(dāng)前語言 this.current_lang = language; }; }; globetrotter.language = "Japanese"; globetrotter.countryCount(); // 1 globetrotter.language = "Spanish"; globetrotter.countryCount(); // 2
上面講的關(guān)于getters的也同樣適用于setters,但有一點(diǎn)不同:
getter不接受參數(shù),setters必須接受正好一個(gè)參數(shù)。
破壞這些規(guī)則中的任意一個(gè)都會(huì)拋出一個(gè)錯(cuò)誤。
既然 Angular 2 正在引入TypeCript并且把class帶到了臺(tái)前,我希望get and set能夠流行起來… 但還有點(diǎn)希望它們不要流行起來。
結(jié)論未來的JavaScript正在變成現(xiàn)實(shí),是時(shí)候把它提供的東西都用起來了。這篇文章里,我們?yōu)g覽了 ES2015的三個(gè)很流行的特性:
let和const帶來的塊級作用域;
箭頭函數(shù)帶來的this的詞法作用域;
簡寫屬性和方法,以及getter和setter函數(shù)的回顧。
使用 ES6 編寫更好的 JavaScript Part II:深入探究 [類] 辭舊迎新在本文的開始,我們要說明一件事:
從本質(zhì)上說,ES6的classes主要是給創(chuàng)建老式構(gòu)造函數(shù)提供了一種更加方便的語法,并不是什么新魔法 —— Axel Rauschmayer,Exploring ES6作者
從功能上來講,class聲明就是一個(gè)語法糖,它只是比我們之前一直使用的基于原型的行為委托功能更強(qiáng)大一點(diǎn)。本文將從新語法與原型的關(guān)系入手,仔細(xì)研究ES2015的class關(guān)鍵字。文中將提及以下內(nèi)容:
定義與實(shí)例化類;
使用extends創(chuàng)建子類;
子類中super語句的調(diào)用;
以及重要的標(biāo)記方法(symbol method)的例子。
在此過程中,我們將特別注意 class 聲明語法從本質(zhì)上是如何映射到基于原型代碼的。
讓我們從頭開始說起。
退一步說:Classes不是什么JavaScript的『類』與Java、Python或者其他你可能用過的面向?qū)ο笳Z言中的類不同。其實(shí)后者可能稱作面向『類』的語言更為準(zhǔn)確一些。
在傳統(tǒng)的面向類的語言中,我們創(chuàng)建的類是對象的模板。需要一個(gè)新對象時(shí),我們實(shí)例化這個(gè)類,這一步操作告訴語言引擎將這個(gè)類的方法和屬性復(fù)制到一個(gè)新實(shí)體上,這個(gè)實(shí)體稱作實(shí)例。實(shí)例是我們自己的對象,且在實(shí)例化之后與父類毫無內(nèi)在聯(lián)系。
而JavaScript沒有這樣的復(fù)制機(jī)制。在JavaScript中『實(shí)例化』一個(gè)類創(chuàng)建了一個(gè)新對象,但這個(gè)新對象卻不獨(dú)立于它的父類。
正相反,它創(chuàng)建了一個(gè)與原型相連接的對象。即使是在實(shí)例化之后,對于原型的修改也會(huì)傳遞到實(shí)例化的新對象去。
原型本身就是一個(gè)無比強(qiáng)大的設(shè)計(jì)模式。有許多使用了原型的技術(shù)模仿了傳統(tǒng)類的機(jī)制,class便為這些技術(shù)提供了簡潔的語法。
總而言之:
JavaScript不存在Java和其他面向?qū)ο笳Z言中的類概念;
JavaScript 的class很大程度上只是原型繼承的語法糖,與傳統(tǒng)的類繼承有很大的不同。
搞清楚這些之后,讓我們先看一下class。
類基礎(chǔ):聲明與表達(dá)式我們使用class 關(guān)鍵字創(chuàng)建類,關(guān)鍵字之后是變量標(biāo)識(shí)符,最后是一個(gè)稱作類主體的代碼塊。這種寫法稱作類的聲明。沒有使用extends關(guān)鍵字的類聲明被稱作基類:
"use strict"; // Food 是一個(gè)基類 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)
需要注意到以下事情:
類只能包含方法定義,不能有數(shù)據(jù)屬性;
定義方法時(shí),可以使用簡寫方法定義;
與創(chuàng)建對象不同,我們不能在類主體中使用逗號(hào)分隔方法定義;
我們可以在實(shí)例化對象上直接引用類的屬性(如 LINE A)。
類有一個(gè)獨(dú)有的特性,就是 contructor 構(gòu)造方法。在構(gòu)造方法中我們可以初始化對象的屬性。
構(gòu)造方法的定義并不是必須的。如果不寫構(gòu)造方法,引擎會(huì)為我們插入一個(gè)空的構(gòu)造方法:
"use strict"; class NoConstructor { /* JavaScript 會(huì)插入這樣的代碼: constructor () { } */ } const nemo = new NoConstructor(); // 能工作,但沒啥意思
將一個(gè)類賦值給一個(gè)變量的形式叫類表達(dá)式,這種寫法可以替代上面的語法形式:
"use strict"; // 這是一個(gè)匿名類表達(dá)式,在類主體中我們不能通過名稱引用它 const Food = class { // 和上面一樣的類定義…… } // 這是一個(gè)命名類表達(dá)式,在類主體中我們可以通過名稱引用它 const Food = class FoodClass { // 和上面一樣的類定義…… // 添加一個(gè)新方法,證明我們可以通過內(nèi)部名稱引用 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); // 引用錯(cuò)誤 } catch (err) { // pass }
這一行為與匿名函數(shù)與命名函數(shù)表達(dá)式很類似。
使用extends創(chuàng)建子類以及使用super調(diào)用使用extends創(chuàng)建的類被稱作子類,或派生類。這一用法簡單明了,我們直接在上面的例子中構(gòu)建:
"use strict"; // FatFreeFood 是一個(gè)派生類 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!"
派生類擁有我們上文討論的一切有關(guān)基類的特性,另外還有如下幾點(diǎn)新特點(diǎn):
子類使用class關(guān)鍵字聲明,之后緊跟一個(gè)標(biāo)識(shí)符,然后使用extend關(guān)鍵字,最后寫一個(gè)任意表達(dá)式。這個(gè)表達(dá)式通常來講就是個(gè)標(biāo)識(shí)符,但理論上也可以是函數(shù)。
如果你的派生類需要引用它的父類,可以使用super關(guān)鍵字。
一個(gè)派生類不能有一個(gè)空的構(gòu)造函數(shù)。即使這個(gè)構(gòu)造函數(shù)就是調(diào)用了一下super(),你也得把它顯式的寫出來。但派生類卻可以沒有構(gòu)造函數(shù)。
在派生類的構(gòu)造函數(shù)中,必須先調(diào)用super,才能使用this關(guān)鍵字(譯者注:僅在構(gòu)造函數(shù)中是這樣,在其他方法中可以直接使用this)。
在JavaScript中僅有兩個(gè)super關(guān)鍵字的使用場景:
在子類構(gòu)造函數(shù)中調(diào)用。如果初始化派生類是需要使用父類的構(gòu)造函數(shù),我們可以在子類的構(gòu)造函數(shù)中調(diào)用super(parentConstructorParams),傳遞任意需要的參數(shù)。
引用父類的方法。在常規(guī)方法定義中,派生類可以使用點(diǎn)運(yùn)算符來引用父類的方法:super.methodName。
我們的 FatFreeFood 演示了這兩種情況:
在構(gòu)造函數(shù)中,我們簡單的調(diào)用了super,并將脂肪的量傳入為0。
在我們的print方法中,我們先調(diào)用了super.print,之后才添加了其他的邏輯。
不管你信不信,我反正是信了以上說的已涵蓋了有關(guān)class的基礎(chǔ)語法,這就是你開始實(shí)驗(yàn)需要掌握的全部內(nèi)容。
深入學(xué)習(xí)原型現(xiàn)在我們開始關(guān)注class是怎么映射到JavaScript內(nèi)部的原型機(jī)制的。我們會(huì)關(guān)注以下幾點(diǎn):
使用構(gòu)造調(diào)用創(chuàng)建對象;
原型連接的本質(zhì);
屬性和方法委托;
使用原型模擬類。
使用構(gòu)造調(diào)用創(chuàng)建對象
構(gòu)造函數(shù)不是什么新鮮玩意兒。使用new關(guān)鍵字調(diào)用任意函數(shù)會(huì)使其返回一個(gè)對象 —— 這一步稱作創(chuàng)建了一個(gè)構(gòu)造調(diào)用,這種函數(shù)通常被稱作構(gòu)造器:
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; } // 使用 "new" 關(guān)鍵字調(diào)用 Food 方法,就是構(gòu)造調(diào)用,該操作會(huì)返回一個(gè)對象 const chicken_breast = new Food("Chicken Breast", 26, 0, 3.5); console.log(chicken_breast.protein) // 26 // 不用 "new" 調(diào)用 Food 方法,會(huì)返回 "undefined" const fish = Food("Halibut", 26, 0, 2); console.log(fish); // "undefined"
當(dāng)我們使用new關(guān)鍵字調(diào)用函數(shù)時(shí),JS內(nèi)部執(zhí)行了下面四個(gè)步驟:
創(chuàng)建一個(gè)新對象(這里稱它為O);
給O賦予一個(gè)連接到其他對象的鏈接,稱為原型;
將函數(shù)的this引用指向O;
函數(shù)隱式返回O。
在第三步和第四步之間,引擎會(huì)執(zhí)行你函數(shù)中的具體邏輯。
知道了這一點(diǎn),我們就可以重寫Food方法,使之不用new關(guān)鍵字也能工作:
"use strict"; // 演示示例:消除對 "new" 關(guān)鍵字的依賴 function Food (name, protein, carbs, fat) { // 第一步:創(chuàng)建新對象 const obj = { }; // 第二步:鏈接原型——我們在下文會(huì)更加具體地探究原型的概念 Object.setPrototypeOf(obj, Food.prototype); // 第三步:設(shè)置 "this" 指向我們的新對象 // 盡然我們不能再運(yùn)行的執(zhí)行上下文中重置 `this` // 我們在使用 "obj" 取代 "this" 來模擬第三步 obj.name = name; obj.protein = protein; obj.carbs = carbs; obj.fat = fat; // 第四步:返回新創(chuàng)建的對象 return obj; } const fish = Food("Halibut", 26, 0, 2); console.log(fish.protein); // 26
四步中的三步都是簡單明了的。創(chuàng)建一個(gè)對象、賦值屬性、然后寫一個(gè)return聲明,這些操作對大多數(shù)開發(fā)者來說沒有理解上的問題——然而這就是難倒眾人的黑魔法原型。
直觀理解原型鏈在通常情況下,JavaScript中的包括函數(shù)在內(nèi)的所有對象都會(huì)鏈接到另一個(gè)對象上,這就是原型。
如果我們訪問一個(gè)對象本身沒有的屬性,JavaScript就會(huì)在對象的原型上檢查該屬性。換句話說,如果你對一個(gè)對象請求它沒有的屬性,它會(huì)對你說:『這個(gè)我不知道,問我的原型吧』。
在另一個(gè)對象上查找不存在屬性的過程稱作委托。
"use strict"; // joe 沒有 toString 方法…… const joe = { name : "Joe" }, sara = { name : "Sara" }; Object.hasOwnProperty(joe, toString); // false Object.hasOwnProperty(sara, toString); // false // ……但我們還是可以調(diào)用它! joe.toString(); // "[object Object]",而不是引用錯(cuò)誤! sara.toString(); // "[object Object]",而不是引用錯(cuò)誤!
盡管我們的 toString 的輸出完全沒啥用,但請注意:這段代碼沒有引起任何的ReferenceError!這是因?yàn)楸M管joe和sara沒有toString的屬性,但他們的原型有啊。
當(dāng)我們尋找sara.toString()方法時(shí),sara說:『我沒有toString屬性,找我的原型吧』。正如上文所說,JavaScript會(huì)親切的詢問Object.prototype 是否含有toString屬性。由于原型上有這一屬性,JS 就會(huì)把Object.prototype上的toString返回給我們程序并執(zhí)行。
sara本身沒有屬性沒關(guān)系——我們會(huì)把查找操作委托到原型上。
換言之,我們就可以訪問到對象上并不存在的屬性,只要其的原型上有這些屬性。我們可以利用這一點(diǎn)將屬性和方法賦值到對象的原型上,然后我們就可以調(diào)用這些屬性,好像它們真的存在在那個(gè)對象上一樣。
更給力的是,如果幾個(gè)對象共享相同的原型——正如上面的joe和sara的例子一樣——當(dāng)我們給原型賦值屬性之后,它們就都可以訪問了,無需將這些屬性多帶帶拷貝到每一個(gè)對象上。
這就是為何大家把它稱作原型繼承——如果我的對象沒有,但對象的原型有,那我的對象也能繼承這個(gè)屬性。
事實(shí)上,這里并沒有發(fā)生什么『繼承』。在面向類的語言里,繼承指從父類復(fù)制屬性到子類的行為。在JavaScript里,沒發(fā)生這種復(fù)制的操作,事實(shí)上這就是原型繼承與類繼承相比的一個(gè)主要優(yōu)勢。
在我們探究原型究竟是怎么來的之前,我們先做一個(gè)簡要回顧:
joe和sara沒有『繼承』一個(gè)toString的屬性;
joe和sara實(shí)際上根本沒有從Object.prototype上『繼承』;
joe和sara是鏈接到了Object.prototype上;
joe和sara鏈接到了同一個(gè)Object.prototype上。
如果想找到一個(gè)對象的(我們稱它作O)原型,我們可以使用 Object.getPrototypeof(O)。
然后我們再強(qiáng)調(diào)一遍:對象沒有『繼承自』他們的原型。他們只是委托到原型上。
以上。
接下來讓我們深入一下。
設(shè)置對象的原型我們已了解到基本上每個(gè)對象(下文以O(shè)指代)都有原型(下文以P指代),然后當(dāng)我們查找O上沒有的屬性,JavaScript引擎就會(huì)在P上尋找這個(gè)屬性。
至此我們有兩個(gè)問題:
以上情況函數(shù)怎么玩?
這些原型是從哪里來的?
名為Object的函數(shù)
在JavaScript引擎執(zhí)行程序之前,它會(huì)創(chuàng)建一個(gè)環(huán)境讓程序在內(nèi)部執(zhí)行,在執(zhí)行環(huán)境中會(huì)創(chuàng)建一個(gè)函數(shù),叫做Object, 以及一個(gè)關(guān)聯(lián)對象,叫做Object.prototype。
換句話說,Object和Object.prototype在任意執(zhí)行中的JavaScript程序中永遠(yuǎn)存在。
這個(gè)Object乍一看好像和其他函數(shù)沒什么區(qū)別,但特別之處在于它是一個(gè)構(gòu)造器——在調(diào)用它時(shí)返回一個(gè)新對象:
"use strict"; typeof new Object(); // "object" typeof Object(); // 這個(gè) Object 函數(shù)的特點(diǎn)是不需要使用 new 關(guān)鍵字調(diào)用
這個(gè)Object.prototype對象是個(gè)……對象。正如其他對象一樣,它有屬性。
關(guān)于Object和Object.prototype你需要知道以下幾點(diǎn):
Object函數(shù)有一個(gè)叫做.prototype的屬性,指向一個(gè)對象(Object.prototype);
Object.prototype對象有一個(gè)叫做.constructor的屬性,指向一個(gè)函數(shù)(Object)。
實(shí)際上,這個(gè)總體方案對于JavaScript中的所有函數(shù)都是適用的。當(dāng)我們創(chuàng)建一個(gè)函數(shù)——下文稱作 someFunction——這個(gè)函數(shù)就會(huì)有一個(gè)屬性.prototype,指向一個(gè)叫做someFunction.prototype 的對象。
與之相反,someFunction.prototype對象會(huì)有一個(gè)叫做.contructor的屬性,它的引用指回函數(shù)someFunction。
"use strict"; function foo () { console.log("Foo!"); } console.log(foo.prototype); // 指向一個(gè)叫 "foo" 的對象 console.log(foo.prototype.constructor); // 指向 "foo" 函數(shù) foo.prototype.constructor(); // 輸出 "Foo!" —— 僅為證明確實(shí)有 "foo.prototype.constructor" 這么個(gè)方法且指向原函數(shù)
需要記住以下幾個(gè)要點(diǎn):
所有的函數(shù)都有一個(gè)屬性,叫做 .prototype,它指向這個(gè)函數(shù)的關(guān)聯(lián)對象。
所有函數(shù)的原型都有一個(gè)屬性,叫做 .constructor,它指向這個(gè)函數(shù)本身。
一個(gè)函數(shù)原型的 .constructor 并非必須指向創(chuàng)建這個(gè)函數(shù)原型的函數(shù)……有點(diǎn)繞,我們等下會(huì)深入探討一下。
設(shè)置函數(shù)的原型有一些規(guī)則,在開始之前,我們先概括設(shè)置對象原型的三個(gè)規(guī)則:
『默認(rèn)』規(guī)則;
使用new隱式設(shè)置原型;
使用Object.create顯式設(shè)置原型。
默認(rèn)規(guī)則考慮下這段代碼:
"use strict"; const foo = { status : "foobar" };
十分簡單,我們做的事兒就是創(chuàng)建一個(gè)叫foo的對象,然后給他一個(gè)叫status的屬性。
然后JavaScript在幕后多做了點(diǎn)工作。當(dāng)我們在字面上創(chuàng)建一個(gè)對象時(shí),JavaScript將對象的原型指向Object.prototype并設(shè)置其原型的.constructor指向Object:
"use strict"; const foo = { status : "foobar" }; Object.getPrototypeOf(foo) === Object.prototype; // true foo.constructor === Object; // true使用new隱式設(shè)置原型
讓我們再看下之前調(diào)整過的 Food 例子。
"use strict"; function Food (name, protein, carbs, fat) { this.name = name; this.protein = protein; this.carbs = carbs; this.fat = fat; }
現(xiàn)在我們知道函數(shù)Food將會(huì)與一個(gè)叫做Food.prototype的對象關(guān)聯(lián)。
當(dāng)我們使用new關(guān)鍵字創(chuàng)建一個(gè)對象,JavaScript將會(huì):
設(shè)置這個(gè)對象的原型指向我們使用new調(diào)用的函數(shù)的.prototype屬性;
設(shè)置這個(gè)對象的.constructor指向我們使用new調(diào)用到的構(gòu)造函數(shù)。
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顯式設(shè)置原型
最后我們可以使用Object.create方法手工設(shè)置對象的原型引用。
"use strict"; const foo = { speak () { console.log("Foo!"); } }; const bar = Object.create(foo); bar.speak(); // "Foo!" Object.getPrototypeOf(bar) === foo; // true
還記得使用new調(diào)用函數(shù)的時(shí)候,JavaScript在幕后干了哪四件事兒嗎?Object.create就干了這三件事兒:
創(chuàng)建一個(gè)新對象;
設(shè)置它的原型引用;
返回這個(gè)新對象。
你可以自己去看下MDN上寫的那個(gè)polyfill。
(譯者注:polyfill就是給老代碼實(shí)現(xiàn)現(xiàn)有新功能的補(bǔ)丁代碼,這里就是指老版本JS沒有Object.create函數(shù),MDN上有手工擼的一個(gè)替代方案)
直接使用原型來模擬面向類的行為需要一些技巧。
"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); } // 設(shè)置 "subclass" 關(guān)系 // ===================== // LINE A :: 使用 Object.create 手動(dòng)設(shè)置 FatFreeFood"s 『父類』. FatFreeFood.prototype = Object.create(Food.prototype); // LINE B :: 手工重置 constructor 的引用 Object.defineProperty(FatFreeFood.constructor, "constructor", { enumerable : false, writeable : true, value : FatFreeFood });
在Line A,我們需要設(shè)置FatFreeFood.prototype使之等于一個(gè)新對象,這個(gè)新對象的原型引用是Food.prototype。如果沒這么搞,我們的子類就不能訪問『超類』的方法。
不幸的是,這個(gè)導(dǎo)致了相當(dāng)詭異的結(jié)果:FatFreeFood.constructor是Function,而不是FatFreeFood。為了保證一切正常,我們需要在Line B手工設(shè)置FatFreeFood.constructor。
讓開發(fā)者從使用原型對類行為笨拙的模仿中脫離苦海是class關(guān)鍵字的產(chǎn)生動(dòng)機(jī)之一。它確實(shí)也提供了避免原型語法常見陷阱的解決方案。
現(xiàn)在我們已經(jīng)探究了太多關(guān)于JavaScript的原型機(jī)制,你應(yīng)該更容易理解class關(guān)鍵字讓一切變得多么簡單了吧!
深入探究下方法現(xiàn)在我們已了解到JavaScript原型系統(tǒng)的必要性,我們將深入探究一下類支持的三種方法,以及一種特殊情況,以結(jié)束本文的討論。
構(gòu)造器;
靜態(tài)方法;
原型方法;
一種原型方法的特殊情況:『標(biāo)記方法』。
并非我提出的這三組方法,這要?dú)w功于Rauschmayer博士在探索ES6一書中的定義。
類構(gòu)造器一個(gè)類的constructor方法用于關(guān)注我們的初始化邏輯,constructor方法有以下幾個(gè)特殊點(diǎn):
只有在構(gòu)造方法里,我們才可以調(diào)用父類的構(gòu)造器;
它在背后處理了所有設(shè)置原型鏈的工作;
它被用作類的定義。
第二點(diǎn)就是在JavaScript中使用class的一個(gè)主要好處,我們來引用一下《探索 ES6》書里的15.2.3.1 的標(biāo)題:
子類的原型就是超類
正如我們所見,手工設(shè)置非常繁瑣且容易出錯(cuò)。如果我們使用class關(guān)鍵字,JavaScript在內(nèi)部會(huì)負(fù)責(zé)搞定這些設(shè)置,這一點(diǎn)也是使用class的優(yōu)勢。
第三點(diǎn)有點(diǎn)意思。在JavaScript中類僅僅是個(gè)函數(shù)——它等同于與類中的constructor方法。
"use strict"; class Food { // 和之前一樣的類定義…… } typeof Food; // "function"
與一般把函數(shù)作為構(gòu)造器的方式不同,我們不能不用new關(guān)鍵字而直接調(diào)用類構(gòu)造器:
const burrito = Food("Heaven", 100, 100, 25); // 類型錯(cuò)誤
這就引發(fā)了另一個(gè)問題:當(dāng)我們不用new調(diào)用函數(shù)構(gòu)造器的時(shí)候發(fā)生了什么?
簡短的回答是:對于任何沒有顯式返回的函數(shù)來說都是返回undefined。我們只需要相信用我們構(gòu)造函數(shù)的用戶都會(huì)使用構(gòu)造調(diào)用。這就是社區(qū)為何約定構(gòu)造方法的首字母大寫:提醒使用者要用new來調(diào)用。
"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"
長一點(diǎn)的回答是:返回undefined,除非你手工檢測是否使用被new調(diào)用,然后進(jìn)行自己的處理。
ES2015引入了一個(gè)屬性使得這種檢測變得簡單: new.target.
new.target是一個(gè)定義在所有使用new調(diào)用的函數(shù)上的屬性,包括類構(gòu)造器。 當(dāng)我們使用new關(guān)鍵字調(diào)用函數(shù)時(shí),函數(shù)體內(nèi)的new.target的值就是這個(gè)函數(shù)本身。如果函數(shù)沒有被new調(diào)用,這個(gè)值就是undefined。
"use strict"; // 強(qiáng)行構(gòu)造調(diào)用 function Food (name, protein, carbs, fat) { // 如果用戶忘了手工調(diào)用一下 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); // 糟了,不過沒關(guān)系! 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的更多細(xì)節(jié),而且給有興趣者配上了ES2015規(guī)范作為參考。規(guī)范里有關(guān) [[Construct]] 的描述很有啟發(fā)性。
靜態(tài)方法靜態(tài)方法是構(gòu)造方法自己的方法,不能被類的實(shí)例化對象調(diào)用。我們使用static關(guān)鍵字定義靜態(tài)方法。
"use strict"; class Food { // 和之前一樣…… // 添加靜態(tài)方法 static describe () { console.log(""Food" 是一種存儲(chǔ)了營養(yǎng)信息的數(shù)據(jù)類型"); } } Food.describe(); // ""Food" 是一種存儲(chǔ)了營養(yǎng)信息的數(shù)據(jù)類型"
靜態(tài)方法與老式構(gòu)造函數(shù)中直接屬性賦值相似:
"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(`你創(chuàng)建了 ${Food.count} 個(gè) food`); }; const dummy = new Food(); Food.describe(); // "你創(chuàng)建了 1 個(gè) food"原型方法
任何不是構(gòu)造方法和靜態(tài)方法的方法都是原型方法。之所以叫原型方法,是因?yàn)槲覀冎巴ㄟ^給構(gòu)造函數(shù)的原型上附加方法的方式來實(shí)現(xiàn)這一功能。
"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; } // 『原型方法』的命名大概來自我們之前通過給構(gòu)造函數(shù)的原型上附加方法的方式來實(shí)現(xiàn)這一功能。 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() ); };
應(yīng)該說明,在方法定義時(shí)完全可以使用生成器。
"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. }標(biāo)志方法
最后我們說說標(biāo)志方法。這是一些名為Symbol值的方法,當(dāng)我們在自定義對象中使用內(nèi)置構(gòu)造器時(shí),JavaScript引擎可以識(shí)別并使用這些方法。
MDN文檔提供了一個(gè)Symbol是什么的簡要概覽:
Symbol是一個(gè)唯一且不變的數(shù)據(jù)類型,可以作為一個(gè)對象的屬性標(biāo)示符。
創(chuàng)建一個(gè)新的symbol,會(huì)給我們提供一個(gè)被認(rèn)為是程序里的唯一標(biāo)識(shí)的值。這一點(diǎn)對于命名對象的屬性十分有用:我們可以確保不會(huì)不小心覆蓋任何屬性。使用Symbol做鍵值也不是無數(shù)的,所以他們很大程度上對外界是不可見的(也不完全是,可以通過Reflect.ownKeys獲得)
"use strict"; const secureObject = { // 這個(gè)鍵可以看作是唯一的 [new Symbol("name")] : "Dr. Secure A. F." }; console.log( Object.getKeys(superSecureObject) ); // [] -- 標(biāo)志屬性不太好獲取 console.log( Reflect.ownKeys(secureObject) ); // [Symbol("name")] -- 但也不是完全隱藏的
對我們來講更有意思的是,這給我們提供了一種方式來告訴 JavaScript 引擎使用特定方法來達(dá)到特定的目的。
所謂的『眾所周知的Symbol』是一些特定對象的鍵,當(dāng)你在定義對象中使用時(shí)他們時(shí),JavaScript引擎會(huì)觸發(fā)一些特定方法。
這對于JavaScript來說有點(diǎn)怪異,我們還是看個(gè)例子吧:
"use strict"; // 繼承 Array 可以讓我們直觀的使用 "length" // 同時(shí)可以讓我們訪問到內(nèi)置方法,如 // map、filter、reduce、push、pop 等 class FoodSet extends Array { // foods 把傳遞的任意參數(shù)收集為一個(gè)數(shù)組 // 參見: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)) } // 自定義迭代器行為,請注意,這不是多么好用的迭代器,但是個(gè)不錯(cuò)的例子 // 鍵名前必須寫星號(hào) * [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}`; } } } // 當(dāng)我們的用戶使用內(nèi)置的數(shù)組方法,返回一個(gè)數(shù)組類型對象 // 而不是 FoodSet 類型的。這使得我們的 FoodSet 可以被一些 // 期望操作數(shù)組的代碼操作 static get [Symbol.species] () { return Array; } } const foodset = new FoodSet(new Food("Fish", 26, 0, 16), new Food("Hamburger", 26, 48, 24)); // 當(dāng)我們使用 for ... of 操作 FoodSet 時(shí),JavaScript 將會(huì)使用 // 我們之前用 [Symbol.iterator] 做鍵值的方法 for (let food of foodset) { // 打印全部 food console.log( food ); } // 當(dāng)我們執(zhí)行數(shù)組的 `filter` 方法時(shí),JavaScript 創(chuàng)建并返回一個(gè)新對象 // 我們在什么對象上執(zhí)行 `filter` 方法,新對象就使用這個(gè)對象作為默認(rèn)構(gòu)造器來創(chuàng)建 // 然而大部分代碼都希望 filter 返回一個(gè)數(shù)組,于是我們通過重寫 [Symbol.species] // 的方式告訴 JavaScript 使用數(shù)組的構(gòu)造器 const healthy_foods = foodset.filter((food) => food.name !== "Hamburger"); console.log( healthy_foods instanceof FoodSet ); // console.log( healthy_foods instanceof Array );
當(dāng)你使用for...of遍歷一個(gè)對象時(shí),JavaScript將會(huì)嘗試執(zhí)行對象的迭代器方法,這一方法就是該對象 Symbol.iterator屬性上關(guān)聯(lián)的方法。如果我們提供了自己的方法定義,JavaScript就會(huì)使用我們自定義的。如果沒有自己制定的話,如果有默認(rèn)的實(shí)現(xiàn)就用默認(rèn)的,沒有的話就不執(zhí)行。
Symbo.species更奇異了。在自定義的類中,默認(rèn)的Symbol.species函數(shù)就是類的構(gòu)造函數(shù)。當(dāng)我們的子類有內(nèi)置的集合(例如Array和Set)時(shí),我們通常希望在使用父類的實(shí)例時(shí)也能使用子類。
通過方法返回父類的實(shí)例而不是派生類的實(shí)例,使我們更能確保我們子類在大多數(shù)代碼里的可用性。而Symbol.species可以實(shí)現(xiàn)這一功能。
如果不怎么需要這個(gè)功能就別費(fèi)力去搞了。Symbol的這種用法——或者說有關(guān)Symbol的全部用法——都還比較罕見。這些例子只是為了演示:
我們可以在自定義類中使用JavaScript內(nèi)置的特定構(gòu)造器;
用兩個(gè)普通的例子展示了怎么實(shí)現(xiàn)這一點(diǎn)。
結(jié)論ES2015的class關(guān)鍵字沒有帶給我們 Java 里或是SmallTalk里那種『真正的類』。寧可說它只是提供了一種更加方便的語法來創(chuàng)建通過原型關(guān)聯(lián)的對象,本質(zhì)上沒有什么新東西。
使用ES6寫更好的JavaScript part III:好用的集合和反引號(hào) 簡介ES2015發(fā)生了一些重大變革,像promises和generators. 但并非新標(biāo)準(zhǔn)的一切都高不可攀。 – 相當(dāng)一部分新特性可以快速上手。
在這篇文章里,我們來看下新特性帶來的好處:
新的集合: map,weakmap,set, weakset
大部分的new String methods
模板字符串。
我們開始這個(gè)系列的最后一章吧。
模板字符串模板字符串 解決了三個(gè)痛點(diǎn),允許你做如下操作:
定義在字符串內(nèi)部的表達(dá)式,稱為 字符串插值。
寫多行字符串無須用換行符 (n) 拼接。
使用“raw”字符串 – 在反斜杠內(nèi)的字符串不會(huì)被轉(zhuǎn)義,視為常量。
“use strict”; /* 三個(gè)模板字符串的例子: 字符串插值,多行字符串,raw 字符串。 ================================= */ // ================================== // 1. 字符串插值 :: 解析任何一個(gè)字符串中的表達(dá)式。 console.log(1 + 1 = ${1 + 1}); // ================================== // 2. 多行字符串 :: 這樣寫: let childe_roland = I saw them and I knew them all. And yet
Dauntless the slug-horn to my lips I set,
And blew “Childe Roland to the Dark Tower came.” // … 代替下面的寫法: child_roland = ‘I saw them and I knew them all. And yet ’ + ‘Dauntless the slug-horn to my lips I set, ’ + ‘And blew “Childe Roland to the Dark Tower came.”’; // ================================== // 3. raw 字符串 :: 在字符串前加 raw 前綴,javascript 會(huì)忽略轉(zhuǎn)義字符。 // 依然會(huì)解析包在 ${} 的表達(dá)式 const unescaped = String.rawThis ${string()} doesn"t contain a newline! function string () { return “string”; } console.log(unescaped); // ‘This string doesn’t contain a newline! ’ – 注意 會(huì)被原樣輸出 // 你可以像 React 使用 JSX 一樣,用模板字符串創(chuàng)建 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); // 這樣使用略顯笨,自己試試吧! // 另一個(gè)常用的例子是打印變量名: const user = { name : ‘Joe’ }; console.log(“User’s name is ” + user.name + “.”); // 有點(diǎn)冗長 console.log(User"s name is ${user.name}.); // 這樣稍好一些
使用字符串插值,用反引號(hào)代替引號(hào)包裹字符串,并把我們想要的表達(dá)式嵌入在${}中。
對于多行字符串,只需要把你要寫的字符串包裹在反引號(hào)里,在要換行的地方直接換行。 JavaScript 會(huì)在換行處插入新行。
使用原生字符串,在模板字符串前加前綴String.raw,仍然使用反引號(hào)包裹字符串。
模板字符串或許只不過是一種語法糖 … 但它比語法糖略勝一籌。
新的字符串方法ES2015也給String新增了一些方法。他們主要?dú)w為兩類:
通用的便捷方法
擴(kuò)充 Unicode 支持的方法。
在本文里我們只講第一類,同時(shí)unicode特定方法也有相當(dāng)好的用例 。如果你感興趣的話,這是地址在MDN的文檔里,有一個(gè)關(guān)于字符串新方法的完整列表。
startsWith & endsWith對新手而言,我們有String.prototype.startsWith。 它對任何字符串都有效,它需要兩個(gè)參數(shù):
一個(gè)是 search string 還有
整形的位置參數(shù) n。這是可選的。
String.prototype.startsWith方法會(huì)檢查以nth位起的字符串是否以search string開始。如果沒有位置參數(shù),則默認(rèn)從頭開始。
如果字符串以要搜索的字符串開頭返回 true,否則返回 false。
"use strict"; const contrived_example = "This is one impressively contrived example!"; // 這個(gè)字符串是以 "This is one" 開頭嗎? console.log(contrived_example.startsWith("This is one")); // true // 這個(gè)字符串的第四個(gè)字符以 "is" 開頭? console.log(contrived_example.startsWith("is", 4)); // false // 這個(gè)字符串的第五個(gè)字符以 "is" 開始? console.log(contrived_example.startsWith("is", 5)); // trueendsWith
String.prototype.endsWith和startswith相似: 它也需要兩個(gè)參數(shù):一個(gè)是要搜索的字符串,一個(gè)是位置。
然而String.prototype.endsWith位置參數(shù)會(huì)告訴函數(shù)要搜索的字符串在原始字符串中被當(dāng)做結(jié)尾處理。
換句話說,它會(huì)切掉nth后的所有字符串,并檢查是否以要搜索的字符結(jié)尾。
"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 // 通常情況下,傳一個(gè)位置參數(shù)向下面這樣: function substringEndsWith (string, search_string, position) { // Chop off the end of the string const substring = string.slice(0, position); // 檢查被截取的字符串是否已 search_string 結(jié)尾 return substring.endsWith(search_string); }includes
ES2015也添加了String.prototype.includes。 你需要用字符串調(diào)用它,并且要傳遞一個(gè)搜索項(xiàng)。如果字符串包含搜索項(xiàng)會(huì)返回true,反之返回false。
"use strict"; const contrived_example = "This is one impressively contrived example!"; // 這個(gè)字符串是否包含單詞 impressively ? contrived_example.includes("impressively"); // true
ES2015之前,我們只能這樣:
"use strict"; contrived_example.indexOf("impressively") !== -1 // true
不算太壞。但是,String.prototype.includes是 一個(gè)改善,它屏蔽了任意整數(shù)返回值為true的漏洞。
repeat還有String.prototype.repeat??梢詫θ我庾址褂?,像includes一樣,它會(huì)或多或少地完成函數(shù)名指示的工作。
它只需要一個(gè)參數(shù): 一個(gè)整型的count。使用案例說明一切,上代碼:
const na = "na"; console.log(na.repeat(5) + ", Batman!"); // "nanananana, Batman!"raw
最后,我們有String.raw,我們在上面簡單介紹過。
一個(gè)模板字符串以 String.raw 為前綴,它將不會(huì)在字符串中轉(zhuǎn)義:
/* 單右斜線要轉(zhuǎn)義,我們需要雙右斜線才能打印一個(gè)右斜線, 在普通字符串里會(huì)被解析為換行 * */ console.log("This string has fewer backslashes and breaks the line."); // 不想這樣寫的話用 raw 字符串 String.raw`This string has too many backslashes and doesn"t break the line.`Unicode方法
雖然我們不涉及剩余的 string 方法,但是如果我不告訴你去這個(gè)主題的必讀部分就會(huì)顯得我疏忽。
Dr Rauschmayer對于Unicode in JavaScript的介紹
他關(guān)于ES2015’s Unicode Support in Exploring ES6和The Absolute Minimum Every Software Developer Needs to Know About Unicode 的討論。
無論如何我不得不跳過它的最后一部分。雖然有些老但是還是有優(yōu)點(diǎn)的。
這里是文檔中缺失的字符串方法,這樣你會(huì)知道缺哪些東西了。
String.fromCodePoint & String.prototype.codePointAt;
String.prototype.normalize;
Unicode point escapes.
集合ES2015新增了一些集合類型:
Map和WeakMap
Set和WeakSet。
合適的Map和Set類型十分方便使用,還有弱變量是一個(gè)令人興奮的改動(dòng),雖然它對Javascript來說像舶來品一樣。
Mapmap就是簡單的鍵值對。最簡單的理解方式就是和object類似,一個(gè)鍵對應(yīng)一個(gè)值。
"use strict"; // 我們可以把 foo 當(dāng)鍵,bar 當(dāng)值 const obj = { foo : "bar" }; // 對象鍵為 foo 的值為 bar obj.foo === "bar"; // true
新的Map類型在概念上是相似的,但是可以使用任意的數(shù)據(jù)類型作為鍵 – 不止strings和symbols–還有除了pitfalls associated with trying to use an objects a map的一些東西。
下面的片段例舉了 Map 的 API.
"use strict"; // 構(gòu)造器 let scotch_inventory = new Map(); // BASIC API METHODS // Map.prototype.set (K, V) :: 創(chuàng)建一個(gè)鍵 K,并設(shè)置它的值為 V。 scotch_inventory.set("Lagavulin 18", 2); scotch_inventory.set("The Dalmore", 1); // 你可以創(chuàng)建一個(gè) map 里面包含一個(gè)有兩個(gè)元素的數(shù)組 scotch_inventory = new Map([["Lagavulin 18", 2], ["The Dalmore", 1]]); // 所有的 map 都有 size 屬性,這個(gè)屬性會(huì)告訴你 map 里有多少個(gè)鍵值對。 // 用 Map 或 Set 的時(shí)候,一定要使用 size ,不能使用 length console.log(scotch_inventory.size); // 2 // Map.prototype.get(K) :: 返回鍵相關(guān)的值。如果鍵不存在返回 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 提供了多種方法遍歷鍵值。 // 重置值,繼續(xù)探索 scotch_inventory.set("Lagavulin 18", 1); scotch_inventory.set("Glenfiddich 18", 1); /* Map.prototype.forEach(callback[, thisArg]) :: 對 map 里的每個(gè)鍵值對執(zhí)行一個(gè)回調(diào)函數(shù) * 你可以在回調(diào)函數(shù)內(nèi)部設(shè)置 "this" 的值,通過傳遞一個(gè) thisArg 參數(shù),那是可選的而且沒有太大必要那樣做 * 最后,注意回調(diào)函數(shù)已經(jīng)被傳了鍵和值 */ scotch_inventory.forEach(function (quantity, scotch) { console.log(`Excuse me while I sip this ${scotch}.`); }); // Map.prototype.keys() :: 返回一個(gè) 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 的所有鍵值對,提供一個(gè)包含兩個(gè)元素的數(shù)組 // 以后會(huì)經(jīng)??吹?map 里的鍵值對和 "entries" 關(guān)聯(lián) const entries = scotch_inventory.entries(); for (let entry of entries) { console.log(`I remember! I drank ${entry[1]} bottle of ${entry[0]}!`); }
但是Object在保存鍵值對的時(shí)候仍然有用。 如果符合下面的全部條件,你可能還是想用Object:
當(dāng)你寫代碼的時(shí)候,你知道你的鍵值對。
你知道你可能不會(huì)去增加或刪除你的鍵值對。
你使用的鍵全都是 string 或 symbol。
另一方面,如果符合以下任意條件,你可能會(huì)想使用一個(gè) map。
你需要遍歷整個(gè)map – 然而這對 object 來說是難以置信的.
當(dāng)你寫代碼的時(shí)候不需要知道鍵的名字或數(shù)量。
你需要復(fù)雜的鍵,像 Object 或 別的 Map (!).
像遍歷一個(gè)map一樣遍歷一個(gè)object是可行的,但奇妙的是–還會(huì)有一些坑潛伏在暗處。 Map更容易使用,并且增加了一些可集成的優(yōu)勢。然而object是以隨機(jī)順序遍歷的,map是以插入的順序遍歷的。
添加隨意動(dòng)態(tài)鍵名的鍵值對給一個(gè)object是可行的。但奇妙的是: 比如說如果你曾經(jīng)遍歷過一個(gè)偽 map,你需要記住手動(dòng)更新條目數(shù)。
最后一條,如果你要設(shè)置的鍵名不是string或symbol,你除了選擇Map別無選擇。
上面的這些只是一些指導(dǎo)性的意見,并不是最好的規(guī)則。
WeakMap你可能聽說過一個(gè)特別棒的特性垃圾回收器,它會(huì)定期地檢查不再使用的對象并清除。
To quote Dr Rauschmayer:
WeakMap 不會(huì)阻止它的鍵值被垃圾回收。那意味著你可以把數(shù)據(jù)和對象關(guān)聯(lián)起來不用擔(dān)心內(nèi)存泄漏。
換句換說,就是你的程序丟掉了WeakMap鍵的所有外部引用,他能自動(dòng)垃圾回收他們的值。
盡管大大簡化了用例,考慮到SPA(單頁面應(yīng)用) 就是用來展示用戶希望展示的東西,像一些物品描述和一張圖片,我們可以理解為API返回的JSON。
理論上來說我們可以通過緩存響應(yīng)結(jié)果來減少請求服務(wù)器的次數(shù)。我們可以這樣用Map :
"use strict"; const cache = new Map(); function put (element, result) { cache.set(element, result); } function retrieve (element) { return cache.get(element); }
… 這是行得通的,但是有內(nèi)存泄漏的危險(xiǎn)。
因?yàn)檫@是一個(gè)SPA,用戶或許想離開這個(gè)視圖,這樣的話我們的 “視圖”object就會(huì)失效,會(huì)被垃圾回收。
不幸的是,如果你使用的是正常的Map ,當(dāng)這些object不使用時(shí),你必須自行清除。
使用WeakMap替代就可以解決上面的問題:
"use strict"; const cache = new WeakMap(); // 不會(huì)再有內(nèi)存泄露了 // 剩下的都一樣
這樣當(dāng)應(yīng)用失去不需要的元素的引用時(shí),垃圾回收系統(tǒng)可以自動(dòng)重用那些元素。
WeakMap的API和Map相似,但有如下幾點(diǎn)不同:
在WeakMap里你可以使用object作為鍵。 這意味著不能以String和Symbol做鍵。
WeakMap只有set,get,has,和delete方法 – 那意味著你不能遍歷weak map.
WeakMaps沒有size屬性。
不能遍歷或檢查WeakMap的長度的原因是,在遍歷過程中可能會(huì)遇到垃圾回收系統(tǒng)的運(yùn)行: 這一瞬間是滿的,下一秒就沒了。
這種不可預(yù)測的行為需要謹(jǐn)慎對待,TC39(ECMA第39屆技術(shù)委員會(huì))曾試圖避免禁止WeakMap的遍歷和長度檢測。
其他的案例,可以在這里找到Use Cases for WeakMap,來自Exploring ES6.
SetSet就是只包含一個(gè)值的集合。換句換說,每個(gè)set的元素只會(huì)出現(xiàn)一次。
這是一個(gè)有用的數(shù)據(jù)類型,如果你要追蹤唯一并且固定的object ,比如說聊天室的當(dāng)前用戶。
Set和Map有完全相同的API。主要的不同是Set沒有set方法,因?yàn)樗荒艽鎯?chǔ)鍵值對。剩下的幾乎相同。
"use strict"; // 構(gòu)造器 let scotch_collection = new Set(); // 基本的 API 方法 // Set.prototype.add (O) :: 和 set 一樣,添加一個(gè)對象 scotch_collection.add("Lagavulin 18"); scotch_collection.add("The Dalmore"); // 你也可以用數(shù)組構(gòu)造一個(gè) set scotch_collection = new Set(["Lagavulin 18", "The Dalmore"]); // 所有的 set 都有一個(gè) length 屬性。這個(gè)屬性會(huì)告訴你 set 里有多少對象 // 用 set 或 map 的時(shí)候,一定記住用 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 提供了多種方法遍歷 * 重新設(shè)置值,繼續(xù)探索 */ scotch_collection.add("Lagavulin 18"); scotch_collection.add("Glenfiddich 18"); /* Set.prototype.forEach(callback[, thisArg]) :: 執(zhí)行一個(gè)函數(shù),回調(diào)函數(shù) * set 里在每個(gè)的鍵值對。 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 的所有鍵值對,提供一個(gè)包含兩個(gè)元素的數(shù)組 * 這有點(diǎn)多余,但是這種方法可以保留 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
WeakSet相對于Set就像WeakMap相對于 Map :
在WeakSet里object的引用是弱類型的。
WeakSet沒有property屬性。
不能遍歷WeakSet。
Weak set的用例并不多,但是這兒有一些Domenic Denicola稱呼它們?yōu)椤皃erfect for branding” – 意思就是標(biāo)記一個(gè)對象以滿足其他需求。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/79610.html
摘要:前言在理想的狀態(tài)下,你可以在深入了解之前了解和開發(fā)的所有知識(shí)。繼承另一個(gè)類的類,通常稱為類或類,而正在擴(kuò)展的類稱為類或類。這種類型的組件稱為無狀態(tài)功能組件。在你有足夠的信心構(gòu)建用戶界面之后,最好學(xué)習(xí)。 原文地址:JavaScript Basics Before You Learn React 原文作者: Nathan Sebhastian 寫在前面 為了不浪費(fèi)大家的寶貴時(shí)間,在開...
摘要:從最開始的到封裝后的都在試圖解決異步編程過程中的問題。為了讓編程更美好,我們就需要引入來降低異步編程的復(fù)雜性。寫一個(gè)符合規(guī)范并可配合使用的寫一個(gè)符合規(guī)范并可配合使用的理解的工作原理采用回調(diào)函數(shù)來處理異步編程。 JavaScript怎么使用循環(huán)代替(異步)遞歸 問題描述 在開發(fā)過程中,遇到一個(gè)需求:在系統(tǒng)初始化時(shí)通過http獲取一個(gè)第三方服務(wù)器端的列表,第三方服務(wù)器提供了一個(gè)接口,可通過...
摘要:所以,打包工具就出現(xiàn)了,它可以幫助做這些繁瑣的工作。打包工具介紹僅介紹款主流的打包工具,,,,以發(fā)布時(shí)間為順序。它定位是模塊打包器,而屬于構(gòu)建工具。而且在其他的打包工具在處理非網(wǎng)頁文件比如等基本還是需要借助它來實(shí)現(xiàn)。 本文當(dāng)時(shí)寫在本地,發(fā)現(xiàn)換電腦很不是方便,在這里記錄下。 前端的打包工具 打包工具可以更好的管理html,css,javascript,使用可以錦上添花,不使用也沒關(guān)系...
摘要:的翻譯文檔由的維護(hù)很多人說,阮老師已經(jīng)有一本關(guān)于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發(fā)過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復(fù)雜性。 JavaScript Promise 迷你書(中文版) 超詳細(xì)介紹promise的gitbook,看完再不會(huì)promise...... 本書的目的是以目前還在制定中的ECMASc...
摘要:前言這里我們不討論作用域鏈的問題,這些問題您可以看下我之前寫的東西,通過這一段代碼,讓我們重新認(rèn)識(shí)。這回我們主要來分享一下,中作用域的創(chuàng)建方式。立即執(zhí)行函數(shù)是個(gè)不錯(cuò)的選擇,但具名的立即執(zhí)行函數(shù)可以讓代碼本身更具有可讀性,是個(gè)最佳實(shí)踐。 前言 這里我們不討論作用域鏈的問題,這些問題您可以看下我之前寫的東西,通過這一段代碼,讓我們重新認(rèn)識(shí)JavaScript。這回我們主要來分享一下,Jav...
閱讀 3506·2021-11-24 11:17
閱讀 2293·2021-11-15 11:38
閱讀 3376·2021-10-14 09:42
閱讀 2949·2019-08-30 15:54
閱讀 2036·2019-08-28 18:09
閱讀 548·2019-08-26 11:48
閱讀 1639·2019-08-26 10:48
閱讀 2160·2019-08-26 10:45