摘要:在編程文化中,我們有一個(gè)名為面向?qū)ο缶幊痰臇|西,這是一組技術(shù),使用對(duì)象和相關(guān)概念作為程序組織的中心原則。這是構(gòu)造器函數(shù)的作用。因此,上面的類(lèi)聲明等同于上一節(jié)中的構(gòu)造器定義。
來(lái)源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:The Secret Life of Objects
譯者:飛龍
協(xié)議:CC BY-NC-SA 4.0
自豪地采用谷歌翻譯
部分參考了《JavaScript 編程精解(第 2 版)》
抽象數(shù)據(jù)類(lèi)型是通過(guò)編寫(xiě)一種特殊的程序來(lái)實(shí)現(xiàn)的,該程序根據(jù)可在其上執(zhí)行的操作來(lái)定義類(lèi)型。
Barbara Liskov,《Programming with Abstract Data Types》
第 4 章介紹了 JavaScript 的對(duì)象(object)。 在編程文化中,我們有一個(gè)名為面向?qū)ο缶幊蹋∣OP)的東西,這是一組技術(shù),使用對(duì)象(和相關(guān)概念)作為程序組織的中心原則。
雖然沒(méi)有人真正同意其精確定義,但面向?qū)ο缶幊桃呀?jīng)成為了許多編程語(yǔ)言的設(shè)計(jì),包括 JavaScript 在內(nèi)。 本章將描述這些想法在 JavaScript 中的應(yīng)用方式。
封裝面向?qū)ο缶幊痰暮诵乃枷胧菍⒊绦蚍殖尚⌒推?,并讓每個(gè)片段負(fù)責(zé)管理自己的狀態(tài)。
通過(guò)這種方式,一些程序片段的工作方式的知識(shí)可以局部保留。 從事其他方面的工作的人,不必記住甚至不知道這些知識(shí)。 無(wú)論什么時(shí)候這些局部細(xì)節(jié)發(fā)生變化,只需要直接更新其周?chē)拇a。
這種程序的不同片段通過(guò)接口(interface),函數(shù)或綁定的有限集合交互,它以更抽象的級(jí)別提供有用的功能,并隱藏它的精確實(shí)現(xiàn)。
這些程序片段使用對(duì)象建模。 它們的接口由一組特定的方法(method)和屬性(property)組成。 接口的一部分的屬性稱(chēng)為公共的(public)。 其他外部代碼不應(yīng)該接觸屬性的稱(chēng)為私有的(private)。
許多語(yǔ)言提供了區(qū)分公共和私有屬性的方法,并且完全防止外部代碼訪問(wèn)私有屬性。 JavaScript 再次采用極簡(jiǎn)主義的方式,沒(méi)有。 至少目前還沒(méi)有 - 有個(gè)正在開(kāi)展的工作,將其添加到該語(yǔ)言中。
即使這種語(yǔ)言沒(méi)有內(nèi)置這種區(qū)別,JavaScript 程序員也成功地使用了這種想法。 通常,可用的接口在文檔或數(shù)字一中描述。 在屬性名稱(chēng)的的開(kāi)頭經(jīng)常會(huì)放置一個(gè)下劃線(_)字符,來(lái)表明這些屬性是私有的。
將接口與實(shí)現(xiàn)分離是一個(gè)好主意。 它通常被稱(chēng)為封裝(encapsulation)。
方法方法不過(guò)是持有函數(shù)值的屬性。 這是一個(gè)簡(jiǎn)單的方法:
let rabbit = {}; rabbit.speak = function(line) { console.log(`The rabbit says "${line}"`); }; rabbit.speak("I"m alive."); // → The rabbit says "I"m alive."
方法通常會(huì)在對(duì)象被調(diào)用時(shí)執(zhí)行一些操作。將函數(shù)作為對(duì)象的方法調(diào)用時(shí),會(huì)找到對(duì)象中對(duì)應(yīng)的屬性并直接調(diào)用。當(dāng)函數(shù)作為方法調(diào)用時(shí),函數(shù)體內(nèi)叫做this的綁定自動(dòng)指向在它上面調(diào)用的對(duì)象。
function speak(line) { console.log(`The ${this.type} rabbit says "${line}"`); } let whiteRabbit = {type: "white", speak: speak}; let fatRabbit = {type: "fat", speak: speak}; whiteRabbit.speak("Oh my ears and whiskers, " + "how late it"s getting!"); // → The white rabbit says "Oh my ears and whiskers, how // late it"s getting!" hungryRabbit.speak("I could use a carrot right now."); // → The hungry rabbit says "I could use a carrot right now."
你可以把this看作是以不同方式傳遞的額外參數(shù)。 如果你想顯式傳遞它,你可以使用函數(shù)的call方法,它接受this值作為第一個(gè)參數(shù),并將其它處理為看做普通參數(shù)。
speak.call(hungryRabbit, "Burp!"); // → The hungry rabbit says "Burp!"
這段代碼使用了關(guān)鍵字this來(lái)輸出正在說(shuō)話的兔子的種類(lèi)。我們回想一下apply和bind方法,這兩個(gè)方法接受的第一個(gè)參數(shù)可以用來(lái)模擬對(duì)象中方法的調(diào)用。這兩個(gè)方法會(huì)把第一個(gè)參數(shù)復(fù)制給this。
由于每個(gè)函數(shù)都有自己的this綁定,它的值依賴(lài)于它的調(diào)用方式,所以在用function關(guān)鍵字定義的常規(guī)函數(shù)中,不能引用外層作用域的this。
箭頭函數(shù)是不同的 - 它們不綁定他們自己的this,但可以看到他們周?chē)ǘx位置)作用域的this綁定。 因此,你可以像下面的代碼那樣,在局部函數(shù)中引用this:
function normalize() { console.log(this.coords.map(n => n / this.length)); } normalize.call({coords: [0, 2, 3], length: 5}); // → [0, 0.4, 0.6]
如果我使用function關(guān)鍵字將參數(shù)寫(xiě)入map,則代碼將不起作用。
原型我們來(lái)仔細(xì)看看以下這段代碼。
let empty = {}; console.log(empty.toString); // → function toString(){…} console.log(empty.toString()); // → [object Object]
我從一個(gè)空對(duì)象中取出了一個(gè)屬性。 好神奇!
實(shí)際上并非如此。我只是掩蓋了一些 JavaScript 對(duì)象的內(nèi)部工作細(xì)節(jié)罷了。每個(gè)對(duì)象除了擁有自己的屬性外,都包含一個(gè)原型(prototype)。原型是另一個(gè)對(duì)象,是對(duì)象的一個(gè)屬性來(lái)源。當(dāng)開(kāi)發(fā)人員訪問(wèn)一個(gè)對(duì)象不包含的屬性時(shí),就會(huì)從對(duì)象原型中搜索屬性,接著是原型的原型,依此類(lèi)推。
那么空對(duì)象的原型是什么呢?是Object.prototype,它是所有對(duì)象中原型的父原型。
console.log(Object.getPrototypeOf({}) == Object.prototype); // → true console.log(Object.getPrototypeOf(Object.prototype)); // → null
正如你的猜測(cè),Object.getPrototypeOf返回一個(gè)對(duì)象的原型。
JavaScript 對(duì)象原型的關(guān)系是一種樹(shù)形結(jié)構(gòu),整個(gè)樹(shù)形結(jié)構(gòu)的根部就是Object.prototype。Object.prototype提供了一些可以在所有對(duì)象中使用的方法。比如說(shuō),toString方法可以將一個(gè)對(duì)象轉(zhuǎn)換成其字符串表示形式。
許多對(duì)象并不直接將Object.prototype作為其原型,而會(huì)使用另一個(gè)原型對(duì)象,用于提供一系列不同的默認(rèn)屬性。函數(shù)繼承自Function.prototype,而數(shù)組繼承自Array.prototype。
console.log(Object.getPrototypeOf(Math.max) == Function.prototype); // → true console.log(Object.getPrototypeOf([]) == Array.prototype); // → true
對(duì)于這樣的原型對(duì)象來(lái)說(shuō),其自身也包含了一個(gè)原型對(duì)象,通常情況下是Object.prototype,所以說(shuō),這些原型對(duì)象可以間接提供toString這樣的方法。
你可以使用Object.create來(lái)創(chuàng)建一個(gè)具有特定原型的對(duì)象。
let protoRabbit = { speak(line) { console.log(`The ${this.type} rabbit says "${line}"`); } }; let killerRabbit = Object.create(protoRabbit); killerRabbit.type = "killer"; killerRabbit.speak("SKREEEE!"); // → The killer rabbit says "SKREEEE!"
像對(duì)象表達(dá)式中的speak(line)這樣的屬性是定義方法的簡(jiǎn)寫(xiě)。 它創(chuàng)建了一個(gè)名為speak的屬性,并向其提供函數(shù)作為它的值。
原型對(duì)象protoRabbit是一個(gè)容器,用于包含所有兔子對(duì)象的公有屬性。每個(gè)獨(dú)立的兔子對(duì)象(比如killerRabbit)可以包含其自身屬性(比如本例中的type屬性),也可以派生其原型對(duì)象中公有的屬性。
類(lèi)JavaScript 的原型系統(tǒng)可以解釋為對(duì)一種面向?qū)ο蟮母拍睿ǚQ(chēng)為類(lèi)(class))的某種非正式實(shí)現(xiàn)。 類(lèi)定義了對(duì)象的類(lèi)型的形狀 - 它具有什么方法和屬性。 這樣的對(duì)象被稱(chēng)為類(lèi)的實(shí)例(instance)。
原型對(duì)于屬性來(lái)說(shuō)很實(shí)用。一個(gè)類(lèi)的所有實(shí)例共享相同的屬性值,例如方法。 每個(gè)實(shí)例上的不同屬性,比如我們的兔子的type屬性,需要直接存儲(chǔ)在對(duì)象本身中。
所以為了創(chuàng)建一個(gè)給定類(lèi)的實(shí)例,你必須使對(duì)象從正確的原型派生,但是你也必須確保,它本身具有這個(gè)類(lèi)的實(shí)例應(yīng)該具有的屬性。 這是構(gòu)造器(constructor)函數(shù)的作用。
function makeRabbit(type) { let rabbit = Object.create(protoRabbit); rabbit.type = type; return rabbit; }
JavaScript 提供了一種方法,來(lái)使得更容易定義這種類(lèi)型的功能。 如果將關(guān)鍵字new放在函數(shù)調(diào)用之前,則該函數(shù)將被視為構(gòu)造器。 這意味著具有正確原型的對(duì)象會(huì)自動(dòng)創(chuàng)建,綁定到函數(shù)中的this,并在函數(shù)結(jié)束時(shí)返回。
構(gòu)造對(duì)象時(shí)使用的原型對(duì)象,可以通過(guò)構(gòu)造器的prototype屬性來(lái)查找。
function Rabbit(type) { this.type = type; } Rabbit.prototype.speak = function(line) { console.log(`The ${this.type} rabbit says "${line}"`); }; let weirdRabbit = new Rabbit("weird");
構(gòu)造器(實(shí)際上是所有函數(shù))都會(huì)自動(dòng)獲得一個(gè)名為prototype的屬性,默認(rèn)情況下它包含一個(gè)普通的,來(lái)自Object.prototype的空對(duì)象。 如果需要,可以用新對(duì)象覆蓋它。 或者,你可以將屬性添加到現(xiàn)有對(duì)象,如示例所示。
按照慣例,構(gòu)造器的名字是大寫(xiě)的,這樣它們可以很容易地與其他函數(shù)區(qū)分開(kāi)來(lái)。
重要的是,理解原型與構(gòu)造器關(guān)聯(lián)的方式(通過(guò)其prototype屬性),與對(duì)象擁有原型(可以通過(guò)Object.getPrototypeOf查找)的方式之間的區(qū)別。 構(gòu)造器的實(shí)際原型是Function.prototype,因?yàn)闃?gòu)造器是函數(shù)。 它的prototype屬性擁有原型,用于通過(guò)它創(chuàng)建的實(shí)例。
console.log(Object.getPrototypeOf(Rabbit) == Function.prototype); // → true console.log(Object.getPrototypeOf(weirdRabbit) == Rabbit.prototype); // → true類(lèi)的表示法
所以 JavaScript 類(lèi)是帶有原型屬性的構(gòu)造器。 這就是他們的工作方式,直到 2015 年,這就是你編寫(xiě)他們的方式。 最近,我們有了一個(gè)不太笨拙的表示法。
class Rabbit { constructor(type) { this.type = type; } speak(line) { console.log(`The ${this.type} rabbit says "${line}"`); } } let killerRabbit = new Rabbit("killer"); let blackRabbit = new Rabbit("black");
class關(guān)鍵字是類(lèi)聲明的開(kāi)始,它允許我們?cè)谝粋€(gè)地方定義一個(gè)構(gòu)造器和一組方法。 可以在聲明的大括號(hào)內(nèi)寫(xiě)入任意數(shù)量的方法。 一個(gè)名為constructor的對(duì)象受到特別處理。 它提供了實(shí)際的構(gòu)造器,它將綁定到名稱(chēng)"Rabbit"。 其他函數(shù)被打包到該構(gòu)造器的原型中。 因此,上面的類(lèi)聲明等同于上一節(jié)中的構(gòu)造器定義。 它看起來(lái)更好。
類(lèi)聲明目前只允許方法 - 持有函數(shù)的屬性 - 添加到原型中。 當(dāng)你想在那里保存一個(gè)非函數(shù)值時(shí),這可能會(huì)有點(diǎn)不方便。 該語(yǔ)言的下一個(gè)版本可能會(huì)改善這一點(diǎn)。 現(xiàn)在,你可以在定義該類(lèi)后直接操作原型來(lái)創(chuàng)建這些屬性。
像function一樣,class可以在語(yǔ)句和表達(dá)式中使用。 當(dāng)用作表達(dá)式時(shí),它沒(méi)有定義綁定,而只是將構(gòu)造器作為一個(gè)值生成。 你可以在類(lèi)表達(dá)式中省略類(lèi)名稱(chēng)。
let object = new class { getWord() { return "hello"; } }; console.log(object.getWord()); // → hello覆蓋派生的屬性
將屬性添加到對(duì)象時(shí),無(wú)論它是否存在于原型中,該屬性都會(huì)添加到對(duì)象本身中。 如果原型中已經(jīng)有一個(gè)同名的屬性,該屬性將不再影響對(duì)象,因?yàn)樗F(xiàn)在隱藏在對(duì)象自己的屬性后面。
Rabbit.prototype.teeth = "small"; console.log(killerRabbit.teeth); // → small killerRabbit.teeth = "long, sharp, and bloody"; console.log(killerRabbit.teeth); // → long, sharp, and bloody console.log(blackRabbit.teeth); // → small console.log(Rabbit.prototype.teeth); // → small
下圖簡(jiǎn)單地描述了代碼執(zhí)行后的情況。其中Rabbit和Object原型畫(huà)在了killerRabbit之下,我們可以從原型中找到對(duì)象中沒(méi)有的屬性。
覆蓋原型中存在的屬性是很有用的特性。就像示例展示的那樣,我們覆蓋了killerRabbit的teeth屬性,這可以用來(lái)描述實(shí)例(對(duì)象中更為泛化的類(lèi)的實(shí)例)的特殊屬性,同時(shí)又可以讓簡(jiǎn)單對(duì)象從原型中獲取標(biāo)準(zhǔn)的值。
覆蓋也用于向標(biāo)準(zhǔn)函數(shù)和數(shù)組原型提供toString方法,與基本對(duì)象的原型不同。
console.log(Array.prototype.toString == Object.prototype.toString); // → false console.log([1, 2].toString()); // → 1,2
調(diào)用數(shù)組的toString方法后得到的結(jié)果與調(diào)用.join(",")的結(jié)果十分類(lèi)似,即在數(shù)組的每個(gè)值之間插入一個(gè)逗號(hào)。而直接使用數(shù)組調(diào)用Object.prototype.toString則會(huì)產(chǎn)生一個(gè)完全不同的字符串。由于Object原型提供的toString方法并不了解數(shù)組結(jié)構(gòu),因此只會(huì)簡(jiǎn)單地輸出一對(duì)方括號(hào),并在方括號(hào)中間輸出單詞"object"和類(lèi)型的名稱(chēng)。
console.log(Object.prototype.toString.call([1, 2])); // → [object Array]映射
我們?cè)谏弦徽轮锌吹搅擞成洌╩ap)這個(gè)詞,用于一個(gè)操作,通過(guò)對(duì)元素應(yīng)用函數(shù)來(lái)轉(zhuǎn)換數(shù)據(jù)結(jié)構(gòu)。 令人困惑的是,在編程時(shí),同一個(gè)詞也被用于相關(guān)而不同的事物。
映射(名詞)是將值(鍵)與其他值相關(guān)聯(lián)的數(shù)據(jù)結(jié)構(gòu)。 例如,你可能想要將姓名映射到年齡。 為此可以使用對(duì)象。
let ages = { Boris: 39, Liang: 22, Júlia: 62 }; console.log(`Júlia is ${ages["Júlia"]}`); // → Júlia is 62 console.log("Is Jack"s age known?", "Jack" in ages); // → Is Jack"s age known? false console.log("Is toString"s age known?", "toString" in ages); // → Is toString"s age known? true
在這里,對(duì)象的屬性名稱(chēng)是人們的姓名,并且該屬性的值為他們的年齡。 但是我們當(dāng)然沒(méi)有在我們的映射中列出任何名為toString的人。 似的,因?yàn)楹?jiǎn)單對(duì)象是從Object.prototype派生的,所以它看起來(lái)就像擁有這個(gè)屬性。
因此,使用簡(jiǎn)單對(duì)象作為映射是危險(xiǎn)的。 有幾種可能的方法來(lái)避免這個(gè)問(wèn)題。 首先,可以使用null原型創(chuàng)建對(duì)象。 如果將null傳遞給Object.create,那么所得到的對(duì)象將不會(huì)從Object.prototype派生,并且可以安全地用作映射。
console.log("toString" in Object.create(null)); // → false
對(duì)象屬性名稱(chēng)必須是字符串。 如果你需要一個(gè)映射,它的鍵不能輕易轉(zhuǎn)換為字符串 - 比如對(duì)象 - 你不能使用對(duì)象作為你的映射。
幸運(yùn)的是,JavaScript 帶有一個(gè)叫做Map的類(lèi),它正是為了這個(gè)目的而編寫(xiě)。 它存儲(chǔ)映射并允許任何類(lèi)型的鍵。
let ages = new Map(); ages.set("Boris", 39); ages.set("Liang", 22); ages.set("Júlia", 62); console.log(`Júlia is ${ages.get("Júlia")}`); // → Júlia is 62 console.log("Is Jack"s age known?", ages.has("Jack")); // → Is Jack"s age known? false console.log(ages.has("toString")); // → false
set,get和has方法是Map對(duì)象的接口的一部分。 編寫(xiě)一個(gè)可以快速更新和搜索大量值的數(shù)據(jù)結(jié)構(gòu)并不容易,但我們不必?fù)?dān)心這一點(diǎn)。 其他人為我們實(shí)現(xiàn),我們可以通過(guò)這個(gè)簡(jiǎn)單的接口來(lái)使用他們的工作。
如果你確實(shí)有一個(gè)簡(jiǎn)單對(duì)象,出于某種原因需要將它視為一個(gè)映射,那么了解Object.keys只返回對(duì)象的自己的鍵,而不是原型中的那些鍵,會(huì)很有用。 作為in運(yùn)算符的替代方法,你可以使用hasOwnProperty方法,該方法會(huì)忽略對(duì)象的原型。
console.log({x: 1}.hasOwnProperty("x")); // → true console.log({x: 1}.hasOwnProperty("toString")); // → false多態(tài)
當(dāng)你調(diào)用一個(gè)對(duì)象的String函數(shù)(將一個(gè)值轉(zhuǎn)換為一個(gè)字符串)時(shí),它會(huì)調(diào)用該對(duì)象的toString方法來(lái)嘗試從它創(chuàng)建一個(gè)有意義的字符串。 我提到一些標(biāo)準(zhǔn)原型定義了自己的toString版本,因此它們可以創(chuàng)建一個(gè)包含比"[object Object]"有用信息更多的字符串。 你也可以自己實(shí)現(xiàn)。
Rabbit.prototype.toString = function() { return `a ${this.type} rabbit`; }; console.log(String(blackRabbit)); // → a black rabbit
這是一個(gè)強(qiáng)大的想法的簡(jiǎn)單實(shí)例。 當(dāng)一段代碼為了與某些對(duì)象協(xié)作而編寫(xiě),這些對(duì)象具有特定接口時(shí)(在本例中為toString方法),任何類(lèi)型的支持此接口的對(duì)象都可以插入到代碼中,并且它將正常工作。
這種技術(shù)被稱(chēng)為多態(tài)(polymorphism)。 多態(tài)代碼可以處理不同形狀的值,只要它們支持它所期望的接口即可。
我在第四章中提到for/of循環(huán)可以遍歷幾種數(shù)據(jù)結(jié)構(gòu)。 這是多態(tài)性的另一種情況 - 這樣的循環(huán)期望數(shù)據(jù)結(jié)構(gòu)公開(kāi)的特定接口,數(shù)組和字符串是這樣。 你也可以將這個(gè)接口添加到你自己的對(duì)象中! 但在我們實(shí)現(xiàn)它之前,我們需要知道什么是符號(hào)。
符號(hào)多個(gè)接口可能為不同的事物使用相同的屬性名稱(chēng)。 例如,我可以定義一個(gè)接口,其中toString方法應(yīng)該將對(duì)象轉(zhuǎn)換為一段紗線。 一個(gè)對(duì)象不可能同時(shí)滿足這個(gè)接口和toString的標(biāo)準(zhǔn)用法。
這是一個(gè)壞主意,這個(gè)問(wèn)題并不常見(jiàn)。 大多數(shù) JavaScript 程序員根本就不會(huì)去想它。 但是,語(yǔ)言設(shè)計(jì)師們正在思考這個(gè)問(wèn)題,無(wú)論如何都為我們提供了解決方案。
當(dāng)我聲稱(chēng)屬性名稱(chēng)是字符串時(shí),這并不完全準(zhǔn)確。 他們通常是,但他們也可以是符號(hào)(symbol)。 符號(hào)是使用Symbol函數(shù)創(chuàng)建的值。 與字符串不同,新創(chuàng)建的符號(hào)是唯一的 - 你不能兩次創(chuàng)建相同的符號(hào)。
let sym = Symbol("name"); console.log(sym == Symbol("name")); // → false Rabbit.prototype[sym] = 55; console.log(blackRabbit[sym]); // → 55
將Symbol轉(zhuǎn)換為字符串時(shí),會(huì)得到傳遞給它的字符串,例如,在控制臺(tái)中顯示時(shí),符號(hào)可以更容易識(shí)別。 但除此之外沒(méi)有任何意義 - 多個(gè)符號(hào)可能具有相同的名稱(chēng)。
由于符號(hào)既獨(dú)特又可用于屬性名稱(chēng),因此符號(hào)適合定義可以和其他屬性共生的接口,無(wú)論它們的名稱(chēng)是什么。
const toStringSymbol = Symbol("toString"); Array.prototype[toStringSymbol] = function() { return `${this.length} cm of blue yarn`; }; console.log([1, 2].toString()); // → 1,2 console.log([1, 2][toStringSymbol]()); // → 2 cm of blue yarn
通過(guò)在屬性名稱(chēng)周?chē)褂梅嚼ㄌ?hào),可以在對(duì)象表達(dá)式和類(lèi)中包含符號(hào)屬性。 這會(huì)導(dǎo)致屬性名稱(chēng)的求值,就像方括號(hào)屬性訪問(wèn)表示法一樣,這允許我們引用一個(gè)持有該符號(hào)的綁定。
let stringObject = { [toStringSymbol]() { return "a jute rope"; } }; console.log(stringObject[toStringSymbol]()); // → a jute rope迭代器接口
提供給for/of循環(huán)的對(duì)象預(yù)計(jì)為可迭代對(duì)象(iterable)。 這意味著它有一個(gè)以Symbol.iterator符號(hào)命名的方法(由語(yǔ)言定義的符號(hào)值,存儲(chǔ)為Symbol符號(hào)的一個(gè)屬性)。
當(dāng)被調(diào)用時(shí),該方法應(yīng)該返回一個(gè)對(duì)象,它提供第二個(gè)接口迭代器(iterator)。 這是執(zhí)行迭代的實(shí)際事物。 它擁有返回下一個(gè)結(jié)果的next方法。 這個(gè)結(jié)果應(yīng)該是一個(gè)對(duì)象,如果有下一個(gè)值,value屬性會(huì)提供它;沒(méi)有更多結(jié)果時(shí),done屬性應(yīng)該為true,否則為false。
請(qǐng)注意,next,value和done屬性名稱(chēng)是純字符串,而不是符號(hào)。 只有Symbol.iterator是一個(gè)實(shí)際的符號(hào),它可能被添加到不同的大量對(duì)象中。
我們可以直接使用這個(gè)接口。
let okIterator = "OK"[Symbol.iterator](); console.log(okIterator.next()); // → {value: "O", done: false} console.log(okIterator.next()); // → {value: "K", done: false} console.log(okIterator.next()); // → {value: undefined, done: true}
我們來(lái)實(shí)現(xiàn)一個(gè)可迭代的數(shù)據(jù)結(jié)構(gòu)。 我們將構(gòu)建一個(gè)matrix類(lèi),充當(dāng)一個(gè)二維數(shù)組。
class Matrix { constructor(width, height, element = (x, y) => undefined) { this.width = width; this.height = height; this.content = []; for (let y = 0; y < height; y++) { for (let x = 0; x < width; x++) { this.content[y * width + x] = element(x, y); } } } get(x, y) { return this.content[y * this.width + x]; } set(x, y, value) { this.content[y * this.width + x] = value; } }
該類(lèi)將其內(nèi)容存儲(chǔ)在width × height個(gè)元素的單個(gè)數(shù)組中。 元素是按行存儲(chǔ)的,因此,例如,第五行中的第三個(gè)元素存儲(chǔ)在位置4 × width + 2中(使用基于零的索引)。
構(gòu)造器需要寬度,高度和一個(gè)可選的內(nèi)容函數(shù),用來(lái)填充初始值。 get和set方法用于檢索和更新矩陣中的元素。
遍歷矩陣時(shí),通常對(duì)元素的位置以及元素本身感興趣,所以我們會(huì)讓迭代器產(chǎn)生具有x,y和value屬性的對(duì)象。
class MatrixIterator { constructor(matrix) { this.x = 0; this.y = 0; this.matrix = matrix; } next() { if (this.y == this.matrix.height) return {done: true}; let value = {x: this.x, y: this.y, value: this.matrix.get(this.x, this.y)}; this.x++; if (this.x == this.matrix.width) { this.x = 0; this.y++; } return {value, done: false}; } }
這個(gè)類(lèi)在其x和y屬性中跟蹤遍歷矩陣的進(jìn)度。 next方法最開(kāi)始檢查是否到達(dá)矩陣的底部。 如果沒(méi)有,則首先創(chuàng)建保存當(dāng)前值的對(duì)象,之后更新其位置,如有必要?jiǎng)t移至下一行。
讓我們使Matrix類(lèi)可迭代。 在本書(shū)中,我會(huì)偶爾使用事后的原型操作來(lái)為類(lèi)添加方法,以便單個(gè)代碼段保持較小且獨(dú)立。 在一個(gè)正常的程序中,不需要將代碼分成小塊,而是直接在class中聲明這些方法。
Matrix.prototype[Symbol.iterator] = function() { return new MatrixIterator(this); };
現(xiàn)在我們可以用for/of來(lái)遍歷一個(gè)矩陣。
let matrix = new Matrix(2, 2, (x, y) => `value ${x},${y}`); for (let {x, y, value} of matrix) { console.log(x, y, value); } // → 0 0 value 0,0 // → 1 0 value 1,0 // → 0 1 value 0,1 // → 1 1 value 1,1讀寫(xiě)器和靜態(tài)
接口通常主要由方法組成,但也可以持有非函數(shù)值的屬性。 例如,Map對(duì)象有size屬性,告訴你有多少個(gè)鍵存儲(chǔ)在它們中。
這樣的對(duì)象甚至不需要直接在實(shí)例中計(jì)算和存儲(chǔ)這樣的屬性。 即使直接訪問(wèn)的屬性也可能隱藏了方法調(diào)用。 這種方法稱(chēng)為讀取器(getter),它們通過(guò)在方法名稱(chēng)前面編寫(xiě)get來(lái)定義。
let varyingSize = { get size() { return Math.floor(Math.random() * 100); } }; console.log(varyingSize.size); // → 73 console.log(varyingSize.size); // → 49
每當(dāng)有人讀取此對(duì)象的size屬性時(shí),就會(huì)調(diào)用相關(guān)的方法。 當(dāng)使用寫(xiě)入器(setter)寫(xiě)入屬性時(shí),可以做類(lèi)似的事情。
class Temperature { constructor(celsius) { this.celsius = celsius; } get fahrenheit() { return this.celsius * 1.8 + 32; } set fahrenheit(value) { this.celsius = (value - 32) / 1.8; } static fromFahrenheit(value) { return new Temperature((value - 32) / 1.8); } } let temp = new Temperature(22); console.log(temp.fahrenheit); // → 71.6 temp.fahrenheit = 86; console.log(temp.celsius); // → 30
Temperature類(lèi)允許你以攝氏度或華氏度讀取和寫(xiě)入溫度,但內(nèi)部?jī)H存儲(chǔ)攝氏度,并在fahrenheit讀寫(xiě)器中自動(dòng)轉(zhuǎn)換為攝氏度。
有時(shí)候你想直接向你的構(gòu)造器附加一些屬性,而不是原型。 這樣的方法將無(wú)法訪問(wèn)類(lèi)實(shí)例,但可以用來(lái)提供額外方法來(lái)創(chuàng)建實(shí)例。
在類(lèi)聲明內(nèi)部,名稱(chēng)前面寫(xiě)有static的方法,存儲(chǔ)在構(gòu)造器中。 所以Temperature類(lèi)可以讓你寫(xiě)出Temperature.fromFahrenheit(100),來(lái)使用華氏溫度創(chuàng)建一個(gè)溫度。
繼承已知一些矩陣是對(duì)稱(chēng)的。 如果沿左上角到右下角的對(duì)角線翻轉(zhuǎn)對(duì)稱(chēng)矩陣,它保持不變。 換句話說(shuō),存儲(chǔ)在x,y的值總是與y,x相同。
想象一下,我們需要一個(gè)像Matrix這樣的數(shù)據(jù)結(jié)構(gòu),但是它必需保證一個(gè)事實(shí),矩陣是對(duì)稱(chēng)的。 我們可以從頭開(kāi)始編寫(xiě)它,但這需要重復(fù)一些代碼,與我們已經(jīng)寫(xiě)過(guò)的代碼很相似。
JavaScript 的原型系統(tǒng)可以創(chuàng)建一個(gè)新類(lèi),就像舊類(lèi)一樣,但是它的一些屬性有了新的定義。 新類(lèi)派生自舊類(lèi)的原型,但為set方法增加了一個(gè)新的定義。
在面向?qū)ο蟮木幊绦g(shù)語(yǔ)中,這稱(chēng)為繼承(inheritance)。 新類(lèi)繼承舊類(lèi)的屬性和行為。
class SymmetricMatrix extends Matrix { constructor(size, element = (x, y) => undefined) { super(size, size, (x, y) => { if (x < y) return element(y, x); else return element(x, y); }); } set(x, y, value) { super.set(x, y, value); if (x != y) { super.set(y, x, value); } } } let matrix = new SymmetricMatrix(5, (x, y) => `${x},${y}`); console.log(matrix.get(2, 3)); // → 3,2
extends這個(gè)詞用于表示,這個(gè)類(lèi)不應(yīng)該直接基于默認(rèn)的Object原型,而應(yīng)該基于其他類(lèi)。 這被稱(chēng)為超類(lèi)(superclass)。 派生類(lèi)是子類(lèi)(subclass)。
為了初始化SymmetricMatrix實(shí)例,構(gòu)造器通過(guò)super關(guān)鍵字調(diào)用其超類(lèi)的構(gòu)造器。 這是必要的,因?yàn)槿绻@個(gè)新對(duì)象的行為(大致)像Matrix,它需要矩陣具有的實(shí)例屬性。 為了確保矩陣是對(duì)稱(chēng)的,構(gòu)造器包裝了content方法,來(lái)交換對(duì)角線以下的值的坐標(biāo)。
set方法再次使用super,但這次不是調(diào)用構(gòu)造器,而是從超類(lèi)的一組方法中調(diào)用特定的方法。 我們正在重新定義set,但是想要使用原來(lái)的行為。 因?yàn)?b>this.set引用新的set方法,所以調(diào)用這個(gè)方法是行不通的。 在類(lèi)方法內(nèi)部,super提供了一種方法,來(lái)調(diào)用超類(lèi)中定義的方法。
繼承允許我們用相對(duì)較少的工作,從現(xiàn)有數(shù)據(jù)類(lèi)型構(gòu)建稍微不同的數(shù)據(jù)類(lèi)型。 它是面向?qū)ο髠鹘y(tǒng)的基礎(chǔ)部分,與封裝和多態(tài)一樣。 盡管后兩者現(xiàn)在普遍被認(rèn)為是偉大的想法,但繼承更具爭(zhēng)議性。
盡管封裝和多態(tài)可用于將代碼彼此分離,從而減少整個(gè)程序的耦合,但繼承從根本上將類(lèi)連接在一起,從而產(chǎn)生更多的耦合。 繼承一個(gè)類(lèi)時(shí),比起單純使用它,你通常必須更加了解它如何工作。 繼承可能是一個(gè)有用的工具,并且我現(xiàn)在在自己的程序中使用它,但它不應(yīng)該成為你的第一個(gè)工具,你可能不應(yīng)該積極尋找機(jī)會(huì)來(lái)構(gòu)建類(lèi)層次結(jié)構(gòu)(類(lèi)的家族樹(shù))。
instanceof運(yùn)算符在有些時(shí)候,了解某個(gè)對(duì)象是否繼承自某個(gè)特定類(lèi),也是十分有用的。JavaScript 為此提供了一個(gè)二元運(yùn)算符,名為instanceof。
console.log( new SymmetricMatrix(2) instanceof SymmetricMatrix); // → true console.log(new SymmetricMatrix(2) instanceof Matrix); // → true console.log(new Matrix(2, 2) instanceof SymmetricMatrix); // → false console.log([1] instanceof Array); // → true
該運(yùn)算符會(huì)瀏覽所有繼承類(lèi)型。所以SymmetricMatrix是Matrix的一個(gè)實(shí)例。 該運(yùn)算符也可以應(yīng)用于像Array這樣的標(biāo)準(zhǔn)構(gòu)造器。 幾乎每個(gè)對(duì)象都是Object的一個(gè)實(shí)例。
本章小結(jié)對(duì)象不僅僅持有它們自己的屬性。對(duì)象中有另一個(gè)對(duì)象:原型,只要原型中包含了屬性,那么根據(jù)原型構(gòu)造出來(lái)的對(duì)象也就可以看成包含了相應(yīng)的屬性。簡(jiǎn)單對(duì)象直接以Object.prototype作為原型。
構(gòu)造器是名稱(chēng)通常以大寫(xiě)字母開(kāi)頭的函數(shù),可以與new運(yùn)算符一起使用來(lái)創(chuàng)建新對(duì)象。 新對(duì)象的原型是構(gòu)造器的prototype屬性中的對(duì)象。 通過(guò)將屬性放到它們的原型中,可以充分利用這一點(diǎn),給定類(lèi)型的所有值在原型中分享它們的屬性。 class表示法提供了一個(gè)顯式方法,來(lái)定義一個(gè)構(gòu)造器及其原型。
你可以定義讀寫(xiě)器,在每次訪問(wèn)對(duì)象的屬性時(shí)秘密地調(diào)用方法。 靜態(tài)方法是存儲(chǔ)在類(lèi)的構(gòu)造器,而不是其原型中的方法。
給定一個(gè)對(duì)象和一個(gè)構(gòu)造器,instanceof運(yùn)算符可以告訴你該對(duì)象是否是該構(gòu)造器的一個(gè)實(shí)例。
可以使用對(duì)象的來(lái)做一個(gè)有用的事情是,為它們指定一個(gè)接口,告訴每個(gè)人他們只能通過(guò)該接口與對(duì)象通信。 構(gòu)成對(duì)象的其余細(xì)節(jié),現(xiàn)在被封裝在接口后面。
不止一種類(lèi)型可以實(shí)現(xiàn)相同的接口。 為使用接口而編寫(xiě)的代碼,自動(dòng)知道如何使用提供接口的任意數(shù)量的不同對(duì)象。 這被稱(chēng)為多態(tài)。
實(shí)現(xiàn)多個(gè)類(lèi),它們僅在一些細(xì)節(jié)上有所不同的時(shí),將新類(lèi)編寫(xiě)為現(xiàn)有類(lèi)的子類(lèi),繼承其一部分行為會(huì)很有幫助。
習(xí)題 向量類(lèi)型編寫(xiě)一個(gè)構(gòu)造器Vec,在二維空間中表示數(shù)組。該函數(shù)接受兩個(gè)數(shù)字參數(shù)x和y,并將其保存到對(duì)象的同名屬性中。
向Vec原型添加兩個(gè)方法:plus和minus,它們接受另一個(gè)向量作為參數(shù),分別返回兩個(gè)向量(一個(gè)是this,另一個(gè)是參數(shù))的和向量與差向量。
向原型添加一個(gè)getter屬性length,用于計(jì)算向量長(zhǎng)度,即點(diǎn)(x,y)與原點(diǎn)(0,0)之間的距離。
// Your code here. console.log(new Vec(1, 2).plus(new Vec(2, 3))); // → Vec{x: 3, y: 5} console.log(new Vec(1, 2).minus(new Vec(2, 3))); // → Vec{x: -1, y: -1} console.log(new Vec(3, 4).length); // → 5分組
標(biāo)準(zhǔn)的 JavaScript 環(huán)境提供了另一個(gè)名為Set的數(shù)據(jù)結(jié)構(gòu)。 像Map的實(shí)例一樣,集合包含一組值。 與Map不同,它不會(huì)將其他值與這些值相關(guān)聯(lián) - 它只會(huì)跟蹤哪些值是該集合的一部分。 一個(gè)值只能是一個(gè)集合的一部分 - 再次添加它沒(méi)有任何作用。
寫(xiě)一個(gè)名為Group的類(lèi)(因?yàn)?b>Set已被占用)。 像Set一樣,它具有add,delete和has方法。 它的構(gòu)造器創(chuàng)建一個(gè)空的分組,add給分組添加一個(gè)值(但僅當(dāng)它不是成員時(shí)),delete從組中刪除它的參數(shù)(如果它是成員),has 返回一個(gè)布爾值,表明其參數(shù)是否為分組的成員。
使用===運(yùn)算符或類(lèi)似于indexOf的東西來(lái)確定兩個(gè)值是否相同。
為該類(lèi)提供一個(gè)靜態(tài)的from方法,該方法接受一個(gè)可迭代的對(duì)象作為參數(shù),并創(chuàng)建一個(gè)分組,包含遍歷它產(chǎn)生的所有值。
// Your code here. class Group { // Your code here. } let group = Group.from([10, 20]); console.log(group.has(10)); // → true console.log(group.has(30)); // → false group.add(10); group.delete(10); console.log(group.has(10)); // → false可迭代分組
使上一個(gè)練習(xí)中的Group類(lèi)可迭代。 如果你不清楚接口的確切形式,請(qǐng)參閱本章前面迭代器接口的章節(jié)。
如果你使用數(shù)組來(lái)表示分組的成員,則不要僅僅通過(guò)調(diào)用數(shù)組中的Symbol.iterator方法來(lái)返回迭代器。 這會(huì)起作用,但它會(huì)破壞這個(gè)練習(xí)的目的。
如果分組被修改時(shí),你的迭代器在迭代過(guò)程中出現(xiàn)奇怪的行為,那也沒(méi)問(wèn)題。
// Your code here (and the code from the previous exercise) for (let value of Group.from(["a", "b", "c"])) { console.log(value); } // → a // → b // → c借鑒方法
在本章前面我提到,當(dāng)你想忽略原型的屬性時(shí),對(duì)象的hasOwnProperty可以用作in運(yùn)算符的更強(qiáng)大的替代方法。 但是如果你的映射需要包含hasOwnProperty這個(gè)詞呢? 你將無(wú)法再調(diào)用該方法,因?yàn)閷?duì)象的屬性隱藏了方法值。
你能想到一種方法,對(duì)擁有自己的同名屬性的對(duì)象,調(diào)用hasOwnProperty嗎?
let map = {one: true, two: true, hasOwnProperty: true}; // Fix this call console.log(map.hasOwnProperty("one")); // → true
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/105023.html
摘要:在本例中,使用屬性指定鏈接的目標(biāo),其中表示超文本鏈接。您應(yīng)該認(rèn)為和元數(shù)據(jù)隱式出現(xiàn)在示例中,即使它們沒(méi)有實(shí)際顯示在文本中。 來(lái)源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:JavaScript and the Browser 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)》 ...
摘要:相反,當(dāng)響應(yīng)指針事件時(shí),它會(huì)調(diào)用創(chuàng)建它的代碼提供的回調(diào)函數(shù),該函數(shù)將處理應(yīng)用的特定部分?;卣{(diào)函數(shù)可能會(huì)返回另一個(gè)回調(diào)函數(shù),以便在按下按鈕并且將指針移動(dòng)到另一個(gè)像素時(shí)得到通知。它們?yōu)榻M件構(gòu)造器的數(shù)組而提供。 來(lái)源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Project: A Pixel Art Editor 譯者:飛龍 協(xié)議:CC BY-NC-SA 4...
摘要:來(lái)源編程精解中文第三版翻譯項(xiàng)目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版,這是一本關(guān)于指導(dǎo)電腦的書(shū)。在可控的范圍內(nèi)編寫(xiě)程序是編程過(guò)程中首要解決的問(wèn)題。我們可以用中文來(lái)描述這些指令將數(shù)字存儲(chǔ)在內(nèi)存地址中的位置。 來(lái)源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Introduction 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地...
摘要:來(lái)源編程精解中文第三版翻譯項(xiàng)目原文譯者飛龍協(xié)議自豪地采用谷歌翻譯部分參考了編程精解第版確定編程語(yǔ)言中的表達(dá)式含義的求值器只是另一個(gè)程序。若文本不是一個(gè)合法程序,解析器應(yīng)該指出錯(cuò)誤。 來(lái)源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:Project: A Programming Language 譯者:飛龍 協(xié)議:CC BY-NC-SA 4.0 自豪地采用...
摘要:在其沙箱中提供了將文本轉(zhuǎn)換成文檔對(duì)象模型的功能。瀏覽器使用與該形狀對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu)來(lái)表示文檔。我們將這種表示方式稱(chēng)為文檔對(duì)象模型,或簡(jiǎn)稱(chēng)。樹(shù)回想一下第章中提到的語(yǔ)法樹(shù)。語(yǔ)言的語(yǔ)法樹(shù)有標(biāo)識(shí)符值和應(yīng)用節(jié)點(diǎn)。元素表示標(biāo)簽的節(jié)點(diǎn)用于確定文檔結(jié)構(gòu)。 來(lái)源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項(xiàng)目原文:The Document Object Model 譯者:飛龍 協(xié)議...
閱讀 3242·2021-11-25 09:43
閱讀 3755·2021-08-31 09:41
閱讀 1434·2019-08-30 15:56
閱讀 2373·2019-08-30 15:55
閱讀 3179·2019-08-30 13:48
閱讀 2946·2019-08-29 15:15
閱讀 1113·2019-08-29 15:14
閱讀 2766·2019-08-28 18:26