Hatena::ブログ(Diary)

rikubaの日記 RSSフィード

 

2016-06-22

ホイール操作でスライドをめくるユーザースクリプト

以前作ったslideshare scrollというユーザースクリプトでQiitaのスライドにも対応しようと思ったものの、3年前のコードで無駄が多いので一から書き直した。

createWheelHandlerで前後ボタンの取得を遅延させているのは、Qiitaのスライドが非同期で組み立てられるため。

2016-06-08

GitHub上のJSコードでモジュールをリンクで辿れるようにするユーザースクリプト

OctoLinkerという、より高機能な拡張機能が既に存在した。

を作った。GitHub上でのコードリーディングが少し楽になる(じっくり読むならgit clone)。

f:id:rikuba:20160608190258p:image

Firefox(Greasemonkey)とChrome(Tempermonkey)で動作を確認した。

TypeScriptにも対応している。JavaScript系以外の言語にも対応することを考慮してコードを書いたつもりだったけど、JS・npm依存なコードが増えて結局JS専用みたいになった(langsにプロパティを増やせば拡張可能)。個人的にはJS以外のコードを読む機会が少ないので、とりあえずここまで。

node.jsのAPIとして提供されるモジュールは、node.jsのドキュメントへのリンクとした。デフォルトでは外部モジュールはnpmのページをスクレイピングしてコードリポジトリのURLを取ってくるため、開くまでに少し時間がかかる(モジュール名から直接コードリポジトリのURLを取得できるAPIが公式にあればいいのになぁ)。

デフォルトでは内部モジュールは通常の遷移、外部モジュールは新規タブに開くようにしてある。linkToModule関数の引数のデフォルト値を変えることでこの挙動を変更できる。

[最初に空配列を用意し、必要なデータをpushしていって、最後にその配列を返す]といった関数、もう全部Generator関数で書きたくなる(絶対遅くなる)。

2014-05-31

JavaScriptでgetterとsetterのどちらか一方のみをオーバーライドする

JavaScript でオブジェクトに accessor property を定義したとき、継承先でそのプロパティの getter, setter のどちらか一方のみを override するのは一筋縄にはいかない。

例として、長方形を表す Rectangle と、正方形を表す Square を定義することを考える。SquareRectangle を継承する。

function Rectangle(width, height) {
    this.width = width;
    this.height = height;
}

Rectangle.prototype = {
    get width() { return this._width; },
    set width(value) { this._width = +value; },

    get height() { return this._height; },
    set height(value) { this._height = +value; },

    get area() { return this.width * this.height; }
};

function Square(sideLength) {
    this.width = sideLength;
}

Square.prototype = {
    __proto__: Rectangle.prototype,

    // widthのsetterのみoverrideしたつもり
    set width(value) { this._width = this._height = +value; },

    // heightのsetterのみoverrideしたつもり
    set height(value) { this._width = this._height = +value; }
};

//
console.log('Rectangle --------');
log(new Rectangle(10, 20));

console.log('Square -----------');
log(new Square(10));

function log(rect) {
    console.log('width  : ' + rect.width);
    console.log('height : ' + rect.height);
    console.log('area   : ' + rect.area);
}

Rectangle.prototype では、width, height は常に数値となるように setter で渡された値を型変換している。Square.prototype ではそれに加えて、widthheight が常に一致するように setter を上書きしている……?

コンソールへの出力は以下の通り。

"Rectangle --------"
"width  : 10"
"height : 20"
"area   : 200"
"Square -----------"
"width  : undefined"
"height : undefined"
"area   : NaN"

……あれ? Squarewidth, height を取得できていない。そのせいで areaundefined * undefined となり、NaN を返している。

accessor property は getter, setter の2つで1つだ。Square.prototype に setter のみを定義しているが getter は定義していない。これにより getter はないものとして扱われ、継承元([[Prototype]])に定義されている getter は隠蔽される。

コードを簡潔にするためにオブジェクト初期化子で accessor property を定義しているが、Object.definePropertyObject.prototype.__defineSetter__ を使おうとも結果は同じである。


accessor property ではなく accessor method として定義したとしたらどうだろう。

function Rectangle(width, height) {
    this.setWidth(width || 0);
    this.setHeight(height || 0);
}

Rectangle.prototype = {
    getWidth: function () { return this._width; },
    setWidht: function (value) { this._width = +value; },

    getHeight: function () { return this._height; },
    setHeight: function (value) { this._height = +value; },

    getArea: function () { return this.getWidth() * this.getHeight(); }
};

function Square(sideLength) {
    this.setWidth(sideLength || 0);
}

Square.prototype = {
    __proto__: Rectangle.prototype,

    setWidth: function (value) { this._width = this._height = +value; },
    setHeight: function (value) { this._width = this._height = +value; }
};

//
console.log('Rectangle --------');
log(new Rectangle(10, 20));

console.log('Square -----------');
log(new Square(10));

function log(rect) {
    console.log('width  : ' + rect.getWidth());
    console.log('height : ' + rect.getHeight());
    console.log('area   : ' + rect.getArea());
}

コンソールへの出力は以下の通り。

"Rectangle --------"
"width  : 10"
"height : 20"
"area   : 200"
"Square -----------"
"width  : 10"
"height : 10"
"area   : 100"

getter と setter が別のプロパティとして定義されるため、単純に片方を上書きすればいい。

メソッド呼び出しになる点に目をつむれば、これで十分だ。実際、ES3 では accessor property を定義する方法がなかったため、こうするしかなかった。


accessor property の使いやすさと、accessor method の上書きのしやすさを両立したい。そこで、実際の処理は accessor method に定義し、accessor property はその accessor method に処理を委譲することにする。

function Rectangle(width, height) {
    this.width = width;
    this.height = height;
}

Rectangle.prototype = {
    get width() { return this.get_width(); },
    get_width: function () { return this._width; },

    set width(value) { this.set_width(value); },
    set_width: function (value) { this._width = +value; },

    get height() { return this.get_height(); },
    get_height: function () { return this._height; },

    set height(value) { this.set_height(value); },
    set_height: function (value) { this._height = +value; },

    get area() { return this.width * this.height; }
};

function Square(sideLength) {
    this.width = sideLength;
}

Square.prototype = {
    __proto__: Rectangle.prototype,

    // setter method を上書きする
    set_width: function (value) { this._width = this._height = +value; },

    // setter method を上書きする
    set_height: function (value) { this._width = this._height = +value; }
};

//
console.log('Rectangle --------');
log(new Rectangle(10, 20));

console.log('Square -----------');
log(new Square(10));

function log(rect) {
    console.log('width  : ' + rect.width);
    console.log('height : ' + rect.height);
    console.log('area   : ' + rect.area);
}

コンソールへの出力は以下の通り。

"Rectangle --------"
"width  : 10"
"height : 20"
"area   : 200"
"Square -----------"
"width  : 10"
"height : 10"
"area   : 100"

継承元の Rectangle.prototype の定義が少し複雑になるが、出力は問題ない。


毎回委譲するだけの accessor property を書くのは面倒なので、ヘルパー関数を用意する。ついでにまだ標準化されていない __proto__ を使うのをやめ、ES5 準拠のコードに書き直す。

(function (_) {
    var Rectangle = this.Rectangle = new RectanglePrototype().constructor;

    function RectanglePrototype() {
        _.constructor(this, function Rectangle(width, height) {
            this.width = width;
            this.height = height;
        });

        _.accessor(this, 'width', {
            get: function () { return this._width; },
            set: function (value) { this._width = +value; }
        });

        _.accessor(this, 'height', {
            get: function () { return this._height; },
            set: function (value) { this._height = value; }
        });

        _.accessor(this, 'area', {
            get: function () { return this.width * this.height; }
        });
    };

    SquarePrototype.prototype = Rectangle.prototype;
    var Square = this.Square = new SquarePrototype().constructor;

    function SquarePrototype() {
        _.constructor(this, function Square(sideLength) {
            this.width = sideLength;
        });

        _.accessor(this, 'width', { set: setLength });
        _.accessor(this, 'height', { set: setLength });

        function setLength(value) {
            this._width = this._height = value;
        }
    }
}.call(this, new function Underbar() {
    this.constructor = function (object, constr) {
        constr.prototype = object;
        Object.defineProperty(object, 'constructor', {
            value: constr,
            configurable: true,
            enumerable: false,
            writable: true
        });
    };

    this.accessor = function (object, name, accessor) {
        var getter = 'get_' + name;
        var setter = 'set_' + name;

        if (accessor.get) object[getter] = accessor.get;
        if (accessor.set) object[setter] = accessor.set;

        Object.defineProperty(object, name, {
            get: function () { return this[getter](); },
            set: function (value) { this[setter](value); },
            configurable: true,
            enumerable: true
        });
    };
}));

//
console.log('Rectangle --------');
log(new Rectangle(10, 20));

console.log('Square -----------');
log(new Square(10));

function log(rect) {
    console.log('width  : ' + rect.width);
    console.log('height : ' + rect.height);
    console.log('area   : ' + rect.area);
}

コンソールへの出力は以下の通り。

"Rectangle --------"
"width  : 10"
"height : 20"
"area   : 200"
"Square -----------"
"width  : 10"
"height : 10"
"area   : 100"

うまくいった。


aObject.width が存在せず、aObject.get_width メソッドが定義されている場合、aObject.width としたときにはその get_width メソッドが呼び出される、みたいなシンプルな仕組みがあればなあと思う。ES6 の Proxy を使えばできそう。

2014-04-28

switch文によるコールバック地獄回避

url.txtに書かれているURLを取得し、そのURLのリソースの内容を取得し、1秒後にその内容をalertするという例(のための例)を考える。

まずは普通に書いてみる。

var client = new XMLHttpRequest;
client.open('GET', 'url.txt');
client.onload = function (event) {
    var url = client.response;
    client.open('GET', url);
    client.onload = function (event) {
        setTimeout(function () {
            alert(client.response);
        }, 1000);
    };
    client.send(null);
};
client.send(null);

次にswitch文を使って書いてみる。

var step = 0;
var client;
(function callback(arg) {
    switch (step++) {
    case 0: client = new XMLHttpRequest;
            client.open('GET', 'url.txt');
            client.onload = callback;
            client.send(null);
    break;  
    case 1: var url = client.response;
            client.open('GET', url);
            client.onload = callback;
            client.send(null);
    break;  
    case 2: setTimeout(callback, 1000);
    break;
    case 3: alert(client.response);
    }
}());

おまじないのswitch文によってベースとなるインデントが深くなるものの、メインとなるコードは縦に揃う。コードの流れもopensendopensendsetTimeoutとなり直観的。

……Duff's deviceのコードを見て思いついただけ。

2014-04-09

JavaScriptでカスラムエラーをどう作るか

はじめに:IE 11、Firefox 28、Chrome 34で試している

JavaScriptのErrorオブジェクトにはstackプロパティにコールスタックを表す文字列がセットされる。これは現行のECMAScript仕様では規定されておらず、実装によって違いがある。

function foo() { bar(); }
function bar() { baz(); }
function baz() { throw new Error('X'); }

(function main() {
    try {
        foo();
    } catch (error) {
        console.log(error.stack);
    }
})();

Chromeでのコンソール出力の例。

Error: X
    at baz (http://localhost/CustomError.html:8:24)
    at bar (http://localhost/CustomError.html:7:18)
    at foo (http://localhost/CustomError.html:6:18)
    at main (http://localhost/CustomError.html:12:9)
    at http://localhost/CustomError.html:16:3

このように、ネイティブに用意されているエラーオブジェクト用のコンストラクタ(Error, TypeError, RangeErrorなど)を使う場合、stackプロパティはどの実装でもセットされる。ただし、セットされるタイミングは少し異なり、Firefox・Chromeではオブジェクト作成時にセットされ、IEではthrowされるときにセットされる。

var e = new Error('X');
console.log(e.stack); // IEではまだセットされていない
try {
    throw e;
} catch (e) {
    console.log(e.stack); // セットされる
}

ネイティブに用意されているコンストラクタでは物足りない場合、Errorを継承した独自のエラーコンストラクタを作ることを思いつく。ところが、これだとIEでしかstackプロパティがセットされない。

function TimeoutError(message) {
    Error.call(this, message);
}
TimeoutError.prototype = Object.create(Error.prototype, {
    constructor: Object.getOwnPropertyDescriptor(TimeoutError.prototype, 'constructor'),
    name: { value: 'TimeoutError', configurable: true, enumerable: true, writable: true }
});

var e = new TimeoutError('session timeout');
try {
    throw e;
} catch (e) {
    console.log(e.stack);
    // IEではコールスタックを表す文字列
    // Firefoxでは空文字列
    // Chromeではundefined
}

IEではthrowされるオブジェクトがError.prototypeを継承していればstackプロパティがセットされるようだ。Firefox・Chromeはネイティブコンストラクタでのみstackプロパティがセットされるように見える。


ではどうすればいいだろうか。コンストラクタを用意せず、継承もせず、こんなので十分だと思う。

var createError = (function () {
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    return function createError(name, message, properties) {
        var e = new Error(message);
        e.name = name;
        if (properties)
            for (var key in properties)
                if (hasOwnProperty.call(properties, key))
                    e[key] = properties[key];
        try {
            throw e; // for IE
        } catch (e) {
            return e;
        }
    };
})();

var timeoutError = createError('TimeoutError', 'session timeout');
var fetchError = createError('FetchError', '404 Not Found', { statusCode: 404 });

エラーオブジェクトを扱うときは、instanceofではなくnameプロパティからその種類を判別すればよい。ネイティブのエラーオブジェクトも同じように判別できる(ECMAScript仕様にて規定)。

new TypeError().name === 'TypeError';

fetch(url, { timeout: 10 * 1000 }).then(function (text) {
    // do something
}, function (error) {
    switch (error.name) {
    
    case 'TimeoutError':
        alert('タイムアウトしました');
        break;
    
    case 'FetchError':
        if (error.statusCode === 404) {
            alert('リソースが見つかりません');
            break;
        }
        alert('リソースを取得できませんでした(' + error.statusCode + ')');
    }
});

createError関数の分、コールスタックを一行消費してしまうのが惜しい。IE・Chromeでは、stackプロパティのコールスタックは10行までのようだ。

(function () {
  function a() { b(); }
  function b() { c(); }
  function c() { d(); }
  function d() { e(); }
  function e() { f(); }
  function f() { g(); }
  function g() { h(); }
  function h() { i(); }
  function i() { j(); }
  function j() { k(); }
  function k() { l(); }
  function l() { throw new Error('HELLO?'); }

  try {
    a();
  } catch (e) {
    console.log(e.stack);
  }
})();

Chromeでのコンソール出力の例。

Error: HELLO?
    at l (http://localhost/CustomError.html:30:24)
    at k (http://localhost/CustomError.html:29:18)
    at j (http://localhost/CustomError.html:28:18)
    at i (http://localhost/CustomError.html:27:18)
    at h (http://localhost/CustomError.html:26:18)
    at g (http://localhost/CustomError.html:25:18)
    at f (http://localhost/CustomError.html:24:18)
    at e (http://localhost/CustomError.html:23:18)
    at d (http://localhost/CustomError.html:22:18)
    at c (http://localhost/CustomError.html:21:18)

ES6のObject.assignがあれば、それで十分かもしれない。

throw Object.assign(new Error('404 Not Found'), { name: 'FetchError', statusCode: 404 });
 
プロフィール

rikuba

JavaScriptのことばっかり