天天看點

JavaScript 程式設計精解 中文第三版 六、對象的秘密

六、對象的秘密

原文: The Secret Life of Objects 譯者: 飛龍 協定: CC BY-NC-SA 4.0 自豪地采用 谷歌翻譯 部分參考了 《JavaScript 程式設計精解(第 2 版)》

抽象資料類型是通過編寫一種特殊的程式來實作的,該程式根據可在其上執行的操作來定義類型。

Barbara Liskov,《Programming with Abstract Data Types》

JavaScript 程式設計精解 中文第三版 六、對象的秘密

第 4 章介紹了 JavaScript 的對象(object)。 在程式設計文化中,我們有一個名為面向對象程式設計(OOP)的東西,這是一組技術,使用對象(和相關概念)作為程式組織的中心原則。

雖然沒有人真正同意其精确定義,但面向對象程式設計已經成為了許多程式設計語言的設計,包括 JavaScript 在内。 本章将描述這些想法在 JavaScript 中的應用方式。

封裝

面向對象程式設計的核心思想是将程式分成小型片段,并讓每個片段負責管理自己的狀态。

通過這種方式,一些程式片段的工作方式的知識可以局部保留。 從事其他方面的工作的人,不必記住甚至不知道這些知識。 無論什麼時候這些局部細節發生變化,隻需要直接更新其周圍的代碼。

這種程式的不同片段通過接口(interface),函數或綁定的有限集合互動,它以更抽象的級别提供有用的功能,并隐藏它的精确實作。

這些程式片段使用對象模組化。 它們的接口由一組特定的方法(method)和屬性(property)組成。 接口的一部分的屬性稱為公共的(public)。 其他外部代碼不應該接觸屬性的稱為私有的(private)。

許多語言提供了區分公共和私有屬性的方法,并且完全防止外部代碼通路私有屬性。 JavaScript 再次采用極簡主義的方式,沒有。 至少目前還沒有 - 有個正在開展的工作,将其添加到該語言中。

即使這種語言沒有内置這種差別,JavaScript 程式員也成功地使用了這種想法。 通常,可用的接口在文檔或數字一中描述。 在屬性名稱的的開頭經常會放置一個下劃線(

_

)字元,來表明這些屬性是私有的。

将接口與實作分離是一個好主意。 它通常被稱為封裝(encapsulation)。

6.2 方法

方法不過是持有函數值的屬性。 這是一個簡單的方法:

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

之下,我們可以從原型中找到對象中沒有的屬性。

JavaScript 程式設計精解 中文第三版 六、對象的秘密

覆寫原型中存在的屬性是很有用的特性。就像示例展示的那樣,我們覆寫了

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           

6.8 多态

當你調用一個對象的

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

提供了一種方法,來調用超類中定義的方法。

繼承允許我們用相對較少的工作,從現有資料類型建構稍微不同的資料類型。 它是面向對象傳統的基礎部分,與封裝和多态一樣。 盡管後兩者現在普遍被認為是偉大的想法,但繼承更具争議性。

盡管封裝和多态可用于将代碼彼此分離,進而減少整個程式的耦合,但繼承從根本上将類連接配接在一起,進而産生更多的耦合。 繼承一個類時,比起單純使用它,你通常必須更加了解它如何工作。 繼承可能是一個有用的工具,并且我現在在自己的程式中使用它,但它不應該成為你的第一個工具,你可能不應該積極尋找機會來建構類層次結構(類的家族樹)。

6.12 

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

運算符可以告訴你該對象是否是該構造器的一個執行個體。

可以使用對象的來做一個有用的事情是,為它們指定一個接口,告訴每個人他們隻能通過該接口與對象通信。 構成對象的其餘細節,現在被封裝在接口後面。

不止一種類型可以實作相同的接口。 為使用接口而編寫的代碼,自動知道如何使用提供接口的任意數量的不同對象。 這被稱為多态。

實作多個類,它們僅在一些細節上有所不同的時,将新類編寫為現有類的子類,繼承其一部分行為會很有幫助。

6.14 習題

6.14.1 向量類型

編寫一個構造器

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