JavaScript 編程精解 中文第三版 6、對象的祕密

來源: 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來輸出正在說話的兔子的種類。咱們回想一下applybind方法,這兩個方法接受的第一個參數能夠用來模擬對象中方法的調用。這兩個方法會把第一個參數複製給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.prototypeObject.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

下圖簡單地描述了代碼執行後的狀況。其中RabbitObject原型畫在了killerRabbit之下,咱們能夠從原型中找到對象中沒有的屬性。

覆蓋原型中存在的屬性是頗有用的特性。就像示例展現的那樣,咱們覆蓋了killerRabbitteeth屬性,這能夠用來描述實例(對象中更爲泛化的類的實例)的特殊屬性,同時又可讓簡單對象從原型中獲取標準的值。

覆蓋也用於向標準函數和數組原型提供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

setgethas方法是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

請注意,nextvaluedone屬性名稱是純字符串,而不是符號。 只有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中(使用基於零的索引)。

構造器須要寬度,高度和一個可選的內容函數,用來填充初始值。 getset方法用於檢索和更新矩陣中的元素。

遍歷矩陣時,一般對元素的位置以及元素自己感興趣,因此咱們會讓迭代器產生具備xyvalue屬性的對象。

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};
  }
}

這個類在其xy屬性中跟蹤遍歷矩陣的進度。 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

該運算符會瀏覽全部繼承類型。因此SymmetricMatrixMatrix的一個實例。 該運算符也能夠應用於像Array這樣的標準構造器。 幾乎每一個對象都是Object的一個實例。

本章小結

對象不只僅持有它們本身的屬性。對象中有另外一個對象:原型,只要原型中包含了屬性,那麼根據原型構造出來的對象也就能夠當作包含了相應的屬性。簡單對象直接以Object.prototype做爲原型。

構造器是名稱一般以大寫字母開頭的函數,能夠與new運算符一塊兒使用來建立新對象。 新對象的原型是構造器的prototype屬性中的對象。 經過將屬性放到它們的原型中,能夠充分利用這一點,給定類型的全部值在原型中分享它們的屬性。 class表示法提供了一個顯式方法,來定義一個構造器及其原型。

你能夠定義讀寫器,在每次訪問對象的屬性時祕密地調用方法。 靜態方法是存儲在類的構造器,而不是其原型中的方法。

給定一個對象和一個構造器,instanceof運算符能夠告訴你該對象是不是該構造器的一個實例。

可使用對象的來作一個有用的事情是,爲它們指定一個接口,告訴每一個人他們只能經過該接口與對象通訊。 構成對象的其他細節,如今被封裝在接口後面。

不止一種類型能夠實現相同的接口。 爲使用接口而編寫的代碼,自動知道如何使用提供接口的任意數量的不一樣對象。 這被稱爲多態。

實現多個類,它們僅在一些細節上有所不一樣的時,將新類編寫爲現有類的子類,繼承其一部分行爲會頗有幫助。

習題

向量類型

編寫一個構造器Vec,在二維空間中表示數組。該函數接受兩個數字參數xy,並將其保存到對象的同名屬性中。

Vec原型添加兩個方法:plusminus,它們接受另外一個向量做爲參數,分別返回兩個向量(一個是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同樣,它具備adddeletehas方法。 它的構造器建立一個空的分組,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
相關文章
相關標籤/搜索