摘要:在編程文化中,我們有一個名為面向對象編程的東西,這是一組技術,使用對象和相關概念作為程序組織的中心原則。這是構造器函數的作用。因此,上面的類聲明等同于上一節中的構造器定義。
來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:The Secret Life of Objects
譯者:飛龍
協議:CC BY-NC-SA 4.0
自豪地采用谷歌翻譯
部分參考了《JavaScript 編程精解(第 2 版)》
抽象數據類型是通過編寫一種特殊的程序來實現的,該程序根據可在其上執行的操作來定義類型。
Barbara Liskov,《Programming with Abstract Data Types》
第 4 章介紹了 JavaScript 的對象(object)。 在編程文化中,我們有一個名為面向對象編程(OOP)的東西,這是一組技術,使用對象(和相關概念)作為程序組織的中心原則。
雖然沒有人真正同意其精確定義,但面向對象編程已經成為了許多編程語言的設計,包括 JavaScript 在內。 本章將描述這些想法在 JavaScript 中的應用方式。
封裝面向對象編程的核心思想是將程序分成小型片段,并讓每個片段負責管理自己的狀態。
通過這種方式,一些程序片段的工作方式的知識可以局部保留。 從事其他方面的工作的人,不必記住甚至不知道這些知識。 無論什么時候這些局部細節發生變化,只需要直接更新其周圍的代碼。
這種程序的不同片段通過接口(interface),函數或綁定的有限集合交互,它以更抽象的級別提供有用的功能,并隱藏它的精確實現。
這些程序片段使用對象建模。 它們的接口由一組特定的方法(method)和屬性(property)組成。 接口的一部分的屬性稱為公共的(public)。 其他外部代碼不應該接觸屬性的稱為私有的(private)。
許多語言提供了區分公共和私有屬性的方法,并且完全防止外部代碼訪問私有屬性。 JavaScript 再次采用極簡主義的方式,沒有。 至少目前還沒有 - 有個正在開展的工作,將其添加到該語言中。
即使這種語言沒有內置這種區別,JavaScript 程序員也成功地使用了這種想法。 通常,可用的接口在文檔或數字一中描述。 在屬性名稱的的開頭經常會放置一個下劃線(_)字符,來表明這些屬性是私有的。
將接口與實現分離是一個好主意。 它通常被稱為封裝(encapsulation)。
方法方法不過是持有函數值的屬性。 這是一個簡單的方法:
let rabbit = {}; rabbit.speak = function(line) { console.log(`The rabbit says "${line}"`); }; rabbit.speak("I"m alive."); // → The rabbit says "I"m alive."
方法通常會在對象被調用時執行一些操作。將函數作為對象的方法調用時,會找到對象中對應的屬性并直接調用。當函數作為方法調用時,函數體內叫做this的綁定自動指向在它上面調用的對象。
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看作是以不同方式傳遞的額外參數。 如果你想顯式傳遞它,你可以使用函數的call方法,它接受this值作為第一個參數,并將其它處理為看做普通參數。
speak.call(hungryRabbit, "Burp!"); // → The hungry rabbit says "Burp!"
這段代碼使用了關鍵字this來輸出正在說話的兔子的種類。我們回想一下apply和bind方法,這兩個方法接受的第一個參數可以用來模擬對象中方法的調用。這兩個方法會把第一個參數復制給this。
由于每個函數都有自己的this綁定,它的值依賴于它的調用方式,所以在用function關鍵字定義的常規函數中,不能引用外層作用域的this。
箭頭函數是不同的 - 它們不綁定他們自己的this,但可以看到他們周圍(定義位置)作用域的this綁定。 因此,你可以像下面的代碼那樣,在局部函數中引用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關鍵字將參數寫入map,則代碼將不起作用。
原型我們來仔細看看以下這段代碼。
let empty = {}; console.log(empty.toString); // → function toString(){…} console.log(empty.toString()); // → [object Object]
我從一個空對象中取出了一個屬性。 好神奇!
實際上并非如此。我只是掩蓋了一些 JavaScript 對象的內部工作細節罷了。每個對象除了擁有自己的屬性外,都包含一個原型(prototype)。原型是另一個對象,是對象的一個屬性來源。當開發人員訪問一個對象不包含的屬性時,就會從對象原型中搜索屬性,接著是原型的原型,依此類推。
那么空對象的原型是什么呢?是Object.prototype,它是所有對象中原型的父原型。
console.log(Object.getPrototypeOf({}) == Object.prototype); // → true console.log(Object.getPrototypeOf(Object.prototype)); // → null
正如你的猜測,Object.getPrototypeOf返回一個對象的原型。
JavaScript 對象原型的關系是一種樹形結構,整個樹形結構的根部就是Object.prototype。Object.prototype提供了一些可以在所有對象中使用的方法。比如說,toString方法可以將一個對象轉換成其字符串表示形式。
許多對象并不直接將Object.prototype作為其原型,而會使用另一個原型對象,用于提供一系列不同的默認屬性。函數繼承自Function.prototype,而數組繼承自Array.prototype。
console.log(Object.getPrototypeOf(Math.max) == Function.prototype); // → true console.log(Object.getPrototypeOf([]) == Array.prototype); // → true
對于這樣的原型對象來說,其自身也包含了一個原型對象,通常情況下是Object.prototype,所以說,這些原型對象可以間接提供toString這樣的方法。
你可以使用Object.create來創建一個具有特定原型的對象。
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!"
像對象表達式中的speak(line)這樣的屬性是定義方法的簡寫。 它創建了一個名為speak的屬性,并向其提供函數作為它的值。
原型對象protoRabbit是一個容器,用于包含所有兔子對象的公有屬性。每個獨立的兔子對象(比如killerRabbit)可以包含其自身屬性(比如本例中的type屬性),也可以派生其原型對象中公有的屬性。
類JavaScript 的原型系統可以解釋為對一種面向對象的概念(稱為類(class))的某種非正式實現。 類定義了對象的類型的形狀 - 它具有什么方法和屬性。 這樣的對象被稱為類的實例(instance)。
原型對于屬性來說很實用。一個類的所有實例共享相同的屬性值,例如方法。 每個實例上的不同屬性,比如我們的兔子的type屬性,需要直接存儲在對象本身中。
所以為了創建一個給定類的實例,你必須使對象從正確的原型派生,但是你也必須確保,它本身具有這個類的實例應該具有的屬性。 這是構造器(constructor)函數的作用。
function makeRabbit(type) { let rabbit = Object.create(protoRabbit); rabbit.type = type; return rabbit; }
JavaScript 提供了一種方法,來使得更容易定義這種類型的功能。 如果將關鍵字new放在函數調用之前,則該函數將被視為構造器。 這意味著具有正確原型的對象會自動創建,綁定到函數中的this,并在函數結束時返回。
構造對象時使用的原型對象,可以通過構造器的prototype屬性來查找。
function Rabbit(type) { this.type = type; } Rabbit.prototype.speak = function(line) { console.log(`The ${this.type} rabbit says "${line}"`); }; let weirdRabbit = new Rabbit("weird");
構造器(實際上是所有函數)都會自動獲得一個名為prototype的屬性,默認情況下它包含一個普通的,來自Object.prototype的空對象。 如果需要,可以用新對象覆蓋它。 或者,你可以將屬性添加到現有對象,如示例所示。
按照慣例,構造器的名字是大寫的,這樣它們可以很容易地與其他函數區分開來。
重要的是,理解原型與構造器關聯的方式(通過其prototype屬性),與對象擁有原型(可以通過Object.getPrototypeOf查找)的方式之間的區別。 構造器的實際原型是Function.prototype,因為構造器是函數。 它的prototype屬性擁有原型,用于通過它創建的實例。
console.log(Object.getPrototypeOf(Rabbit) == Function.prototype); // → true console.log(Object.getPrototypeOf(weirdRabbit) == Rabbit.prototype); // → true類的表示法
所以 JavaScript 類是帶有原型屬性的構造器。 這就是他們的工作方式,直到 2015 年,這就是你編寫他們的方式。 最近,我們有了一個不太笨拙的表示法。
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關鍵字是類聲明的開始,它允許我們在一個地方定義一個構造器和一組方法。 可以在聲明的大括號內寫入任意數量的方法。 一個名為constructor的對象受到特別處理。 它提供了實際的構造器,它將綁定到名稱"Rabbit"。 其他函數被打包到該構造器的原型中。 因此,上面的類聲明等同于上一節中的構造器定義。 它看起來更好。
類聲明目前只允許方法 - 持有函數的屬性 - 添加到原型中。 當你想在那里保存一個非函數值時,這可能會有點不方便。 該語言的下一個版本可能會改善這一點。 現在,你可以在定義該類后直接操作原型來創建這些屬性。
像function一樣,class可以在語句和表達式中使用。 當用作表達式時,它沒有定義綁定,而只是將構造器作為一個值生成。 你可以在類表達式中省略類名稱。
let object = new class { getWord() { return "hello"; } }; console.log(object.getWord()); // → hello覆蓋派生的屬性
將屬性添加到對象時,無論它是否存在于原型中,該屬性都會添加到對象本身中。 如果原型中已經有一個同名的屬性,該屬性將不再影響對象,因為它現在隱藏在對象自己的屬性后面。
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
下圖簡單地描述了代碼執行后的情況。其中Rabbit和Object原型畫在了killerRabbit之下,我們可以從原型中找到對象中沒有的屬性。
覆蓋原型中存在的屬性是很有用的特性。就像示例展示的那樣,我們覆蓋了killerRabbit的teeth屬性,這可以用來描述實例(對象中更為泛化的類的實例)的特殊屬性,同時又可以讓簡單對象從原型中獲取標準的值。
覆蓋也用于向標準函數和數組原型提供toString方法,與基本對象的原型不同。
console.log(Array.prototype.toString == Object.prototype.toString); // → false console.log([1, 2].toString()); // → 1,2
調用數組的toString方法后得到的結果與調用.join(",")的結果十分類似,即在數組的每個值之間插入一個逗號。而直接使用數組調用Object.prototype.toString則會產生一個完全不同的字符串。由于Object原型提供的toString方法并不了解數組結構,因此只會簡單地輸出一對方括號,并在方括號中間輸出單詞"object"和類型的名稱。
console.log(Object.prototype.toString.call([1, 2])); // → [object Array]映射
我們在上一章中看到了映射(map)這個詞,用于一個操作,通過對元素應用函數來轉換數據結構。 令人困惑的是,在編程時,同一個詞也被用于相關而不同的事物。
映射(名詞)是將值(鍵)與其他值相關聯的數據結構。 例如,你可能想要將姓名映射到年齡。 為此可以使用對象。
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
在這里,對象的屬性名稱是人們的姓名,并且該屬性的值為他們的年齡。 但是我們當然沒有在我們的映射中列出任何名為toString的人。 似的,因為簡單對象是從Object.prototype派生的,所以它看起來就像擁有這個屬性。
因此,使用簡單對象作為映射是危險的。 有幾種可能的方法來避免這個問題。 首先,可以使用null原型創建對象。 如果將null傳遞給Object.create,那么所得到的對象將不會從Object.prototype派生,并且可以安全地用作映射。
console.log("toString" in Object.create(null)); // → false
對象屬性名稱必須是字符串。 如果你需要一個映射,它的鍵不能輕易轉換為字符串 - 比如對象 - 你不能使用對象作為你的映射。
幸運的是,JavaScript 帶有一個叫做Map的類,它正是為了這個目的而編寫。 它存儲映射并允許任何類型的鍵。
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對象的接口的一部分。 編寫一個可以快速更新和搜索大量值的數據結構并不容易,但我們不必擔心這一點。 其他人為我們實現,我們可以通過這個簡單的接口來使用他們的工作。
如果你確實有一個簡單對象,出于某種原因需要將它視為一個映射,那么了解Object.keys只返回對象的自己的鍵,而不是原型中的那些鍵,會很有用。 作為in運算符的替代方法,你可以使用hasOwnProperty方法,該方法會忽略對象的原型。
console.log({x: 1}.hasOwnProperty("x")); // → true console.log({x: 1}.hasOwnProperty("toString")); // → false多態
當你調用一個對象的String函數(將一個值轉換為一個字符串)時,它會調用該對象的toString方法來嘗試從它創建一個有意義的字符串。 我提到一些標準原型定義了自己的toString版本,因此它們可以創建一個包含比"[object Object]"有用信息更多的字符串。 你也可以自己實現。
Rabbit.prototype.toString = function() { return `a ${this.type} rabbit`; }; console.log(String(blackRabbit)); // → a black rabbit
這是一個強大的想法的簡單實例。 當一段代碼為了與某些對象協作而編寫,這些對象具有特定接口時(在本例中為toString方法),任何類型的支持此接口的對象都可以插入到代碼中,并且它將正常工作。
這種技術被稱為多態(polymorphism)。 多態代碼可以處理不同形狀的值,只要它們支持它所期望的接口即可。
我在第四章中提到for/of循環可以遍歷幾種數據結構。 這是多態性的另一種情況 - 這樣的循環期望數據結構公開的特定接口,數組和字符串是這樣。 你也可以將這個接口添加到你自己的對象中! 但在我們實現它之前,我們需要知道什么是符號。
符號多個接口可能為不同的事物使用相同的屬性名稱。 例如,我可以定義一個接口,其中toString方法應該將對象轉換為一段紗線。 一個對象不可能同時滿足這個接口和toString的標準用法。
這是一個壞主意,這個問題并不常見。 大多數 JavaScript 程序員根本就不會去想它。 但是,語言設計師們正在思考這個問題,無論如何都為我們提供了解決方案。
當我聲稱屬性名稱是字符串時,這并不完全準確。 他們通常是,但他們也可以是符號(symbol)。 符號是使用Symbol函數創建的值。 與字符串不同,新創建的符號是唯一的 - 你不能兩次創建相同的符號。
let sym = Symbol("name"); console.log(sym == Symbol("name")); // → false Rabbit.prototype[sym] = 55; console.log(blackRabbit[sym]); // → 55
將Symbol轉換為字符串時,會得到傳遞給它的字符串,例如,在控制臺中顯示時,符號可以更容易識別。 但除此之外沒有任何意義 - 多個符號可能具有相同的名稱。
由于符號既獨特又可用于屬性名稱,因此符號適合定義可以和其他屬性共生的接口,無論它們的名稱是什么。
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
通過在屬性名稱周圍使用方括號,可以在對象表達式和類中包含符號屬性。 這會導致屬性名稱的求值,就像方括號屬性訪問表示法一樣,這允許我們引用一個持有該符號的綁定。
let stringObject = { [toStringSymbol]() { return "a jute rope"; } }; console.log(stringObject[toStringSymbol]()); // → a jute rope迭代器接口
提供給for/of循環的對象預計為可迭代對象(iterable)。 這意味著它有一個以Symbol.iterator符號命名的方法(由語言定義的符號值,存儲為Symbol符號的一個屬性)。
當被調用時,該方法應該返回一個對象,它提供第二個接口迭代器(iterator)。 這是執行迭代的實際事物。 它擁有返回下一個結果的next方法。 這個結果應該是一個對象,如果有下一個值,value屬性會提供它;沒有更多結果時,done屬性應該為true,否則為false。
請注意,next,value和done屬性名稱是純字符串,而不是符號。 只有Symbol.iterator是一個實際的符號,它可能被添加到不同的大量對象中。
我們可以直接使用這個接口。
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}
我們來實現一個可迭代的數據結構。 我們將構建一個matrix類,充當一個二維數組。
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; } }
該類將其內容存儲在width × height個元素的單個數組中。 元素是按行存儲的,因此,例如,第五行中的第三個元素存儲在位置4 × width + 2中(使用基于零的索引)。
構造器需要寬度,高度和一個可選的內容函數,用來填充初始值。 get和set方法用于檢索和更新矩陣中的元素。
遍歷矩陣時,通常對元素的位置以及元素本身感興趣,所以我們會讓迭代器產生具有x,y和value屬性的對象。
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}; } }
這個類在其x和y屬性中跟蹤遍歷矩陣的進度。 next方法最開始檢查是否到達矩陣的底部。 如果沒有,則首先創建保存當前值的對象,之后更新其位置,如有必要則移至下一行。
讓我們使Matrix類可迭代。 在本書中,我會偶爾使用事后的原型操作來為類添加方法,以便單個代碼段保持較小且獨立。 在一個正常的程序中,不需要將代碼分成小塊,而是直接在class中聲明這些方法。
Matrix.prototype[Symbol.iterator] = function() { return new MatrixIterator(this); };
現在我們可以用for/of來遍歷一個矩陣。
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讀寫器和靜態
接口通常主要由方法組成,但也可以持有非函數值的屬性。 例如,Map對象有size屬性,告訴你有多少個鍵存儲在它們中。
這樣的對象甚至不需要直接在實例中計算和存儲這樣的屬性。 即使直接訪問的屬性也可能隱藏了方法調用。 這種方法稱為讀取器(getter),它們通過在方法名稱前面編寫get來定義。
let varyingSize = { get size() { return Math.floor(Math.random() * 100); } }; console.log(varyingSize.size); // → 73 console.log(varyingSize.size); // → 49
每當有人讀取此對象的size屬性時,就會調用相關的方法。 當使用寫入器(setter)寫入屬性時,可以做類似的事情。
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類允許你以攝氏度或華氏度讀取和寫入溫度,但內部僅存儲攝氏度,并在fahrenheit讀寫器中自動轉換為攝氏度。
有時候你想直接向你的構造器附加一些屬性,而不是原型。 這樣的方法將無法訪問類實例,但可以用來提供額外方法來創建實例。
在類聲明內部,名稱前面寫有static的方法,存儲在構造器中。 所以Temperature類可以讓你寫出Temperature.fromFahrenheit(100),來使用華氏溫度創建一個溫度。
繼承已知一些矩陣是對稱的。 如果沿左上角到右下角的對角線翻轉對稱矩陣,它保持不變。 換句話說,存儲在x,y的值總是與y,x相同。
想象一下,我們需要一個像Matrix這樣的數據結構,但是它必需保證一個事實,矩陣是對稱的。 我們可以從頭開始編寫它,但這需要重復一些代碼,與我們已經寫過的代碼很相似。
JavaScript 的原型系統可以創建一個新類,就像舊類一樣,但是它的一些屬性有了新的定義。 新類派生自舊類的原型,但為set方法增加了一個新的定義。
在面向對象的編程術語中,這稱為繼承(inheritance)。 新類繼承舊類的屬性和行為。
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這個詞用于表示,這個類不應該直接基于默認的Object原型,而應該基于其他類。 這被稱為超類(superclass)。 派生類是子類(subclass)。
為了初始化SymmetricMatrix實例,構造器通過super關鍵字調用其超類的構造器。 這是必要的,因為如果這個新對象的行為(大致)像Matrix,它需要矩陣具有的實例屬性。 為了確保矩陣是對稱的,構造器包裝了content方法,來交換對角線以下的值的坐標。
set方法再次使用super,但這次不是調用構造器,而是從超類的一組方法中調用特定的方法。 我們正在重新定義set,但是想要使用原來的行為。 因為this.set引用新的set方法,所以調用這個方法是行不通的。 在類方法內部,super提供了一種方法,來調用超類中定義的方法。
繼承允許我們用相對較少的工作,從現有數據類型構建稍微不同的數據類型。 它是面向對象傳統的基礎部分,與封裝和多態一樣。 盡管后兩者現在普遍被認為是偉大的想法,但繼承更具爭議性。
盡管封裝和多態可用于將代碼彼此分離,從而減少整個程序的耦合,但繼承從根本上將類連接在一起,從而產生更多的耦合。 繼承一個類時,比起單純使用它,你通常必須更加了解它如何工作。 繼承可能是一個有用的工具,并且我現在在自己的程序中使用它,但它不應該成為你的第一個工具,你可能不應該積極尋找機會來構建類層次結構(類的家族樹)。
instanceof運算符在有些時候,了解某個對象是否繼承自某個特定類,也是十分有用的。JavaScript 為此提供了一個二元運算符,名為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
該運算符會瀏覽所有繼承類型。所以SymmetricMatrix是Matrix的一個實例。 該運算符也可以應用于像Array這樣的標準構造器。 幾乎每個對象都是Object的一個實例。
本章小結對象不僅僅持有它們自己的屬性。對象中有另一個對象:原型,只要原型中包含了屬性,那么根據原型構造出來的對象也就可以看成包含了相應的屬性。簡單對象直接以Object.prototype作為原型。
構造器是名稱通常以大寫字母開頭的函數,可以與new運算符一起使用來創建新對象。 新對象的原型是構造器的prototype屬性中的對象。 通過將屬性放到它們的原型中,可以充分利用這一點,給定類型的所有值在原型中分享它們的屬性。 class表示法提供了一個顯式方法,來定義一個構造器及其原型。
你可以定義讀寫器,在每次訪問對象的屬性時秘密地調用方法。 靜態方法是存儲在類的構造器,而不是其原型中的方法。
給定一個對象和一個構造器,instanceof運算符可以告訴你該對象是否是該構造器的一個實例。
可以使用對象的來做一個有用的事情是,為它們指定一個接口,告訴每個人他們只能通過該接口與對象通信。 構成對象的其余細節,現在被封裝在接口后面。
不止一種類型可以實現相同的接口。 為使用接口而編寫的代碼,自動知道如何使用提供接口的任意數量的不同對象。 這被稱為多態。
實現多個類,它們僅在一些細節上有所不同的時,將新類編寫為現有類的子類,繼承其一部分行為會很有幫助。
習題 向量類型編寫一個構造器Vec,在二維空間中表示數組。該函數接受兩個數字參數x和y,并將其保存到對象的同名屬性中。
向Vec原型添加兩個方法:plus和minus,它們接受另一個向量作為參數,分別返回兩個向量(一個是this,另一個是參數)的和向量與差向量。
向原型添加一個getter屬性length,用于計算向量長度,即點(x,y)與原點(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分組
標準的 JavaScript 環境提供了另一個名為Set的數據結構。 像Map的實例一樣,集合包含一組值。 與Map不同,它不會將其他值與這些值相關聯 - 它只會跟蹤哪些值是該集合的一部分。 一個值只能是一個集合的一部分 - 再次添加它沒有任何作用。
寫一個名為Group的類(因為Set已被占用)。 像Set一樣,它具有add,delete和has方法。 它的構造器創建一個空的分組,add給分組添加一個值(但僅當它不是成員時),delete從組中刪除它的參數(如果它是成員),has 返回一個布爾值,表明其參數是否為分組的成員。
使用===運算符或類似于indexOf的東西來確定兩個值是否相同。
為該類提供一個靜態的from方法,該方法接受一個可迭代的對象作為參數,并創建一個分組,包含遍歷它產生的所有值。
// 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可迭代分組
使上一個練習中的Group類可迭代。 如果你不清楚接口的確切形式,請參閱本章前面迭代器接口的章節。
如果你使用數組來表示分組的成員,則不要僅僅通過調用數組中的Symbol.iterator方法來返回迭代器。 這會起作用,但它會破壞這個練習的目的。
如果分組被修改時,你的迭代器在迭代過程中出現奇怪的行為,那也沒問題。
// 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借鑒方法
在本章前面我提到,當你想忽略原型的屬性時,對象的hasOwnProperty可以用作in運算符的更強大的替代方法。 但是如果你的映射需要包含hasOwnProperty這個詞呢? 你將無法再調用該方法,因為對象的屬性隱藏了方法值。
你能想到一種方法,對擁有自己的同名屬性的對象,調用hasOwnProperty嗎?
let map = {one: true, two: true, hasOwnProperty: true}; // Fix this call console.log(map.hasOwnProperty("one")); // → true
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/105023.html
摘要:在本例中,使用屬性指定鏈接的目標,其中表示超文本鏈接。您應該認為和元數據隱式出現在示例中,即使它們沒有實際顯示在文本中。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:JavaScript and the Browser 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地采用谷歌翻譯 部分參考了《JavaScript 編程精解(第 2 版)》 ...
摘要:相反,當響應指針事件時,它會調用創建它的代碼提供的回調函數,該函數將處理應用的特定部分。回調函數可能會返回另一個回調函數,以便在按下按鈕并且將指針移動到另一個像素時得到通知。它們為組件構造器的數組而提供。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Project: A Pixel Art Editor 譯者:飛龍 協議:CC BY-NC-SA 4...
摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協議自豪地采用谷歌翻譯部分參考了編程精解第版,這是一本關于指導電腦的書。在可控的范圍內編寫程序是編程過程中首要解決的問題。我們可以用中文來描述這些指令將數字存儲在內存地址中的位置。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Introduction 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地...
摘要:來源編程精解中文第三版翻譯項目原文譯者飛龍協議自豪地采用谷歌翻譯部分參考了編程精解第版確定編程語言中的表達式含義的求值器只是另一個程序。若文本不是一個合法程序,解析器應該指出錯誤。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:Project: A Programming Language 譯者:飛龍 協議:CC BY-NC-SA 4.0 自豪地采用...
摘要:在其沙箱中提供了將文本轉換成文檔對象模型的功能。瀏覽器使用與該形狀對應的數據結構來表示文檔。我們將這種表示方式稱為文檔對象模型,或簡稱。樹回想一下第章中提到的語法樹。語言的語法樹有標識符值和應用節點。元素表示標簽的節點用于確定文檔結構。 來源:ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:The Document Object Model 譯者:飛龍 協議...
閱讀 2991·2021-11-25 09:43
閱讀 3636·2021-08-31 09:41
閱讀 1247·2019-08-30 15:56
閱讀 2136·2019-08-30 15:55
閱讀 2998·2019-08-30 13:48
閱讀 2821·2019-08-29 15:15
閱讀 989·2019-08-29 15:14
閱讀 2661·2019-08-28 18:26