乾貨!你一直想知道的關於JavaScript scope的一切

對於一個JavaScript初學者(甚至是有經驗的JavaScript開發者)而言,JavaScript語言中關於「域」(scope)的一些概念並非那麼直白或是容易理解的。javascript

由此,這篇文章旨在幫助那些在據說過諸如域(scope),閉包(closure),關鍵字this,命名空間(namespace),函數域(function scope),全局域(global scope),詞法做用域(lexical scope)以及公共域和私有域(public/private scope)等詞彙後,想要進一步學習JavaScript的朋友。java

但願這篇文章能夠幫助你找到下列問題的答案:node

  • 什麼是域?程序員

  • 什麼是全局域、本地域?編程

  • 什麼是命名空間以及其與域之間的不一樣?設計模式

  • 什麼是關鍵字以及它是如何受域影響的?數組

  • 什麼是功能域、詞法做用域?緩存

  • 什麼是閉包?安全

  • 什麼是公共域、私有域?閉包

  • 如何將上述概念融會貫通?

什麼是域?

在JavaScript裏,域指的是代碼當前的上下文語境。域能夠是公共定義的,也能夠是本地定義的。理解JavaScript中的域,是你寫出無可挑剔的代碼以及成爲更好的程序員的關鍵。

什麼是全局域?

在你開始寫一行JavaScript代碼的時候,你正處在咱們所說的全局域中。此時咱們定義一個變量,那它就被定義在全局域中:

// global scope
var name = 'Todd';

全局域是你最好的朋友,同時也是你最心悸的夢魘。學會控制各類域並不難,當你這麼作以後,你就不會再遇到有關全局域的問題(多發生在與命名空間衝突時)。你或許常常聽到有人說「全局域太糟糕了」,但卻未聽他們評判過箇中原因。其實,全局域並無那麼糟糕,由於你要在全局域當中創造能夠被其餘域所訪問的模塊和APIs,因此你必須學會揚長避短地使用它。

彷佛你們都喜歡如此寫jQuery代碼,是否是你也這麼幹呢:

jQuery('.myClass');

。。。這樣咱們正在公共域中訪問jQuery,咱們能夠把這種訪問稱之爲命名空間。命名名空間在某些條件下能夠理解爲域,但一般它指的是最上層的域。在上面的例子裏,jQuery做爲命名空間存在公共域中。jQuery 命名空間在全局域中被定義,全局域就是jQuery庫的命名空間,由於全部在命名空間中的東西都成爲這個命名空間的派生。

什麼是本地域?

本地域是指那些在全局域中定義的域。通常只能有一個全局域,定義其中的每個函數都有本身的本地域。任何定義在其它函數裏的函數都有一個鏈接那個外部函數的本地域。

假設我定義了一個函數,並在其中建立了幾個變量,那這些變量就屬於本地域。看下面的例子:

// 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是屬於本地域的,它沒有暴露給它的父域,所以它是未定義的。

 函數域

在JavaScript中全部的域都是而且只能是被函數域(function scope)所建立,它們不能被for/while循環或者if/switch表達式建立。New function = new scope - 僅此而已。一個簡單的例子來講明域的建立:

// Scope A
var myFunction = function () {
  // Scope B
  var myOtherFunction = function () {
    // Scope C
  };
};

建立新的域以及建立本地變量、函數、對象都是如此簡單。

詞法定義域

每當你看到一個函數在另外一個函數裏的時候,內部的那個函數能夠訪問外部的函數,這被稱做詞法定義域或是閉包 - 有時也被稱做靜態域。又來了,看下面這個例子:

// Scope A
var myFunction = function () {
  // Scope B
  var name = 'Todd'; // defined in Scope B
  var myOtherFunction = function () {
    // Scope C: `name` is accessible here!
  };
};

你會注意到 myOtherFunction 只是被簡單的定義一下並無被調用。調用順序也會對域中變量該如何反應起到做用,這裏我已經定義了一個函數而後在另外一個Console下面調用了它:

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,但卻歷來不是變量('Todd')自己。

 域鏈

域鏈給一個已知的函數創建了做用域。正如咱們所知的那樣,每個被定義的函數都有本身的嵌套做用域,同時,任何被定義在其餘函數中的函數都有一個本地域鏈接着外部的函數 - 這種鏈接被稱做鏈。這就是在代碼中定義做用域的地方。當咱們在處理一個變量的時候,JavaScript就會開始從最裏層的域向外查找直到找到要找的那個變量、對象或函數。

 閉包

閉包和詞法做用域很是相近。一個關於閉包如何工做的更好或者更實際的例子就是返回一個函數的引用。咱們能夠返回域中的東西,使得它們能夠被其父域所用。

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

AngularJS就爲其 $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 關鍵字,但不是咱們全部人都理解以及區別 this 在調用當中的變化。默認狀況下 this 值得是作外層的公共對象 - window( node.js 裏是 exports)。大概其看一下以不一樣方式調用函數時 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>
var toggleNav = function () {
  console.log(this); // this = <nav> element
};
nav.addEventListener('click', toggleNav, false);

這裏還有個問題,就算在同一個函數中,做用域也是會變,this 的值也是會變:

var nav = document.querySelector('.nav'); // <nav>
var toggleNav = function () {
  console.log(this); // <nav> element
  setTimeout(function () {
    console.log(this); // [object Window]
  }, 1000);
};
nav.addEventListener('click', toggleNav, false);

那這裏究竟發生了什麼?咱們新建立了一個不會從事件控制器調用的做用域,因此它也如咱們所預期的那樣,默認是指向 window 對象的。 若是咱們想要訪問這個 this 值,有幾件事咱們可讓咱們達到目的。可能之前你就知道了,咱們能夠用一個像 that 這樣的變量來緩存對 this 的引用:

var nav = document.querySelector('.nav'); // <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);

用 call,apply 和 bind 改變做用域

有時你會根據須要更改做用域。一個簡單的證實如何在循環中更改做用域:

var links = document.querySelectorAll('nav li');
for (var i = 0; i < links.length; i++) {
  console.log(this); // [object Window]
}

在這裏 this 值 不是指咱們的元素,咱們沒有調用任何東西或者改變做用域。讓咱們來看一下如何改變做用域(看上去咱們改變的是做用域,可是咱們真正在作的倒是更改函數被調用的上下文語境)。

.call() and .apply()

.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

You'll let .call() handle it and chain the method:

myFunction.call(scope); // invoke myFunction using .call()

.bind()

不一樣於上述方法,使用 .bind() 不會調用一個函數, 它只是在函數運行前綁定了一個值。ECMASCript5 當中才引入這個方法實在是太晚太惋惜了,由於它是如此的美妙。如你所知,咱們不能出傳遞參數給函數,就像這樣:

// 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);

這個函數不會執行,而且做用域能夠根據須要更改,可是參數仍是在等待被傳入。

私有域和公共域

在許多編程語言中,你將聽到關於公共域和私有域,在 JavaScript 裏沒有這樣的東西。可是咱們能夠經過像閉包同樣的東西來模擬公共域和私有域。

咱們能夠經過使用 JavaScript 設計模式好比模塊模式,來建立公共域和私有域。一個簡單的建立私有域的途徑就是把咱們的函數包裝進一個函數中。如咱們以前學到的,函數建立做用域來使其中的東西不可被全局域訪問:

(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

成功!咱們就此建立了一個私有域。可是若是我像讓這個函數變成公共的,要怎麼作呢?有一個很好的模式(被稱做模塊模式)容許咱們正確地處理函數做用域。這裏我在全局命名空間裏創建了一個包含我全部相關代碼的模塊:

// define module
var Module = (function () {
  return {
    myMethod: function () {
      console.log('myMethod has been called.');
    }
  };
})();

// call module + methods
Module.myMethod();

在這裏,return 的東西就是 public 方法返回的東西,它能夠被全局域訪問。咱們的模塊來關心咱們的命名空間,它能夠包含咱們想要任意多的方法在裏面:

// define module
var Module = (function () {
  return {
    myMethod: function () {

    },
    someOtherMethod: function () {

    }
  };
})();

// call module + methods
Module.myMethod();
Module.someOtherMethod();

那私有方法呢?這裏是不少開發者作錯的地方,他們把全部的函數都堆砌在全局域裏以致於污染了整個全局命名空間。可工做的函數代碼不必定非在全局域裏才行,除非像 APIs 這種要在全局域裏能夠被訪問的函數。這裏咱們來寫一個沒有被返回出來的函數:

var Module = (function () {
  var privateMethod = function () {

  };
  return {
    publicMethod: function () {

    }
  };
})();

這就意味着 publicMethod 能夠被調用,可是 privateMethod 則不行,由於它被域私有了!這些私有的函數能夠是任何你能想到的對象或方法。

可是這裏還有個有點擰巴的地兒,那就是任何在同一個域中的東西均可以訪問同一域中的其餘東西,就算在這兒函數被返回出去之後。也就是說,咱們的公共函數能夠訪問私有函數,因此私有函數依然能夠和全局域互動,可是不能被全局域訪問。

var Module = (function () {
  var privateMethod = function () {

  };
  return {
    publicMethod: function () {
      // has access to `privateMethod`, we can call it:
      // privateMethod();
    }
  };
})();

這種互動是充滿力量同時又保證了代碼安全。JavaScript中很重要的一塊就是保證代碼的安全,這就解釋了爲何咱們不能接受把全部的函數都放在公共域中,由於這樣的話,他們都被暴露出來很容易受到攻擊。

下面有個例子,返回了一個對象,用到了 public 和 private 方法:

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 () {

  };
})();

這裏咱們能夠藉助面向對象的方式來添加對函數的引用:

var Module = (function () {
  var _privateMethod = function () {

  };
  var publicMethod = function () {

  };
  return {
    publicMethod: publicMethod,
    anotherPublicMethod: anotherPublicMethod
  }
})();

BenGa0翻譯, 原文連接:http://toddmotto.com/everything-you-wanted-to-know-about-javascript-scope/

相關文章
相關標籤/搜索