しまてく

学んだ技術を書きためるブログ

SecretsOfTheJavaScriptNinja 3章

はじめに

このエントリは若手IT勉強会のSecretsOfTheJavaScriptNinja読書会の為の
まとめエントリです。
拙い翻訳をしながらざっくりまとめてます。

Secrets of the JavaScript Ninjaとは

Secrets of the JavaScript Ninja

Secrets of the JavaScript Ninja

jQueryの作者であるJohn Resigが絶賛執筆中のJavaScript本です。
現在はまだ未完の為、電子書籍という形で購入して読み進めていきます。

3章の内容

  • 関数の重要性の概要
  • オブジェクトとして関数を使う方法
  • 関数のコンテキスト
  • 可変引数の扱い
  • 関数のタイプ決定

関数定義

関数の定義方法として、大きく分けて2つ
// (1) オーソドックスな方法
function foo(){
    return true;
}

// (2) 無名関数を使う方法
var bar = function(){
    return true;
};
window.baz = function(){
    return true;
};

これらの定義方法はグローバルスコープで定義される場合、完全に等しいものになります。
 

関数のスコープ

function stealthCheck(){
    var ret = stealth() == stealth();
    assert( ret, "returnステートメントの下には行く事はないが、このretはtrueになる。" );
    return true;
    function stealth(){ return true; }
}
stealthCheck();

関数は特別扱いをされていて、「function foo(){}」のように定義された関数の場合は
関数を呼び出す個所よりも後ろで定義されていても呼び出し可能という特性があります。


ただし、「var foo = function(){}」のような無名関数の場合は、コード中で定義を行った場所以降
でしか呼び出すことはできません。

無名関数

  • (普通の)無名関数
// こんな感じでコールバックなどで使ったり。
setInterval(function(){
    // この無名関数は100ミリ秒ごとに呼ばれる
}, 100);
  • 名前付き無名関数

一見して矛盾しているような関数ですが、限定的に名前が付いている関数の事です。
一言で言えば「関数の中からは名前付き」「関数の外からは無名関数」って事ですね。

var ninja = {
    yell: function yell(n){
        // この関数の中でだけyellという名前が使える。
        return n > 0 ? yell(n-1) + "a" : "hiy";
    }
};
assert( ninja.yell(4) == "hiyaaaa", "ちゃんと関数内で参照できる。" );
var samurai = { yell: ninja.yell };
var ninja = {};
assert( samurai.yell(4) == "hiyaaaa", "関数をコピーしても大丈夫。" );


ただ、より良い方法として、argumentsオブジェクトのcalleeプロパティを使用する方法があります。

var ninja = {
    yell: function(n){
        return n > 0 ? arguments.callee(n-1) + "a" : "hiy";
    }
};
assert( ninja.yell(4) == "hiyaaaa", "arguments.calleeでも自己参照ができる。" );

名前付き無名変数は古いブラウザでは使えないものもあったり、やや冗長になりがちなので
関数のなかで自分自身を参照したい場合はaruguments.calleeを用いるのがいいのではないでしょうか。

オブジェクトとしての関数

JavaScriptでは関数(Functionオブジェクト)もObjectオブジェクトから派生しているオブジェクトなので
以下のようなObjectオブジェクトの特性を持っています。

  1. プロパティを持てる
  2. prototypeを持てる
  3. オブジェクトの能力を持っている(派生してるからね)

違うところは、関数として呼び出せる所です。

var obj = {};
var fn = function(){};
obj.prop = "some value";
fn.prop = "some value";
assert( obj.prop == fn.prop, "オブジェクトも関数もプロパティを持てる" );

関数の保持

実際にjQueryの内部でイベントハンドラのコールバック関数の保持をするのにやっているような処理の例

var store = {
    id: 1,
    cache: {},
    add: function( fn ) {
        if ( !fn.id ) {
            fn.id = store.id++;
            return !!(store.cache[fn.id] = fn);
        }
    }
};
function ninja(){}
assert( store.add( ninja ), "関数は安全に追加される。" );
assert( !store.add( ninja ), "同じものは1度しか追加されない。" );

キャッシュ機能付き関数

名前でDOM要素を取得する際にキャッシュに入れておき、次の要求時に即座に返す方法

function getElements( name ) {
    return getElements.cache[ name ] = getElements.cache[ name ] || document.getElementsByTagName( name );
}
getElements.cache = {};

高速化にはすごく寄与するけど、メモリの使いすぎには注意するべきだよ、と。

関数のコンテキスト

「関数のコンテキスト == "実行されている関数を含むオブジェクト"」です。
例えば、オブジェクトの内部では上書かれない限りそのコンテキストは常にそのオブジェクト
への参照を保持します。

var katana = {
    isSharp: true,
    use: function(){
        this.isSharp = !this.isSharp; // オブジェクト自身のプロパティを触れる。
    }
};
katana.use();
assert( !katana.isSharp, "Verify the value of isSharp has been changed." );


しかし、オブジェクトのプロパティとして定義されない関数のコンテキストは
グローバルになります。

function foo(){
    assert( this === window, "グローバルオブジェクト(ブラウザで実行する場合はwindow)になる" );
}
コンテキストを修正する2つの方法
  1. Function#call
  2. Function#apply

の2つです。
それぞれ用途は同じですが、使い方が違います。


どちらも第1引数は同じで、関数を呼んだ後のコンテキストとなるオブジェクトです。
callは第2引数以降が呼び出す関数の引数になります(ばらばらで渡す)。
applyは第2引数が呼び出す関数の引数になります(配列として渡す)。

function add(a, b){
    return a + b;
}
assert( add.call(this, 1, 2) == 3, ".call() takes individual arguments" );
assert( add.apply(this, [1, 2]) == 3, ".apply() takes an array of arguments");
ループ

forを隠ぺいしたループの実装

// この関数をどっかで定義しておく
function loop(array, fn){
    for ( var i = 0; i < array.length; i++ )
        if ( fn.call( array, array[i], i ) === false )
            break;
}

// 実際に使うのはこんな感じ
// jQuery.eachの実装と大体同じようなもの。
var num = 0;
loop([0, 1, 2], function(value, i){
    assert(value == num++, "Make sure the contents are as we expect it.");
});

上記の例だと、loop関数の中でcallメソッドを使ってコンテキストをarrayにしているので、
loop関数に渡したコールバック関数のなかでthisを参照するとオリジナルの配列にアクセスできます。

配列内の最大値・最小値

正直に最大値や最小値を求めようとするよりも良い方法があります。
Mathオブジェクトのminメソッドとmaxメソッドを借りてしまいましょう。

function smallest(array){
    return Math.min.apply( Math, array );
}
function largest(array){
    return Math.max.apply( Math, array );
}
assert(smallest([0, 1, 2, 3]) == 0, "Locate the smallest value.");
assert(largest([0, 1, 2, 3]) == 3, "Locate the largest value.");

関数のlength

全ての関数には、関数自体にlengthプロパティがあります。
さらに、argumentsオブジェクトにもlengthプロパティがあります。

これらの違いは、

  • 関数のlengthはその関数が期待する引数の数
  • argumentsオブジェクトのlengthは実際に渡された引数の数

となります。

function foo(a,b){
    assert(foo.length === 2, "期待する引数は2つ");
    assert(arguments.length === 3, "渡された引数は3つ");
}
foo(1,2,3);

これを使うと、引数の数の違いによるメソッドオーバーロードができますよーという例が
本には書いてありました。

関数の型

指定されたオブジェクトが本当に関数で、呼び出すことができるのか?という判定をする
クロスブラウザな方法の例をご紹介します。


大抵はtypeofステートメントだけで十分に関数かどうか判定できます。

function ninja(){}
assert( typeof ninja == "function", "Functions have a type of function" );


しかし以下のようなブラウザを相手にする時にはtypeofステートメントでは上手くいきません。