ホームページ  >  記事  >  ウェブフロントエンド  >  JavaScriptのスコープとクロージャの深い理解_基礎知識

JavaScriptのスコープとクロージャの深い理解_基礎知識

WBOY
WBOYオリジナル
2016-05-16 16:35:561152ブラウズ

範囲

スコープは変数と関数のスコープです。JavaScript の関数内で宣言されたすべての変数は常に関数本体内で参照できます。JavaScript にはグローバル スコープとローカル スコープはありません。変数はグローバル変数よりも優先されます。いくつかの例を使用して、JavaScript のスコープの「隠されたルール」を理解しましょう (これらはフロントエンドのインタビューでもよく聞かれる質問です)。

1. 変数を事前に宣言します
例 1:

var scope="global";
function scopeTest(){
  console.log(scope);
  var scope="local" 
}
scopeTest(); //undefined

ここでの出力は未定義であり、エラーは報告されません。これは、前述の関数内の宣言が常に関数本体で表示されるためです。

var scope="global";
function scopeTest(){
  var scope;
  console.log(scope);
  scope="local" 
}
scopeTest(); //local

var を忘れた場合、変数はグローバル変数として宣言されることに注意してください。

2. ブロックレベルのスコープなし

他の一般的に使用されている言語とは異なり、JavaScript にはブロックレベルのスコープがありません:

function scopeTest() {
  var scope = {};
  if (scope instanceof Object) {
    var j = 1;
    for (var i = 0; i < 10; i++) {
      //console.log(i);
    }
    console.log(i); //输出10
  }
  console.log(j);//输出1

}

JavaScript の変数のスコープは関数レベルです。つまり、関数内のすべての変数は関数全体で定義されます。これにより、注意を払わないと遭遇する可能性があるいくつかの「隠れたルール」も生じます。 >

var scope = "hello";
function scopeTest() {
  console.log(scope);//①
  var scope = "no";
  console.log(scope);//②
}
①で出力された値が未定義であることが判明しました。これは、すでにグローバル変数の値を定義済みです。ここはこんにちはではないでしょうか。実際、上記のコードは次と同等です:

var scope = "hello";
function scopeTest() {
  var scope;
  console.log(scope);//①
  scope = "no";
  console.log(scope);//②
}
事前に宣言しておき、グローバル変数はローカル変数よりも優先度が低いというこの 2 つのルールに従って、なぜ undefine が出力されるのかを理解するのは難しくありません。

スコープチェーン

JavaScript では、各関数に独自の実行コンテキストがあり、この環境でコードが実行されると、変数オブジェクトのスコープ チェーンが作成され、順序付けされたアクセスが保証されます。変数オブジェクトに。

スコープ チェーンのフロント エンドは、現在のコード実行環境の変数オブジェクトであり、多くの場合、「アクティブ オブジェクト」と呼ばれます。オブジェクトに変数属性が含まれている場合、変数の検索は最初のチェーンのオブジェクトから開始されます。そうでない場合、検索は停止します。グローバル オブジェクトが見つかるまで上位スコープ チェーンの検索が続けられます:

スコープ チェーンの段階的な検索もプログラムのパフォーマンスに影響します。これが、変数スコープ チェーンの使用を避ける主な理由の 1 つです。グローバル変数。

閉店

基本概念

スコープはクロージャを理解するための前提条件です。クロージャとは、外部スコープ内の変数が現在のスコープ内で常にアクセスできることを意味します。

function createClosure(){
  var name = "jack";
  return {
    setStr:function(){
      name = "rose";
    },
    getStr:function(){
      return name + ":hello";
    }
  }
}
var builder = new createClosure();
builder.setStr();
console.log(builder.getStr()); //rose:hello
上記の例では、関数内に 2 つのクロージャが返されます。どちらのクロージャも外部スコープへの参照を保持しているため、外部関数内の変数はどこで呼び出されても常にアクセスできます。関数内で定義された関数は、外部関数のアクティブなオブジェクトを独自のスコープ チェーンに追加します。そのため、上記の例では、内部関数を通じて外部関数のプロパティにアクセスできます。プライベート変数をシミュレートします。

注: クロージャには追加の関数スコープがあるため (内部匿名関数は外部関数のスコープを持ちます)、過度に使用するとメモリ使用量が増加する可能性があります。

クロージャ内の変数

クロージャを使用する場合、スコープチェーンメカ​​ニズムの影響により、クロージャは内部関数の最後の値しか取得できません。これによる副作用として、内部関数がループ内にある場合、その値が取得されます。変数は常に最後の値です。


  //该实例不太合理,有一定延迟因素,此处主要为了说明闭包循环中存在的问题
  function timeManage() {
    for (var i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);
      },1000)
    };
  }
上記のプログラムでは、期待したように 1 ~ 5 の数字が入力されず、5 回すべて 5 が出力されます。別の例を見てみましょう:

function createClosure(){
  var result = [];
  for (var i = 0; i < 5; i++) {
    result[i] = function(){
      return i;
    }
  }
  return result;
}
createClosure()[0]() の呼び出しの戻り値は 5 ですが、createClosure()[4]() の戻り値は 5 のままです。上記の 2 つの例を通して、ループで内部関数を使用するときにクロージャが抱える問題がわかります。各関数のスコープ チェーンには外部関数 (timeManage、createClosure) のアクティブなオブジェクトが格納されるため、それらはすべて同じ変数を参照します。 i. 外部関数がリターンすると、このときの i の値は 5 なので、各内部関数 i の値も 5 になります。

では、この問題をどうやって解決すればいいのでしょうか?期待される結果を匿名ラッパー (匿名の自己実行関数式) 経由で強制的に返すことができます:

function timeManage() {
  for (var i = 0; i < 5; i++) {
    (function(num) {
      setTimeout(function() {
        console.log(num);
      }, 1000);
    })(i);
  }
}
または、クロージャ匿名関数で匿名関数の代入を返します:

function timeManage() {
  for (var i = 0; i < 10; i++) {
    setTimeout((function(e) {
      return function() {
        console.log(e);
      }
    })(i), 1000)
  }
}
//timeManager();输出1,2,3,4,5
function createClosure() {
  var result = [];
  for (var i = 0; i < 5; i++) {
    result[i] = function(num) {
      return function() {
        console.log(num);
      }
    }(i);
  }
  return result;
}
//createClosure()[1]()输出1;createClosure()[2]()输出2

无论是匿名包裹器还是通过嵌套匿名函数的方式,原理上都是由于函数是按值传递,因此会将变量i的值复制给实参num,在匿名函数的内部又创建了一个用于返回num的匿名函数,这样每个函数都有了一个num的副本,互不影响了。

闭包中的this

在闭包中使用this时要特别注意,稍微不慎可能会引起问题。通常我们理解this对象是运行时基于函数绑定的,全局函数中this对象就是window对象,而当函数作为对象中的一个方法调用时,this等于这个对象(TODO 关于this做一次整理)。由于匿名函数的作用域是全局性的,因此闭包的this通常指向全局对象window:

var scope = "global";
var object = {
  scope:"local",
  getScope:function(){
    return function(){
      return this.scope;
    }
  }
}

调用object.getScope()()返回值为global而不是我们预期的local,前面我们说过闭包中内部匿名函数会携带外部函数的作用域,那为什么没有取得外部函数的this呢?每个函数在被调用时,都会自动创建this和arguments,内部匿名函数在查找时,搜索到活跃对象中存在我们想要的变量,因此停止向外部函数中的查找,也就永远不可能直接访问外部函数中的变量了。总之,在闭包中函数作为某个对象的方法调用时,要特别注意,该方法内部匿名函数的this指向的是全局变量。

幸运的是我们可以很简单的解决这个问题,只需要把外部函数作用域的this存放到一个闭包能访问的变量里面即可:

var scope = "global";
var object = {
  scope:"local",
  getScope:function(){
    var that = this;
    return function(){
      return that.scope;
    }
  }
}
object.getScope()()返回值为local。

内存与性能

由于闭包中包含与函数运行期上下文相同的作用域链引用,因此,会产生一定的负面作用,当函数中活跃对象和运行期上下文销毁时,由于必要仍存在对活跃对象的引用,导致活跃对象无法销毁,这意味着闭包比普通函数占用更多的内存空间,在IE浏览器下还可能会导致内存泄漏的问题,如下:

 function bindEvent(){
  var target = document.getElementById("elem");
  target.onclick = function(){
    console.log(target.name);
  }
 }

上面例子中匿名函数对外部对象target产生一个引用,只要是匿名函数存在,这个引用就不会消失,外部函数的target对象也不会被销毁,这就产生了一个循环引用。解决方案是通过创建target.name副本减少对外部变量的循环引用以及手动重置对象:

 function bindEvent(){
  var target = document.getElementById("elem");
  var name = target.name;
  target.onclick = function(){
    console.log(name);
  }
  target = null;
 }

闭包中如果存在对外部变量的访问,无疑增加了标识符的查找路径,在一定的情况下,这也会造成性能方面的损失。解决此类问题的办法我们前面也曾提到过:尽量将外部变量存入到局部变量中,减少作用域链的查找长度。

总结:闭包不是javascript独有的特性,但是在javascript中有其独特的表现形式,使用闭包我们可以在javascript中定义一些私有变量,甚至模仿出块级作用域,但闭包在使用过程中,存在的问题我们也需要了解,这样才能避免不必要问题的出现。

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