javascript理解之變量作用域與閉包

何爲閉包

閉包是指能夠訪問自由變量的函數 (變量在本地使用,但在閉包中定義)。換句話說,定義在閉包中的函數可以“記憶”它被創建時候的環境。

函數作用域與聲明提前

var scope= "global scope";
function f(){
  console.log(scope); //undefined,並沒有出現"global scope"
  var scope="local scope";
  console.log(scope);//local scope
}

爲什麼會這樣呢?
Javascript的函數作用域是指在函數內聲明的所有變量在函數體內始終是可見的,這個特性也被叫做“提前聲明”。
即Javascript函數裏所有聲明的變量(但不涉及賦值)都被提前至函數體的頂部。
上面的代碼類似於

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

作用域鏈

首先,每一段代碼(全局代碼或函數)都有一個與之關聯的作用域鏈(scope chain),這個作用域鏈可以是一個對象列表或者鏈表,這組對象定義了這段代碼“作用域中”的變量。
當javscript需要查找變量x的值時候(變量解析(variable resolution)),它會從鏈中的第一個對象開始查找,如果這個對象有名爲x的屬性,就會使用這個x的值;否則,找鏈中的下一個對象;以此類推,直到最頂端,如果仍然沒有找到,就拋出引用錯誤(ReferenceError)異常。

在Javascript的最頂層代碼,作用域鏈由一個全局對象組成。
在不包含嵌套的函數體內,作用域鏈上有兩個對象:第一個是定義的函數參數(lenth, caller, arguments… )和局部變量的對象;第二個是全局對象。
這裏寫圖片描述
在一個嵌套的函數體內,作用域鏈上至少有三個對象。
這裏寫圖片描述
對於嵌套函數而言,每次調用外部函數,內部函數又會重新定義一遍。
因爲雖然。內部函數的代碼不同,但是作用域鏈發生了變化。

何爲閉包

各種專業文獻上的”閉包”(closure)定義非常抽象,很難看懂。我的理解是,閉包就是能夠讀取其他函數內部變量的函數。
由於在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成”定義在一個函數內部的函數”。
所以,在本質上,閉包就是將函數內部和函數外部連接起來的一座橋樑。

用途一:外部函數可以讀取內部函數的變量

  function f1(){
    var n=999;
    function f2(){
      alert(n); 
    }
    return f2;
  }
  var result=f1();
  result(); // 999

這裏外部函數可以讀取f1()中的n,是因爲內部函數f2()可以讀取f1的局部變量,通過返回f2,f2的作用域鏈有n,因此可以獲取n的值。
我的理解:
這裏寫圖片描述

用途二:使內部函數的變量一直在內存中

var uniqueInteger = (function() {
    var counter = 0;
    return function() {
        return counter++;
    }
}());

這裏寫圖片描述

實用的閉包

一般說來,可以使用只有一個方法的對象的地方,都可以使用閉包。

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */

在一個Counter裏面,increment,decrement,value共享作用域環境,即可以訪問同一個privateCounter和changeBy(),這樣就可以實現一個簡單的技術器。
這也是閉包內存共享的一個優點。
而Counter1和Counter2 的創建環境不一樣,作用域鏈也不同,訪問的privateCounter和changeBy()在不同的內存地址。
每次調用 makeCounter() 函數期間,其環境是不同的。每次調用中, privateCounter 中含有不同的實例。

在循環中創建閉包:一個常見錯誤

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }

}

setupHelp(); ////每次都停留在'Your age (you must be over 16)'
    //我們想要的效果
    document.getElementById(helpText[0].id).onfocus = function() {
        showHelp(helpText[0].help);
    };
    document.getElementById(helpText[1].id).onfocus = function() {
        showHelp(helpText[1].help);
    };
    document.getElementById(helpText[2].id).onfocus = function() {
        showHelp(helpText[2].help);
    };
    //實際效果
    document.getElementById(helpText[0].id).onfocus = function() {
        showHelp(helpText[2].help);
    };
    document.getElementById(helpText[1].id).onfocus = function() {
        showHelp(helpText[2].help);
    };
    document.getElementById(helpText[2].id).onfocus = function() {
        showHelp(helpText[2].help);
    };

該問題的原因在於賦給 onfocus 是閉包(setupHelp)中的匿名函數而不是閉包對象;在閉包(setupHelp)中一共創建了三個匿名函數(這裏指的匿名函數是說的getElenmentById(..).onfocus() =fucntion (){…})
但是它們都共享同一個環境(item)。在 onfocus 的回調被執行時,循環早已經完成,且此時 item 變量(由所有三個閉包所共享)已經指向了 helpText 列表中的最後一項。

理解:執行setupHelp()函數創建函數環境,調用getElenmentById(..).onfocus()的時候實質上都是對同一環境中的item進行修改。
在初始化環境的時候,showHelp(helpText[2].help)就已經執行完畢了。
三次循環先執行結束,實際上在執行onfocus() 的時候,傳入showHelp的都是都是對 helpText[2].help了。

解決這個問題的一種方案是使onfocus指向一個新的閉包對象。

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

最後,使用閉包要慎重的

因爲涉及到佔用內存,因爲每次生成一個函數對象就會在內存中開闢一定的空間來存儲它的環境,因此,適當的使用閉包。

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}
var newobj1=MyObject('x','hello');
var newobj2=MyObject('y','javascript');
newobj1.getName()==newobj2.getName();//false

這樣寫的壞處就是,每次構造MyObject的時候都會爲getName()和getMessage()劃分新的內存,這樣是很不划算的。
而把他寫成下面的形式就好了。
因爲原型中的方法指向同一片內存(突然在想可不可以把prototype理解爲指針,它指向的區域放了原型函數以及用戶定義的原型函數)

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};
var newobj1=MyObject('x','hello');
var newobj2=MyObject('y','javascript');
newobj1.getName()==newobj2.getName();//true

這篇文章參考了《javascript權威指南》
以及https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
以及http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html
務必把閉包弄清楚

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章