どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。
前回のおさらい
前回は、4章後半を、3本まとめてお届けしました。
今回は、いよいよ5章に突入です。オブジェクトが終わって、配列の話になるのかな?と、おもいきや、まだまだオブジェクトのお話のようです。。。
なので、「軽量ディクショナリはObjectの直接インスタンスから構築しよう」「プロトタイプ汚染を予防するために、nullプロトタイプを使う」 を2本まとめてお届けします。
テキスト第5章「配列とディクショナリ」の項目43、44に対応しています。
今日のアジェンダ
- 軽量ディクショナリはObjectの直接インスタンスから構築しよう
- プロトタイプ汚染を予防するために、nullプロトタイプを使う
軽量ディクショナリはObjectの直接インスタンスから構築しよう
JavaScriptのオブジェクトは、幅広い用途で使われています。
function myFunction6_43_00() {
/* オブジェクトとは */
/** キーと値がセットになったレコードである。 */
const persons = {
person1: { name: 'Tsujike', age: 40, hometown: 'Hokkaido' },
person2: { name: 'Sawada', age: 32, hometown: 'Kumamoto' },
person3: { name: 'Kohata', age: 28, hometown: 'Fukushima' }
}
/** メソッドを継承するオブジェクト指向のデータ抽象である */
const Person = function (name) {
this.name = name;
}
Person.prototype.getName = function () {
return this.name;
}
const p1 = new Person('Yamaguchi');
console.log(p1.getName(), typeof p1);
/** 配列の要素である */
const array = [
{ id: 001, name: 'Tsujike', age: 40 },
{ id: 002, name: 'Sawada', age: 32 },
{ id: 001, name: 'Kohata', age: 28 }
];
/** ハッシュ(連想配列)である */
const dictionary = { "a": 100, "b": 200, "c": 300 }
}
とくに、オブジェクトをデータ構造として、とらえたものが、JavaScript Object Notation(JSON) として、広く扱われているのは、周知の事実です。
前章では、構造化されたオブジェクトと継承を、プロトタイプとともに学習しました。
この章では、オブジェクトをコレクションとして扱う方法を、おなじくプロトタイプとともに、学んでいきます。
継承、プロトタイプチェーンのおさらい
for in文の考察をするまえに、継承とプロトタイプチェーンのおさらいをします。
リテラルで生成されたオブジェクト(以降、obj)は、Objectオブジェクトからprototypeを継承しています。
objは、obj自身にプロパティを定義できますし、プロトタイプチェーンを遡った場所にあるプロパティを書き換えることもできます。(モンキーパッチ=悪手)
個人的に5章は、プロパティをちゃんと理解してディクショナリとしてEffectiveに扱おう! という内容だと理解しました。
プロパティを理解するためのコードを書いてみたので、これをベースに進めたいと思います。
function myFunction6_43_00_02() {
//オブジェクトの生成
const obj = { name: 'Tom' };
//Objectオブジェクトから、Object.prototypeを継承している
console.log(obj.__proto__ === Object.prototype); //true
//Object.prototypeには、valueOf()や、hasOwnProperty()など、6つのインスタンスメソッドがある。(非推奨は除く)
//objはObject.prototypeから、インスタンスメソッド(プロパティ)を継承している
console.log(obj.__proto__.valueOf); //[Function: toString]
console.log(obj.__proto__.hasOwnProperty); //[Function: hasOwnProperty]
//obj自身には、プロパティは存在していません
// console.log(obj.prototype.valueOf); //TypeError: Cannot read property 'valueOf' of undefined
// console.log(obj.prototype.hasOwnProperty); //TypeError: Cannot read property 'hasOwnProperty' of undefined
//自身のプロパティに存在しない場合は、ひとつ上の階層のprototypeをルックアップするのが、プロトタイプチェーン
console.log(obj.valueOf()); //{ name: 'Tom' }
console.log(obj.hasOwnProperty('name')); //true
//プロパティは、自身に定義できる
obj.age = 40;
//インスタンスメソッドと同じ名前のプロパティも定義できる
obj.valueOf = function () { return '改造します' };
//自身のプロパティに存在するので、プロトタイプチェーンは遡らない。
console.log(obj.valueOf()); //'改造します'
//for in文は、まず、自身のプロパティの中で、列挙可能なプロパティを取り出します。
for (const key in obj) {
console.log(key); //name age valueOf
}
//プロトタイプチェーン(Object.prototypeとイコール)に、独自のプロパティを定義する
obj.__proto__.tsujike = function () { return '独自プロパティ1' };
//objはプロトタイプチェーンを遡って、tsujikeを呼び出せる
console.log(obj.tsujike()); //'独自プロパティ1'
//for in文は、この独自プロパティも取り出してしまう(これを列挙させないよう定義するのがObject.defineProperty())
for (const key in obj) {
console.log(key); //name age valueOf tsujike
}
//もちろん、イコールなので、Object.prototypeに定義した、独自プロパティでも同じことが起きる
Object.prototype.kenzo = function () { return '独自プロパティ2' };
console.log(obj.kenzo()); //'独自プロパティ2'
for (const key in obj) {
console.log(key); //name age valueOf tsujike kenzo
}
//hasOwnProperty()は、あくまで、自身のプロパティをチェックしている
console.log(obj.hasOwnProperty('valueOf')); //true
console.log(obj.hasOwnProperty('kenzo')); //false
}
for in文
みなさんが普段お目にかかるfor in文は、このような連想配列の列挙だと思います。
function myFunction6_43_01() {
const dict = { alice: 34, bob: 24, chris: 62 };
const people = [];
for (const name in dict) {
people.push(`${name}:${dict[name]} `);
}
console.log(people); // [ 'alice:34 ', 'bob:24 ', 'chris:62 ' ]
}
for in文は、プロトタイプチェーンを遡って、列挙可能なプロパティも取り出します。
以下のコードをでは、alice, bob, chrisに加えて、count, toStringの2つのプロパティが列挙可能になっていますので、カウントが5になります。
function myFunction6_43_02() {
function NaiveDict() { }
NaiveDict.prototype.count = function () {
let i = 0;
for (const name in this) {
i++;
}
return i;
};
NaiveDict.prototype.toString = function () {
return "[object NaiveDict]";
};
const dict = new NaiveDict();
dict.alice = 34;
dict.bob = 24;
dict.chris = 62;
console.log(dict.count()); //5
}
Array型であれ、String型であれ、プロパティを追加で定義できる(連想配列の辞書のような使い方ができる)のは、JavaScriptのすごい(自由な)ところです。
しかし、for in文が、プロトタイプチェーンを遡って、プロパティを取り出す影響は計り知れません。
function myFunction6_43_03() {
const dict = new Array();
dict.alice = 34;
dict.bob = 24;
dict.chris = 62;
console.log(dict.bob); //24
const dict2 = new String();
dict2.alice = 34;
dict2.bob = 24;
dict2.chris = 62;
console.log(dict2.bob); //24
Array.prototype.first = function () {
return this[0];
}
Array.prototype.last = function () {
return this[this.length - 1];
}
const names = [];
for (const name in dict) {
names.push(name);
}
console.log(names); // [ 'alice', 'bob', 'chris', 'first', 'last' ]
}
テキストでは、独自プロパティ(メソッド)を追加したりしなさんな、と言っています。
オブジェクトを生成して、ディクショナリ(連想配列の辞書のような使い方)としてなら、まだマシだということです。
function myFunction6_43_04() {
const dict = {};
dict.alice = 34;
dict.bob = 24;
dict.chris = 62;
const names = [];
for (const name in dict) {
names.push(name);
}
console.log(names); //[ 'alice', 'bob', 'chris' ]
}
これで、すべてが解決したわけではありません。
まだまだプロトタイプは汚染可能です。それをどうやって防ぐのでしょうか。(そもそも組み込みオブジェクトのプロパティを使って、連想配列の辞書のような使い方をしなければいい、みたいなツッコミはなしです)
プロトタイプ汚染を予防するために、nullプロトタイプを使う
では、コンストラクタのprototypeプロパティを、nullかundefinedにすればいい、と考えた天才肌の方もいるでしょう。
しかし、インスタンスでは、そんなの関係ねー、と言わんばかりの結果です。
function myFunction6_44_01() {
function C() { }
C.prototype = null;
const c = new C();
console.log(Object.getPrototypeOf(c) === null); //false
console.log(Object.getPrototypeOf(c) === Object.prototype); //true
}
そこで、ES5から実装された、Object.create()メソッドの登場です。
Object.create()メソッドは、組み込みオブジェクトのObjectオブジェクトの 静的メンバー (インスタンスを生成しなくても直接呼び出せるメンバー)です。
既存のオブジェクトを、新しく生成されるオブジェクトのプロトタイプとして使用して、新しいオブジェクトを生成します。
つまり、新しく生成されるオブジェクトのprototypeを強制的にnullにできるのです。(prototypeチェーンの終着駅はnullです)
おなじアプローチで、__proto__も可能ですが、非推奨ですし、Object.create(null)のほうがよいでしょう。
function myFunction6_44_02() {
const x = Object.create(null);
console.log(Object.getPrototypeOf(x) === null); //true
const y = { __proto__: null };
console.log(y instanceof Object); //false
}
テキストでは、「__proto__をディクショナリのキーに絶対使うな」 と忠告がありましたが、(誰がそんなことするんだよ)というツッコミは心の奥にしまっておきましょう。
まとめ
以上で、「軽量ディクショナリはObjectの直接インスタンスから構築しよう」「プロトタイプ汚染を予防するために、nullプロトタイプを使う」を2本まとめてお届けしました。
次回は、「プロトタイプ汚染を防御するためにhasOwnPropertyを使う」 をお届けします。