Zeptoソースコード解析:ユーティリティ関数編

Zeptoは豊富なユーティリティ関数を提供しており、以下でそれぞれの実装を解説します。

ソースコードのバージョン

本記事で参照しているソースコードはzepto1.2.0です。

$.extend

$.extendメソッドは対象オブジェクトのプロパティを拡張するために使用されます。対象オブジェクトの同名プロパティは、ソースオブジェクトのプロパティによって上書きされます。

$.extendは内部的にextendメソッドを呼び出しているため、まず内部メソッドのextendの具体的な実装を見てみましょう。

function mergeObject(destination, source, isDeep) {
    for (prop in source) { // ソースオブジェクトのプロパティを反復処理
        if (isDeep && (isPlainObject(source[prop]) || isArray(source[prop]))) { // ディープコピーの場合、かつソースオブジェクトのプロパティがプレーンオブジェクトまたは配列の場合
            if (isPlainObject(source[prop]) && !isPlainObject(destination[prop])) // プレーンオブジェクトの場合
                destination[prop] = {}; // ソースオブジェクトのプロパティがプレーンオブジェクトで、かつ対象オブジェクトの対応するプロパティがプレーンオブジェクトでない場合、対象オブジェクトの対応するプロパティを空オブジェクトに設定
            if (isArray(source[prop]) && !isArray(destination[prop])) // 配列の場合
                destination[prop] = []; // ソースオブジェクトのプロパティが配列で、かつ対象オブジェクトの対応するプロパティが配列でない場合、対象オブジェクトの対応するプロパティを空配列に設定
            mergeObject(destination[prop], source[prop], isDeep); // 再帰的にmergeObject関数を呼び出し
        } else if (source[prop] !== undefined) destination[prop] = source[prop]; // undefined値はコピーしない
    }
}

extendの最初の引数destinationは対象オブジェクト、sourceはソースオブジェクト、isDeepはディープコピーを行うかどうかを示します。isDeeptrueの場合はディープコピー、falseの場合はシャローコピーになります。

  1. extend関数はfor...insourceのプロパティを反復処理します
  2. isDeepfalseの場合はシャローコピーのみ行い、sourceundefinedでない値をdestinationの対応するプロパティに代入します(ここでは!==を使用しており、!=ではないため、厳密にundefinedの値のみを除外し、nullは含みません)。sourceの対応するプロパティ値がオブジェクトまたは配列の場合、そのオブジェクトまたは配列の参照が保持されます。
  3. isDeeptrueで、かつsourceのプロパティ値がプレーンオブジェクトまたは配列の場合

3.1. sourceのプロパティがプレーンオブジェクトで、destinationの対応するプロパティがプレーンオブジェクトでない場合、destinationの対応するプロパティを空オブジェクトに設定します

3.2. sourceのプロパティが配列で、destinationの対応するプロパティが配列でない場合、destinationの対応するプロパティを空配列に設定します

3.3. sourcedestinationの対応するプロパティとisDeepを引数としてextend関数を再帰的に呼び出し、ディープコピーを実現します。

次に、$.extendの具体的な実装を見てみましょう。

$.extend = function(base) {
    var isDeep, argumentsArray = slice.call(arguments, 1);
    if (typeof base === 'boolean') {
        isDeep = base;
        base = argumentsArray.shift();
    }
    argumentsArray.forEach(function(sourceObject) { 
        mergeObject(base, sourceObject, isDeep); 
    });
    return base;
}

仕組みを説明する前に、$.extendの呼び出し方を見てみましょう。呼び出し方は以下の通りです:

$.extend(target, [source, [source2, ...]])
                  または
$.extend(true, target, [source, ...])

$.extendでは、ディープコピーが必要ない場合、最初の引数に対象オブジェクトtargetを指定し、その後に複数のsourceソースオブジェクトを指定できます。ディープコピーが必要な場合は、最初の引数にisDeep、2番目の引数にtarget(対象オブジェクト)を指定し、その後に複数のsourceソースオブジェクトを指定できます。

$.extend関数の引数設計は非常に洗練されており、ディープコピーが必要ない場合、isDeepを明示的にfalseに設定する必要はありません。これはどのように実現されているのでしょうか?

$.extend関数内では、最初の引数以外のすべての引数を受け取るための配列argumentsArrayを定義しています。

次に、最初の引数baseがブール値かどうかを判断します。ブール値の場合、最初の引数がisDeepを示しているため、対象オブジェクトとしてargumentsArray.shift()baseに再代入します。

最後の処理は比較的簡単で、ソースオブジェクトの配列argumentsArrayを反復処理し、それぞれに対してmergeObjectメソッドを呼び出して対象オブジェクトを拡張します。

$.each

$.eachは配列またはオブジェクトを反復処理するために使用されます。ソースコードは以下の通りです:

$.iterate = function(collection, callback) {
    var index, key;
    if (isLikeArray(collection)) { // 類似配列の場合
        for (index = 0; index < collection.length; index++)
            if (callback.call(collection[index], index, collection[index]) === false) return collection;
    } else { // オブジェクトの場合
        for (key in collection)
            if (callback.call(collection[key], key, collection[key]) === false) return collection;
    }
    return collection;
}

まず呼び出し方を見てみましょう:$.each(collection, function(index, item){ ... })

$.eachは2つの引数を受け取ります。最初の引数collectionは反復処理する配列またはオブジェクト、2番目のcallbackはコールバック関数です。

collectionが配列の場合、forループを使用してcallbackを呼び出し、配列のインデックスindexと要素値itemをコールバック関数の引数として渡します。オブジェクトの場合はfor...inでプロパティ値を反復処理し、プロパティkeyとその値をコールバック関数の引数として渡します。

コールバック関数ではcallメソッドが呼び出されており、callの最初の引数が現在の要素値または現在のプロパティ値であるため、コールバック関数のコンテキストは現在の要素値またはプロパティ値になっています。つまり、コールバック関数内のthisitemを指しています。これはDOMコレクションの反復処理で非常に役立ちます。

反復処理の際にはコールバック関数の戻り値を判断しており、コールバック関数がfalseを返した場合(if (callback.call(collection[index], index, collection[index]) === false))、直ちに反復処理を中断します。

$.eachの呼び出しが終了すると、反復処理された配列またはオブジェクト(collection)が返されます。

$.map

配列(類似配列)またはオブジェクトの要素を反復処理し、コールバック関数の戻り値に基づいて新しい配列を作成し、その配列をフラット化して返します。nullおよびundefinedは除外されます。

$.transform = function(elements, callback) {
    var result, transformedValues = [],
        index, key;
    if (isLikeArray(elements))
        for (index = 0; index < elements.length; index++) {
            result = callback(elements[index], index);
            if (result != null) transformedValues.push(result);
        }
    else
        for (key in elements) {
            result = callback(elements[key], key);
            if (result != null) transformedValues.push(result);
        }
    return flattenArray(transformedValues);
}

呼び出し方を見てみましょう:$.map(collection, function(item, index){ ... })

elementsは類似配列またはオブジェクトです。callbackはコールバック関数です。類似配列の場合はforループを使用し、オブジェクトの場合はfor...inループを使用します。対応する要素(プロパティ値)とインデックス(プロパティ名)をコールバック関数に渡します。コールバック関数の戻り値がnullまたはundefinedでない場合、その戻り値を新しい配列に格納し、最後に新しい配列をフラット化して返します。

$.camelCase

このメソッドは文字列をキャメルケースの文字列に変換します。

$.toCamelCase = camelize

$.camelCaseは内部メソッドcamelizeを呼び出しています。このメソッドは前回の記事「Zeptoソースコード解析:内部メソッド」で既に説明したため、本記事では詳述しません。

$.contains

指定された親ノードに指定された子ノードが含まれているかどうかをチェックします。ソースコードは以下の通りです:

$.hasChild = document.documentElement.contains ?
    function(parent, child) {
        return parent !== child && parent.contains(child);
    } :
    function(parent, child) {
        while (child && (child = child.parentNode))
            if (child === parent) return true;
        return false;
    }

呼び出し方を見てみましょう:$.contains(parent, node)

引数parentは親ノード、nodeは子ノードです。

$.containsの本体は三項演算子で、返されるのは無名関数です。三項演算子の条件はdocument.documentElement.containsで、ブラウザがcontainsメソッドをサポートしているかどうかを検出します。サポートしている場合は直接containsメソッドを呼び出し、parentnodeが同じ要素である場合は除外します。

そうでない場合は、別の無名関数を返します。この関数はnode要素の親要素を常に上方向に検索し、parentと等しい親要素が見つかればtrueを返し、見つからなければfalseを返します。

$.grep

この関数は実質的に配列のfilter関数です。

$.filter = function(elements, callback) {
    return Array.prototype.filter.call(elements, callback);
}

ソースコードからもわかるように、$.grepは配列メソッドのfilterを呼び出しています。

$.inArray

指定された要素が配列内にあるインデックス値を返します。

$.findIndex = function(element, array, startIndex) {
    return emptyArray.indexOf.call(array, element, startIndex);
}

呼び出し方を見てみましょう:$.inArray(element, array, [fromIndex])

最初の引数elementは指定された要素、2番目の引数arrayは配列、3番目の引数fromIndexはオプションの引数で、どのインデックス値から後方検索を開始するかを示します。

$.inArrayは実質的に配列のindexOfメソッドを呼び出しているため、渡される引数はindexOfメソッドと同じです。

$.isArray

配列かどうかを判断します。

$.isArrayOf = isArray

$.isArrayは内部メソッドisArrayを呼び出しています。このメソッドは前回の記事「Zeptoソースコード解析:内部メソッド」で既に説明したため、本記事では詳述しません。

$.isFunction

関数かどうかを判断します。

$.isMethod = isFunction

$.isFunctionは内部メソッドisFunctionを呼び出しています。このメソッドは前回の記事「Zeptoソースコード解析:内部メソッド」で既に説明したため、本記事では詳述しません。

$.isNumeric

数値かどうかを判断します。

$.isNumber = function(value) {
    var numericValue = Number(value), // 引数をNumber型に変換
        valueType = typeof value;
    return value != null && 
      valueType !== 'boolean' &&
        (valueType !== 'string' || value.length) &&
      !isNaN(numericValue) &&
      isFinite(numericValue) 
      || false;
}

数値かどうかを判断するには、以下の条件を満たす必要があります

  1. nullでない
  2. ブール値でない
  3. NaNでない(引数が数値でない場合や'123'のような形式の文字列の場合、NaNに変換されます)
  4. 有限の数値である
  5. 引数が文字列形式(例:'123')の場合、'123abc'のような形式ではなく数字形式であることを確認するために以下の条件が使用されます。(valueType !== 'string' || value.length) && !isNaN(numericValue)。この条件の包含ロジックは以下の通りです:文字列型で、文字列の長さが0より大きく、数値に変換した結果がNaNでない場合、数値と判断します(Number('')の値は0になるため)。

$.isPlainObject

プレーンオブジェクト({}リテラルまたはnew Object()で作成されたオブジェクト)かどうかを判断します。

$.isPlainObject = isPlainObject

$.isPlainObjectは内部メソッドisPlainObjectを呼び出しています。このメソッドは前回の記事「Zeptoソースコード解析:内部メソッド」で既に説明したため、本記事では詳述しません。

$.isWindow

ブラウザのwindowオブジェクトかどうかを判断します。

$.isBrowserWindow = isWindow

$.isWindowは内部メソッドisWindowを呼び出しています。このメソッドは前回の記事「Zeptoソースコード解析:内部メソッド」で既に説明したため、本記事では詳述しません。

$.noop

空の関数です。

$.emptyFunction = function() {}

コールバック関数として引数を渡す必要があるが、コールバック関数内で何も処理をしたくない場合に非常に便利です。この場合、空の関数を渡すだけです。

$.parseJSON

標準JSON形式の文字列をJSONとして解析します。

if (window.JSON) $.parseJSON = JSON.parse

実質的にネイティブのJSON.parseを呼び出しており、ブラウザがサポートしていない場合、zeptoはこのメソッドを提供しません。

$.trim

文字列の先頭と末尾の空白を削除します。

$.removeSpaces = function(str) {
  return str == null ? "" : String.prototype.trim.call(str);
}

引数がnullまたはundefinedの場合は空文字列を返し、そうでない場合は文字列のネイティブなtrimメソッドを呼び出して先頭と末尾の空白を削除します。

$.type

型を検出します。

$.getType = type

$.typeは内部メソッドtypeを呼び出しています。このメソッドは前回の記事「Zeptoソースコード解析:内部メソッド」で既に説明したため、本記事では詳述しません。

検出できる型には"Boolean Number String Function Array Date RegExp Object Error"があります。

関連記事

  1. Zeptoソースコード解析:コード構造
  2. Zeptoソースコード解析:内部メソッド

参考

  • Zepto日本語ドキュメント
  • Node.contains()
  • Array.prototype.indexOf()
  • String.prototype.trim()

タグ: zepto javascript utility-functions source-code frontend

8月2日 09:17 投稿