The Secret Life of Objects¶
Eloquent JavaScript Chapter 6 の読書ノート。
本章では JavaScript 流オブジェクト指向プログラミングの様式を見ていく。C++ や Python との考え方の違いに注目するといいかもしれない。私がモダンな Java を知っていたらとよかったのだが。
Encapsulation¶
オブジェクト指向プログラミングの基本的な考え方とは、プログラムを小さな欠片に分割して、それぞれの状態を管理するというものだ。
このようなプログラムの断片は、オブジェクトを使ってモデル化される。そのインターフェースはメソッドとプロパティーの(クライアントに対しては限定された)集合で構成される。
多くの言語は、
public
とprivate
のプロパティーを区別する方法を提供しているが、ミニマリスト的な方法論を採っている JavaScript では、少なくとも今のところはない。現在、この機能を言語に追加する作業が行われているそうだ。
JavaScript のプログラマーは
public
/private
を区別するという考えを利用している。一般的に、利用可能なインターフェイスは、ドキュメントやコメントに記述されている。また、プロパティがプライベートであることを示すのに、変数名の最初の一文字を_
とするのも一般的だ。ということは Python のプログラマーに近い。
インターフェースと実装を分離することは素晴らしい考えだ。この考え方をカプセル化と呼ぶ。
Methods¶
メソッドは関数値を保持するプロパティーに過ぎない。
let rabbit = {};
rabbit.speak = function(line) {
console.log(`The rabbit says '${line}'`);
};
rabbit.speak("I'm alive.");
function speak(line) {
console.log(`The ${this.type} rabbit says '${line}'`);
}
let whiteRabbit = {type: "white", speak};
let hungryRabbit = {type: "hungry", speak};
whiteRabbit.speak("Oh my ears and whiskers, how late it's getting!");
hungryRabbit.speak("I could use a carrot right now.");
メソッドの元になる関数に対して call
メソッドをオブジェクトを第一引数として呼び出すと、オブジェクトのメソッドの形式でそれが呼び出される:
speak.call(hungryRabbit, "Burp!");
キーワード
this
を次のようにして用いることもある:
function normalize() {
console.log(this.coords.map(n => n / this.length));
}
normalize.call({coords: [0, 2, 3], length: 5});
仮に map
の呼び出しに対する実引数がキーワード function
で書かれた関数だったら、このコードは動作しなかった。
上のコードは本書から無修正で引用したものだが、この関数はベクトルの正規化になっていない。
Prototype¶
オブジェクトのほとんどには、プロトタイプと呼ばれる、プロパティーの代替となる別のオブジェクトがある。
オブジェクトが持っていないプロパティーが参照されると、まずはそのオブジェクトのプロトタイプが検索される。そこにもないときには、次にそのプロトタイプのプロトタイプが検索される、以下それを繰り返す、という具合だ。最後に
Object.prototype
に到達する。Object.getPrototypeOf
は与えられたオブジェクトのプロトタイプを返す。
console.assert(Object.getPrototypeOf({}) == Object.prototype);
console.assert(Object.getPrototypeOf(Object.prototype) == null);
オブジェクトの多くは、そのプロトタイプとして
Object.prototype
を直接は持っていない。関数は
Function.prototype
から派生する。配列は
Array.prototype
から派生する。
console.assert(Object.getPrototypeOf(Math.max) == Function.prototype);
console.assert(Object.getPrototypeOf([]) == Array.prototype);
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!");
オブジェクト式の
speak(line)
のようなプロパティーは、メソッドを定義するための略記法。つまりspeak: speak(line){ ... }
ということだ。これは Prototype デザインパターンそのものであり、興味深い。
Classes¶
JavaScript のプロトタイプシステムは、クラスと呼ばれるオブジェクト指向の概念を、やや非公式に取り入れたものと解釈される。
クラスは、どのようなメソッドやプロパティを持っているかなどの型と呼ばれる概念を定義する。
クラスに対して、どんな値のプロパティーを持つかなどを設定したものをインスタンスと呼ぶ。
プロトタイプは、メソッドなど、クラスのすべてのインスタンスが同じ値を持つプロパティーを定義するのに便利だ。
function makeRabbit(type) {
let rabbit = Object.create(protoRabbit);
rabbit.type = type;
return rabbit;
}
JavaScript はコンストラクターという考え方もサポートする。キーワード new
を関数呼び出しの前に置くと、その関数はコンストラクターとして扱われる。正しいプロトタイプを持つオブジェクトが自動的に作成され、return
文はないものの、それが返される。
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
プロパティーを取得することで確認できる。コンストラクターの名前は、他の関数と簡単に区別できるように、慣習的に大文字で表記する。
プロトタイプがコンストラクターに関連付けられているかどうかを理解することは重要。
console.assert(Object.getPrototypeOf(Rabbit) == Function.prototype);
console.assert(Object.getPrototypeOf(weirdRabbit) == Rabbit.prototype);
Class notation¶
前節で JavaScript のクラスは prototype
を持つコンストラクター関数であることを見た。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
という名前にバインドされる。その他の関数は、そのコンストラクターのプロトタイプにまとめられる。
現在、この形式のクラス定義でプロトタイプに追加できるのは、関数を保持するプロパティであるメソッドだけとなっている。
関数と同様に、クラスは文の中でも式の中でも使うことができる。
クラス式では、クラス名を省略することが許される。
let object = new class { getWord() { return "hello"; } }; console.log(object.getWord());
Overriding derived properties¶
オブジェクトにプロパティーを追加すると、それがプロトタイプに存在するかどうかに関わらず、オブジェクト自体に追加される。
プロトタイプに同じ名前のプロパティーが既に存在していた場合、オブジェクトのプロパティーの後ろに隠れてしまうので、これはオブジェクトに影響を与えない。
Rabbit.prototype.teeth = "small";
console.assert(killerRabbit.teeth == "small");
killerRabbit.teeth = "long, sharp, and bloody";
console.assert(killerRabbit.teeth == "long, sharp, and bloody");
console.assert(blackRabbit.teeth == "small");
console.assert(Rabbit.prototype.teeth == "small");
配列に対する
.toString()
はオブジェクト一般のそれとは挙動が異なる。配列に対して.join(",")
を呼び出したのと同じような結果になる。
Map¶
今までは Python の dict
のようなデータ構造として次のようにしていた:
let ages = {
Boris: 39,
Liang: 22,
Júlia: 62
};
console.log(`Júlia is ${ages["Júlia"]}`);
console.log("Is Jack's age known?", "Jack" in ages); // false
console.log("Is toString's age knew?", "toString" in ages); // true; これがまずい
これは危ないのでクラス 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")); // false
console.log(ages.has("toString")); // false
メソッド
set
,get
,has
はMap
オブジェクトのインターフェースの一部だ。何らかの理由でマップとして扱う必要がある普通のオブジェクトがある場合、
Object.keys()
がそのオブジェクト自身のキーしか返さないことを知っていると便利だ。演算子
in
の代わりにメソッドhasOwnProperty
を使うこともできる。これはオブジェクトのプロトタイプを無視する。console.assert({x: 1}.hasOwnProperty("x")); console.assert(!{x: 1}.hasOwnProperty("toString"));
Polymorphism¶
例えば次のように toString()
を上書きすることができる。こういう技法をポリモーフィズムという。
Rabbit.prototype.toString = function() {
return `a ${this.type} rabbit`;
};
例えば for
/of
ループもポリモーフィズムの一種だ。データ構造が特定のインターフェイスを提供することを期待している。
Symbols¶
プロパティー名は通常は
String
だがSymbol
であることがある。Symbol
のオブジェクトは一意的であり、同じ値を二度作ることができない。let sym = Symbol("name"); console.assert(sym != Symbol("name")); Rabbit.prototype[sym] = 55; console.assert(blackRabbit[sym] == 55);
次のような不思議なことができる。
const toStringSymbol = Symbol("toString"); Array.prototype[toStringSymbol] = function() { return `${this.length} cm of blue yarn`; }; console.assert([1, 2].toString() == "1,2"); console.assert([1, 2][toStringSymbol]() == "2 cm of blue yarn");
今の時点でこの機能に深入りするのは危険な気がする。
The iterator interface¶
for
/of
ループに渡されるオブジェクトは、iterable であることが期待される。あるオブジェクトが iterable であるとは、それが名前が 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}
このメソッドが呼ばれると iterator なるものを返す。これが実際に反復処理を行う。これには次の結果を返すメソッド
next
を持ち、次の結果を返す。その結果とは、次の値を提供するプロパティーvalue
とプロパティーdone
を持つオブジェクトだ。これ以上の結果がない場合はtrue
を、それ以外の場合はfalse
になるはずだ。プロパティー
next
,value
,done
の名前は、Symbol
ではなくString
であることに注意が要る。というより、Symbol
なのはiterator
だけだ。
Todo
行列の要素を反復するためのコードが少しむずかしい。
Iterator デザインパターンの基本に則っているコードであることは読める。
Getters, setters, and statics¶
Python で言う
@propery
のようなメソッド機能が JavaScript でもサポートされている。Python で言う
@staticmethod
のような機能もサポートされている。
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);
}
}
Inheritance¶
JavaScript でもサブクラスを定義することができる。本書の例をそのまま引用する。
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}`);
構文としては Java に酷似していて、キーワード
extends
を用いて基底クラスを指定し、サブクラスの定義を始める。メソッドからキーワード
super
を使って、基底クラス自身のメンバーを参照する。Python と使われ方が異なるので注意。いきなりドットを書く。
this
ではなくsuper
という感じか。
継承は、カプセル化やポリモーフィズムと並んで、オブジェクト指向の基本だ。後者の二つが素晴らしいアイデアだと一般的に評価されているのに対し、継承に対しては手ばなしに素晴らしいとは言えない。後者二つはプログラム要素間の結合の度合いを明らかに下げるが、継承はむしろ上げてしまう。
The instanceof operator¶
二項演算子 instanceof
はあるオブジェクトが特定のクラスか、それの派生クラスであるかどうかをテストする。第一オペランドと第二オペランドにオブジェクトとコンストラクター関数をそれぞれ渡す。
console.assert(new SymmetricMatrix(2) instanceof SymmetricMatrix);
console.assert(new SymmetricMatrix(2) instanceof Matrix);
console.assert(!(new Matrix(2, 2) instanceof SymmetricMatrix));
console.assert([1] instanceof Array);
Summary¶
オブジェクトに付随するプロトタイプという概念がある。
単純なオブジェクトはプロトタイプとして
Object.prototype
を有する。コンストラクターの
通常は名前が大文字で始まる関数となっている。
コンストラクターを演算子
new
と一緒に使用して新しいオブジェクトを作成する。オブジェクトのプロトタイプは、コンストラクターのプロパティー
prototype
で指定されたオブジェクトとなる。
オブジェクトのプロパティーにアクセスするたびに、関連するメソッドを暗黙的に呼び出すという getter や setter を定義することができる。
静的メソッドとは、クラスのプロトタイプではなく、コンストラクターに格納されているメソッドだ。
演算子
instanceof
はオブジェクトのクラスをテストする。オブジェクトは、そのインターフェースを介してのみオブジェクトとアクセスできるようにすると利用させやすい。
複数の型が同じインターフェイスを実装することができる。インターフェイスを使用するように書かれたコードは、そのインターフェイスを備える、異なるオブジェクトを操作する方法が自動的にわかっている。この仕組みをポリモーフィズムという。
詳細がわずかしか違わないクラスを複数定義する場合は、継承の技法が有効だ。
Exercises¶
A vector type¶
問題 二次元空間のベクトルを表現するクラス Vec
を書け。これは数 x
,
y
を引数として取り、同じ名前のプロパティーに保存されるものとする。
次に Vec
のプロトタイプにメソッド plus
および minus
を与えろ。これらは別のベクトルを引数とし、メソッド名の示唆する新しいベクトルを返す。
取得プロパティー length
をプロトタイプに加えろ。ベクトルの大きさを計算してそれを返すものだ。
解答 そういえば JavaScript には演算子のオーバーロード機能はないのだろうか。
class Vec{
constructor(x, y){
this.x = x;
this.y = y;
}
get length() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
};
Vec.prototype.plus = function(other){
return new Vec(this.x + other.x, this.y + other.y);
}
Vec.prototype.minus = function(other){
return new Vec(this.x - other.x, this.y - other.y);
}
Groups¶
問題 次の仕様に従うクラス Group
を書け。
このクラスには標準 JavaScript にあるクラス
Set
のようにメソッドadd
,delete
,has
がある。コンストラクターは空の
Group
を作成する。メソッド
add
は引数がまだメンバーでない場合に限り、それをGroup
に追加する。メソッド
delete
は引数がメンバーである場合にそれをGroup
から削除する。メソッド
has
は引数がGroup
のメンバーであるかどうかを示す真偽値を返す。
二つの値が同じかどうかを判断するには演算子
===
またはindexOf
などの同等の何かを使え。ノート:メソッド
Array.prototype.indexOf
が暗に使う比較演算は演算子===
に基づく。
このクラスには iterable を引数として取り、それを反復して生成されたすべての値を含む``Group`` を作成する静的メソッド
from
を実装しろ。
解答 基本型 Array
を使って実装して構わないと解釈する。本物の集合クラスを書くのは無理がある。
class Group{
constructor(){
this.content = [];
}
add(element){
if(!this.has(element)){
this.content.push(element);
}
}
delete(element){
const where = this.content.indexOf(element);
if(where != -1){
this.content.splice(where, 1);
}
}
has(element){
return this.content.indexOf(element) != -1;
}
static from(iterable){
const group = new Group;
for(const i of iterable){
group.add(i);
}
return group;
}
}
Iterable groups¶
問題 前の演習で定義したクラス Group
を iterable にしろ。
Group
のメンバーを表すのに配列を使用した場合、配列に対してメソッド
Symbol.iterator
を呼び出して作成した反復子を返すだけではいけない(それでうまくいったとしても、この演習の目的に反する)。
なお、反復処理中に Group
が変更されたとき、反復子の動作がおかしくなるのは構わないとする。
解答 学習中に飛ばした箇所なので、今泥縄でやってみる。
まず Group.prototype
を次のように拡張する:
Group.prototype[Symbol.iterator] = function(){
return new GroupIterator(this);
};
まだ GroupIterator
を書いていないので、書く:
class GroupIterator{
constructor(group){
this.i = 0;
this.group = group;
}
next(){
const content = this.group.content;
if(this.i == content.length){
return {done: true};
}
const value = {
i: this.i,
value: content[this.i]
};
++this.i;
return {value, done: false};
}
}
Borrowing a method¶
問題 この章の前半で、オブジェクトの hasOwnProperty
は、プロトタイプのプロパティーを無視したい場合に、演算子 in
の代わりに、より堅牢な手段として使用できることを述べた。
しかし、マップに "hasOwnProperty"
という単語を含める必要がある場合はどうだろうか。オブジェクト自身のプロパティがメソッドの値を隠してしまうため、そのメソッドを呼び出すことができなくなる。
hasOwnProperty
という名前のプロパティーを持っているオブジェクトに対して、隠された hasOwnProperty
を参照する方法はあるか。
解答 ある。Object.prototype
と call
を理解していることが急所の問題だ。
const myobj = {
myproperty: "my value",
hasOwnProperty: i => false,
};
console.assert(!myobj.hasOwnProperty('myproperty'));
console.assert(Object.prototype.hasOwnProperty.call(myobj, 'myproperty'));
console.assert(Object.prototype.hasOwnProperty.call(myobj, 'hasOwnProperty'));
参考: Object.prototype.hasOwnProperty() - JavaScript | MDN
以上