JavaScriptはプロトタイプベースのオブジェクト指向言語です。これは、クラスという概念がなく、オブジェクトをベースにした継承が行われるということを意味します。すべてのオブジェクトはプロトタイプを持ち、そのプロトタイプを元にして新しいオブジェクトを作成することができます。

オブジェクトとは

編集

JavaScriptは、プロトタイプベースのオブジェクト指向プログラミング言語です。 全てのオブジェクトのプロトタイプチェインをたどると、標準組み込みオブジェクト Object に行き着きます。

Object は他の多くの言語で連想配列ハッシュテーブルと呼ばれるもので重複しないキー(典型的には文字列)とそれに一対一に対応する値の対(プロパティ)を保持します。

JavaScriptのオブジェクトは、プロパティとメソッドを持ちます。プロパティとは、オブジェクトが持つ値のことであり、メソッドとは、オブジェクトが持つ関数のことです。プロパティとメソッドは、ドット.を使ってアクセスできます。

JavaScriptのオブジェクトには、プロトタイプという特別なプロパティがあります。プロトタイプは、新しいオブジェクトが作成されたときに、そのオブジェクトが継承する元となるオブジェクトです。オブジェクトが持つプロパティやメソッドが見つからなかった場合、JavaScriptはそのオブジェクトのプロトタイプを探し、そこで定義されているプロパティやメソッドを使います。このように、プロトタイプを辿って継承することをプロトタイプチェーンと呼びます。

JavaScriptのオブジェクト階層のRootには、Objectオブジェクトが位置しています。すべてのオブジェクトは、Objectオブジェクトを継承しているため、Objectオブジェクトのメソッドやプロパティを使用することができます。

以下は、オブジェクトのプロトタイプを使った継承の例です。

// Personオブジェクトのプロトタイプを定義
const personPrototype = {
    introduce() {
        console.log(`My name is ${this.name} and I am ${this.age} years old.`);
    }
};

// Personオブジェクトを作成
const person = Object.create(personPrototype);
person.name = 'John';
person.age = 30;

// personオブジェクトにintroduceメソッドを定義
person.introduce();

// Studentオブジェクトのプロトタイプを定義
const studentPrototype = Object.create(personPrototype);
studentPrototype.enroll = course => console.log(`I am enrolled in ${course}.`);

// Studentオブジェクトを作成
const student = Object.create(studentPrototype);
student.name = 'Mary';
student.age = 20;

// studentオブジェクトのintroduceメソッドを呼び出し
student.introduce();

// studentオブジェクトのenrollメソッドを呼び出し
student.enroll('Math');

この例では、personPrototypeオブジェクトとstudentPrototypeオブジェクトを定義しています。personPrototypeオブジェクトには、introduceメソッドが定義されています。studentPrototypeオブジェクトはpersonPrototypeオブジェクトを継承し、enrollメソッドを定義しています。

次に、personオブジェクトとstudentオブジェクトを作成しています。personオブジェクトはpersonPrototypeオブジェクトを継承しています。studentオブジェクトはstudentPrototypeオブジェクトを継承しています。それぞれ、nameageプロパティを設定しています。

最後に、introduceメソッドとenrollメソッドを呼び出しています。personオブジェクトはpersonPrototypeオブジェクトのintroduceメソッドを呼び出し、studentオブジェクトはstudentPrototypeオブジェクトのenrollメソッドを呼び出しています。

プロトタイプチェイン

編集

JavaScriptのオブジェクトは、プロトタイプベースのオブジェクト指向言語であり、プロトタイプチェインを使った継承が可能です。すべてのオブジェクトは、Objectオブジェクトを継承しているため、Objectオブジェクトのメソッドやプロパティを使用することができます。

以下はJavaScriptのプロトタイプチェインを図示したものです。

+----------------------+            +---------------------+
|          obj         |            |     Object.prototype|
+----------------------+            +---------------------+
            ^                                   ^
            |                                   |
      [[Prototype]]                       [[Prototype]]
            |                                   |
+----------------------+            +---------------------+
|    obj.__proto__     | ---------->|  Object.prototype   |
+----------------------+            +---------------------+
            ^                                   ^
            |                                   |
      [[Prototype]]                       [[Prototype]]
            |                                   |
+-------------------------+         +----------------------------+
| obj.__proto__.__proto__ | ------> | Object.prototype.__proto__ |
+-------------------------+         +----------------------------+
            ^                                   ^
            |                                   |
     (null or end)                         [[Prototype]]
                                                |
                                    +---------------------+
                                    |  null or end        |
                                    +---------------------+

オブジェクト obj[[Prototype]] 内部プロパティは、 obj.__proto__ として参照できます。 obj.__proto__ は、 obj の親となるオブジェクトを参照しています。 これにより、 obj のプロパティにアクセスしようとしたときに、プロパティが obj 自身に存在しない場合には、 obj.__proto__ が参照するオブジェクトのプロパティを順に探索していくことができます。

obj.__proto__ が参照するオブジェクトの [[Prototype]] プロパティも同様に、 __proto__ として参照できます。 このようにして、オブジェクトの親となるオブジェクトの親となるオブジェクト、といったように、オブジェクトのプロトタイプチェインを辿ることができます。

上記の図では、最上位の Object.prototype オブジェクトがルートとなっており、すべてのオブジェクトのプロトタイプチェインは Object.prototype を経由しています。 最下位のオブジェクトの __proto__ プロパティは、 null または undefined となっています。

プロトタイプチェインを辿ることで、親オブジェクトのプロパティにアクセスできる一方で、プロパティの変更も親オブジェクトに影響を与える可能性があります。 そのため、プロトタイプチェインに沿ってプロパティを変更する場合には、意図しない副作用が起こらないように注意が必要です。

プリミティブ

編集

JavaScriptにおいて、プリミティブとは、単純なデータ型のことを指します。以下はJavaScriptにおけるプリミティブの種類です。

  • 文字列 (string)
  • 数値 (number)
  • 長整数 (bigint)
  • 真偽値 (boolean)
  • null
  • undefined
  • シンボル (symbol)

これらのデータ型は、それ自体がオブジェクトではなく、単純な値そのものであるため、プリミティブと呼ばれます[1]。 たとえば、"Hello, world!"は文字列のプリミティブ値であり、42は数値のプリミティブ値です。

プリミティブは、それぞれが固有の値を持ち、イミュータブル (immutable) であるため、変更することができません。 また、プリミティブは、オブジェクトとは異なり、メソッドを持ちません。たとえば、"Hello, world!"は文字列のプリミティブ値であるため、メソッドを持っていません。 しかし、Stringオブジェクトは、文字列に対して多くの便利なメソッドを提供しています。

プリミティブ値は、JavaScriptにおいて非常に重要な役割を果たしています。 JavaScriptにおけるすべての値は、プリミティブ値かオブジェクトのいずれかであり、JavaScriptの多くの機能がこれらの値を基に構築されています。

ボックス化

編集

ここまで読んで少しおかしいと思った人がいるかも知れません。 たとえば"Hello"について考えてみましょう。この文字列リテラルの length(長さ)というプロパティを表示してみます(プリミティブはプロパティを持たないはず)。

console.log("Hello".length); // コンソールに 5 を表示

何事もなく長さが表示されました。

これは、プリミティブのプロパティを参照するとプリミティブに対応したラッパーオブジェクト(この場合はStringオブジェクト)がプリミティブの値をコンストラクタのパラメータとして生成され、生成されたラッパーオブジェクトのプロパティが参照されるからです。

console.log((new String("Hello")).length);

に相当することが自動的に行われます。

常々のプログラミングでプリミティブとラッパーオブジェクトの違いを意識する事は稀ですが、両者は明確に異なります(例えば、eval()関数に文字列を渡すと評価されStringオブジェクトを渡すと文字列がかえります)。

console.log(typeof "Hello");               // string
console.log(typeof (new String("Hello"))); // object

このようにラッパーオブジェクトの型は Object です。

以下が、JavaScriptにおけるプリミティブ値とその対応するラッパーオブジェクトの一覧表です。

プリミティブ型とラッパーオブジェクトの対応
プリミティブ型 ラッパーオブジェクト
Boolean Boolean
Null ラッパーオブジェクトは存在しない
Undefined ラッパーオブジェクトは存在しない
Number Number
BigInt BigInt
String String
Symbol Symbol

ラッパーオブジェクトは、プリミティブ値をラップすることで、それに対してメソッドやプロパティを提供します。ただし、プリミティブ値は暗黙的にラッパーオブジェクトに変換されるため、ラッパーオブジェクトを意識する必要がない場合もあります。

BigIntはECMAScript 2020から導入されたプリミティブ型の1つです。BigIntは、整数値の範囲を超えるような大きな数値を表現するために使われます。

ラッパーオブジェクトはBigIntです。

構文

編集

もっとも単純なオブジェクトは次のような形をしています。

オブジェクトリテラル

編集
const obj = {
    name: "愛",
    age : 14
};

プロパティアクセサ

編集

オブジェクトにドット記法またはブラケット記法でキーの文字列を指定すると対応する値を取り出すことができます。

ドット記法
オブジェクト . プロパティキー
ブラケット記法
オブジェクト [ プロパティキー文字列 ]
プロパティアクセサの使用例
console.log( obj.name );   // 愛
console.log( obj['age'] ); // 14

このようなデータ構造を連想配列(れんそうはいれつ、associative array)といいます。 キーと値のペアはプロパティ (property) と呼ばれます(プロパティキーをプロパティと言いがちです)。

JavaScriptにおいてオブジェクトは連想配列です[2]。たとえばStringオブジェクトのlengthプロパティというのは、まさにStringという連想配列のprototypeプロパティのlengthプロパティのことを指しています。

プロパティのキーは通常識別子で数値を指定すると文字列に変換されます。

const obj = {};
obj[10] = "xyz";
console.log(Object.keys(obj)); // ["10"]

また、プロパティのキーにシンボルをとることも出来ます。プロパティの値には数値や文字列だけでなく関数などあらゆるオブジェクトとプリミティブをとることができます。

Objectオブジェクト

編集

Objectオブジェクトは、JavaScriptの中心的なオブジェクトの一つであり、他の全てのオブジェクトの親であるため、JavaScriptで最も基本的なオブジェクトの1つです。

Objectオブジェクトには、以下のような機能があります。

  • プロパティ:オブジェクトが持つ情報(値)を表します。
  • メソッド:オブジェクトで実行できる操作を表します。
  • コンストラクタ:新しいオブジェクトを作成するための関数です。

また、JavaScriptの全てのオブジェクトは、Objectオブジェクトを継承しているため、Objectオブジェクトが持つプロパティやメソッドを利用することができます。例えば、Object.keys()メソッドは、与えられたオブジェクトの全てのキーを配列として返すメソッドです。

Objectオブジェクトは、JavaScriptでのオブジェクト指向プログラミングにおいて、非常に重要な役割を果たしています。また、JavaScriptの基本的なデータ型である文字列、数値、長整数、真偽値、null、undefinedも、実際にはObjectオブジェクトであるため、JavaScriptにおける全てのデータは、最終的にObjectオブジェクトに帰着するということができます。

プロパティ

編集
Object.prototype
Objectプロトタイプオブジェクト

静的メソッド

編集

JavaScriptには、クラス(ES6から導入)ではなく、オブジェクト指向のプロトタイプベースの言語であるため、静的メソッドという概念はありません。しかし、グローバルオブジェクトやネイティブオブジェクト(Array、Math、String、Objectなど)には、クラスメソッドと呼ばれる、そのオブジェクト自体に関連する静的メソッドが存在します[3]

以下に、JavaScriptでよく使われるグローバルオブジェクトやネイティブオブジェクトに存在する静的メソッドの一部を挙げます。

  • Array.from() - 配列風のオブジェクトから、配列を生成します。
  • Array.isArray() - 引数が配列かどうかを判定します。
  • Object.assign() - 1つ以上のオブジェクトのプロパティを、別のオブジェクトにコピーします。
  • Object.keys() - オブジェクトの全てのキーを配列として取得します。
  • Math.abs() - 引数の絶対値を返します。
  • Math.max() - 引数の中で最大値を返します。
  • String.fromCharCode() - Unicode値に対応する文字列を返します。
  • parseInt() - 文字列を整数に変換します。

これらの静的メソッドは、それぞれのオブジェクトに対して呼び出すのではなく、オブジェクト名(グローバルオブジェクトの場合は、window)とメソッド名をドットで繋いで呼び出します。例えば、Array.from()やMath.max()などです。

Object.assign()
1つまたは複数のソース・オブジェクトからターゲット・オブジェクトに、列挙可能なすべての自己プロパティの値をコピーします。
Object.create()
Object.defineProperties()
オブジェクトに複数のプロパティを定義します。
Object.defineProperty()
オブジェクトにプロパティを定義します。
Object.entries()
列挙可能な文字列プロパティのすべての [key, value] ペアを配列で返します。
Object.freeze()
プロパティを全て凍結します
Object.fromEntries()
Object.getOwnPropertyDescriptors()
オブジェクトの複数のプロパティ記述子を返します。
Object.getOwnPropertyDescriptor()
オブジェクトのプロパティ記述子を返します。
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.getPrototypeOf()
オブジェクトのプロトタイプを返します。
Object.isExtensible()
Object.isFrozen()
Object.isSealed()
Object.is()
Object.keys()
オブジェクトの列挙可能なすべてのプロパティを配列で返します。
Object.preventExtensions()
Object.seal()
Object.setPrototypeOf()
Object.values()

この一覧の雛形は下記のように Object.getOwnPropertyDescriptors(Object) を使って生成しました。

const a = [];
for (p in Object.getOwnPropertyDescriptors(Object)) {
    if (typeof Object[p] === "function") {
        a.push(`; [[{{PAGENAME}}/${p}|Object.${p}()]]`);
    }
}
console.log(a.sort().join("\n"));

アクセサプロパティ

編集

JavaScriptのアクセサプロパティは、オブジェクトのプロパティを操作するための方法の1つで、getとsetの2つの特別な関数を使用します。アクセサプロパティは、プロパティの値を直接操作するのではなく、get関数とset関数を介して値を取得または設定することができます[4]

以下は、アクセサプロパティを使用したオブジェクトの例です。

前置詞 get と set を使う方法

編集

右辺値式としては get を、左辺値式としては set を前置したメソッドを定義すると、メソッド名が仮想のプロパティ名になります。

const person = {
  firstName: "John",
  lastName: "Doe",
  get fullName() {
    return this.firstName + " " + this.lastName;
  },
  set fullName(name) {
    const parts = name.split(" ");
    this.firstName = parts[0];
    this.lastName = parts[1];
  }
};

console.log(person.fullName); // "John Doe"

person.fullName = "Jane Smith";
console.log(person.firstName); // "Jane"
console.log(person.lastName); // "Smith"

上記の例では、personオブジェクトに、firstNameとlastNameの2つの通常のプロパティと、fullNameというアクセサプロパティがあります。fullNameプロパティは、get関数を使用して取得され、set関数を使用して設定されます。fullNameプロパティを介して、firstNameとlastNameプロパティを変更することができます。

アクセサプロパティを使用することで、プロパティにアクセスする際に、通常のプロパティのように値を直接取得するのではなく、関数を介して複雑な計算を行うことができます。また、プロパティの設定時に、値の妥当性をチェックするなど、値に対する制限を設けることができます。

Object.defineProperty() で[[Get]]と[[Set]]を操作する方法

編集

JavaScriptにおけるアクセサプロパティは、オブジェクトのプロパティを取得および設定するための機能で、Object.defineProperty()メソッドを使用して定義されます。このメソッドを使用することで、プロパティの[[Get]]および[[Set]]内部メソッドを定義できます[5]

以下は、Object.defineProperty()メソッドを使用したアクセサプロパティの例です。

const person = {};
let fullName = "John Doe";

Object.defineProperty(person, "fullName", {
  get: function() {
    return fullName;
  },
  set: function(name) {
    const parts = name.split(" ");
    this.firstName = parts[0];
    this.lastName = parts[1];
    fullName = name;
  }
});

console.log(person.fullName); // "John Doe"

person.fullName = "Jane Smith";
console.log(person.firstName); // "Jane"
console.log(person.lastName); // "Smith"

上記の例では、personオブジェクトにfullNameプロパティを定義し、getおよびsetメソッドを使用して値を取得および設定します。fullNameプロパティが呼び出されると、getメソッドが呼び出され、fullNameの値が返されます。fullNameプロパティに値を設定すると、setメソッドが呼び出され、フルネームを分割して、firstNameとlastNameプロパティを設定し、fullNameの値を更新します。

このように、Object.defineProperty()メソッドを使用すると、オブジェクトのプロパティに対して、getおよびsetメソッドを定義することができます。この方法を使用すると、プロパティの取得と設定に関する制御を細かく設定することができます。

get/set前置詞を使う方法ではオブジェクト生成時にしか getterやsetterを定義できませんでしたが、[[Get]]と[[Set]]を操作する方法であれば生成後も追加・変更が可能です。


アクセサプロパテの使用例

編集
class Length {
    constructor() { this.l = void 0; }
    get m() {return this.l; }
    set m(n) { return this.l = n; }
    get cm() { return this.l * 100; }
    set cm(n) { return this.l = n / 100; }
    get mm() { return this.l * 1000; }
    set mm(n) { return this.l = n / 1000; }
}
const l = new Length()
l.m = 1.77;
console.log(`${l.m}メートルは、${l.cm}センチメートルで${l.mm}ミリメートルです。`)

「1.77メートルは、177センチメートルで1770ミリメートルです。」と表示されます。


オブジェクトの拡張

編集

NumberStringArray、Objectなどの標準グローバルオブジェクトにプロパティを追加したり、既存のメソッドをオーバーライドしたりしてこれらのオブジェクトを拡張することができます。

順列を求めるメソッド permutation を配列に追加する

編集
順列を求めるメソッド permutation を配列に追加します
if (!Array.prototype.permutation) {
  Object.defineProperty(Array.prototype, 'permutation', {
    value: function(n = this.length) {
      if (this == null) {
        throw new TypeError('Array.prototype.permutation: ' +
          'called on null or undefined');
      }
      if (typeof n !== 'number') {
        throw new TypeError(`${n} is not a number`);
      }
      if (n < 0 || n > this.length) {
        throw new RangeError(`${n} is out of range`);
      }

      // 基本ケース:空または 1 要素
      if (this.length === 0 || n === 0) {
        return [[]];
      }
      if (n === 1) {
        return this.map(x => [x]);
      }

      // 順列生成
      const result = [];
      for (let i = 0; i < this.length; i++) {
        const rest = [...this.slice(0, i), ...this.slice(i + 1)];
        rest.permutation(n - 1).forEach(e => {
          result.push([this[i], ...e]);
        });
      }
      return result;
    },
    writable: true,
    enumerable: false,
    configurable: true,
  });
}
const perm = "ABC".split("").permutation(3);
console.log(JSON.stringify(perm));
// [["A","B","C"],["A","C","B"],["B","A","C"],["B","C","A"],["C","A","B"],["C","B","A"]]

組合わせを求めるメソッド combination を配列に追加する

編集
組合わせを求めるメソッド combination を配列に追加します
if (!Array.prototype.combination) {
  Object.defineProperty(Array.prototype, 'combination', {
    value: function (n = this.length) {
      if (this == null) {
        throw new TypeError('Array.prototype.combination: called on null or undefined');
      }
      if (typeof n !== 'number') {
        throw new TypeError(`${n} is not a number`);
      }
      if (n < 0 || n > this.length) {
        throw new RangeError(`${n} is out of range`);
      }

      // ベースケース:組み合わせのサイズが 0 または配列が空の場合
      if (n === 0) {
        return [[]];
      }
      if (n === 1) {
        return this.map(x => [x]);
      }

      // 再帰的に組み合わせを生成
      const result = [];
      for (let i = 0; i <= this.length - n; i++) {
        const head = this[i];
        const tailCombinations = this.slice(i + 1).combination(n - 1);
        tailCombinations.forEach(comb => result.push([head, ...comb]));
      }

      return result;
    },
    writable: true,
    enumerable: false,
    configurable: true,
  });
}

// 動作確認
const comb = "ABC".split("").combination(2);
console.log(JSON.stringify(comb));
// [["A","B"],["A","C"],["B","C"]]

メソッドチェーン

編集

空白文字で区切られた単語を含んだ文字列

"h he li be b c n o f ne na mg al si p s cl ar k ca sc ti v cr"

があります。 この文字列から単語を取り出しキャピタライズし番号を振って , で区切って表示してみましょう。

非メソッドチェーン版
const str = "h he li be b c n o f ne na mg al si p s cl ar k ca sc ti v cr";
const ary = str.split(' ');
let ary2 = []
for (const [i, s] of ary.entries())
  ary2[i] = `${1+i}:${s.charAt(0).toUpperCase() + s.slice(1)}`;
let result = ""
for (const word of ary2)
  result += (word+", ")
console.log(result)
/*
 1:H, 2:He, 3:Li, 4:Be, 5:B, 6:C, 7:N, 8:O, 9:F, 10:Ne, 11:Na, 12:Mg, 13:Al, 14:Si, 15:P, 16:S, 17:Cl, 18:Ar, 19:K, 20:Ca, 21:Sc, 22:Ti, 23:V, 24:Cr,
 */

オーソドックスなメソッドとfor-ofで出来ています(もっとオーソドックスにするなら、C言語風の f(;;) を使うべきだったかも)。

メソッドチェーン版
const elements = "h he li be b c n o f ne na mg al si p s cl ar k ca sc ti v cr";
const result = elements
  .split(' ')
  .map((symbol, index) => `${index + 1}:${symbol[0].toUpperCase()}${symbol.slice(1)}`)
  .join(", ");

console.log(result);
/*
 1:H, 2:He, 3:Li, 4:Be, 5:B, 6:C, 7:N, 8:O, 9:F, 10:Ne, 11:Na, 12:Mg, 13:Al, 14:Si, 15:P, 16:S, 17:Cl, 18:Ar, 19:K, 20:Ca, 21:Sc, 22:Ti, 23:V, 24:Cr
 */

この例では Arrayオブジェクトのメソッドが使われているが、他のオブジェクトでもメソッドチェーンを作ることは出来ます。 非メソッドチェーン版とメソッドチェーン版を比較すると変数の数がメソッドチェーン版では減っている事にきがつくとおもいます。メソッドは左結合(左から順に評価、読みやすさのため適宜改行しているので上から下に評価)なので左のメソッドから順に評価され、メソッドの評価した結果は次の(右の)メソッド呼び出しオブジェクト項(メソッドから見ると this)になります。 この様に . を挟んで左から右にメソッドが次々と評価されます。 この連鎖構造を鎖に例えてメソッドチェーンと呼ばれます。

別解
const str = "h he li be b c n o f ne na mg al si p s cl ar k ca sc ti v cr";
let count = 0;
const result = str.replace(/\b(\w)(\w*)/g, (_, p1, p2) => `${count++ ? ',' : ''}${count}:${p1.toUpperCase()}${p2}`);
console.log(result);
/*
 1:H, 2:He, 3:Li, 4:Be, 5:B, 6:C, 7:N, 8:O, 9:F, 10:Ne, 11:Na, 12:Mg, 13:Al, 14:Si, 15:P, 16:S, 17:Cl, 18:Ar, 19:K, 20:Ca, 21:Sc, 22:Ti, 23:V, 24:Cr
 */

メソッドチェーン節の演習問題

編集

非メソッドチェーン版とメソッドチェーン版を比較すると出力に小さな違いがあります。探してみましょう。 また、その原因をしらべ結果を揃えるようコードを改修してみましょう(どちらに合わせても構わないです)。

脚註

編集
  1. ^ member of one of the types Undefined, Null, Boolean, Number, BigInt, Symbol, or String ECMA-262::4.4.5 primitive value
  2. ^ 連想配列専用オブジェクトとしてMapWeakMapがあります。これらが存在するのは Objectはプロパティとくに prototype があり単なる連想配列として扱うと混乱が生じるためです。またMapとWeakMapのキーは全てのオブジェクトとプリミティブが使えますが、Objectオブジェクトのプロパティキーには文字列かSymbolしか使えません。
  3. ^ ECMA-262::20.1.2 Properties of the Object Constructor
  4. ^ ECMA-262::15.4 Method Definitions
  5. ^ ECMA-262::6.1.7.1 Property Attributes

外部リンク

編集