LoginSignup
75
72

More than 5 years have passed since last update.

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

Last updated at Posted at 2013-11-11

だいたい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) の方が高速でメモリ使用量も少ない。

  • 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
})();
75
72
13

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
75
72