記事

javascript - からnewを取り除いてみる

2/2
非標準の__proto__を持つ処理系であれば、ほとんど再現できるのですが…

function newer(cf, args){
    var seed = {};
    seed.__proto__ = cf.prototype;
    return cf.apply(seed, args) || seed;
};

var o,
    Point = function Point(x, y){
        this.x = x;
        this.y = y;
    };
Point.prototype.fromOrigin = function(){
    return Math.sqrt(this.x*this.x + this.y*this.y)
};
try{
    o = newer(Point, [3, 4]);
    p( JSON.stringify(o) );     /* 今度はうまく行ったぞ */
    p( o instanceof Point );    /* よし */
    p( o.fromOrigin() );        /* よし */
    /* でもこれがだめ */
    o = newer(Date, [2011,11,25,12,34,56]);
    p( o );         /* 一見うまく行ってるけど */
    p( typeof o );  /* new なしで Date(2011,11,25,12,34,56) した時と同じ */
}catch(e){
    p(e);
}



見ての通り、newのありなしで挙動を変えてしまうコンストラクター関数(とあえてしつこくそう呼ぶ)の前には無力。それもよりによって標準のDateが!

結局のところ、newなしでnewの挙動を再現できない以上、newを完全に消してしまうことはできなさそうです。

消せないなら隠せばいいじゃない!



というわけで作ったのがこちら。ES5推奨ですが、ES5でなくても動きます。

(function(global){
"use strict";
var news = [],
    make = function(){
        var l = arguments.length, i, _;
        if (!news[l]){
            for (i = 0, _ = []; i < l; i++) _.push('_[' + i + ']');
            news[l] = new Function('$','_',
                'return new $(' + _.join(',') + ')'
            );
        }
        return news[l](this, arguments);
    },
    ignoreProps = {arguments:1,caller:1,callee:1,length:1,name:1},
    maker = function(importProps){
        var that = this,
            ret  = function(){ return make.apply(that, arguments) },
            i;
        ret.origin    = that;
        ret.prototype = that.prototype;
        if (importProps){
            /* if (pseudo)array */
            if (typeof(importProps) === 'object' 
                && typeof(importProps.length) === 'number'){ 
                for (i = 0; i < importProps.length; i++){
                    if (!ret[importProps[i]] && that[importProps[i]]){
                        ret[importProps[i]] = that[importProps[i]];
                    }
                }
            }else if (Object.getOwnPropertyNames){
                Object.getOwnPropertyNames(that).forEach(function(k){
                    if (k in ignoreProps) return;
                    if (!ret[k] && that[k]) ret[k] = that[k];
                });
            }
        };
        return ret;
    };
(function(namespace, method){
    for (var name in method){
        if (Object.defineProperty){
            Object.defineProperty(namespace, name,{
                value:method[name],
                configurable:true,enumerable:false,writable:true
            });
        }else{
            namespace[name] = method[name]
        }
    }
})(Function.prototype, {make:make,maker:maker});
})(this);

Demo



var d, D, toString = Object.prototype.toString;
D = Date.maker();   /* static methods not imported */
/* No new required! makes Pythonistas happy */
d = D(1234567890e3);
p(d);
p(toString.call(d));
/* number of arguments does not matter */
d = D(2011,12-1,25,12,34,56);
p(d);
p(toString.call(d));
/* D.new() is forbidden by JS but you can D.make()
 * perl mongers and rubyists should be happier, too!
 */
d = D.make(1234567890e3);
p(d);
p(toString.call(d));
/* new D also works, keeping old-school JavaScripters happy */
d = new D(1234567890e3);
p(d);
p(toString.call(d));
/* since we have not imported static methods, this fails */
try{
    p(D.now());
}catch(e){
    p(e);
}
/* but you can always falls back to the origin */
p(D.origin.now());
D = Date.maker(Object.defineProperty 
                ? true  /* imports every static methods in ES5 */
                : ["parse", "UTC", "now"]   /* or manually */
    );
p(D.now());



というわけで、newの隠蔽工作には成功したのですが、無料というわけにはいきません。だいたい3倍から10倍のペナルティがかかっています。これに対し、this instanceof Constructorのテクニックを使った場合のペナルティは無視できる程度です。

Benchmark



var times = function(l, f){
        for(var i = 0; i < l; i++) f(i);
    },
    timeit = function(l, f){
        var timer = Date.now();
        times(l, f);
        return Date.now() - timer;
    },
    Point = function Point(x, y){
        this.x = x;
        this.y = y;
    },
    P = Point.maker(),
    SafePoint = function Point(x, y){
        if (this instanceof Point){
            this.x = x;
            this.y = y;
        }else{
            return new Point(x, y);
        }
    };

Point.prototype.fromOrigin = SafePoint.prototype.fromOrigin = function(){
    return Math.sqrt(this.x*this.x + this.y*this.y)
};
/* all 5, give me five! */
p( (new Point(3,4)).fromOrigin() );
p( SafePoint(3,4).fromOrigin() );
p( P(3,4).fromOrigin() );
p( P.make(3,4).fromOrigin() );
p( (new P(3,4)).fromOrigin() );
p( 'new Point:     ' + timeit(1e5, function(){ new Point(3,4) }) );
p( 'new SafePoint: ' + timeit(1e5, function(){ new SafePoint(3,4) }) );
p( 'SafePoint():   ' + timeit(1e5, function(){ SafePoint(3,4) }) );
p( 'P():           ' + timeit(1e5, function(){ P(3,4) }) );
p( 'P.make():      ' + timeit(1e5, function(){ P.make(3,4) }) );
p( 'new P():       ' + timeit(1e5, function(){ new P(3,4) }) );
if (Object.create) p( 'Object.create: ' + timeit(1e5, function(){  
    var o = Object.create(Point);
    o.x = 3; o.y = 4;
}) );



というわけで



hope((new Year).isHappy());
Dan the JavaScripter

トピックス

  1. 一覧を見る

ランキング

  1. 1

    BLOGOSサービス終了のお知らせ

    BLOGOS編集部

    03月31日 16:00

  2. 2

    なぜ日本からは韓国の姿が理解しにくいのか 識者が語る日韓関係の行方

    島村優

    03月31日 15:41

  3. 3

    「いまの正義」だけが語られるネット社会とウェブ言論の未来

    御田寺圭

    03月31日 10:09

  4. 4

    カーオーディオの文化史 〜ドライブミュージックを支えた、技術の結晶たち〜

    速水健朗

    03月30日 16:30

  5. 5

    BLOGOS執筆を通じて垣間見たリーマンショック後10年の企業経営

    大関暁夫

    03月31日 08:27

ログイン

ログインするアカウントをお選びください。
以下のいずれかのアカウントでBLOGOSにログインすることができます。

コメントを書き込むには FacebookID、TwitterID のいずれかで認証を行う必要があります。

※livedoorIDでログインした場合、ご利用できるのはフォロー機能、マイページ機能、支持するボタンのみとなります。