「JavaScriptの理解を深めた人がさらにもう一歩先に進むための本」の感想・忘備録2

「JavaScriptの理解を深めた人がさらにもう一歩先に進むための本」の感想・忘備録1の続き

関数の巻き上げ

JavaScriptでは、関数定義の方法として関数宣言と関数式の2種類が用意されている。
大きな違いとして、関数宣言で定義された関数はコード実行時にスコープの先頭まで巻き上げられる。
以下の例は一見エラーになりそうだが、関数の巻き上げにより実行時には関数定義が先頭に移動している。

var result = sum(1, 2);
function sum(val1 val2) {
    retrun val1 + val2;
}

以下の関数式の例では、巻き上げが行われないためエラーとなる。

var result = sum(1, 2); // エラー
var sum = function(val1 val2) {
    retrun val1 + val2;
}

安全なコンストラクタ

コンストラクタの実態はただの関数なので、new演算子を使わない「ただの関数コール」もできてしまう。
その場合、コンストラクタ内のthisはGlobalオブジェクトとなり、グローバルスコープの汚染に繋がりかねない。
以下のようにinstanceofを使ってthisをチェックすることで、関数コールされてもインスタンスを返却することができる。
コンストラクタは呼び出され方が保証されないため、常にこのようなチェックを行うべきである。
ちなみに、ObjectやArrayなどのネイティブクラスもこのような「安全な」コンストラクタを持っている。

function Phone(name) {
    if (this instanceof Phone) {
        // thisがPhone→new演算子が使われた
        this.name = name;
    }
    else {
        // 関数コールされた
        return new Phone(name);
    }
}

ゲッターとセッター

ゲッターとセッターはES5から追加されているが、認知度は低い。
get, setの後にfunctionを付けずに関数を定義する。

var human = {
    _name : '',
    get name() {
        return this._name;
    },
    set name(val) {
        this._name = val;
    }
}
human.name = 'okada';
console.log(human.name);

ゲッターとセッターの問題点

上述の例のようにせっかくセッタ・ゲッタを定義しても
human._name = 'tanaka';
console.log(human._name);
のように実態を直接参照されてしまっては意味がない。
以下のようにクロージャと即時関数を使うと問題を解決できる。

var human = (function() {
   var  _name;
    return {
        get name() {
             return _name;
        },
        set name(val) {
            _name = val;
        }
    };
})();
human.name = 'okada';
console.log(human.name); // okada
console.log(human._name); // undefined

プロパティの存在確認

if (phone.name)のようにしてはいけない。
0や空文字でもtrueになってしまう。
プロパティの存在確認にはin演算子を使う。

var human = {
    name: '',
    func: function(){}
};
console.log('name' in human); // true
console.log('func' in human); // true
console.log('toString' in human); // true

※この例のようにin演算子はtoString()のようにprototypeが持っているプロパティも対象となる
※オブジェクトのコンストラクタに保持しているかどうかの判定にはhasOwnPropertyメソッドを使う
console.log(human.hasOwnProperty('toString')); // false

プロパティ属性

オブジェクトのプロパティには様々なプロパティ属性が定義されている。
プロパティ属性は標準仕様において二重ブラケット[[]]で表記される。
たとえば、関数オブジェクトはプロパティ属性[[Call]]を持っている。
(コードから直接アクセスすることはできない)
プロパティ属性には以下の種類がある。

  1. データプロパティ
  2. アクセサプロパティ
  3. データプロパティとアクセサプロパティに共通して存在する属性

データプロパティ

データプロパティは値を格納するためのプロパティである。
特に意識しないで用意したプロパティはすべてデータプロパティになるので、一番馴染みのあるプロパティであると思われる。
[[Value]]:値を格納するための属性
[[Writable]]:書き込み可能かどうかをbooleanで格納するための属性

アクセサプロパティ

アクセサプロパティはデータを保持しない代わりにゲッタとセッタを定義する。
最低でもどちらか1つは定義する必要がある。
[[Get]]:ゲッタ関数が格納される
[[Set]]:セッタ関数が格納される
ちなみに、プロパティに対してデータプロパティとアクセサプロパティを同時に持たせようとするとエラーになる。当然のことだが。。

データプロパティとアクセサプロパティに共通して存在する属性

[[Enumerable]]:走査可能かどうかをbooleanで格納するための属性(for…inなどで処理できるか)
[[Configurable]]:変更・削除が可能かどうかをbooleanで格納するための属性

プロパティ属性の変更

Object.definePropertyメソッドで変更できる。
複数のプロパティを操作する場合はObject.definePropertiesメソッドを使う。

var human = {
    name: 'yamada',
    age: 44
}
for (var prop in human) {
    console.log(prop ); // name age
}
Object.defineProperty(human, 'name', {enumerable: false});
for (var prop in human) {
    console.log(prop ); // age
}

プロパティ属性の取得

Object.getOwnPropertyDescriptorメソッドで取得できる。
メソッド名の通り、自身のプロパティ属性は取得できますが、プロトタイプ上のプロパティは対象外である。

var human = {
    name: 'okada'
}
var descriptor = Object.getOwnPropertyDescriptor(human, 'name');
console.log(descriptor.value); // okada
console.log(descriptor.enumerable); // true
console.log(descriptor.writable); // true
console.log(descriptor.configurable); // true

オブジェクトの保護

javaScriptではオブジェクトの保護に3段階の強度レベルが用意されている。
一度保護を行うと、元に戻すことはできない。
Strictモードでは、制限済みのプロパティ操作を行うとエラーとなる。

  1. 拡張禁止
  2. 封印
  3. 凍結

拡張禁止

  • Object.preventExtensionsメソッドを使用する。
  • プロパティの追加ができなくなる(削除・プロパティ属性変更・値の読み書きは可)
  • Object.isExtensibleメソッドで確認できる。
  • 共に引数にオブジェクトを渡す。
  • プロパティ属性[[Extensible]]で管理されている。

封印

  • Object.sealメソッドを使用する。
  • プロパティの追加・削除・プロパティ属性変更ができなくなる。(値の読み書きは可)
  • Object.isSealメソッドで確認できる。
  • 共に引数にオブジェクトを渡す。
  • 封印を管理するプロパティ属性はないが、sealメソッドを実行すると[[Extensible]]と[[Configurable]]がfalseに設定される。

凍結

  • Object.freezeメソッドを使用する。
  • プロパティの追加・削除・プロパティ属性変更・値の書き混みができなくなる。(値の読み取りは可)
  • Object.isFrozenメソッドで確認できる。
  • 共に引数にオブジェクトを渡す。
  • 凍結を管理するプロパティ属性はないが、freezeメソッドを実行すると[[Extensible]]と[[Configurable]]と[[Writable]]がfalseに設定される。

オブジェクトの継承

ES5からObject.create()で継承ができるようになった。
第2引数に追加したプロパティを渡すこともできる。
第1引数にnullを渡すとプロトタイプにnullがセットされる、つまりプロトタイプチェーンを持たないオブジェクトになり、Object.prototypeのメソッドが使えなくなる。
「なんの役に立つんだよ」という声が聞こえてきそうだが、どんなプロパティ名でも競合しないのでハッシュテーブルの作成なんかに適していたりする。

var Phone = {
    call: function() {console.log('hoge');}
};
var Phone2 = Object.create(Phone);
Phone2 .call(); // hoge
// ※ES6からはclass, extendsがサポートされました。

クラス

ES6の目玉機能としてクラスが実装されたが、未だに認知されているとは思えない存在である。
また、2016年7月現在でもブラウザの実装状況は完璧とは言い難いところである。

  • thisに追加されたプロパティがメンバ変数となる
  • メソッドにfunctionの記述は不要である
  • メソッド内からメンバ変数や別のメソッドを呼び出す場合はthisが必要である
  • Javaなどと違ってthisの省略はできない
class Person {
    constructor(name) {
        this.name = name
    }
    getName() {
        return this.name;
    }
    showName() {
        alert(this.getName());
    }
}

【新しい記事】

【古い記事】

コメントを残す

メールアドレスが公開されることはありません。