若干functionalなJavaScript, CoffeeScriptの個人的に使う小技集

  • 79
    Like
  • 13
    Comment
More than 1 year has passed since last update.

だいたいJavaScript、CoffeeScript両方のコードサンプルを書いてあるけど、横着してそうでないのもチラホラ。

letに相当する何か / スコープを守ろう

まずは簡単なところから。

JavaScriptは関数スコープ

forif等でスコープが作られる文化圏から来た人は注意。
思わぬところで思わぬ変数にアクセスできる。

var sum = 0;
for (var i = 0; i < 10; i++) {
  sum += i;
}
console.log(i); // -> 10
// 変数を局所化
var sum = (function () {
  var s = 0;
  for (var i = 0; i < 10; i++) {
    s += i;
  }
  return s;
}());
console.log(i); // -> undefined

カプセル化

prototype + new でOOPっぽく書きたいニーズもあるようだが、それだと全てのプロパティがpublicになるので (クラスのないJavaScriptで public という言い方をしていいのか悩むところだが) privateを実現するにはclosureを使う。

var obj = (function () {
  var invisibleVar = 1;
  return {
    get: function () {return invisibleVar;},
    add: function (num) {invisibleVar += num;}
  };
}());

カプセル化 / CoffeeScript

とくに関数リテラルのすっきり感は異常。

obj = do ->
  invisibleVar = 1;
  {
    get: -> invisibleVar
    add: (num) -> invisibleVar += num
  }

CoffeeScriptにはクラスがあるが、privateなプロパティは作れない。

ファクトリにしてもいい

factory = ->
  invisibleVar = 1;
  {
    get: -> invisibleVar
    add: (num) -> invisibleVar += num
  }
o1 = factory()
o2 = factory()

JavaScriptのサンプルは割愛。

new (prototype) vs closure

ぶっちゃけnew (prototype) の方が高速でメモリ使用量も少ない。

http://macwright.org/2013/01/22/javascript-module-pattern-memory-and-closures.html

  • new (prototype) のメリット
    • 速度とメモリ消費量の点で優れる
    • Object.prototype.isPrototypeOfを使って型の判定みたいなことができる
  • closureのメリット
    • カプセル化ができる
    • new/thisのトリッキーな挙動から逃れられる

CoffeeScriptのswitchで使えるprognっぽい何か

※このセクションは不要だと分かりました

thenの後に複数の式を書きたい、という場合。

thenを書かなければいいだけ、とコメントでご指摘頂いて判明したのですが、当初は以下のように記述していました。
一応誤情報として残しておきます。

score = 76
grade = switch
  when score < 60 then 'F'
  when score < 70 then 'D'
  when score < 80 then 'C'
  when score < 90 then do ->
    r = Math.random()
    score + r
  else 'A'

無名関数でラップしないと構文エラー。

ところがこれで済むようです。

score = 76
grade = switch
  when score < 60 then 'F'
  when score < 70 then 'D'
  when score < 80 then 'C'
  when score < 90
    r = Math.random()
    score + r
  else 'A'

部分適用

蛇足だけどカリー化とは厳密には異なる。

// JavaScript
var fun1 = function (a, b) {
  return a + b;
};

var fun2 = fun1.bind(null, 1);
console.log(fun2(2)); // -> 3
# CoffeeScript
fun1 = (a, b) ->
   a + b;

fun2 = fun1.bind null, 1
console.log(fun2 2) # -> 3

オブジェクトAのメソッドを, オブジェクトBが俺のモノだと実行する

call, applyの違いはメソッドへの引数の渡し方

// JavaScript
a.prototype.method.call(b, arg1, arg2);
a.prototype.method.apply(b, [arg1, arg2]);
# CoffeeScript
a::method.call b, arg1, arg2
a::method.apply b, [arg1, arg2]

配列のようで配列でない何かを配列にする

[]で要素にアクセスできてlengthプロパティを持つものならいけるっぽい

Function.prototype.applyの応用例としてもどうぞ。

// JavaScript
function f () {
  var argArr = Array.prototype.slice.call(arguments);
}
var strArr = Array.prototype.slice.call('hogehoge');
# CoffeeScript
f = ->
  argArr = Array::slice.call arguments
strArr = Array::slice.call 'hogehoge'

文字列の配列化なら正直こっちの方が簡潔

strArr = 'hogehoge'.split ''

配列操作のコールバックで忘れがちな引数

forEachmapsome等の第二引数、第三引数は、indexと元の配列である。
reduceならば第三、第四引数がこれに該当。

// JavaScript
[1, 1, 2, 3, 5].reduce(function (a, b, index, array) {
  return a + b;
}, 'initial value'); // reduce系なら初期値をここでセット
# CoffeeScript
[1, 1, 2, 3, 5].reduce(((a, b, index, array) ->
  a + b
), 'initial value')

# CoffeeScriptならリスト内包表記も使えるから合わせ技が便利よね

元の配列の渡し方はどちらがよい?

その1、レキシカルスコープで渡す

array1.map(function (e, i) {
  return array1[i + 1] + e;
});

その2、コールバックの引数を使う

array1.map(function (e, i, arr) {
  return arr[i + 1] + e;
});

ベンチマークによる速度比較

コールバックの引数を使った方が速い。恐らく変数名の解決のコストがないため。

以下のコードをChromium 30.0で実行したところ、36.4msec vs 218.8msecでコールバックの引数を使った方が高速だった。

function elapse (f) {
   var i,
       times = 10,
       start = (new Date()).getTime();
    for (i = 0; i < times; i++) {
        f();
    }
   console.log(((new Date()).getTime() - start) / times + 'msec');
}

var a = [], i, start;
for (var i = 0; i < 1000*1000; i++) {
    a.push(i);
}

elapse(function () {
    a.forEach(function(e, i, arr) {
        var l = arr.length;
    });
});

elapse(function () {
    a.forEach(function(e, i, arr) {
        var l = a.length;
    });
});

解説

JavaScriptでは、使用された変数の名前が現在実行している関数のスコープにない場合、
その1つ外側の関数のスコープに同名の変数がないか探しにいき、そこでもなければさらに外側を探し…
と変数の解決を行おうとする (グローバルスコープにもなければundefinedとなる) 。

したがって、より近いスコープに変数があるほど効率はよくなる。

なお上記のベンチマークの例だと、配列の変数が1つどころが2つ外側のスコープにあるので、
あまり厳密な測定ではないが、そこは気にしないということで。

関数を否定した関数を返す関数

あんまり使わないかな?

DOMイベントで、何かがOnの時とOffの時にそれぞれコールバックを設定する時に、
Offの時はOnの時の否定を取るような場合に、こういうのを書くとDRYになるかと (日本語が訳わからん) 。

// JavaScript
var fun = function (a, b) {
    return a < b;
};

var negate = function (f) {
  return function () {
    var args = Array.prototype.slice.call(arguments);
    return !f.apply(this, args);
  };
};

var nFun = negate(fun);

console.log(fun(1, 2)); // -> true
console.log(nFun(1, 2)); // -> false
# CoffeeScript
fun = (a, b) -> a < b

negate = (f) ->
  ->
    args = Array::slice.call(arguments);
    !f.apply(this, args);

nFun = negate fun

mix-in

これはまだ試験運用中。

var mixin = function (obj, module) {
  var key, property;
  for (key in module) {
    property = module[key];
    if (module.hasOwnProperty(key) && typeof property === 'function') {
      obj[key] = function () {
        return module[key].apply(obj, Array.prototype.slice(arguments));
      };
    }
  }
  return obj;
};

mixin(myObject, myModule);

CoffeeScriptだとclass等というものが使えるらしい

delegateと見せかけた何か

myModule =
  f: -> 'OK'

mixin = (clazz, module) ->
  moduleName = "__module__#{(new Date()).getTime()}"
  clazz.prototype[moduleName] = module
  for name, property of module when typeof property is 'function'
    # IIFE (後述)
    do (_name = name, fn = property) ->
      clazz.prototype[_name] = ->
        fn.apply(this[moduleName], Array::slice.call(arguments))

class Hoge
  constructor: ->

mixin Hoge, myModule

IIFE (即時関数) とはなんぞや

匿名関数を作ってそのまま実行しているものを指す。冒頭でスコープを作るのに使ったアレ。ここではループの変数を固定化するのに用いている。

# 悪い例
myModule =
  foo: -> 'FOO'
  bar: -> 'BAR'

mixin = (clazz, module) ->
  moduleName = "__module__#{(new Date()).getTime()}"
  clazz.prototype[moduleName] = module
  for name, property of module when typeof property is 'function'
    clazz.prototype[name] = ->
      property.apply(this[moduleName], Array::slice.call(arguments))

mixin(Hoge, myModule)

console.log Hoge::foo()

関数fooをコールするとBARって返ってきやがります。
これは、JavaScriptの変数が関数スコープなのとClosureの合わせ技なためで、forループで使っているpropertyという変数が、ループの最後にセットされた値を参照することになるため。

なので関数の引数として変数をbindingすることによってこの問題を回避する。

for name, property of module when typeof property is 'function'
  do (_name = name, fn = property) ->
    clazz.prototype[_name] = ->
      fn.apply(this[moduleName], Array::slice.call(arguments))

CoffeeScriptをらしく美しく / dowhen

最初、即時関数を素朴にJavaScriptそのまんまで

for name, property of module
  if typeof property is 'function'
    ((_name, fn) ->
      clazz.prototype[_name] = ->
        fn.apply(this[moduleName], Array::slice.call(arguments))
    )(name, property)

とやっていたところ、※欄にてdowhenを使ってより簡潔に記述可能と教えて頂き、現在の形に修正しています。

immutableにしたいかね?

var immutable = Object.freeze({
  foo: 'FOO',
  bar: 'BAR'
});

immutable.foo = 'A'; // 返事がないただの屍

(function  () {
  // use strictによって例外が発生するようになる
  'use strict';
  immutable.foo = 'B'; // TypeError
})();