在學習js的過程對閉包什麼的,理解很差,偶然搜到這篇文章。豁然開朗,隨翻譯。javascript
Javacript 中有一系列做用域的概念。對於新的JS的開發人員沒法理解這些概念,甚至一些經驗豐富的開發者也未必能。這篇文章主要目的幫助理解JavaScript中的一些概念如:scope,closure, this, namespace, function scope, global scope, lexical scope and public/private scope. 但願從這篇文章中能回答以下的問題:java
什麼是做用域(scope)?編程
什麼是全局(Global)和局部(Local)做用域?windows
什麼是命名空間和做用域的區別?設計模式
什麼是this關鍵字且做用域對其的影響?數組
什麼是函數做用域、詞彙做用域?緩存
什麼是閉包?安全
什麼是公有和私有做用域?閉包
如何理解和建立上述內容?app
在JavaScript中,做用域一般是指代碼的上下文(context)。可以定義全局或者局部做用域。理解JavaScript的做用域是編寫強健的代碼和成爲一個好的開發者的前提。你須要掌握在那裏獲取變量和函數,在那裏可以可以改變你的代碼上下文的做用域以及如何可以編寫快速和可讀性強以及便於調試的代碼。
想象做用域很是簡單,咱們在做用域A仍是做用域B?
在寫第一行JavaScript代碼以前,咱們處在全局做用域中。此時咱們定義一個變量,一般都是全局變量。
// global scopevar name = 'Todd';
全局做用域便是你的好友又是你的噩夢。學習控制做用域很簡單,學會後使用全局變量就不會遇到問題(一般爲命名空間衝突)。常常會聽到大夥說 「全局做用域很差」,可是從沒有認真想過爲何。不是全局做用域很差,而是使用問題。在建立跨做用域Modules/APIs的時候,咱們必須在不引發問題的狀況下使用它們。
jQuery('.myClass');
...咱們正在全局做用域中獲取jQuery,咱們能夠把這種引用稱爲命名空間。命名空間一般是指做用域中能夠交換word,可是其一般引用更高級別的做用域。在上面的例子中,jQuery 在全局做用域中,也稱爲命名空間。jQuery 做爲命名空間定義在全局做用域中,其做爲jQuery庫的命令空間,庫中的全部內容成爲命名空間的子項(descendent )。
局部做用域一般位於全局做用域後。通常來講,存在一個全局做用域,每一個函數定義了本身的局部做用域。任何定義於其餘函數內部的函數都有一個局部做用域,該局部做用域連接到外部函數。
若是定義了一個函數並在裏面建立變量,那麼這些變量就是局部變量。例如:
// Scope A: Global scope out here
var myFunction = function () {
// Scope B: Local scope in here};
任何的局部做用變量對全局變量來講是不可見的。除非對外暴露。如在新的做用域內定義了函數和變量,他們爲當前新做用域內的變量,不可以在當前做用域外被訪問到。下面爲一個簡單的說明示例:
var myFunction = function () {
var name = 'Todd';
console.log(name); // Todd};
// Uncaught ReferenceError: name is not defined
console.log(name);
變量name爲局部變量,沒有暴露給父做用域,所以出現not defined。
JavaScript 中函數域爲最小域範圍。for與while循環或者if和switch都不能構建做用域。規則就是,新函數新域。一個建立域的簡單示例以下:
// Scope A
var myFunction = function () {
// Scope B
var myOtherFunction = function () {// Scope C};};
很是方便的建立新的域和本地變量、函數和對象。
當遇到一個函數嵌套到另外一函數中,內部函數可以訪問外部函數的做用域,那麼這種方式叫作詞彙做用域(Lexical Socpe)或者閉包,也稱爲成爲靜態做用域。最能說明該問題的示例以下:
// Scope A
var myFunction = function () {
// Scope B
var name = 'Todd'; // defined in Scope B
var myOtherFunction = function () {
// Scope C: `name` is accessible here!};
};
這裏只是簡單的定義了myOtherFunction,並無調用。這種調用順序也會影響變量的輸出。這裏我在另外一控制檯中再定義和調用一個函數。
var myFunction = function () {
var name = 'Todd';
var myOtherFunction = function () {
console.log('My name is ' + name);
};
console.log(name);
myOtherFunction(); // call function
};
// Will then log out:// `Todd`
// `My name is Todd`
詞彙做用域用起來比較方便,任何父做用域中定義的變量、對象和函數在其域做用鏈中均可以使用。例如:
var name = 'Todd';
var scope1 = function () {
// name is available here
var scope2 = function () {// name is available here too
var scope3 = function () {// name is also available here!};
};
};
惟一須要注意的事情是詞彙域不後項起做用,下面的方式詞彙域是不起做用的:
// name = undefined
var scope1 = function () {
// name = undefined
var scope2 = function () {// name = undefined
var scope3 = function () {var name = 'Todd'; // locally scoped};
};
};
能返回對name的引用,可是永遠也沒法返回變量自己。
函數的做用域由做用域鏈構成。咱們知道,每一個函數能夠定義嵌套的做用域,任何內嵌函數都有一個局部做用域鏈接外部函數。這種嵌套關係咱們能夠稱爲鏈。域通常由代碼中的位置決定。當解釋(resolving)一個變量,一般從做用域鏈的最裏層開始,向外搜索,直到發現要尋找的變量、對象或者函數。
閉包和詞法域( Lexical Scope)很像。返回函數引用,這種實際應用,是一個能夠用來解釋閉包工做原理的好例子。在咱們的域內部,咱們能夠返回對象,可以被父域使用。
var sayHello = function (name) {
var text = 'Hello, ' + name;
return function () {
console.log(text);};
};
這裏咱們使用的閉包,使得咱們的sayHello內部域沒法被公共域訪問到。單獨調用函數並不做任何操做,由於其單純的返回一個函數。
sayHello('Todd'); // nothing happens, no errors, just silence...
函數返回一個函數,也就意味着須要先賦值再調用:
var helloTodd = sayHello('Todd');
helloTodd(); // will call the closure and log 'Hello, Todd'
好吧,欺騙你們感情了。在實際狀況中可能會遇到以下調用閉包的函數,這樣也是行的通的。
sayHello2('Bob')(); // calls the returned function without assignment
Angular js 在$compile方法中使用上面的技術,能夠將當前引用域傳入到閉包中
$compile(template)(scope);
意味着咱們可以猜出他們的代碼(簡化)應該以下:
var $compile = function (template) {
// some magic stuff here// scope is out of scope, though...
return function (scope) {// access to `template` and `scope` to do magic with too};
};
閉包並不必定須要返回函數。單純在中間詞彙域量的範圍外簡單訪問變量就創造了一個閉包。
根據函數被觸發的方式不同,每一個做用域能夠綁定一個不一樣的this值。咱們常用this,可是咱們並非都瞭解其具體指代什麼。 this默認是執行最外層的全局對象,windows對象。咱們可以很容易的列舉出不一樣觸發函數綁定this的值也不一樣:
var myFunction = function () {
console.log(this); // this = global, [object Window]};
myFunction();
var myObject = {};
myObject.myMethod = function () {
console.log(this); // this = Object { myObject }};
var nav = document.querySelector('.nav'); // <nav class="nav">
var toggleNav = function () {
console.log(this); // this = <nav> element};
nav.addEventListener('click', toggleNav, false);
在處理this值的時候,也會遇到問題。下面的例子中,即便在相同的函數內部,做用域和this值也會不一樣。
var nav = document.querySelector('.nav');
// <nav class="nav">
var toggleNav = function () {
console.log(this); // <nav> element
setTimeout(function () {
console.log(this); // [object Window]}, 1000);
};
nav.addEventListener('click', toggleNav, false);
發生了什麼?咱們建立了一個新的做用域且沒有在event handler中觸發,因此其獲得預期的windows對象。若是想this值不受新建立的做用域的影響,咱們可以採起一些作法。之前可能也你也見過,咱們使用that建立一個對this的緩存引用並詞彙綁定:
var nav = document.querySelector('.nav'); // <nav class="nav">
var toggleNav = function () {
var that = this;
console.log(that); // <nav> element
setTimeout(function () {
console.log(that); // <nav> element}, 1000);
};
nav.addEventListener('click', toggleNav, false);
這是使用this的一個小技巧,可以解決新建立的做用域問題。
有時候,須要根據實際的需求來變化代碼的做用域。一個簡單的例子,如在循環中如何改變做用域:
var links = document.querySelectorAll('nav li');
for (var i = 0; i < links.length; i++) {
console.log(this); // [object Window]}
這裏的this並無指向咱們的元素,由於咱們沒有觸發或者改變做用域。咱們來看看如何改變做用域(看起來咱們是改變做用域,其實咱們是改變調用函數執行的上下文)。
.call()和.apply()方法很是友好,其容許給一個函數傳做用域來綁定正確的this值。對上面的例子咱們經過以下改變,可使this爲當前數組裏的每一個元素。
var links = document.querySelectorAll('nav li');
for (var i = 0; i < links.length; i++) {
(function () {
console.log(this);
}).call(links[i]);}
可以看到剛將數組循環的當前元素經過links[i]傳遞進去,這改變了函數的做用域,所以this的值變爲當前循環的元素。這個時候,若是須要咱們可使用this。咱們既可使用.call()又可使用.apply()來改變域。可是這二者使用仍是有區別的,其中.call(scope, arg1, arg2, arg3)輸入單個參數,而.apply(scope, [arg1, arg2])輸入數組做爲參數。
很是重要,須要注意的事情是.call() or .apply()實際已經已經取代了以下調用函數的方式調用了函數。
myFunction(); // invoke myFunction
可使用.call()來鏈式調用:
myFunction.call(scope); // invoke myFunction using .call()
和上面不同的是,.bind()並不觸發函數,它僅僅是在函數觸發前綁定值。很是遺憾的是其只在 ECMASCript 5中才引入。咱們都知道,不能像下面同樣傳遞參數給函數引用:
// works
nav.addEventListener('click', toggleNav, false);
// will invoke the function immediately
nav.addEventListener('click', toggleNav(arg1, arg2), false);// works
nav.addEventListener('click', toggleNav, false);
// will invoke the function immediately
nav.addEventListener('click', toggleNav(arg1, arg2), false);
經過在內部建立一個新的函數,咱們可以修復這個問題(譯註:函數被當即執行):
nav.addEventListener('click', function () {
toggleNav(arg1, arg2);}, false);
可是這樣的話,咱們再次建立了一個沒用的函數,若是這是在循環中綁定事件監聽,會影響代碼性能。這個時候.bind()就派上用場了,在不須要調用的時候就能夠傳遞參數。
nav.addEventListener('click', toggleNav.bind(scope, arg1, arg2), false);
函數並沒被觸發,scope能夠被改變,且參數在等着傳遞。
在許多的編程語言中,存在public和private的做用域,可是在javascript中並不存在。可是在JavaScript中經過閉包來模擬public和private的做用域。
使用JavaScript的設計模式,如Module模式爲例。一個建立private的簡單方式將函數內嵌到另外一個函數中。如咱們上面掌握的,函數決定scope,經過scope排除全局的scope:
(function () {// private scope inside here})();
而後在咱們的應用中添加一些函數:
(function () {
var myFunction = function ()
{// do some stuff here};
})();
這時當咱們調用函數的時候,會超出範圍。
(function () {var myFunction = function () {
// do some stuff here};
})();
myFunction(); // Uncaught ReferenceError: myFunction is not defined
成功的建立了一個私有做用域。那麼怎麼讓函公有呢?有一個很是好的模式(模塊模式)容許經過私有和公共做用域以及一個object對象來正確的設定函數做用域。暫且將全局命名空間稱爲Module,裏面包含了全部與模塊相關的代碼:
// define module
var Module = (function () {
return {myMethod: function () {
console.log('myMethod has been called.');}};
})();
// call module + methods
Module.myMethod();
這兒的return 語句返回了公共的方法,只有經過命名空間纔可以被訪問到。這就意味着,咱們使用Module 做爲咱們的命名空間,其可以包含咱們須要的全部方法。咱們能夠根據實際的需求來擴展咱們的模塊。
// define module
var Module = (function () {
return {myMethod: function () {},
someOtherMethod: function () {}};})();
// call module + methods
Module.myMethod();
Module.someOtherMethod();
那私有方法怎麼辦呢?許多的開發者採起錯誤的方式,其將全部的函數都至於全局做用域中,這致使了對全局命名空間污染。 經過函數咱們能避免在全局域中編寫代碼,經過API調用,保證能夠全局獲取。下面的示例中,經過建立不返回函數的形式建立私有域。
var Module = (function () {
var privateMethod = function () {};
return {
publicMethod: function () {}};})();
這就意味着publicMethod 可以被調用,而privateMethod 因爲私有做用域不能被調用。這些私有做用域函數相似於: helpers, addClass, removeClass, Ajax/XHR calls, Arrays, Objects等。
下面是一個有趣事,相同做用域中的對象只能訪問相同的做用域,即便有函數被返回以後。這就意味咱們的public方法可以訪問咱們的private方法,這些私有方法依然能夠起做用,可是不可以在全局左右域中訪問。
var Module = (function () {
var privateMethod = function () {};
return {publicMethod: function () {
// has access to `privateMethod`, we can call it:
// privateMethod();}};})();
這提供了很是強大交互性和安全性機制。Javascript 的一個很是重要的部分是安全性,這也是爲何咱們不能將全部的函數放在全局變量中,這樣作易於被攻擊。這裏有個經過public和private返回Object對象的例子:
var Module = (function () {
var myModule = {};
var privateMethod = function () {};
myModule.publicMethod = function () {};
myModule.anotherPublicMethod = function () {};
return myModule; // returns the Object with public methods})();
// usage
Module.publicMethod();
一般私有方法的命名開頭使用下劃線,從視覺上將其與公有方法區別開。
var Module = (function () {
var _privateMethod = function () {};
var publicMethod = function () {};})();
當返回匿名對象的時候,經過簡單的函數引用賦值,Module能夠按照對象的方式來用。
var Module = (function ()
{var _privateMethod = function () {};
var publicMethod = function () {};
return {
publicMethod: publicMethod,anotherPublicMethod: anotherPublicMethod}
})();
Happy scoping!