<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                ## 六、對象的秘密 > 原文:[The Secret Life of Objects](http://eloquentjavascript.net/06_object.html) > > 譯者:[飛龍](https://github.com/wizardforcel) > > 協議:[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/) > > 自豪地采用[谷歌翻譯](https://translate.google.cn/) > > 部分參考了[《JavaScript 編程精解(第 2 版)》](https://book.douban.com/subject/26707144/) > 抽象數據類型是通過編寫一種特殊的程序來實現的,該程序根據可在其上執行的操作來定義類型。 > > Barbara Liskov,《Programming with Abstract Data Types》 ![](https://img.kancloud.cn/09/42/094237c44a22af5018d736435b21c107_490x310.jpg) 第 4 章介紹了 JavaScript 的對象(object)。 在編程文化中,我們有一個名為面向對象編程(OOP)的東西,這是一組技術,使用對象(和相關概念)作為程序組織的中心原則。 雖然沒有人真正同意其精確定義,但面向對象編程已經成為了許多編程語言的設計,包括 JavaScript 在內。 本章將描述這些想法在 JavaScript 中的應用方式。 ## 封裝 面向對象編程的核心思想是將程序分成小型片段,并讓每個片段負責管理自己的狀態。 通過這種方式,一些程序片段的工作方式的知識可以局部保留。 從事其他方面的工作的人,不必記住甚至不知道這些知識。 無論什么時候這些局部細節發生變化,只需要直接更新其周圍的代碼。 這種程序的不同片段通過接口(interface),函數或綁定的有限集合交互,它以更抽象的級別提供有用的功能,并隱藏它的精確實現。 這些程序片段使用對象建模。 它們的接口由一組特定的方法(method)和屬性(property)組成。 接口的一部分的屬性稱為公共的(public)。 其他外部代碼不應該接觸屬性的稱為私有的(private)。 許多語言提供了區分公共和私有屬性的方法,并且完全防止外部代碼訪問私有屬性。 JavaScript 再次采用極簡主義的方式,沒有。 至少目前還沒有 - 有個正在開展的工作,將其添加到該語言中。 即使這種語言沒有內置這種區別,JavaScript 程序員也成功地使用了這種想法。 通常,可用的接口在文檔或數字一中描述。 在屬性名稱的的開頭經常會放置一個下劃線(`_`)字符,來表明這些屬性是私有的。 將接口與實現分離是一個好主意。 它通常被稱為封裝(encapsulation)。 ## 方法 方法不過是持有函數值的屬性。 這是一個簡單的方法: ```js let rabbit = {}; rabbit.speak = function(line) { console.log(`The rabbit says '${line}'`); }; rabbit.speak("I'm alive."); // → The rabbit says 'I'm alive.' ``` 方法通常會在對象被調用時執行一些操作。將函數作為對象的方法調用時,會找到對象中對應的屬性并直接調用。當函數作為方法調用時,函數體內叫做`this`的綁定自動指向在它上面調用的對象。 ```js 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`值作為第一個參數,并將其它處理為看做普通參數。 ```js speak.call(hungryRabbit, "Burp!"); // → The hungry rabbit says 'Burp!' ``` 這段代碼使用了關鍵字`this`來輸出正在說話的兔子的種類。我們回想一下`apply`和`bind`方法,這兩個方法接受的第一個參數可以用來模擬對象中方法的調用。這兩個方法會把第一個參數復制給`this`。 由于每個函數都有自己的`this`綁定,它的值依賴于它的調用方式,所以在用`function`關鍵字定義的常規函數中,不能引用外層作用域的`this`。 箭頭函數是不同的 - 它們不綁定他們自己的`this`,但可以看到他們周圍(定義位置)作用域的`this`綁定。 因此,你可以像下面的代碼那樣,在局部函數中引用`this`: ```js 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`,則代碼將不起作用。 ## 原型 我們來仔細看看以下這段代碼。 ```js let empty = {}; console.log(empty.toString); // → function toString(){…} console.log(empty.toString()); // → [object Object] ``` 我從一個空對象中取出了一個屬性。 好神奇! 實際上并非如此。我只是掩蓋了一些 JavaScript 對象的內部工作細節罷了。每個對象除了擁有自己的屬性外,都包含一個原型(prototype)。原型是另一個對象,是對象的一個屬性來源。當開發人員訪問一個對象不包含的屬性時,就會從對象原型中搜索屬性,接著是原型的原型,依此類推。 那么空對象的原型是什么呢?是`Object.prototype`,它是所有對象中原型的父原型。 ```js 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`。 ```js console.log(Object.getPrototypeOf(Math.max) == Function.prototype); // → true console.log(Object.getPrototypeOf([]) == Array.prototype); // → true ``` 對于這樣的原型對象來說,其自身也包含了一個原型對象,通常情況下是`Object.prototype`,所以說,這些原型對象可以間接提供`toString`這樣的方法。 你可以使用`Object.create`來創建一個具有特定原型的對象。 ```js 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)函數的作用。 ```js function makeRabbit(type) { let rabbit = Object.create(protoRabbit); rabbit.type = type; return rabbit; } ``` JavaScript 提供了一種方法,來使得更容易定義這種類型的功能。 如果將關鍵字`new`放在函數調用之前,則該函數將被視為構造器。 這意味著具有正確原型的對象會自動創建,綁定到函數中的`this`,并在函數結束時返回。 構造對象時使用的原型對象,可以通過構造器的`prototype`屬性來查找。 ```js 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`屬性擁有原型,用于通過它創建的實例。 ```js console.log(Object.getPrototypeOf(Rabbit) == Function.prototype); // → true console.log(Object.getPrototypeOf(weirdRabbit) == Rabbit.prototype); // → true ``` ## 類的表示法 所以 JavaScript 類是帶有原型屬性的構造器。 這就是他們的工作方式,直到 2015 年,這就是你編寫他們的方式。 最近,我們有了一個不太笨拙的表示法。 ```js 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`可以在語句和表達式中使用。 當用作表達式時,它沒有定義綁定,而只是將構造器作為一個值生成。 你可以在類表達式中省略類名稱。 ```js let object = new class { getWord() { return "hello"; } }; console.log(object.getWord()); // → hello ``` ## 覆蓋派生的屬性 將屬性添加到對象時,無論它是否存在于原型中,該屬性都會添加到對象本身中。 如果原型中已經有一個同名的屬性,該屬性將不再影響對象,因為它現在隱藏在對象自己的屬性后面。 ```js 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`之下,我們可以從原型中找到對象中沒有的屬性。 ![](https://img.kancloud.cn/8e/f9/8ef9779c11e2e060b2dd67c3b729b186.svg) 覆蓋原型中存在的屬性是很有用的特性。就像示例展示的那樣,我們覆蓋了`killerRabbit`的`teeth`屬性,這可以用來描述實例(對象中更為泛化的類的實例)的特殊屬性,同時又可以讓簡單對象從原型中獲取標準的值。 覆蓋也用于向標準函數和數組原型提供`toString`方法,與基本對象的原型不同。 ```js console.log(Array.prototype.toString == Object.prototype.toString); // → false console.log([1, 2].toString()); // → 1,2 ``` 調用數組的`toString`方法后得到的結果與調用`.join(",")`的結果十分類似,即在數組的每個值之間插入一個逗號。而直接使用數組調用`Object.prototype.toString`則會產生一個完全不同的字符串。由于`Object`原型提供的`toString`方法并不了解數組結構,因此只會簡單地輸出一對方括號,并在方括號中間輸出單詞`"object"`和類型的名稱。 ```js console.log(Object.prototype.toString.call([1, 2])); // → [object Array] ``` ## 映射 我們在上一章中看到了映射(map)這個詞,用于一個操作,通過對元素應用函數來轉換數據結構。 令人困惑的是,在編程時,同一個詞也被用于相關而不同的事物。 映射(名詞)是將值(鍵)與其他值相關聯的數據結構。 例如,你可能想要將姓名映射到年齡。 為此可以使用對象。 ```js 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`派生,并且可以安全地用作映射。 ```js console.log("toString" in Object.create(null)); // → false ``` 對象屬性名稱必須是字符串。 如果你需要一個映射,它的鍵不能輕易轉換為字符串 - 比如對象 - 你不能使用對象作為你的映射。 幸運的是,JavaScript 帶有一個叫做`Map`的類,它正是為了這個目的而編寫。 它存儲映射并允許任何類型的鍵。 ```js 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`方法,該方法會忽略對象的原型。 ```js console.log({x: 1}.hasOwnProperty("x")); // → true console.log({x: 1}.hasOwnProperty("toString")); // → false ``` ## 多態 當你調用一個對象的`String`函數(將一個值轉換為一個字符串)時,它會調用該對象的`toString`方法來嘗試從它創建一個有意義的字符串。 我提到一些標準原型定義了自己的`toString`版本,因此它們可以創建一個包含比`"[object Object]"`有用信息更多的字符串。 你也可以自己實現。 ```js 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`函數創建的值。 與字符串不同,新創建的符號是唯一的 - 你不能兩次創建相同的符號。 ```js let sym = Symbol("name"); console.log(sym == Symbol("name")); // → false Rabbit.prototype[sym] = 55; console.log(blackRabbit[sym]); // → 55 ``` 將`Symbol`轉換為字符串時,會得到傳遞給它的字符串,例如,在控制臺中顯示時,符號可以更容易識別。 但除此之外沒有任何意義 - 多個符號可能具有相同的名稱。 由于符號既獨特又可用于屬性名稱,因此符號適合定義可以和其他屬性共生的接口,無論它們的名稱是什么。 ```js 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 ``` 通過在屬性名稱周圍使用方括號,可以在對象表達式和類中包含符號屬性。 這會導致屬性名稱的求值,就像方括號屬性訪問表示法一樣,這允許我們引用一個持有該符號的綁定。 ```js 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`是一個實際的符號,它可能被添加到不同的大量對象中。 我們可以直接使用這個接口。 ```js 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`類,充當一個二維數組。 ```js 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`屬性的對象。 ```js 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`中聲明這些方法。 ```js Matrix.prototype[Symbol.iterator] = function() { return new MatrixIterator(this); }; ``` 現在我們可以用`for/of`來遍歷一個矩陣。 ```js 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`來定義。 ```js let varyingSize = { get size() { return Math.floor(Math.random() * 100); } }; console.log(varyingSize.size); // → 73 console.log(varyingSize.size); // → 49 ``` 每當有人讀取此對象的`size`屬性時,就會調用相關的方法。 當使用寫入器(setter)寫入屬性時,可以做類似的事情。 ```js 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)。 新類繼承舊類的屬性和行為。 ```js 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`。 ```js 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)`之間的距離。 ```js // 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`方法,該方法接受一個可迭代的對象作為參數,并創建一個分組,包含遍歷它產生的所有值。 ```js // 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`方法來返回迭代器。 這會起作用,但它會破壞這個練習的目的。 如果分組被修改時,你的迭代器在迭代過程中出現奇怪的行為,那也沒問題。 ```js // 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`嗎? ```js let map = {one: true, two: true, hasOwnProperty: true}; // Fix this call console.log(map.hasOwnProperty("one")); // → true ```
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看