ホームページ >ウェブフロントエンド >jsチュートリアル >JavaScript クロージャ パート 2: クロージャの実装

JavaScript クロージャ パート 2: クロージャの実装

黄舟
黄舟オリジナル
2016-12-20 16:07:47806ブラウズ

理論的な部分について説明した後、ECMAScript でクロージャがどのように実装されるかを紹介します。ここでもう一度強調しておく価値があります。ECMAScript は静的 (字句) スコープのみを使用します (一方、Perl などの言語は変数宣言に静的スコープと動的スコープの両方を使用できます)。

var x = 10;
 
function foo() {
  alert(x);
}
 
(function (funArg) {
 
  var x = 20;
 
  // 变量"x"在(lexical)上下文中静态保存的,在该函数创建的时候就保存了
  funArg(); // 10, 而不是20
 
})(foo);

技術的に言えば、関数を作成した親コンテキストのデータは、関数の内部プロパティ [[Scope]] に格納されます。 [[Scope]] が何なのかわからない場合は、まず前の章を読むことをお勧めします。この章では、[[Scope]] について詳しく説明しています。 [[Scope]] とスコープ チェーンの知識を完全に理解していれば、クロージャも完全に理解できるようになります。

関数作成アルゴリズムによると、ECMAScript では、すべての関数がクロージャであることがわかります。これは、関数が作成されたときに上位コンテキストのスコープ チェーンを保存するためです (例外を除く) (この関数が後でアクティブ化されるかどうかに関係なく) - [[スコープ]] は関数の作成時に利用可能です):

var x = 10;
 
function foo() {
  alert(x);
}
 
// foo是闭包
foo: <FunctionObject> = {
  [[Call]]: <code block of foo>,
  [[Scope]]: [
    global: {
      x: 10
    }
  ],
  ... // 其它属性
};

前述したように、最適化の目的で、関数が自由変数を使用しない場合、実装は内部の副作用ドメイン チェーンに保存されない場合があります。ただし、ECMA-262-3 仕様には何も記載されていません。したがって、通常、すべてのパラメータは作成フェーズ中に [[Scope]] 属性に保存されます。

一部の実装では、クロージャ スコープへの直接アクセスが可能です。たとえば、Rhino では、関数の [[Scope]] 属性には、非標準の __parent__ 属性があります:

var global = this;
var x = 10;
 
var foo = (function () {
 
  var y = 20;
 
  return function () {
    alert(y);
  };
 
})();
 
foo(); // 20
alert(foo.__parent__.y); // 20
 
foo.__parent__.y = 30;
foo(); // 30
 
// 可以通过作用域链移动到顶部
alert(foo.__parent__.__parent__ === global); // true
alert(foo.__parent__.__parent__.x); // 10

すべてのオブジェクトは [[Scope]] を参照します

ここにも注意してください: ECMAScript では、クロージャが作成されます同じ親コンテキスト内で [[Scope]] 属性を共有します。言い換えれば、クロージャによる [[Scope]] 内の変数の変更は、他のクロージャによるその変数の読み取りに影響します:

var firstClosure;
var secondClosure;
 
function foo() {
 
  var x = 1;
 
  firstClosure = function () { return ++x; };
  secondClosure = function () { return --x; };
 
  x = 2; // 影响 AO["x"], 在2个闭包公有的[[Scope]]中
 
  alert(firstClosure()); // 3, 通过第一个闭包的[[Scope]]
}
 
foo();
 
alert(firstClosure()); // 4
alert(secondClosure()); // 3

これは、すべての内部関数が同じ親スコープを共有することを意味します

これについては非常によく誤解されています開発者は、ループ ステートメント内で関数を作成する場合 (内部的にカウントする)、期待どおりの結果が得られないことがよくあり、各関数が独自の値を持つことが期待されます。

var data = [];
 
for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}
 
data[0](); // 3, 而不是0
data[1](); // 3, 而不是1
data[2](); // 3, 而不是2

上記の例は、同じコンテキストで作成されたクロージャが [[Scope]] 属性を共有することを証明しています。したがって、上位コンテキストの変数「k」は簡単に変更できます。

activeContext.Scope = [
  ... // 其它变量对象
  {data: [...], k: 3} // 活动对象
];
 
data[0].[[Scope]] === Scope;
data[1].[[Scope]] === Scope;
data[2].[[Scope]] === Scope;

このように、関数を有効にすると、最後に使用したkは3になりました。以下に示すように、クロージャを作成すると、この問題を解決できます:

var data = [];
 
for (var k = 0; k < 3; k++) {
  data[k] = (function _helper(x) {
    return function () {
      alert(x);
    };
  })(k); // 传入"k"值
}
 
// 现在结果是正确的了
data[0](); // 0
data[1](); // 1
data[2](); // 2

上記のコードで何が起こるか見てみましょう?関数「_helper」を作成した後、パラメーター「k」を渡すことで関数がアクティブになります。その戻り値も関数であり、対応する配列要素に格納されます。この手法により次のような効果が得られます。関数がアクティブ化されるたびに、「_helper」はパラメーター「x」を含む新しい変数オブジェクトを作成します。「x」の値は渡された「k」の値になります。このようにして、返される関数の [[Scope]] は次のようになります:

data[0].[[Scope]] === [
  ... // 其它变量对象
  父级上下文中的活动对象AO: {data: [...], k: 3},
  _helper上下文中的活动对象AO: {x: 0}
];
 
data[1].[[Scope]] === [
  ... // 其它变量对象
  父级上下文中的活动对象AO: {data: [...], k: 3},
  _helper上下文中的活动对象AO: {x: 1}
];
 
data[2].[[Scope]] === [
  ... // 其它变量对象
  父级上下文中的活动对象AO: {data: [...], k: 3},
  _helper上下文中的活动对象AO: {x: 2}
];

これを実現するには、この時点で関数の [[Scope]] 属性が実際に必要な値を持っていることがわかります。この目的のために、[[Scope]] に追加の変数オブジェクトを作成する必要があります。返された関数で「k」の値を取得したい場合、値は 3 のままであることに注意してください。

ちなみに、JavaScript を紹介する記事の多くは、追加で作成した関数のみがクロージャであると信じていますが、これは間違いです。実際には、この方法が最も効果的ですが、理論的な観点から見ると、ECMAScript の関数はすべてクロージャです。

ただし、上記の方法だけではありません。 「k」の正しい値は、次のような他の方法でも取得できます:

var data = [];
 
for (var k = 0; k < 3; k++) {
  (data[k] = function () {
    alert(arguments.callee.x);
  }).x = k; // 将k作为函数的一个属性
}
 
// 结果也是对的
data[0](); // 0
data[1](); // 1
data[2](); // 2

Funarg と return

別の機能はクロージャから返されます。 ECMAScript では、クロージャ内の return ステートメントは、制御フローを呼び出しコンテキスト (呼び出し元) に返します。 Ruby などの他の言語では、多くの形式のクロージャがあり、対応するクロージャの戻り値も異なります。次の方法が可能です。呼び出し元に直接返されることもあれば、場合によってはコンテキストから直接終了することもあります。 。

ECMAScript の標準の終了動作は次のとおりです:

function getElement() {
 
  [1, 2, 3].forEach(function (element) {
 
    if (element % 2 == 0) {
      // 返回给函数"forEach"函数
      // 而不是返回给getElement函数
      alert(&#39;found: &#39; + element); // found: 2
      return element;
    }
 
  });
 
  return null;
}

ただし、ECMAScript では、try catch によって次の効果を実現できます:

var $break = {};
 
function getElement() {
 
  try {
 
    [1, 2, 3].forEach(function (element) {
 
      if (element % 2 == 0) {
        // // 从getElement中"返回"
        alert(&#39;found: &#39; + element); // found: 2
        $break.data = element;
        throw $break;
      }
 
    });
 
  } catch (e) {
    if (e == $break) {
      return $break.data;
    }
  }
 
  return null;
}
 
alert(getElement()); // 2

理論バージョン

ここで説明すると、開発者はしばしば誤ってクロージャを単純化し、親からクロージャを理解します。 context 内部関数を返すと、匿名関数のみがクロージャになり得ることも理解されます。

繰り返しますが、スコープ チェーンのため、すべての関数はクロージャです (関数の種類に関係なく、匿名関数、FE、NFE、FD はすべてクロージャです)。

関数の [[Scope]] にはグローバル オブジェクトのみが含まれるため、関数のタイプは 1 つだけです。それは、Function コンストラクターを通じて作成された関数です。この問題をより明確にするために、ECMAScript のクロージャーの 2 つの正しいバージョン定義を示します。

ECMAScript では、クロージャーは以下を参照します。

理論的な観点から: すべての関数。それらはすべて、作成時に上位コンテキストのデータを保存するためです。これは、関数内のグローバル変数へのアクセスは自由変数へのアクセスと同等であるため、単純なグローバル変数にも当てはまります。このとき、最も外側のスコープが使用されます。

実用的な観点から: 以下の関数はクロージャとみなされます:

それが作成されたコンテキストが破棄されたとしても、それはまだ存在します (たとえば、内部関数は親関数から戻ります)

自由変数はコード内で参照されています

以上です JavaScript クロージャ パート 2: クロージャ実装の内容 詳細については、PHP 中国語 Web サイト (www.php.cn) に注目してください。


声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。