LoginSignup
5

More than 5 years have passed since last update.

FuelPHPのArr::set()をJavaScriptで実装して、多階層のオブジェクトに値を簡単に設定する

Last updated at Posted at 2014-07-22

続続・多階層のオブジェクトのアクセスへの挑戦

どうも、前回のタイトルをコピーしたらややこしくなったアレです。

前回の投稿、「FuelPHPのArr::get()をJavaScriptで実装して、多階層のオブジェクトの値を簡単に取得する」で、多階層のオブジェクトから値を取得するコードをJavaScriptで実装しました。
ならば、多階層のオブジェクトに値を設定するコードも書かないとね(ニッコリ)という電波を受信してしまったので、不完全ながら実装しました。不完全な理由については後述します。

※追記(2014/07/23)※
@warotasan様より直々にコメントを頂きまして、上記の不完全な部分はけっこう解消されました。ありがとうございます!!
※追記ここまで※

ソース

Gitアカウントの必要性を感じる今日このごろ…。
前回の投稿から思うところがあり、いろいろ変更しています。今回は \Arr::set() の為のコードで、単純に前回の投稿にマージしてもダメというひどい状態です。

後程、ちゃんと公開できる環境を整えようかと思います。主に自分のために…。

fuel.js

/**
 * @fileoverview FuelPHPのcoreクラスをJS化して使えるようにするためのクラス群
 * 作成した時点でのFuelPHPのバージョンは 1.7.1
 */
var FuelJS = FuelJS || {};

FuelJS.core = FuelJS.core || {};
FuelJS.util = FuelJS.util || {};

/** 
 * \Fuelクラス(の一部)
 *
 * @constructor 
 */
FuelJS.core.Fuel = (function () {
    return {
        /**
         * Takes a value and checks if it is a Closure or not, if it is it
         * will return the result of the closure, if not, it will simply return the
         * value.
         *
         * @param   {*}       value
         * @return  {*}
         */
        value : function (value) {
            return typeof value === 'function' ? value() : value;
        }
    };
} () );

/** 
 * PHPの配列の組み込み関数を再現した(ような)クラス
 *
 * @constructor 
 */
FuelJS.util.Array = (function () {
    return {
        isArray : function (value) {
            return (
                    Array.isArray ?
                    Array.isArray(value) :
                    Object.prototype.toString.call(value) === '[object Array]'
                ) ||
                typeof value === 'object' ||
                Object.prototype.toString.call(value) === '[object Object]';
        },
        arrayKeys : function (value) {
            return (
                Object.keys ?
                Object.keys(value) :
                (function () {
                    Object.keys = (function () {
                        var hasOwnProperty = Object.prototype.hasOwnProperty,
                            hasDontEnumBug = ! ({toString: null}).propertyIsEnumerable('toString'),
                            dontEnums = [
                                'toString',
                                'toLocaleString',
                                'valueOf',
                                'hasOwnProperty',
                                'isPrototypeOf',
                                'propertyIsEnumerable',
                                'constructor'
                            ],
                            dontEnumsLength = dontEnums.length;

                        return function (obj) {
                            if (typeof obj !== 'object' &&
                                typeof obj !== 'function' ||
                                obj === null
                            ) {
                                throw new TypeError('Object.keys called on non-object');
                            }

                            var result = [];
                            for (var prop in obj) {
                                if (hasOwnProperty.call(obj, prop)) {
                                    result.push(prop);
                                }
                            }

                            if (hasDontEnumBug) {
                                for (var i = 0; i < dontEnumsLength; i++) {
                                    if (hasOwnProperty.call(obj, dontEnums[i])) {
                                        result.push(dontEnums[i]);
                                    }
                                }
                            }

                            return result;
                        };
                    } () );

                    Object.keys(value);
                } () )
            );
        }
    };
} () );

/** 
 * \Arrクラス(の一部)
 *
 * @constructor 
 */
FuelJS.core.Arr = (function () {
    var _this = {
        /**
         * Set an array item (dot-notated) to the value.
         * 
         * リファレンス渡しが辛すぎたので少し改造しています。
         * 本来、keyが指定されていない場合にarrayにvalueを代入する挙動ですが
         * JavaScriptで同じ挙動は今のところ再現できず...
         * 
         * @param   {!Object|Array}     array    The array to insert it into
         * @param   {string|Array}      key      The dot-notated key to set or array of keys
         * @param   {*}                 value    The value
         * @return  {void}
         */
        set : function (array, key, value) {
            if (typeof key === 'undefined' || key === null) {
                if (FuelJS.util.Array.isArray(array) &&
                    FuelJS.util.Array.isArray(value)
                ) {
                    array.length = 0;
                    array.push.apply(array, value);
                    return;
                }

                throw new TypeError('Type of "array" or "key" is invalid.'); // ※注1
            }

            var keys, i, len;
            if (FuelJS.util.Array.isArray(key)) {
                keys = FuelJS.util.Array.arrayKeys(key);

                for (i = 0, len = keys.length; i < len; i++) {
                    self.set(array, keys[i], key[keys[i]]);
                    // arguments.callee(array, keys[i], key[keys[i]]); ※注2
                }
            } else {
                keys = (key).toString().split('.');

                var obj = FuelJS.util.Array.isArray(array[keys[0]]) ?
                          array[keys[0]] : {};

                for (i = 1, len = keys.length; i < len ; i++) {
                    key = keys[i];

                    if (typeof obj[key] === 'undefined' ||
                        obj[key] === null ||
                         ! FuelJS.util.Array.isArray(obj[key])
                    ) {
                        obj[key] = {};
                    }
                }

                obj[key] = value;
                for (i = keys.length - 1; 1 < i ; i--) {
                    obj[keys[i - 1]][keys[i]] = obj[keys[i]];
                }

                if (keys[1]) {
                    var retObj = {};
                    retObj[keys[1]] = obj[keys[1]];
                    array[keys[0]]  = retObj;
                } else {
                    array[keys[0]]  = obj[keys[0]];
                }
            }
        }
    };

    var self = _this;

    return self;
} () );

注釈について

  1. この部分は本来、keynull の場合に value そのものが呼び出し元の array に代入されるはずなのですが、JavaScriptの言語仕様上、参照を代入できず値渡しになります。冒頭の「不完全」はこのことを指しています。詳しくは下記の記事に詳しく記載されています。この投稿を書く前にこの記事に出会っていなければ一生ハマり続けていたに違いありませんw
    JavaScriptはオブジェクトについて参照渡しだなんて、信じない@warotasan様)
    ちなみに上の記事のコメント欄にあるように、リファレンス渡しなるものがあるようで、JavaScriptはどうやらその動作で変数への代入が行われているようです。

※追記(2014/07/23)※
コメントにてご指摘いただいたように、配列の場合は array.push.apply(array, value) とすることで再現が可能になりました。ありがとうございました!
※追記ここまで※

  1. 前回の投稿で触れるのをすっかり忘れてしまっていたのですが、関数を再帰的に呼び出す場合、この部分でコメントアウトされたソースコードの通り arguments オブジェクトを使用すれば、 self.set(...) という記述をしなくても済みます。が、今回は統一感というか見た目というかなんかそんな感じで self にしています(目を逸らしながら
    嘘です。一応処理時間比べました。Chromeで1000万回ループしてみたところ、3秒ほど self の方が早く終了しました。相変わらず私が行う比較はどうでも良すぎるレベルですね…。

vs.リファレンス渡し

PHPの参照渡しの動作をどう再現するか、割と苦労しました。というか挫折しました。

  1. 第一試合は「※注1」の説明の通り大敗です…。どなたかいい方法あれば教えていただけると、それはとってもうれしいなって。あ、戻り値を指定する方法は考えました。 ただ、セッターが値を戻してくるという部分に少し違和感があったのと、使い方がFuelPHPの \Arr::set() と違ってしまうというのが自分の中でNGでした。
  2. 第二試合はオブジェクトの参照を保ったまま深い階層にアクセスして値を設定することでした。普通にFuelPHPのロジックを持ってくると参照が外れて何も起こらなくなってしまい、途方に暮れて眠くなったりもしましたが、なんとかなりました。イケてないなーとか思いつつも、夏の休日の昼に朦朧とした意識の中で無い知恵振り絞った結果がこれでした。きっともっといい方法があると思います。言語仕様変えるとk(ry

動作確認

sample

// Alias
var log = function (arg) { console.log(arg); };

var defaultArr = [0, 1, 'asdf', true, {}],
    test = [
        {key : '0',  value: 9},
        {key : '1',  value: 8},
        {key : '2',  value: 7},
        {key : '3',       value: {a : {b : {c : {d : [0, 2, 5]}}}}},
        {key : '3.a',     value: {b : {c : {d : [0, 2, 5]}}}},
        {key : '3.a.b',   value: {c : {d : [0, 2, 5]}}},
        {key : '3.a.b.c', value: {d : ['asdf', 'qwer']}},
        {key : {fuel : 'Arr::set()', 'f.u.e.l' : ['0', '2', '5']}, value : null},
        {key : null, value : [9, 8, 7, {a : {b : {c : {d : [0, 2, 5]}}}}]},
        // keyがnullでvalueが配列でない場合の動作は課題...
        {key : null, value : 'zxcv'}
    ];

for (var i = 0, len = test.length; i < len; i++) {
    // ディープコピーとしては不完全だけどとりあえず
    var array = [].concat(defaultArr);

    FuelJS.core.Arr.set(array, test[i].key, test[i].value);

    log(FuelJS.core.Arr.get(array));
}

/* 結果
[9, 1, 'asdf', true, {}]
[0, 8, 'asdf', true, {}]
[0, 1, 7, true, {}]
[0, 1, 'asdf', {a : {b : {c : {d : [0, 2, 5]}}}}, {}]
[0, 1, 'asdf', {a : {b : {c : {d : [0, 2, 5]}}}}, {}]
[0, 1, 'asdf', {a : {b : {c : {d : [0, 2, 5]}}}}, {}]
[0, 1, 'asdf', {a : {b : {c : {d : ['asdf', 'qwer']}}}}, {}]
[0, 1, 'asdf', true, {}, {fuel : 'Arr::set()'}, f : {u : {e : {l : ['0', '2', '5']}}}]
[9, 8, 7, {a : {b : {c : {d : [0, 2, 5]}}}}]
*/


/* ループしてみる
console.time('\\Arr::set()');
for (var i = 0; i < 10000000; i++) {
    array = [].concat(defaultArr);
    FuelJS.core.Arr.set(array, test[7].key, test[7].value);
}
console.timeEnd('\\Arr::set()');

// Chrome / Win7 / i7-2677M(1.80GHz) / RAM 4GB / 128GB SSD
// self Fuelset: 138348.000ms
// arguments.callee Fuelset: 141545.000ms
*/

最後に

FuelPHPのArrクラスの便利メソッドはこのくらいかなぁとか思っていたりします。飽きたとかいわない。
それはそうと、PHPerの皆様はご存知の通り、PHP自体には配列を操作する便利な関数がたくさん用意されています。で、それをJavaScriptで再現しているサイトを発見したので最後にご紹介。

JavaScript array functions - php.js

PHP5.5で追加されたarray_columnが無いのが少々残念。作るしかないのか…。
などと思う今日このごろ。

ここまでお読み下さりありがとうございました!

参考

JavaScriptはオブジェクトについて参照渡しだなんて、信じない@warotasan様)
JavaScript array functions - php.js
Arr - クラス - FuelPHP ドキュメント
PHP: リファレンス渡し - Manual

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5