質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.52%

  • JavaScript

    16421questions

    JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。

クロージャーと参照について

解決済

回答 3

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 188

murabito

score 21

function outer() {

    var a = 1;
    var b = [{}, {}, {}, {}, {}];

    return function inner() {
        console.log(a);
        a++;
    }

}

const fn = outer();
fn(); 
fn(); 
fn(); 


このコードの場合、outer()を呼んで返されるinner関数は外の変数aは参照し続けるけれども、変数bは参照をしていないので、変数bに代入されていた複数のオブジェクトを持つ配列に割り当てられたメモリは自動的に解放されるという理解であっていますでしょうか?

 キャプチャー

イメージ説明

変数bは見当たらないので、そういうことなのかと思いました。

 解決後の報告

Google Chrome Version 65.0.3325.181 (Official Build) (64-bit)で確認

function outer(lang) {

        var a = 1;
        var b = [{}, {}, {}, {}, {}];

        if (lang === 'jp') {
                return function inner() {
                    console.log(a);
                    console.log(b);
                    a++;
                }
        } else {
                return function inner() {
                    console.log(a);
                    a++;
                }
        }

}

const fn = outer('en');

このようなコードで試したところ、以下のように変数bを参照しない関数を返したとしても、変数aと変数bへの参照を持っていることを確認致しました。

イメージ説明

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

質問への追記・修正、ベストアンサー選択の依頼

  • think49

    2018/04/14 15:50

    「解決後の報告」について。事後報告するのであれば、対象のブラウザ名、ブラウザのバージョンを明らかにするようお願いします。実装依存なので、どこまで参考になる情報なのか判断できなくなっています(IE11等の他のブラウザで再現出来なかったり、同じブラウザでもバージョンアップで挙動が変わる事はありえます)。

    キャンセル

  • murabito

    2018/04/15 16:53

    「Google Chrome Version 65.0.3325.181 (Official Build) (64-bit)で確認」と追記いたしました。

    キャンセル

回答 3

checkベストアンサー

+4

 ガベージコレクション (Garbage Collection)

function outer() {

    var a = 1;
    var b = [{}, {}, {}, {}, {}];

    return function inner() {
        console.log(a);
        a++;
    }

}

const fn = outer();

関数 inner が生きている限り、関数スコープ inner から参照可能な変数は全てメモリに残り続けます。
従って、ab ともにメモリから解放されません。

ただし、関数 inner がどこからも参照されなくなった場合は、変数 a, b を参照する対象(関数)がなくなる為、ガベージコレクション(GC)によって、a, b 共にメモリから解放されます。

 変数 b が見当たらない

変数bは見当たらないので、そういうことなのかと思いました。

ESLint では "'b' is assigned a value but never used." の警告が発生しますね。

Closure Compilerは b の変数文を削除しますし、Google Chrome は変数 b を初めから宣言しない最適化が施されていたとしても不思議ではありません。

元々、メモリの開放処理は標準仕様にはない動作ですので、ブラウザ毎に実装方法が異なります。

Re: murabito さん

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/04/14 13:31

    ただ、「変数 b はどこからも参照視されていない」ので、変数 b をメモリから解放する実装がある可能性は否定できませんね…。
    一般的には親記事の回答で問題ないと思いますが、ブラウザは最適化を続けているので、オープンソースな Chromium, Firefox のコードを読んでみるといいかもしれません(実装依存です)。

    キャンセル

  • 2018/04/14 15:53

    スコープからの参照が残っている以上, ガベージ対象とは出来ないと思います.(テンプレートリテラルやevalからの変数参照が可能なので.)

    キャンセル

  • 2018/04/14 15:57

    > テンプレートリテラルやevalからの変数参照が可能なので.
    こちらが理解できなかったのですが、実証コードを書いて頂くことは可能でしょうか。

    キャンセル

  • 2018/04/14 16:02

    例えば
    function outer() {
    var b = [{}, {}, {}, {}, {}];
    return function inner(src) {
    return eval(src);
    }
    }
    const fn = outer();
    console.log(fn("b").length);
    とかなってると, bを開放して良いもんだか悪いもんだか機械的には判断できない気が.

    キャンセル

  • 2018/04/14 16:16

    なるほど、確かに。
    eval() が存在したら変数を残しておく手しか思いつきませんでした。

    ちなみに、Closure Compilerは変数文を削除し、ReferenceError を誘発させました。
    https://closure-compiler.appspot.com/home
    eval is evilでしょうか。
    (0,eval)(src); や Function(src)() で封じられるローカル変数参照は考慮されていないようです。

    キャンセル

  • 2018/04/14 16:19

    少なくとも「evalなんか使ってたら逆立ちしたって最適化出来ね―」ってことでしょうね

    キャンセル

+2

変数bに代入されていた複数のオブジェクトを持つ配列に割り当てられたメモリは自動的に解放されるという理解であっていますでしょうか?

いいえ,間違っています.

変数a,bを保持しているスコープを関数が参照している限り, 変数bを直接的に操作していないとは言え, 勝手にその内容がメモリから開放されることはありません.

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

+2

意味合いとしては「プログラムからは参照されることがない」のだからgcが回収してくれるだろうと期待したくなりますが、他のお二人が回答しておられるように現状の実装ではクロージャー内部で明示的に参照されてなくても参照される可能性がある変数は全て「暗黙に覚えられてしまう」ようですね。

http://www.ecma-international.org/ecma-262/6.0/#sec-lexical-environments

この辺りを読むと「変数などの識別子とその値の対応を保持するEnvironment Record」なるものが解説されてます。「クロージャーの中のコードが実際に参照するかどうか」とは別に宣言されている変数は全てEnvironment Recordから見えてしまう」実装になっているのかどうかはこれだけ見ても自分にははっきりわかりませんでしたが、少なくとも何らかの最適化によりouterが最適化されない限りはEnvironment Recordに不要な変数が存在し続けているような気がします。

しかし、「実際どうなのか」を見た方が単純に納得しやすい(自分自身が)と思ったので実際node 9.0.0でやってみました。

gc.js

const process = require('process')
const linkClosure = process.argv[2] !== void 0
const linkExplicitly = process.argv[2] === 'explicitly'
const N = 64
const GARBAGE_SIZE = 0x10000

function foo() {
  let a = {next:null, link:null, garbage:null}
  let garbage = Buffer.alloc(GARBAGE_SIZE)

  totalAllocated += GARBAGE_SIZE

  const f = () => {
    if (linkClosure) a.link = f;
    if (linkExplicitly) a.garbage = garbage
    return a
  }
  return f
}

console.log(`linkClosure = ${linkClosure}, linkExplicitly = ${linkExplicitly}`)

let top
let totalAllocated = 0
for (let i = 0; i < 4096; i++) {
  const f = foo()
  const a = f()
  a.next = top
  top = a
}
const mu = process.memoryUsage()
const report = ['rss', 'heapTotal', 'heapUsed', 'external']
        .map(k => fmt(mu[k])).join('/')
console.log(`${fmt(totalAllocated)}: ${report}`)

function fmt(n) {
  const s = n.toString(16)
  return " ".repeat(12 - s.length) + s
}
$ node gc
linkClosure = false, linkExplicitly = false
    10000000:      5605000/      954000/      456ec8/     7fd2020
$ node gc closure
linkClosure = true, linkExplicitly = false
    10000000:     11905000/      a54000/      554b88/    10002020
$ node gc explicitly
linkClosure = true, linkExplicitly = true
    10000000:     11908000/      a54000/      554c60/    10002020


最初の列は実験対象のオブジェクト(garbage)のトータルの確保量、それ以降の列はprocess.memoryUsageのrss,heapTotal,heapUsed,externalの値です。garbageはBuffer.allocateで確保するようにしてますがこの領域はC++領域(external)に確保されるようです。結果をみるとクロージャーを覚えているだけ(node gc closureの実行)でもgarbageがgcで回収されてないことがわかります。


自分はアマチュアのため「うーん仕様をみてもわからん」という場合、割と安直に実験しようとします。プロの方だと「複数のエンジンに共通な仕様が何か」というもっと上のレベルのことを気にされていると思いますが、javascriptの仕様を把握するのはさぞ大変だろうと常々思います。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.52%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • JavaScript

    16421questions

    JavaScriptは、プログラミング言語のひとつです。ネットスケープコミュニケーションズで開発されました。 開発当初はLiveScriptと呼ばれていましたが、業務提携していたサン・マイクロシステムズが開発したJavaが脚光を浴びていたことから、JavaScriptと改名されました。 動きのあるWebページを作ることを目的に開発されたもので、主要なWebブラウザのほとんどに搭載されています。