どうも。つじけ(tsujikenzo)です。このシリーズでは、2021年9月から始まりました「ノンプロ研GAS中級講座6期」について、全7回でお届けします。今日はDay2です。
前回のおさらい
前回は、「スコープと関数」 をお届けしました。
今日は、Day2で 「クラス・ライブラリ」 についてお届けします。
今日のアジェンダ
- クラスとはなにか
- JavaScriptはプロトタイプベース
- コンストラクタとthis
- new演算子とクラス構文
- ノンプログラマーのクラスの使いどころ
クラスとはなにか
プログラミング言語で「クラスとはなにか」という質問ほど、むずかしい質問はありません。
人間のことを知らない宇宙人に対して、「愛とはなにか」を説明することぐらいむずかしいです。
なので、クラスをひとことで説明することに、あまり意味はないと思います。
学習を進めていく上で、「オブジェクトをつくる機能」「プロパティをもつ関数」「ユーザーが定義できる型」など、自分のレベルによって、クラスとはなにかが変わっていくからです。
わたしたちにとって大切なことは、クラスはどんな機能が備わっているのか、クラスの機能や作用を抽象化すると言葉としてどのような定義ができるのか、日々、積み重ねていくことでしょう。
GAS中級講座第5期補講では、クラスの歴史を紹介しました。興味のあるかたは一読ください。
中級講座でオススメする学習方法は 「まず、書き方を徹底して覚えること」 です。実際に、写経やコーディングしながら仲良くなっていきましょう。
class クラス名{
constructor(property){
this.property = property;
}
method(){
return this.property;
}
}
私は、この定義をまるまる辞書登録して、 「class;」 で呼び出せるようにしています。constructorのスペルも、(PCが)完全に覚えています。
JavaScriptはプロトタイプベース
JavaScriptは プロトタイプベース を基本設計に取り入れています。
プロトタイプベースとは、クラスからクローンを作る仕組みです。
「クローンを作る」というと、ややこしく聞こえますが、このように関数(アロー関数)に引数を渡して、オブジェクトを戻り値で取得すると考えてみましょう。
変数pnには、新規のオブジェクトが格納されています。
//関数リテラルによるオブジェクトの生成
function hoko2_01() {
const Person = (name, age) => {
const p = {};
p.name = name;
p.age = age;
return p;
};
const p1 = Person('Bob', 25);
console.log(p1); //{ name: 'Bob', age: 25 }
const p2 = Person('Tom', 32);
console.log(p2); //{ name: 'Tom', age: 32 }
const p3 = Person('Bob', 25);
console.log(p3); //{ name: 'Bob', age: 25 }
console.log(p1 === p3); //false
}
この、戻り値でオブジェクトを返す関数に、プロパティだけでなくメソッドも追加してみましょう。
JavaScriptには、**「同じ関数から作成されたメソッドは、共通のものとしましょう」**という仕組みがあります。
その仕組みを、プロトタイプと呼びます。
function hoko2_03() {
const Person = () => {
const p = {};
p.greet = function () { return `Hello!` };
return p;
};
const p1 = Person();
const p2 = Person();
console.log(p1.greet() === p2.greet()); //true
}
オブジェクトp1、p2それぞれにgreet()メソッドがあるわけではなく、greet()メソッドは常に関数Person()の中にあり、呼び出されるときは、関数Personを参照しにいく仕組みです。
この仕組みを 「プロトタイプチェーン」 と呼びます。
プロトタイプチェーンは、オブジェクトにメソッドが含まれるときだけ生成されます。
コンストラクタとthis
プログラミング言語よって、用語の定義は変わりますので、他の言語と一緒に考えるのはあまりオススメしません。
JavaScriptでいうコンストラクタとは、オブジェクトを初期化しながら生成する関数と言えるでしょう。
先ほどのhoko2_03()の、関数Personはコンストラクタでした。
コンストラクタには、いくつかの役割とルールがあります。
- オブジェクトを生成するときに必ず実行される
- オブジェクトの初期化
- [変数(プロパティ)の定義]
- [引数を受け取る]
- thisが書かれていたら、生成されるオブジェクトを渡す
- 戻り値はもたない
- [オブジェクトの凍結(変更不可状態に凍結する)]
けっこうありますので、すべての機能を覚える必要はありません。
今回は、講座でも紹介した、thisに着目してみましょう。
コンストラクタ関数Personのおさらいです。
- pという空のオブジェクトを作成(初期化)する
- プロパティやメソッド(メソッド内でプロパティも利用できる)を格納する
- 最後にオブジェクトpを返す
function hoko2_04() {
const Person = (name, age) => {
const p = {};
p.name = name;
p.age = age;
p.greet = function () { return `My name is ${p.name}` };
return p;
};
const p1 = Person('Tom');
console.log(p1.name); //Tom
console.log(p1.greet()); //My name is Tom
}
コンストラクタ内では、生成される(returnされる予定の)オブジェクトを、thisというキーワードに置き換えることができます。
オブジェクトをthisに置き換えるイメージがむずかしいので、まずはオブジェクト名のpをthisに置き換えてみましょう。
このようなイメージです。
//このソースコードはエラーになります
function hoko2_05() {
const Person = (name, age) => {
const this = {};
this.name = name;
this.age = age;
this.greet = function () { return `My name is ${this.name}` };
return this;
};
const p1 = Person('Tom');
console.log(p1.name); //Tom
console.log(p1.greet()); //My name is Tom
}
thisは予約語なので、変数名に使えません。なのでこのコードは保存できませんが、イメージはお伝えできたのではないでしょうか。
new演算子とクラス構文
new演算子は、以下の動作を行います。
- 新しいオブジェクト(仮にobjとします)を作る
- objのプロトタイプを、コンストラクタのプロトタイプに変更する
- objをthisとして保有する
- コンストラクタを実行する
- (暗黙的に)thisを返す
new演算子で呼び出されるコンストラクタは、アロー関数が使えないので注意です。
function hoko2_06() {
const Person = function(name, age) {
this.name = name;
this.age = age;
this.greet = function () { return `My name is ${this.name}` };
};
const p1 = new Person('Tom');
console.log(p1.name); //Tom
console.log(p1.greet()); //My name is Tom
}
しかしながら、この書き方だと、プロパティ[greet]が常に複製されて、メモリを食ってしまうことは講座でもお伝えしました。
function hoko2_07() {
const Person = function (name, age) {
this.name = name;
this.age = age;
this.greet = function () { return `My name is ${this.name}` };
};
const p1 = new Person('Tom');
const p2 = new Person('Bob');
console.log(p1.greet === p2.greet); //false
console.log(p1.greet() === p2.greet()); //false
}
なので、prototypeというアイディアで、プロパティ(greeetメソッド)の複製はそれぞれのオブジェクトには作らず、常にコンストラクタを参照するようにします。
function hoko2_08() {
const Person = function (name, age) {
this.name = name;
this.age = age;
};
Person.prototype.greet = function () { return `My name is ${this.name}` };
const p1 = new Person('Tom');
const p2 = new Person('Bob');
console.log(p1.greet === p2.greet); //true
console.log(p1.greet() === p2.greet()); //false
}
そして、ES6から導入されたクラス構文では、上記のソースコードをスッキリ書けるようになりました。
function hoko2_09() {
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `My name is ${this.name}`
}
}
const p1 = new Person('Tom', 32);
const p2 = new Person('Bob', 25);
console.log(p1.greet === p2.greet); //true
console.log(p1.greet() === p2.greet()); //false
}
ノンプログラマーのクラスの使いどころ
ノンプログラマーのクラスの使いどころは、共通する処理を関数化したあとに、クラスのメソッドにすることです。
たとえば、繰り返し使う、日付に関する処理 を、以下のように関数化(パーツ化)します。
//実行用関数
function testfunction() {
const d = new Date('2021/06/20');
console.log(getLastMonthOfDate_(d));
console.log(isWeekEnd_(d));
}
//与えられたDateオブジェクトの前月の月末の日付を整数で返すサブ関数
function getLastMonthOfDate_(date) {
const d = new Date(date);
d.setDate(0);
return d.getDate();
}
//与えられたDateオブジェクトが土日かどうか返すサブ関数
function isWeekEnd_(date) {
const d = new Date(date);
return d.getDay() === 6 || d.getDay() === 0;
}
その、サブ関数をクラス化したものがこちらです。クラスをグローバル領域に書くことで、プロジェクト全体からアクセスできます。
//クラス化したもの
class MyDate {
constructor(date) {
this.d = date;
}
getLastMonthOfDate() {
this.d.setDate(0)
return this.d.getDate();
}
isWeekEnd() {
return this.d.getDay() === 5 || this.d.getDay() === 6;
}
}
//実行用関数
function hoko2_10() {
const d = new Date('2021/06/20');
const d1 = new MyDate(d);
console.log(d1.getLastMonthOfDate());
console.log(d1.isWeekEnd());
}
見た目がかなりスッキリしましたね。
どこに何が書かれているのか、見やすくなりましたので、後でソースコードを変更するときも楽になります。
まとめ
以上で、 「クラス・ライブラリ」 をお届けしました。
以前は、ノンプロ研内でも「ノンプログラマーにはクラスを自作するのはむずかしい」と思われていました。
クラスは組み込みオブジェクトや、GWSで用意されているもので、我々はメンバーを呼び出すだけでよかったからです。
しかし、V8や新IDEの登場で、クラス構文もスッキリかけるようになりました。
今は 「共通する処理はクラス化して、開発・保守を楽にしよう」 というのが主流かもしれません。
小さなコードからでも構いませんが、ぜひリファクタリングする際などは、クラス化にチャレンジしてみましょう。
次回は、 「組み込みオブジェクト」 をお届けします。