來源: ApacheCN『JavaScript 編程精解 中文第三版』翻譯項目原文:The Secret Life of Objectsjavascript
譯者:飛龍html
協議:CC BY-NC-SA 4.0java
自豪地採用谷歌翻譯git
部分參考了《JavaScript 編程精解(第 2 版)》程序員
抽象數據類型是經過編寫一種特殊的程序來實現的,該程序根據可在其上執行的操做來定義類型。github
Barbara Liskov,《Programming with Abstract Data Types》apache
第 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