1. 程式人生 > >JavaScript中scope介紹

JavaScript中scope介紹

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

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

希望這篇文章可以幫助你找到下列問題的答案:

  • 什麼是域?

  • 什麼是全域性域、本地域?

  • 什麼是名稱空間以及其與域之間的不同?

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

  • 什麼是功能域、詞法作用域?

  • 什麼是閉包?

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

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

什麼是域?

在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
  }
})();

相關推薦

JavaScriptscope介紹

對於一個JavaScript初學者(甚至是有經驗的JavaScript開發者)而言,JavaScript語言中關於“域”(scope)的一些概念並不是那麼直白或是容易理解的。由此,這篇文章旨在幫助那些在聽說過諸如域(scope),閉包(closure),關鍵字this,名稱空間(namespace),函式域(

Javascriptvoid介紹

Javascript中void是一個操作符,該操作符指定要計算一個表示式但是不返回值。 void 操作符用法格式如下: 1. javascript:void (expression) 2. javascript:void expression expression 是一個要計算的 Javascript 標

JavaScript 的函數介紹

所有 calculate 曾經 什麽 乘法 函數表達式 second 數字 brush 簡而言之函數只不過是一組執行某個操作的語句。函數可能會有一些輸入參數(在函數體中使用),並在執行後返回值。   JavaScript函數也具有這些特性,但它們不僅僅是常規函數。Java

javascript的DOM介紹(一)

item 檢測 turn 及其 篩選 層次 proto nbsp log 一、基礎知識點 1、DOM是文檔對象模型,是針對HTML和XML文檔的一個API(應用程序接口) 2、DOM描繪了一個層次化的節點數,允許開發人員進行添加,移除個修改等操作 3、IE瀏覽器中所有的DO

JavaScript常用變量介紹

meta def set 變量介紹 特殊 一次 常用 true 賦值 JavaScript聲明變量 JavaScript變量是來保存我們的數據的,想在JavaScrip中聲明一個變量可以通過以下方式: (1)通過var關鍵字聲明變量(2)可以聲明變量的同時給變量賦值(3)可

javascript function(){},new function(),new Function(),Function 簡單介紹

temp 簡單介紹 簡單 tor new struct 不用 color java 函數是JavaScript中很重要的一個語言元素,並且提供了一個function關鍵字和內置對象Function,下面是其可能的用法和它們之間的關系。 function使用方式 v

理解javaScript的作用域和上下文Understanding Scope and Context in JavaScript

譯者注:一直對於作用域和上下文感到很混亂,無意中看到這篇文章,覺得講得很好,故翻譯來與大家分享。翻譯不好之處,請大家多多指教。 原文連結:http://ryanmorr.com/understanding-scope-and-context-in-javascript/   前言部分,不做翻譯

正則表示式簡單介紹以及JavaScript的使用

什麼是正則 在常見的字串檢索或替換中,我們需要提供一種模式表示檢索或替換的規則。正則表示式使用單個字串來描述、匹配一系列符合某個句法規則的字串。 這裡只講解簡單的內容。 簡單的正則 /abc/ 匹配包含 “abc” 的字串,如 “abcd”

JavaScript的object轉換成number或string規則介紹

object自動轉換成string的規則: 1.如果object所屬類覆寫了toString()方法,則呼叫該方法。如果toString()呼叫的返回結果為Primitive(string、number、boolean、undefined、null),則將該Primi

JavaScript的memoization(memoizing) 技術介紹

memoization 一詞是Donald Michie 根據拉丁語memorandum杜撰的一個詞。相應的動詞、過去分詞、ing形式有memoiz、memoized、memoizing. Memoization 是 一種將函式返回值快取起來的方法,Memoizati

Javascript獲取時間new Date()詳細介紹

1、當前系統區域設定格式(toLocaleDateString和toLocaleTimeString)      例子:(new Date()).toLocaleDateString() + " " + (new Date()).toLocaleTimeString()      結果: 2008年1月29

JavaScript 的事件介紹以及相容

**JavaScript 中的事件介紹以及相容 事件型別 (1)事件的定義 指的是文件或者瀏覽器視窗中發生的一些特定互動瞬間。我們可以通過偵聽器(或者處理程式)來預定事件,以便事件發生的時候執行相應的程式碼。 ①事件型別:事件型別是一個用來說明發生什麼型別事件的字串。像滑鼠懸浮,

Javascript的isNaN函式的介紹與使用

NaN "not a number",出現這個數值比較少見,以至於我們可以不用理它。當運算無法返回正確數值時,就會返回"NaN"值。NaN值非常特殊,因為它“不是數字”,所以任何數跟它都不相等,甚至N

JavaScript的作用域鏈(scope chain)

第一篇部落格,略有手抖,不知道應該撿哪句說起。開博最重要的目的就是做一些自己的技術沉澱。之前不是不想開,是因為確實自己確實沒有積累,即使勉強寫出點什麼,我想大抵也是東拼西湊吧,這樣不如不幹。 幹,就像個樣子。分隔符---此為前話。 近幾日精讀JavaScript高階程式設

javascript字串string.replace 第二個引數是函式簡單介紹

例子: const camelizeRE = /-(\w)/g function camelize(str) { return str.replace(camelizeRE, toUp

簡單介紹 javascript __proto__ 屬性的原理

重要說明:本博已遷移到 石佳劼的部落格,有疑問請到 文章新地址 留言!!! 在 javascript 中我們會約定俗成,如果一個方法是被 new 出來使用的,那麼該方法名首字母通常會大寫,例如下面程式碼塊中的 Person。(我們也可以把 Person 看成

JavaScript的ArrayBuffer詳細介紹

相信每一個 javascript 學習者,都會去了解 JS 的各種基本資料型別,陣列就是資料的組合,這是一個很基本也十分簡單的概念,他的內容沒多少,學好它也不是件難事情。但是本文著重要介紹的並不是我們往常看到的 Array,而是 ArrayBuffer。 我寫的很多東西都是因為要完成某些特定的功能而刻意總結

漫談JavaScript的作用域(scope

什麼是作用域 程式的執行,離不開作用域,也必須在作用域中才能將程式碼正確的執行。 所以作用域到底是什麼,通俗的說,可以這樣理解:作用域就是定義變數的位置,是變數和函式的可訪問範圍,控制著變數和函式的可見性和生命週期。 而JavaScript中的作用域,在ES6之前和ES6之後,有兩種不同的情況。 ES6之前,

JavaScript閉包的使用和各種繼承介紹

一、什麼是閉包?     (1)閉包的概念:a、閉包就是函式巢狀時,讓區域性變數變成自由變數的環境,是一種讓區域性變數進化的方式。                      b、定義在一個函式內部的函式。 &

JavaScriptthis的用法

屬於 指向 cti 用法 func 生成 純粹 ava 構造函數 this的使用環境有以下幾種 1、純粹的函數調用 純粹的函數調用屬於函數的最常用的用法,屬於全局性調用。在全局函數中,this等於window。 2、作為對象方法的調用 當函數不是全局調用,而是作為某個對象