Slide 1

Slide 1 text

Prototype.js をコードリーディングして知る JavaScript の黒魔術

Slide 2

Slide 2 text

⾃⼰紹介 よんじゅ Arch Linux と Emacs と Z-shell が好きなフリーランスエンジニア 最近 Spacemacs から Doom Emacs に乗り換えた 9 歳のとき (2002年) に JavaScript を初めて触る Twitter: @sei40kr_sub

Slide 3

Slide 3 text

Prototype.js Prototype JavaScript framework: a foundation for ambitious web user interfaces http://prototypejs.org JavaScript のフルスタックライブラリ (Yahoo UI や Closure Library に網羅範囲は 劣るが...) 最終更新⽇は 2015/09/22

Slide 4

Slide 4 text

jQuery との違い Prototype.js jQuery 実装スタイル OOP & Prototype 拡張 jQuery 以下に Function を⽣やす class 実装 Alex Arnell's Class Inheritance ベース なし Element 拡張 Prototype 拡張 要素をもつ配列をラップしたものに jQuery.fn をマージ ID から要素を取 得 $ なし セレクターから 要素を取得 $$ $

Slide 5

Slide 5 text

Prototype.js の他の特徴 メソッド名などを⾒る限り、かなり Ruby に影響を受けている 今回紹介する Class.create も、コンストラクタを定義するプロパティが initialize

Slide 6

Slide 6 text

Let's do Code Reading! といきたいところですが、まず基本をおさらい Class.create をリーディングしたいので JavaScript の Prototype チェーンにつ いておさらいしておきます

Slide 7

Slide 7 text

prototype コンストラクタのプロパティ そのコンストラクタによって⽣成されたインスタンスに対して存在しないプロパ ティを参照したとき、代わりに prototype に定義されているプロパティが参照 される function Class() {} Class.prototype = { foo: 'foo' }; var instance = new Class(); instance.foo; // 'foo' instance.foo = 'bar'; instance.foo; // 'bar' Class.prototype.foo; // 'foo'

Slide 8

Slide 8 text

prototype.constructor 何も指定しなければコンストラクタ⾃身が代⼊されている 上書きすることもできる Class.prototype.constructor = Object; ただし instanceof は欺けない instance instanceof Class; // true

Slide 9

Slide 9 text

Object.getInstanceOf , __proto__ 途中からインスタンス側からも Object.getPrototypeOf で参照できるようにな った ちなみに __proto__ は⾮推奨だが、このスライドでは説明の便宜上使う。 function Class() {} var instance = new Class(); Object.getPrototypeOf(instance) === Class.prototype; // true instance.__proto__ === Class.prototype; // true

Slide 10

Slide 10 text

すべての道は Object.prototype に通ず Class インスタンス instance のプロパティ prop を参照すると次の順に参照 される instance.prop instance.__proto__.prop instance.__proto__.__proto__.prop prototype ⾃体はほぼ全て Object なので、最終的には Object.prototype に たどり着く。 ただし Object.prototype.__proto__ は null であるため、循環参照は⾏われ ない。 function Class() {} Object.prototype = { prop: 'foo' }; var instance = new Class(); instance.prop; // 'foo'

Slide 11

Slide 11 text

すべての道は Object.prototype に通ず この性質を利⽤しクラス継承の仕組みを実装することができる function ParentClass() {} function ChildClass() {} // prototype の prototype を変更していることに注意! Object.setPrototypeOf(ChildClass.prototype, ParentClass.prototype);

Slide 12

Slide 12 text

prototype を使った⼩技 Function 内部の arguments は Array-like なオブジェクトだが Array ではないた め Array.prototype が使えない __proto__ を無理やり Array.prototype にしてしまうことで使えるメソッドも ある arguments.__proto__ = Array.prototype; arguments.shift(); その他に slice を無理やり使って Array に変換するテクニックもある var args = Array.prototype.slice.call(arguments); args.shift();

Slide 13

Slide 13 text

Prototype 拡張の弊害 var array = [0, 1, 2]; Array.prototype.foo = 'foo'; for (var key in array) { key; // <- 0, 1, 2, 'foo' }

Slide 14

Slide 14 text

Prototype 拡張の弊害 .hasOwnProperty を使おう! (というか Array を for-in で回すのをやめよう) for (var key in array) { if (array.hasOwnProperty(key)) { key; // <- 0, 1, 2 } }

Slide 15

Slide 15 text

DONT_ENUM 属性 よくよく考えると built-in のメソッドは for-in しても列挙されない built-in のプロパティには for-in でも列挙されない属性 DONT_ENUM が付与されて いる V8 JavaScript Engine の src/property-details.h enum PropertyAttributes { DONT_ENUM = ::v8::DontEnum, };

Slide 16

Slide 16 text

コードリーディング

Slide 17

Slide 17 text

Class.create の仕様 クラスメソッドを定義したオブジェクトを引数として渡す initialize はコンストラクタとして扱われる var Animal = Class.create({ initialize: function(name, sound) { this.name = name; this.sound = sound; }, speak: function() { alert(this.name + " says: " + this.sound + "!"); } });

Slide 18

Slide 18 text

Class.create の仕様 第⼀引数に別のクラスを渡すことによってそのクラスを親とする⼦クラスを⽣成 できる メソッドの第⼀引数の名前を $super にすると、それをスーパーメソッドとして 扱える。 var Snake = Class.create(Animal, { initialize: function($super, name) { $super(name, 'hissssssssss'); } }); var ringneck = new Snake("Ringneck"); ringneck.speak(); //-> alerts "Ringneck says: hissssssssss!"

Slide 19

Slide 19 text

Class.create 1. 第⼀引数が Function であればそれを親クラスとみなす。他は定義するメソッドと みなす。 var parent = null, properties = $A(arguments); if (Object.isFunction(properties[0])) parent = properties.shift(); 2. コンストラクタでは単純に initialize を呼び出す function klass() { this.initialize.apply(this, arguments); }

Slide 20

Slide 20 text

Class.create 1. コンストラクタに対してメソッドを追加する。これにより addMethods() が追加 される。 Object.extend(klass, Class.Methods); 2. コンストラクタにクラスのメタデータを格納 klass.superclass = parent; klass.subclasses = [];

Slide 21

Slide 21 text

Class.create prototype の prototype を親クラスの prototype にする if (parent) { subclass.prototype = parent.prototype; klass.prototype = new subclass; } つまり subclass は parent.prototype を prototype にもつオブジェクトを作るため の使い回しコンストラクタ。 ブラウザでサポートされていれば、次のいずれかのように書ける。 klass.prototype = Object.create(parent.prototype); Object.setPrototypeOf(klass.prototype, parent.prototype) klass.prototype.__proto__ = parent.prototype;

Slide 22

Slide 22 text

Class.create クラスメソッドを追加する for (var i = 0, length = properties.length; i < length; i++) klass.addMethods(properties[i]);

Slide 23

Slide 23 text

Class.Methods.addMethods 既に DONT_ENUM 属性が付与されている built-in プロパティと同名のプロパティを定義 したときに for-in で列挙されないバグに対する workaround var IS_DONTENUM_BUGGY = (function(){ for (var p in { toString: 1 }) { if (p === 'toString') return false; } return true; })(); if (IS_DONTENUM_BUGGY) { if (source.toString != Object.prototype.toString) properties.push("toString"); if (source.valueOf != Object.prototype.valueOf) properties.push("valueOf"); }

Slide 24

Slide 24 text

Class.Methods.addMethods ancestor && Object.isFunction(value) && value.argumentNames()[0] == "$super"

Slide 25

Slide 25 text

Function.prototype.toString Function を toString() するとソースコードが⽂字列で返る function f(x) { x; } f.toString() // 'function f(x) { x; }' ただし built-in な関数や Command Line API などの関数の中身は⾒れない Object.toString(); // 'function() { [native code] }' $$.toString(); // 'function() { [Command Line API] }'

Slide 26

Slide 26 text

Function.prototype.argumentNames これを利⽤して引数を格納する変数名を正規表現で取得する (マジか) function argumentNames() { var names = this.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1] .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '') .replace(/\s+/g, '').split(','); return names.length == 1 && !names[0] ? [] : names; }

Slide 27

Slide 27 text

Class.Methods.addMethods Function.prototype.wrap は引数の先頭から分割代⼊を⾏うための独⾃メソッ ド (ここでは第⼀引数に $super を分割代⼊) 親クラスのメソッドをラップした関数を即時関数で返しているのはなぜだろう var method = value; value = (function(m) { return function() { return ancestor[m].apply(this, arguments); }; })(property).wrap(method);

Slide 28

Slide 28 text

例: 変数のスコープ var f = new Array(3); for (var i = 0; i < 3; i++) { f[i] = function() { return i; }; } f[0](); // 0 ではなく 2 が返る 上記のスクリプトでは、変数 i はループを実⾏したスコープを escape した後も ⽣存している ただし値はループ終了時の値になっている

Slide 29

Slide 29 text

例: 変数のスコープ 各ループがスコープを共有しているのが問題 ならば各ループの中で新しいスコープを作ってその時点での値をコピーすればい い var f = new Array(3); for (var i = 0; i < 3; i++) { f[i] = (function(j) { return function() { return j; }; })(j); } f[0](); // 0

Slide 30

Slide 30 text

Class.Methods.addMethods 最後に toString() したときにラップする前の関数のソースコードを返すようにする (細かい...) value.valueOf = (function(method) { return function() { return method.valueOf.call(method); }; })(method); value.toString = (function(method) { return function() { return method.toString.call(method); }; })(method);

Slide 31

Slide 31 text

他に読んでほしい箇所 Element Element.extend Array.prototype.each

Slide 32

Slide 32 text

まとめ JavaScript なにもわかりません!

Slide 33

Slide 33 text

おしまい