JavaScript

JavaScriptのクロージャはメモリーリークをちゃんと理解して使おう

はじめに

前にブログで書いた記事なのですが、せっかくなのでQiitaにも投稿します。

脱初級者の壁として君臨しているクロージャ。クロージャの使い方はわかったけど、いろんな記事を見るとクロージャは問題点もあるみたい。それに、そもそもクロージャの使い所がいまいちわかんないと思ってクロージャに再度立ち向かおうと思った次第です。同じような悩みを抱えているデザイナーさん、コーダーさん、フロントエンドエンジニアさんの参考になれば嬉しいです。

クロージャとは

とりあえずおさらい & 補足をします。

よく見かけるクロージャの見本がこちら。

function closure(initVal){
  var count = initVal;

  var innerFunc = function() {
    return ++count;
  }
  return innerFunc;
}

var myClosure = closure(100);
myClosure(); // 101
myClosure(); // 102
myClosure(); // 103

ここで簡単にクロージャについて説明します。ちなみに、最近読んだ本で何となくJavaScriptを書いていた人が一歩先に進むための本が説明としてわかりやすかったので、そちらを引用させていただきながら。
まずクロージャとは

ローカル変数を参照している、関数の中に定義している関数

ということらしいです。なので今回の場合だとinnerFunc関数がクロージャに該当しますね。では、なぜ、myClosure()が呼び出されるたびに結果が増えていくかというのがわかるとクロージャがどんなものなのかわかってきます。

まず、通常の関数の中に定義されているローカル変数は、関数の処理が終わった時点で破棄されます。しかし、先ほどのコードだとmyClosureがローカル変数countを参照し続けています。そのことによって結果が増えていきます。では、なぜこのようなことが起きるのかというと

  1. closure関数ではローカル変数countを参照している関数innerFuncが返却されている
  2. innerFuncそのものはmyClosureに格納される
  3. myClosureはグローバル変数なため、グローバルオブジェクトが存在し続ける限り解放されることがない
  4. なので、ローカル変数countも破棄されない
  5. countは破棄されないので、closure呼び出し時に代入された値が保持される
  6. よってcountは加算されていく

こんな仕組みで動いているのがクロージャです。スコープチェーンと破棄されるタイミングさえ掴めれば理解できそうです。
ざっとクロージャとはどんなものかおさらいできたところで、今回の本題に入りたいと思います。

クロージャによるメモリーリーク

コメントでいただいたこちらの記事がわかりやすかったのでこちらを参照させていただきます。

var theThing = null;
var replaceThing = function () {

  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 'originalThing' への参照
      console.log("hi");
  };

  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
};

setInterval(replaceThing, 1000);

どうしてメモリーリークが起きてしまうのかはこちらの解説が非常にわかりやすいので、ぜひ目を通して見てください。他にも「4種類の一般的な JavaScript 共通のメモリリーク」として

  • グローバル変数
  • 放置されるタイマーもしくはコールバック
  • DOM 参照

についても詳しく解説されています。

ちなみに、Google ChromeのDevToolsで調べてみると

cap.jpg

このように毎秒ごとにメモリ使用量が増えてしまっているのがわかります。途中、手動GCをしても増え続けてしまっています。

このメモリーリークを修正すると、replaceThingの最後にoriginalThing = nullを追加するだけです。

var theThing = null;
var replaceThing = function () {

  var originalThing = theThing;
  var unused = function () {
    if (originalThing) // 'originalThing' への参照
      console.log("hi");
  };

  theThing = {
    longStr: new Array(1000000).join('*'),
    someMethod: function () {
      console.log("message");
    }
  };
  originalThing = null
};

setInterval(replaceThing, 1000);

クロージャの使い所とは

クロージャはメモリーリークを引き起こしてしまう可能性があるというのがわかってきました。では、実際クロージャはどんなケースで使えばいいのでしょうか?

状態を覚えておきたい時

よくあるのが、

jQuery(function($){
  var isClicked = false;
  $('.btn').click(function(){
    if (isClicked) {
      console.log('クリック済みです');
    }
    isClicked = true;
  });
});

こんな感じなのですね。クリックしたかどうかを覚えておく時に使ったりします。コーポレートサイトなんかでjQueryを使っている時なんかはいいのかもしれません。ただ、特にメモリーリークがおきやすいSPAだと、Reactなどを使っているケースがほとんどだと思います。その場合State管理しているので別段クロージャを使わなくていいですね。

private プロパティの定義

Javascriptでは、「プライベートメンバ」という機能がありません。全てのメンバは常にパブリックになってしまいます。下記のprototypeの例だと、nameが外部から操作されてしまっているのがわかります。

mitsuruog/clean-code-javascript: Clean Code concepts adapted for JavaScript

const Employee = function(name) {
  this.name = name;
};

Employee.prototype.getName = function() {
  return this.name;
};

const employee = new Employee('John Doe');
console.log('Employee name:' + employee.getName()); // Employee name: John Doe
delete employee.name;
console.log('Employee name:' + employee.getName()); // Employee name: undefined

クロージャのテクニックを使って、private プロパティのようなものを作ることができます。下記の場合だと、delete employee.name;で操作できていないことがわかりますね。

function makeEmployee(name) {
  return {
    getName: function() {
      return name;
    }
  };
}

const employee = makeEmployee('John Doe');
console.log('Employee name:' + employee.getName()); // Employee name: John Doe
delete employee.name;
console.log('Employee name:' + employee.getName()); // Employee name: John Doe

ただクロージャを使えばプライベートメンバを作れるのですが、PrototypeベースというJavaScriptの利点をなくしてしまう他、インスタンス化する度にメソッドを定義するためメモリも余計に使ってしまう恐れもあります。これを回避するために、this._nameのようにして紳士協定でprivate プロパティを作る方法があります。

const Employee = function(name) {
  this._name = name;
};

Employee.prototype.getName = function() {
  return this._name;
};

ES6ではWeakMapを使ってprivate プロパティを作れる

Classをインスタンス化する際、そのインスタンス(this)をWeakMapにsetすればWeakMap.get(this)でメンバにアクセスできるようになります。

参考
- 【JavaScript】privateなプロパティやメソッドを定義する | Web活

var Func = (function() {
  var privates = new WeakMap();

  function Func() {
    privates.set(this, {});

    privates.get(this).prop = 1;
  }

  Func.prototype.method = function() {
    console.log('******************')
    console.log(privates.get(this).prop)
    console.log('******************')
  };

  return Func;
})();

let p = new Func();
p.method();

まとめ

今回クロージャについて再度調べてみました。クロージャのデメリットとメリットについてまとめてみました。
正直、ES6以前ではprivateプロパティを作るという点でいいんですが、どうしてもメモリ使用量とのトレードオフになってしまいますね。紳士協定に寄る対応策もありますがクロージャの使い所を間違えないようにしたいですね。んで、もしクロージャを使っているのであればメモリーの使用についてはしっかりと計測をしてメモリーリークがおきていないか把握したいところです。

ただ、ES6環境であればWeakMapでprivateプロパティを作れるのは大きいですね。

多数のコメントいただきありがとうございました

最初に投稿した時点では内容に間違いがありましたので、大幅な修正をさせていただきました。