どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。
前回のおさらい
前回は、「一貫した規約を維持しよう」と「メソッドチェーンをサポートしよう」を、2本お届けしました。
今日は2回目で、「undefinedは「値なし」として扱う」をお届けします。
テキスト第6章「ライブラリとAPI設計」の項目54に対応しています。
今日のアジェンダ
- undefinedとは
- 値なし以外にundefinedを使う
- undefinedを表すプロパティを使う
- undefinedのテスト
- デフォルト引数を使うときの真偽値
undefinedとは
JavaScriptのnullとundefinedは、どちらも「値がないこと」を表す特殊な値です。
この2つの値の違いの考察は、いつかチャレンジしてみたいと思いますが、まず、JavaScriptが、undefinedを返す状況を復習しましょう。
function myFunction8_54_01() {
/** 初期化されていない変数 */
let x;
console.log(x); // => undefined
/** 存在しないプロパティ */
const obj = {};
console.log(obj.property); // => undefined
/** 値のない戻り値 */
const f = () => { return; };
console.log(f()); // => undefined
/** 戻り値のない関数 */
const g = () => { };
console.log(g()); // => undefined
/** 値が指定されなかった引数 */
const h = (x) => { return x; };
console.log(h()); // => undefined
}
本来、undefinedは、値がないことを表すべきです。
しかしながら、undefinedも「値」である以上、effectiveな使い方ができそうです。(逆に、注意喚起でもあります)
「値なし」以外にundefinedを使う
RangeオブジェクトのsetBackground()メソッドは、引数になにも渡されないばあいは、デフォルトの背景色が設定されます。(通常は無色)
function myFunction8_54_02() {
/** クラスCustomSheet */
class CustomSheet {
/** カスタムシートのコンストラクタ
* @constructor
*/
constructor() {
this.range = SpreadsheetApp.getActiveSheet().getRange('A1');
}
/** セルの背景色にハイライトをつけるメソッド */
highlight(color) {
this.range.setBackground(color);
}
}
const element = new CustomSheet();
element.highlight('red');
element.highlight();
}
もし、特定のcolorではなく、ランダムな色を設定したい場合は、undefinedを使う発想もあります。
function myFunction8_54_03() {
/** クラスCustomSheet */
class CustomSheet {
/** カスタムシートのコンストラクタ
* @constructor
*/
constructor() {
this.range = SpreadsheetApp.getActiveSheet().getRange('A1');
}
/** セルの背景色にハイライトをつけるメソッド */
highlight(color) {
if (color !== undefined) {
this.range.setBackground(color);
return
}
const max = 3, min = 1;
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
switch (randomNum) {
case 1:
color = 'red';
break;
case 2:
color = 'blue';
break;
case 3:
color = 'yellow';
break;
}
}
}
element.highlight(undefined);
}
しかし、存在しないプロパティなど、undefinedが渡されたときは、デフォルトのcolorを設定したばあい、破綻します。
const config = {};
element.highlight(config.highlightColor); // 存在しないプロパティ → 「デフォルト値」にしたいが、ランダムになってしまう
そのようなばあいは、「random」という引数を用意した方が、わかりやすいです。
function myFunction8_54_04() {
/** クラスCustomSheet */
class CustomSheet {
/** カスタムシートのコンストラクタ
* @constructor
*/
constructor() {
this.range = SpreadsheetApp.getActiveSheet().getRange('A1');
}
/** セルの背景色にハイライトをつけるメソッド */
highlight(color) {
if (color !== 'random') {
this.range.setBackground(color);
return
}
const max = 3, min = 1;
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
switch (randomNum) {
case 1:
color = 'red';
break;
case 2:
color = 'blue';
break;
case 3:
color = 'yellow';
break;
}
this.range.setBackground(color);
}
}
const element = new CustomSheet();
element.highlight('random');
element.highlight(undefined);
}
undefinedを表すプロパティを使う
いちばんいい方法は、randomプロパティを持つオブジェクトを引数にとることです。
function myFunction8_54_05() {
/** クラスCustomSheet */
class CustomSheet {
/** カスタムシートのコンストラクタ
* @constructor
*/
constructor() {
this.range = SpreadsheetApp.getActiveSheet().getRange('A1');
}
/** セルの背景色にハイライトをつけるメソッド */
highlight(obj) {
if (!obj.random) {
this.range.setBackground(obj.color);
return
}
const max = 3, min = 1;
const randomNum = Math.floor(Math.random() * (max - min + 1)) + min;
switch (randomNum) {
case 1:
color = 'red';
break;
case 2:
color = 'blue';
break;
case 3:
color = 'yellow';
break;
}
this.range.setBackground(color);
}
}
const element = new CustomSheet();
element.highlight({ random: true, color: '' });
element.highlight({ random: false, color: 'purple' });
element.highlight({ random: false });
}
undefinedのテスト
コンストラクタServerは、第2引数を取らないばあいは、デフォルト値として、’localhost’をもつようにしたいです。
「第2引数を取らない」という条件判定にたいし、argumentsオブジェクトのlengthで判定してみましょう。
しかし、存在しないプロパティによるundefinedが渡されたときに、破綻します。
function myFunction8_54_06() {
/** クラスServer */
class Server {
constructor(port, hostname) {
this.port = port;
this.hostname = arguments.length < 2 ? 'localhost' : String(hostname);
}
}
const s1 = new Server(80, 'example.com');
console.log(s1); // => { port: 80, hostname: 'example.com' }
const s2 = new Server(80);
console.log(s2); // => { port: 80, hostname: 'localhost' }
const config = {};
const s3 = new Server(80, config.hostname);
console.log(s3); // => { port: 80, hostname: undefined }
}
これを解決するためには、undefinedを判定する必要があります。
V8から導入された、デフォルト引数を活用しましょう。
function myFunction8_54_07() {
/** クラスServer */
class Server {
constructor(port, hostname = 'localhost') {
this.port = port;
this.hostname = String(hostname);
}
}
const s1 = new Server(80, 'example.com');
console.log(s1); // => { port: 80, hostname: 'example.com' }
const s2 = new Server(80);
console.log(s2); // => { port: 80, hostname: 'localhost' }
const config = {};
const s3 = new Server(80, config.hostname);
console.log(s3); // => { port: 80, hostname: 'localhost' }
}
論理演算子で判定する方法も、ご紹介しておきます。(前述のデフォルト引数の方がいいでしょう)
function myFunction8_54_08() {
/** クラスServer */
class Server {
constructor(port, hostname) {
this.port = port;
this.hostname = String(hostname || 'localhost');
}
}
const s1 = new Server(80, 'example.com');
console.log(s1); // => { port: 80, hostname: 'example.com' }
const s2 = new Server(80);
console.log(s2); // => { port: 80, hostname: 'localhost' }
const config = {};
const s3 = new Server(80, config.hostname);
console.log(s3); // => { port: 80, hostname: 'localhost' }
}
デフォルト引数を使うときの真偽値
論理演算子でデフォルト引数を判定するばあいは、注意が必要です。
以下のような、思いがけないバグを生んでしまいます。
function myFunction8_54_09() {
/** クラスElement */
class Element {
constructor(width, height) {
this.width = width || 320;
this.height = height || 240;
}
}
const c1 = new Element(0, 0);
console.log(c1.width); // => 320
console.log(c1.height); // => 240
}
やはり、デフォルト引数が有効です。
function myFunction8_54_10() {
/** クラスElement */
class Element {
constructor(width = 320, height = 240) {
this.width = width;
this.height = height;
}
}
const c2 = new Element(0, 0);
console.log(c2.width); // => 0
console.log(c2.height); // => 0
}
まとめ
以上で、「undefinedは「値なし」として扱う」をお届けしました。
次回は、「オプションオブジェクトで、キーワード付き引数群を受け取ろう」 をお届けします。
参考資料
このシリーズの目次
- [EffectiveJavaScript輪読会8]一貫した規約を維持しよう
- [EffectiveJavaScript輪読会8]undefinedは「値なし」として扱う
- [EffectiveJavaScript輪読会8]オプションオブジェクトで、キーワード付き引数群を受け取ろう
- [EffectiveJavaScript輪読会8]不必要な状態を排除する
- [EffectiveJavaScript輪読会8]柔軟なインターフェイスのために、構造的な型付けを使う
- [EffectiveJavaScript輪読会8]配列と「配列のようなもの」を区別しよう
- [EffectiveJavaScript輪読会8]過剰な型強制を防ごう