プロが教える店舗&オフィスのセキュリティ対策術

JavaScriptの匿名関数から関数オブジェクトが作られるのはいつでしょうか。
※ブラウザの実装によるかもしれませんが・・・

Callオブジェクトがその関数の実行時に毎回生成されるので、
処理コスト的に考えれば、関数オブジェクトはコンパイル時の1回だけで済むはずですよね?
関数コード自体はどんな呼び方しても共通なはずですから、
単に実行時に適切なCallオブジェクトをくっつけてあげるだけでいいはず・・・
(Functionコンストラクタ使うなら別でしょうけど)

でも、クロージャについて「その都度、関数オブジェクトが生成されるのでメモリ効率が悪い」
という意見を割とあちこちのブログとかで見ます。
Callオブジェクトにでかいローカル変数が含まれると無駄、ってのなら分かるんですが。

このあたり、ECMAScriptの仕様では決められていない部分なのでしょうか。

A 回答 (8件)

あー、ごめんなさい。

ご質問の意図を理解していませんでした。

----
以下の関数は、x の値によって戻り値が一意に決まります。構文は JavaScript 1.7 です。

(function(x) x + 1) (1); // 2

以下の関数は、x の値だけでは戻り値を予測できず、外部変数 a に左右されます。

var a = 1; (function(x) x + a) (1); // 2?

関数と、外部変数をセットにするのがクロージャです。この外部変数はこの関数でのみ使われます。

let(a = 1) (function(x) x + a) (1); // 2

しかし、let 式は現行の ECMAScript に存在しません。同じことを実現するには、関数をもう一つ使わねばなりません。これを私は「いわゆるクロージャ」と呼んでいます。

(function() { var a = 1; return function(x) { return x + a; } (1); }) ();

一応、関数に名前を付けてしまいましょう。

(function f() { var a = 1; return function g(x) { return x + a; } (1); }) ();

---
さて、関数 g のローカル変数 x の値は、関数 g の呼び出しオブジェクトが知っています。これを Call_g とし、Callg["x"] によって x の値が分かるものとします。

しかし、外部変数 a に関しては、Call_g["a"] が存在しません。そこで、Call_g は関数 g に「あなたの文脈を教えてくれ」と頼みます(Call_g 自体は g の文脈を知りません)。関数 g は「私は関数 f を呼び出した Call_f によって作られた」と答えます。こうして、変数 a の値は Call_f["a"] で分かりました。

さて、No.5 のお礼で述べておられるように、クロージャが関数と変数束縛(すなわち Call オブジェクト)のセットであるのはその通りです。しかし、ここで Call オブジェクトが 2 種類あることに注意して下さい。すなわち、外部変数 a を束縛している Call_f と、ローカル変数 x を束縛している Call_g です。

もしこれが、関数 g と Call_g のセットであれば、仰る通り、メモリには何ら影響しないでしょう。しかし、ここで問題になっているクロージャとは、関数 g と Call_f のセットです。では、関数 g が Call_f を参照するには、どうすれば良いでしょうか。

ここで仮に、変数が変更不可で a = 1 が保証されるなら、関数 f を呼び出すたびに同じ関数 g を返しても構いません。しかし ECMAScript では変数 a を書き換えるような関数 g を返すこともできます。特定の変数 a、すなわち特定の Call_f とセットになる関数 g であるためには、ひとつの関数 g で複数の Call_f を管理するより、Call_f ごとに関数 g を生成した方が、結果的に安上がりではないでしょうか。

後は、個々の実装の最適化に委ねるのみです。
    • good
    • 0

No.5 お礼より:



> その部分は仕様で定義されていない、というのが答えのようですね

まず、「関数コード」というのは「FunctionBody として解析されるソースコード」であり、「入れ子のコードを含まない」(ES3: 10.1.2、ES5: 10.1)と定義されています。

「関数を生成する」とは、関数コードを呼び出すことができ、そのコード位置における実行コンテキスト(変数束縛済み)をスコープに含むようなオブジェクトを生成することです(ES3/5: 13.2)。

「関数コードの実行コンテキストに入る」とは、以下のようなことです。まず、関数コードを呼び出せる関数オブジェクトが、所与の引数リストと、関数コード内の宣言リストから、変数束縛を行います(ES5: 10.4.3, 10.5)。そして、関数コードを FunctionBody として評価した結果を返します(ES3/5: 13.2.1)。

ですから、ECMAScript 規定では、関数オブジェクトをコールするたびに、関数コードすなわち関数のソーステキストが評価される、と読めます。そして、関数コードに含まれる関数宣言と変数宣言が、実行コンテキストの変数束縛リストに含まれます。

---
しかしながら、上記はあくまで内部プロパティの説明です。内部プロパティとは、ECMAScript の振る舞いを説明するためのものであり、必ずしもそのように実装しろ、というものではありません。結果的に同じ振る舞いになれば、それで良いのです。

ここで、プログラムの意味論と、プログラムの実装を区別して下さい。ECMAScript という言語の意味論としては、実行コンテキストごとに(あるいは実行ステップごとに)新しい関数が生成されます。No.5 の最後の例で言えば、g1 != g2 でなければなりません。

そのことと、ECMAScript エンジンの実装は、別の話です。仮に g1 == g2 になるように実装しても、利用者の目には g1 != g2 であるかのように見えれば良いのです。

ES3 の結合オブジェクトの文言が ES5 で削除されたのは、意味論と実装とを混同しないようにするため、と見ることもできるかもしれません。

> コーディングの指針(クロージャの使用の是非、許される程度)が欲しくて

そもそも、ECMAScript の関数型的な特徴を踏まえ、参照透過性を考慮すれば、いわゆるクロージャを単なるブロックのように使うことは避けるでしょうし、大きなクロージャでスパゲッティにならないよう関数を機能ごとに分割するでしょう(クロージャの濫用は、グローバル変数の濫用よりずっと性質の悪いものです)。

繰り返しますが、ECMAScript の意味論としては g1 != g2 です。同じ関数を何度も生成する(かのように見える)のを避けたければ、やりようはいろいろあるでしょうし、むしろその方が「良い」コードになりそうな気がします。
    • good
    • 0

#2 です。



JavaScriptコードをパース(構文解析)する話をされているのでしょうか。だとすれば読むのはこちらです。
http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma26 …
http://es5.github.com/#x7-toc
script要素の内容のパースは HTML5 (HTML Standard) の範疇になります。
http://www.whatwg.org/specs/web-apps/current-wor …

ただ、これはクロージャ云々よりずっと前の話です。
いわゆるクロージャは外部関数が [[Call]] された時に内部関数が実体化する過程で生まれますから、構文解析はとっくに終わっています。

また、コンパイルやメモリの仕組みは実装依存(ブラウザ依存)になります。
ECMAScript 仕様には「コンパイル」も「メモリ」も出てきません。
『JavaScript 第五版』は良い本ですが、便宜上の用語がいくつかあり、ECMAScript に出てくる用語とは違った表現になる場合があることに注意してください。
仕様理解に努めるなら、仕様書と比較しながら読み進めると良いと思います。
    • good
    • 0
この回答へのお礼

ありがとうございます。

申し訳ありませんが、構文解析の話ではありません。
私の拙い言葉よりも、先にあげたURLの記事を読んで頂けると良さそうです。
その記事の内容が正しいのかどうかが気になっています。

お礼日時:2012/01/25 18:10

念のため、ご質問は『ECMAScript の仕様』でどうなっているかですので、それに沿って説明します。

個々の実装については、それぞれのソースコードなどを確認して下さい。

---
No.3 お礼より:
> 関数コードと、その実行コンテキストがセットであれば良いわけで

それでは足りません。関数コードが「ソースコードのどこにあるか」が必要です。

function F(x) {
 function G(y) {
  return H(x + y);
 }
 function H(z) {
  alert(z);
 }
 G(2);
}
F(1); // 3

・Global の実行コンテキスト C0 に入る(F が実体化)
・関数 F の実行コンテキスト Cf に入る(Cf は C0 を参照、x と G と H が実体化)
・関数 G の実行コンテキスト Cg に入る(Cg は Cf を参照、y が実体化)
・関数 H の実行コンテキスト Ch に入る(Ch は Cf を参照、z が実体化)
・関数 G の実行コンテキスト Cg に戻る(Ch を廃棄)
・関数 F の実行コンテキスト Cf に戻る(Cg を廃棄)
・Global の実行コンテキスト C0 に戻る(Cf を廃棄)

ここで、G から H の呼び出しにおいて、Ch が Cg を参照するのでなく(動的スコープ)、Cf を参照している(静的スコープ)ことに注意して下さい。これを実現するには、単に H が F の「関数コード」にアクセスできるだけでは駄目です。

一番手っ取り早いのは、F の実行コンテキスト Cf に入った時点で H を実体化し、H の内部プロパティ [[Scope]] に Cf をセットしてしまうことです。ですから、ECMAScript 3/5 の規定上は、実行コンテキストごとに「関数コード」から「関数オブジェクト」が生成されることになります。

---
「変数(関数)の実体化」について、ECMAScript 規定にアルゴリズムの記載はあるものの、具体的な実装方法については範囲外です。一応、ECMAScript 5 のアルゴリズムでは、関数オブジェクトが内部プロパティ [[Code]] を持ち、関数の実体化は [[Code]] から関数を生成する、として説明されています。

あるいは、プログラムコードをいったん全部解析して抽象構文木でも構築すれば、関数オブジェクト自体の生成は一度で済むでしょう。しかしながら、上に書いたように、関数オブジェクトを実体化するには内部プロパティ [[Scope]] をセットする必要があります。そのため、構文木に属する「雛形としての関数オブジェクト」から、実行コンテキストごとにディープクローンを生成することになる、かもしれません。

---
繰り返しますが、ECMAScript 3 には結合オブジェクトという規定があり、同じ位置・同じ関数コードで、内部プロパティだけが異なる関数オブジェクトであれば、ひとつにまとめて良い、ということになっていました。

function F(x) {
 return function G(y) {
  alert(x + y);
 }
}

var g1 = F(1);
var g2 = F(2);

alert(g1 == g2); // 結合オブジェクトをサポートしているなら true

しかし、この規定は ECMAScript 5 で削除されています。最適化については、それぞれの実装のソースコードを確認して下さい。

---
なお、window は DOM の一部ですので、ECMAScript では規定されていません(「例えば、HTML DOM では Global オブジェクトの window プロパティが Global 自身である」と言及があるだけ)。

参考URL:http://web.archive.org/web/20061018193747/http:/ …
    • good
    • 0
この回答へのお礼

詳細な回答ありがとうございます。
今さらですが、私の用語の使い方が恐らく正しくないことについて謝罪します。
混乱させてしまい申し訳ありません。

例示されたソースコード、つまり静的スコープ(クロージャの性質)については、よく理解しています。
関数コードという表現が悪かったですね。
言いたかったのは、関数コードと静的スコープのセットです。
先に挙げたURLの言葉に沿うならば、関数オブジェクトとCallオブジェクトの雛型です。
これの生成は1回だけで良いだろう、ということを言いたかったのです。

ただどうやら、その部分は仕様で定義されていない、というのが答えのようですね。
コーディングの指針(クロージャの使用の是非、許される程度)が欲しくて、
実際に速度とメモリの効率にどんなロスがあるかを知りたかったのですが・・・
ブラウザの実装次第なのであれば、実際に問題が起きるまでは可読性・保守性重視でやるしかなさそうですね・・・

お礼日時:2012/01/25 11:24

すみません、長くなりました。

JavaScript以外の言語の表現を使います。(JavaScriptに概念がないため)


>でも、クロージャについて「その都度、関数オブジェクトが生成されるのでメモリ効率が悪い」

クロージャはインスタンス内のプライベート変数です。
そのため(インスタンスを生成しなければならないため)、クロージャを使ったからといってメモリ効率が悪いとは言い切れないです。
newしてなくてもインスタンスというのは、他の言語のOOPと比較すると、めずらしいというか変に思われると思いますが。


function makeClosure(s,t){
var txt=s+'!';
var func=function(){ alert(txt); };
return function(){setTimeout( func, t ); };
}

function main(){
var a=makeClosure('hello', 1000);
var b=makeClosure('good', 2000);
a(); //'hello'
b(); //'good'
}
main();

関数コードは1つでも、その内容はa、b別々に保持しておく必要があります。

変数a、bはmain()が終わると消滅しますが、
makeClosure内のtxtの寿命(スコープではなく)がメモリリークになる可能性があります。


ギリギリまで簡略化したものでも動的に関数オブジェクトを生成するかどうかというのは、ブラウザ依存だと思います。

function func(){
return true;

/* var returnCode=true;
return returnCode;
と同じ。trueがクロージャになっている*/
}
var a=func();
var b=func();


もしこの「メモリ効率が悪い」という言葉がシングルトンを意識してのものだとしたとしても、
JavaScriptでシングルトンは作れないようです。

>No.2
>console.log(hello1.print === javascript1.print); // false
>console.log(hello2.print === javascript2.print); // true
そんな違いがあったんですね。
単にメソッドがあるかどうかを検索する順番だけかと思ってましたが。。。

ということで、シングルトンを期待して作ってみましたが、、、

function func(){
this.txt='aiueo';
}
func.prototype.count=0;
func.prototype.increment=function(){this.count++;}

alert('txt '+func.txt); // 期待'aiueo'、結果undefined

//func.increment(); // 期待実行可、結果undefined(Exception)
alert('func.count: '+func.count); // 期待0、結果undefined

var n=new func();
n.increment();
alert('n.count: '+n.count); // 期待1、結果1

var b=new func();
b.increment();
alert('b.count: '+b.count); // 期待2、結果1

alert('n.count: '+n.count); // 期待2、結果1
alert('func.count: '+func.count); // 期待2、結果undefined

結果、シングルトンにはなりませんでした。


---------
RegExpは組み込み関数の中で唯一(だったはず)、シングルトンになっています。

var s='homepage-home';
var r1=new RegExp('(.+)-(.+)', '');
var r2=new RegExp('(www)', '');
s.match(r1);
alert(RegExp.$1);// 'homepage'。r1.$1ではなくRegExpを使用する。

s.match(r2); // ヒットしない
alert(RegExp.$1); // 'homepage'。r2がヒットしない場合は上書きされないため、r1の結果が残っている
alert(RegExp.$2); // 'home'。r1の結果の$2


-----------------
windowというのはECMAScriptの「ルートオブジェクト」で定義されている物です。
グローバル変数のみ、varキーワードが付いていてもwindow配下のプロパティになります。
このルールもJavaScriptをややこしくしている原因かもしれません。

var a; // window.aと同等
function f(){
var b; // f.bではない
}

--------------------
自称、似非シングルトン。(擬似ではなく似非。個人的にはこの方法を良く使います。)
>単に実行時に適切なCallオブジェクトをくっつけてあげるだけでいいはず・・・
勘違いしてたら済みません。たぶん、それっぽいことをしてるんじゃないかと、、、

var myObj=new (function(){
 var txt='';
 var interval=0;
 this.makeClosure=function(s,t){
  txt=s+'!';
  interval=t;
  return this;
 };
 this.alert=function(){ alert(txt); };
 this.run=function(){
  setTimeout( this.alert, interval );
  return this;
 };

})();

function main(){
var a=myObj.makeClosure('hello', 1000);
var b=myObj.makeClosure('good', 2000);
a.run(); // 'good'
b.run(); // 'good'
}
main();

function main2(){
myObj.makeClosure('excelent', 1000).run(); // 'excelent'
}
//main2();

*setTimeoutから呼ばれたmyObj.alertの中では、thisの内容がwindowになるので、もしthis==myObjにしたかったら手を加える必要があります。
    • good
    • 0

ECMAScript における関数オブジェクトは次のように生成されます。



・実行コンテキストと同時に生成される関数宣言
・実行コンテキストを伴って実行されるコード中の関数式、Function 式。

※「Call オブジェクト」と内部実装表現を書く参考書もありますが、ここでは ECMA 262 規定にある「実行コンテキスト」の用語を使います。

この中で「匿名」になりえるものは関数式しかありません。ということは、実行コンテキストごとに、かつ、コードの実行ステップごとに関数オブジェクトが生成されます。

> 関数オブジェクトはコンパイル時の1回だけで済むはず

関数オブジェクトはスコープチェーンを持つ必要がありますので、コンパイル時だけではチェックできません。スコープチェーンは、変数の実体化された実行コンテキストがないとセットできないからです(ここで、終了した実行コンテキストなのにスコープチェーンを解除できないバグが、いわゆるメモリリーク問題になりやすい)。

上に書いたように、関数宣言ならば実行コンテキストと同時に生成されます。また、コード実行時に評価される関数式をどうしても使わざるをえないなら、一度の生成で済むよう場所とスコープを工夫して下さい。

> 単に実行時に適切なCallオブジェクトをくっつけてあげるだけでいいはず

ECMAScript 3 には「結合オブジェクト」(§13.1.2)として、ある種の条件が揃ったときに、まさにそのアイデアによってメモリを節約して良いことになっていました。しかし、NN6 ぐらいにしか実装されず、ECMAScript 5 で文言が削除されています。

---
ちなみに、ECMAScript 3 の正規表現リテラルは、プログラム開始時に 1 回だけコンパイルされます(§7.8.5)。つまり、プログラムコード中の正規表現リテラルは、実行コンテキストに関わらず同一である可能性があります(昔の Firefox、Opera)。ですから、g フラグ付きの正規表現リテラルを扱うときは十分に注意して下さい。正規表現オブジェクトのプロパティが書き換えられたまま、別の実行コンテキストに持ち越される可能性があるからです。

このことは、No.0 の仰る『関数オブジェクトはコンパイル時の1回だけで済むはず』と同じアイデアと言えます。ならば、同じような問題を抱える可能性があります。関数オブジェクトに付けたプロパティが、プログラム実行中に共有されてしまうことです(それはそれで便利かもしれませんが)。

正規表現リテラルに関するこの文言は ECMAScript 5 で削除されました。

---
結論としては、仰るようなアイデアを無くす方向に ECMA 262 が修正されています。実装がそんなに簡単ではないからです。当面、必要ならばコンパイラの方で工夫してもらうことになります(だから、Google が Dart なんてものも出したわけで)。
    • good
    • 0
この回答へのお礼

ありがとうございます。

> 関数オブジェクトはスコープチェーンを持つ必要がありますので、コンパイル時だけでは
チェックできません。スコープチェーンは、変数の実体化された実行コンテキストがないとセットできないからです

いえ、ですから、関数コードと、その実行コンテキストがセットであれば良いわけで、
関数コードは最初から最後まで変わりませんよね?
私の用語の使い方が間違っていたら申し訳ありません。
この最初から最後まで絶対に変わらない関数コードを、関数オブジェクトと表現し、
実行時に実行コンテキスト(Callオブジェクト)が作成され、
関数オブジェクトと合わせて実行されるのでは、という話です。

メモリ上に同じ関数コードがいくつも保持されるのでメモリが無駄、という話をよく見るので・・・

あとこんなページも見つけましたが、結局これは誤りということなのでしょうか。
http://fairy.ouchi.to/notes/2011/06/09-prog-a

お礼日時:2012/01/24 08:55

変数オブジェクトが実体化するタイミングを確認する事をお勧めします。


http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma26 …

---
function Hello1 (string) {
 this.string = string;
 this.print = function () { return '"' + this.string + '"'; };
}

function Hello2 (string) {
 this.string = string;
}
Hello2.prototype.print = function () { return '"' + this.string + '"'; };

var hello1 = new Hello1('Hello, World!'),
javascript1 = new Hello1('Hello, JavaScript!'),
hello2 = new Hello2('Hello, World!'),
javascript2 = new Hello2('Hello, JavaScript!');

console.log(hello1.print === javascript1.print); // false
console.log(hello2.print === javascript2.print); // true
---

Hello1#print はコンストラクタ呼び出しされる度に新しい関数オブジェクトを生成します。
対して、Hello2#print はプロトタイプチェーンを辿って同じ関数オブジェクトを参照します。(つまり、メモリ効率が良い)

私はコンパイラ型言語に詳しくありませんが、「関数オブジェクトはコンパイル時の1回だけで済むはず」は ECMAScript においては当てはまりません。(そもそも、JavaScriptはインタプリタ言語です…。)
FunctionDeclaration と FunctionExpression で評価するタイミングが異なりますし、関数コードにおいて変数が実体化されるのは該当変数の上位にある関数が [[Call]] された時です。
    • good
    • 0
この回答へのお礼

ありがとうございます。
確かにこの例のような説明をあちこちで見ます。

ただ、JavaScriptがインタプリタ言語であることは知っていますが、
コンパイルという表現も割と使われているので、実行時のコード変換?を指すと思っていました。

http://fairy.ouchi.to/notes/2011/06/09-prog-a
ここの内容は誤りということでしょうか。

お礼日時:2012/01/24 08:52

以下のコードで「function(){alert(text);}」は1箇所にしか書かれていませんが、


その実体であるfunc_aとfunc_bは別物になります。

function get_function(text){
return function(){alert(text);}
}

var func_a = get_function("a");
var func_b = get_function("b");

func_a(); //→a
func_b(); //→b

alert(func_a===func_b); //→false
    • good
    • 0
この回答へのお礼

ありがとうございます。

その説明自体はよく見るので分かっています。
この質問は、本当にそうなの?という話です。

お礼日時:2012/01/24 08:57

お探しのQ&Aが見つからない時は、教えて!gooで質問しましょう!