どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。
前回のおさらい
前回は、「プロトタイプ汚染を防御するためにhasOwnPropertyを使う」をお届けしました。
今回は、「順序を持つコレクションには、ディクショナリではなく配列を使おう」「Object.prototypeには、列挙されるプロパティを決して追加しない」を2本」 を2本まとめてお届けします。
テキスト第5章「配列とディクショナリ」の項目46、47に対応しています。
今日のアジェンダ
- 取り出し順の保証
- プロパティに浮動小数点
- Object.prototypeには、列挙されるプロパティを決して追加しない
- Object.defineProperty
取り出し順の保証
みなさん、ご存知のように、JavaScriptのオブジェクトは、取り出し順が保証されていません。
これは、ランダムというわけではありません。
アルファベット順なのか、数値があれば数値が先なのか、わかりませんが、とにかく、順番が保証されていません。
ただし、配列のインデックスのような、0,1,2という数値を持ったものは、その順で取り出す気がします。
function myFunction6_46_01() {
function repeat(highScores) {
let result = '';
let i = 1;
for (const name in highScores) {
const score = JSON.stringify(highScores[name]);
result += `${i}.${name}:${score}\n`;
i++;
}
return result;
}
const array = [
{ name: 'Tom', points: 100 },
{ name: 'Bob', points: 200 },
{ name: 'John', points: 300 }
];
console.log(repeat(array));
/*
1.0: { "name": "Tom", "points": 100 }
2.1: { "name": "Bob", "points": 200 }
3.2: { "name": "John", "points": 300 }
*/
}
なので、for in文ではなく、配列のインデックスを使って、取り出しましょうというのが、テキストのコードです。
function myFunction6_46_02() {
function repeat(highScores) {
let result = '';
for (let i = 0; i < highScores.length; i++) {
const score = highScores[i];
result += `${i + 1}.${score.name}:${score.points}\n`;
}
return result;
}
const array = [
{ name: 'Tom', points: 100 },
{ name: 'Bob', points: 200 },
{ name: 'John', points: 300 }
];
console.log(repeat(array));
}
プロパティに浮動小数点
重箱の隅をつつくEffectiveJavaScriptっぽくなってきました。プロパティが浮動小数点だったばあいはどうでしょうか。
function myFunction6_46_03() {
const ratings = {
'Goodwill Hunting': 0.8,
'Mystic River': 0.7,
'21': 0.6,
'Doubt': 0.9
};
let total = 0, count = 0;
for (const key in ratings) {
console.log(key); //21, Goodwill Hunting, Mystic River, Doubt
total += ratings[key];
count++;
}
total /= count;
console.log(total); //(0.6 + 0.8 + 0.7 + 0.9) / 4 = 0.7499999999999999
}
これは、項目2でもお伝えしたように(担当回でした)、一度整数にしてから処理するといいでしょう。
function myFunction6_46_04() {
const ratings = {
'Goodwill Hunting': 0.8,
'Mystic River': 0.7,
'21': 0.6,
'Doubt': 0.9
};
let total = 0, count = 0;
for (const key in ratings) {
total += ratings[key] * 10;
count++;
}
total = total / count / 10;
console.log(total); //(6 + 8 + 7 + 9) / 4 / 10 = 0.75
}
Object.prototypeには、列挙されるプロパティを決して追加しない
項目43でもお伝えしました。
for in文は、列挙可能なプロパティを、プロトタイプチェーンをたどって取り出します。
以下の例では、allKeysという独自プロパティを定義してしまったので、for in文でプロパティを取り出すと、1個増えてしまっています。
function myFunction6_47_01() {
Object.prototype.allKeys = function () {
const result = [];
for (const key in this) {
result.push(key);
}
return result;
}
const ratings = {
'Goodwill Hunting': 0.8,
'Mystic River': 0.7,
'21': 0.6,
'Doubt': 0.9
};
console.log(ratings.allKeys().length); //5
}
プロパティではなく、関数にすればいいよ、というのが、ひとつの策のようです。
function myFunction6_47_02() {
function allKeys(obj) {
const result = [];
for (const key in obj) {
result.push(key);
}
return result;
}
const ratings = {
'Goodwill Hunting': 0.8,
'Mystic River': 0.7,
'21': 0.6,
'Doubt': 0.9
};
console.log(allKeys(ratings).length); //4
}
Object.defineProperty
Object.defineProperty()メソッドは、組み込みオブジェクトのObjectオブジェクトの静的メソッドです。
あるオブジェクトに、新しいプロパティを直接定義したり、オブジェクトの既存のプロパティを変更したりして、そのオブジェクトを返します。
for in文がプロトタイプチェーンを遡って、列挙してしまう問題解決として、Object.prototypeのallKeysプロパティの、enumerableプロパティ(列挙するかどうか定義する)を、不可(false)にしました。
function myFunction6_47_03() {
Object.defineProperty(Object.prototype, 'allKeys', {
value: function () {
const result = [];
for (const key in this) {
result.push(key);
}
return result;
},
writable: true,
enumerable: false,
configurable: true
});
const ratings = {
'Goodwill Hunting': 0.8,
'Mystic River': 0.7,
'21': 0.6,
'Doubt': 0.9
};
for (const key in ratings) {
console.log(key); //21, Goodwill Hunting, Mystic River, 21, Doubt
}
}
もし、Object.prototypeに、オリジナルのプロパティを、定義したいのなら、Object.definePropertyで列挙不可にする、という一手があるのかもしれません。
Effectiveですね。
use strictモードでは、書き込み禁止になっているプロパティに対して書き込もうとすると、例外が発生します。
合わせて、協力な、プロパティの汚染防止になりますね。
function myFunction6_47_04() {
const object1 = {};
Object.defineProperty(object1, 'property1', {
value: 42,
writable: false
});
//use strictモードでは、例外が発生する
object1.property1 = 77;
console.log(object1.property1);
// expected output: 42
}
最後に、プロパティの汚染についてまとめてみます。(不完全ではありますが、ここまでのまとめです。)
これも更新しながら、最終的に「これだ!」というものをいつか提案できればと思います。
function myFunction6_47_05() {
/** モンキーパッチの復習 */
//リテラルでオブジェクトを生成する
const obj = { name: 'Tom' };
//生成されたオブジェクトの、継承元=Objectオブジェクトの、prototype.toStringにアクセスしてみる
console.log(obj.__proto__.toString); // [Function: toString]
//それを、書き換える。
obj.__proto__.toString = function () { return 'モンキーパッチです' };
//継承元のprototypeを書き換える、モンキーパッチ(悪手)
console.log(obj.name); //Tom
console.log(obj.toString()); //'モンキーパッチです'
/** プロトタイプを汚染させない */
//Object.create(null)でオブジェクトを生成する
const obj2 = Object.create(null, { name: { value: 'Tom' } });
//nameプロパティはobj2自身に定義されていない
console.log(obj2);//{}
//プロトタイプチェーンを遡ると発見
console.log(obj2.name); //Tom
//レシーバがnullなので、プロトタイプは変更させない
// console.log(obj2.__proto__.toString);//TypeError: Cannot read property 'toString' of undefined
/** 自身のルックアッププロパティを汚染させない */
//定義の時点で、列挙不可にする
const obj3 = {};
Object.defineProperty(obj3, 'name', {
enumerable: false, //列挙するかどうか
configurable: false,
writable: false,
value: 'Tom'
});
//numerableがfalse
console.log(obj3); //{}
//プロトタイプチェーンを遡ると発見
console.log(obj3.name); //Tom
//writable: falseで、自身のルックアッププロパティを変更させない
obj3.name = 'Bob';
console.log(obj3.name); //'Tom'
//継承元=Objectオブジェクトの、prototype.toString、書き換えると
obj3.__proto__.toString = function () { return 'obj3です' };
//プロトタイプは汚染される(Object.definePropertyとは無関係)
console.log(obj3.toString()); //obj3です
/** 自身のルックアッププロパティを汚染させない2 */
//Object.freezeで凍結する
const obj4 = { name: 'Tom' };
Object.freeze(obj4);
//自身のルックアッププロパティは変更できない
obj4.name = 'Bob';
console.log(obj4); //{ name: 'Tom' }
//プロトタイプは変更される
obj4.__proto__.toString = function () { return 'obj4です' };
console.log(obj4.toString()); //obj4です
//大元は汚染されている
console.log({}.toString()); //obj4です
}
まとめ
以上で、「順序を持つコレクションには、ディクショナリではなく配列を使おう」「Object.prototypeには、列挙されるプロパティを決して追加しない」を2本お届けしました。
第6回目の輪読会は、ここまでです。
次回輪読会をお楽しみに。