帶你走進JS做用域(Scope)的世界

該文章是直接翻譯國外一篇文章,關於做用域(Scope)。
都是基於原文處理的,其餘的都是直接進行翻譯可能有些生硬,因此爲了行文方便,就作了一些簡單的本地化處理。
同時也新增了本身的理解和對應的思考過程,若有不對,請在評論區指出
若是想直接根據原文學習,能夠忽略此文。javascript

若是你以爲能夠,請多點贊,鼓勵我寫出更精彩的文章🙏。
若是你感受有問題,也歡迎在評論區評論,三人行,必有我師焉
前端

TL;DR

  • 概念介紹
  • 最少訪問原則
  • JS中的做用域
  • 全局做用域
  • 局部做用域
  • 塊級聲明
  • 上下文(Context)
  • 執行上下文(Execution Context)
  • 詞法做用域(Lexical Scope)
  • 閉包
  • 公共做用域和私有做用域
  • 經過.call(), .apply() and .bind()修改上下文

概念介紹

在JS中有一個比較特殊的特性:做用域(Scope)。儘管做用域這個概念對於許多新手不是很容易能掌握,可是咱們經過一些簡單的示例來,來解釋這些概念。java

Scope可以讓你的代碼在運行的時候,有權訪問一些變量函數對象。換句話說,做用域決定:你所寫代碼中可以訪問哪些變量或者其餘資源數據。編程

最少訪問原則

在你的代碼中如何才能夠實現讓變量「私有化」,換句話說,如何讓變量成爲受限的,而不是隨處可見的,這是Scope所關注的點。經過Scope咱們可讓代碼變的更加安全。而在電腦安全策略中一個很重要的原則就是只有擁有對應權限的用戶才能夠訪問對應變量。而這個原則一樣適用於編程。在大多數編程語言中,咱們稱其爲做用域設計模式

JS中的做用域

在JS中存在兩類做用域:數組

  • 全局做用域
  • 局部做用域

在函數中定義的變量稱爲局部做用域,在函數外包定義的變量稱爲全局做用域。函數在調用時,會新建一個做用域。瀏覽器

全局做用域

當你在一個文檔中開始寫代碼的時候,其實已經位於全局做用域範圍以內了。而貫穿整個JS文檔,有且只有一個全局做用域。若是一個變量定義在函數體以外,那這個變量確定是一個全局做用域。安全

var name = '北宸';
複製代碼

定義在全局做用域中的變量能夠在其餘做用域中被獲取或者修改。閉包

var name = '北宸';

console.log(name); // '北宸'

function logName() {
    console.log(name); // 'name' 可以被獲取也能夠被修改
}

logName(); // '北宸'
複製代碼

局部做用域

在函數中定義的變量是屬於局部做用域的。而且,每次調用函數生成的局部做用域也是不同的。也就是說,相同名字的變量能夠出如今不一樣的函數中。這是由於,這些變量是和他們本身的做用域掛鉤的,每個函數被調用,都生成新的局部做用域,而且對其餘函數的做用域沒有訪問權限。app

// 全局做用域
function someFunction() {
    // 局部做用域#1
    function someOtherFunction() {
        // 局部做用域#2
    }
}

// 全局做用域
function anotherFunction() {
    // 局部做用域#3
}
// 全局做用域
複製代碼

塊級聲明

ifswitch或者是for循環、while被稱爲塊級聲明。他們和函數不一樣,經過他們包裹的代碼,不會生成新的做用域。在塊級聲明中定義的變量仍是屬於塊級聲明所在做用域範圍以內。

if (true) {
    // 不會新增做用域
    var name = '北宸'; // name 仍是屬於全局做用域
}

console.log(name); // '北宸'
複製代碼

ECMAScript 6新添了letconst關鍵字。而該關鍵字能夠替代var的使用。

var name = '北宸';

let likes = '南蓁';
const skills = '我是一個萌萌噠的漢子';
複製代碼

var關鍵字不一樣的是:letconst關鍵字能夠在塊級聲明中定義變量,從而生成一個新的局部做用域

if (true) {
    
    // if語句,不會生成做用域t

    // 經過var定義的變量屬於全局做用域範圍
    var name = '北宸';
    // 局部做用域
    let likes = '南蓁';
    // 局部做用域
    const skills = '我是一個萌萌噠的漢子';
}

console.log(name); // '北宸'
console.log(likes); // Uncaught ReferenceError: likes is not defined
console.log(skills); // Uncaught ReferenceError: skills is not defin
複製代碼

讓咱們分析一波,咱們經過斷點跟蹤,發現經過letconst在塊級聲明中定義的變量,它的存放方式不用var定義的變量的存放方式是不同的。

name是掛載在 Window上也就是咱們說的全局做用域( Global)。

全局做用域的生存時間是貫穿整個項目,而局部做用域是伴隨函數的生命週期存在而存在。

上下文(Context)

許多開發者對做用域(Scope)和上下文(Context)不是很容易區分,老是認爲他們是一個東西。可是實際上他們是不同的。做用域是咱們上面討論的那樣,說的直白一點,就是數據權限問題。而上下文指代this的值。

Scope側重於變量的可見性,而Context指向this的值。咱們能夠經過一些方法來修改上下文。而在全局做用域中,上下文恆等於Window(在瀏覽器環境)

// Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
console.log(this);

function logFunction() {
    console.log(this);
}
// Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
// js中this的指向是在調用時候,肯定的
logFunction(); 

複製代碼

若是函數定義在對象中,而且在對象做用域範圍中調用,this就指向對象

class User {
    logName() {
        console.log(this);
    }
}

(new User).logName(); // User {}
複製代碼

Notice:若是經過new關鍵字來調用函數,context將會被賦值爲函數的實例。

function logFunction() {
    console.log(this);
}

new logFunction(); // logFunction {}
複製代碼

在嚴格模式Strict Mode下調用函數,context的值爲undefined

執行上下文(Execution Context)

爲了避免被上文中所講的所影響,須要特別指出執行上下文(Execution Context)中的context指向的是做用域(Scope),而不是上下文(context)。這是一個讓人很容易誤會的名字,可是這是JS的規範或者是語法命名,咱們只能去適應他。

JS是一個單線程的語言,因此在同一時刻,只能執行一個任務。而剩餘的任務,就會在執行環境(Execution Context)中以隊列的形式等待。正如上文說的,當JS編譯器開始處理代碼的時候,上下文(做用域)默認爲全局做用域。該全局上下文被填充執行上下文(Execution Context)中,做爲第一個上下文,隨後開始執行對應代碼。

隨後,每當函數被調用,新建的上下文將會被以隊列的形式追加到執行上下文中(這個追加過程是在函數調用的瞬間完成的)。另一個函數調用,也會周而復始的執行如上追加操做。

每一個函數建立屬於本身的執行上下文。

當JS編譯器處理完某個上下文中的代碼時,該上下文將會被執行上下文移除,同時執行上下文中的current context指向被移除上下文的父級上下文。JS編譯器老是處理位於執行堆棧頂層(這其實是代碼中做用域的最內層 )的上下文。

全局上下文只有一個,而函數上下文能夠存在多個

執行上下文存在兩個階段:構建執行

構建階段

構建階段是處於函數被調用可是還未執行的時候。將依次發生以下過程:

  • 生成變量(活動)對象
  • 構建做用域鏈
  • 爲上下文(this)賦值

變量(活動)對象

變量對象也被稱爲活動對象,它包含很全部在執行上下文的特定分支中定義的變量,函數還有其餘的聲明。當函數被調用,JS編譯器就會掃描函數中的全部資源,包括函數參數,變量還有其餘聲明。而後將全部的資源打包到一個對象中,這個對象就是變量對象

'variableObject': {
    // 包含 函數參數, 內部變量 和函數聲明
}

複製代碼

做用域鏈

在執行上下文的構建階段,做用域鏈的建立在變量對象以後。做用域鏈包含變量對象。做用域鏈用於查找和定位變量。當代碼中須要某個變量時,JS老是從代碼的最內層開始查找,若是在當前做用域中沒有找到,就繼續向父級做用域查找,周而復始,直到查找定位到變量定義的做用域。經過查找變量的機制可知,做用域鏈能夠簡單定義爲一個對象,在對象內包含了表明其執行上下文的變量對象,還有該執行上下文的父級變量對象。而父級變量對象又能夠包含父級的父級的變量對象,直到某一級的父級變量對象爲null時中止。這裏涉及到做用域鏈的查找機制,從另一個角度分析,做用域鏈是查找頂級變量對象,而JS中對象最後的歸宿都是null。這一點能夠參考理解JS中的原型(Prototypes)這篇文章。(也算是從另一個角度來考慮做用域鏈)

'scopeChain': {
    // 包含它本身的變量對象和表明其父級執行上下文的變量對象
}
複製代碼

執行上下文對象

執行上下文能夠簡單的經過以下對象進行抽象表示:

executionContextObject = {
    'scopeChain': {}, //包含其自身的變量對象和其父級的執行上下文的變量對象 
    'variableObject': {}, // 包含函數參數,內部變量,還有方法定義
    'this': valueOfThis
}
複製代碼

代碼執行階段

在執行上下文的第二階段爲代碼執行階段,其餘的值被賦值,還有代碼最終執行

詞法做用域(Lexical Scope)

詞法做用域說的是,在一個函數的做用域中定義一個子函數,該子函數擁有對外層函數變量和其餘資源的訪問權限。也就是說,子函數在詞法上是與外層函數的執行上下文耦合的。詞法做用域有時候也被稱爲靜態做用域(Static Scope)

function grandfather() {
    var name = 'Hammad';
    // likes 在此處不能被訪問
    function parent() {
        // name 在此處能被訪問
        // likes 在此處不能被訪問
        function child() {
            //做用域鏈的最內層 
            // name 在此處能被訪問
            var likes = 'Coding';
        }
    }
}
複製代碼

Notice:看上面的例子咱們發現,詞法做用域是向前可見的,也就說在父級中定義的變量可以在子級執行上下文中訪問。例如,name。可是,詞法做用域是向後不可見的,子級定義的變量不能夠在父級執行上下文中訪問。

也演變出一個變量查找規則,若是在不一樣的執行上下文存在相同的變量,而JS引擎在定位變量是按着由上到下的順序遍歷執行堆棧。在執行堆棧中存在多個同名變量,在最內層函數中(執行堆棧最頂層)擁有最高的訪問權限。(在最內層執行上下文中訪問變量)

閉包(Closures)

閉包的概念相似於詞法做用域。當一個內層函數嘗試訪問外層函數的做用域鏈時,閉包產生了。這意味着變量位於詞法做用域以外。閉包能訪問內層函數本身的做用域鏈,它父級的做用域鏈和全局做用域。

閉包不只能訪問在外層函數定義的變量,並且能夠訪問外層函數的參數列表。

閉包也能夠在外層函數被執行以後繼續訪問外層函數的變量。也意味着,被返回的函數擁有訪問外層函數的全部資源。

當一個函數調用時,返回了一個內層函數,返回的內層函數不會立馬被調用。你必須用一個變量去接收被返回的內層函數的引用。而且將接收返回值的變量做爲函數去調用。

function greet() {
    name = '北宸';
    return function () {
        console.log('Hi ' + name);
    }
}

greet(); // 只是一個簡單的函數調用

// 返回的內層函數被greetLetter接收
greetLetter = greet();

 // 用調用函數的方式來處理變量
greetLetter(); // 'Hi 北宸'

複製代碼

咱們也可不用經過變量來接收返回的內層函數,直接利用()()調用:

function greet() {
    name = '北宸';
    return function () {
        console.log('Hi ' + name);
    }
}

greet()(); // 'Hi 北宸'

複製代碼

若是想詳細瞭解,如何構建和使用閉包,能夠參考JS閉包(Closures)瞭解一下

公共做用域和私有做用域

在許多其餘編程語言中,你能夠在class中設置屬性和方法爲publicprivate或者是protected的做用域。以下是PHP的使用方式

// 公共做用域
public $property;
public function method() {
  // ...
}

// 私有做用域
private $property;
private function method() {
  // ...
}

//受保護做用域
protected $property;
protected function method() {
  // ...
}
複製代碼

經過對函數使用方式進行限定,使得免受一些沒必要要的訪問。可是在JS中,沒有像publicprivate的關鍵詞去限制變量。可是咱們能夠經過閉包模擬這種特性。爲了可以將咱們的代碼與全局做用域分離,咱們須要封裝以下的函數格式:

(function () {
  // 私有做用域
})();
複製代碼

函數後面的括號表示,在函數定義的完成就告訴JS編譯器當即調用該函數。咱們能夠在函數中定義一些在外部沒法獲取的變量和方法。若是咱們想讓其中定義的變量,部分對外部可見,咱們能夠經過閉包實現。而封裝數據的方式也被稱爲Module Pattern(設計模式中的一種)。

模塊模式(Module Pattern)

var Module = (function() {
    function privateMethod() {
        // 私有
    }

    return {
        publicMethod: function() {
            // 此處對私有變量擁有訪問權限
        }
    };
})();
複製代碼

模塊模式經過利用閉包,返回了一個匿名對象,在該對象中就是對應模塊能被外界訪問的公共(public)屬性,而公共屬性或者方法具備對模塊中私有屬性的訪問權限。

在項目開發或者模塊定義中,咱們能夠將私有屬性用_開頭的變量與公共屬性作區分。

var Module = (function () {
    function _privateMethod() {
        // do something
    }
    function publicMethod() {
        // do something
    }
    return {
        publicMethod: publicMethod,
    }
})();
複製代碼

若是想對模塊模式瞭解更多,或者瞭解JS的模塊發展歷程,能夠參考騷年,你對前端模塊化了解多少

經過.call(), .apply() and .bind()修改上下文

CallApply函數用於改變調用函數的上下文。

function hello() {
    // do something...
}

hello(); // 正常調用
hello.call(context); // 將context做爲第一個參數傳入
hello.apply(context); // 將context做爲第一個參數傳入
複製代碼

.call().apply()之間的區別是,call接收的是以逗號分隔的參數list,而apply接收的是數組。

function introduce(name, interest) {
    console.log('Hi! 我是'+ name +' ,我喜歡'+ interest +'.');
    console.log('this' +'值爲'+this)
}

introduce('北宸', 'Coding'); //
introduce.call(window, 'Batman', 'to save Gotham'); // 在context以後,以逗號分隔傳遞
introduce.apply('Hi', ['Bruce Wayne', 'businesses']); //在context以後,傳入數組

複製代碼

call的運行速度比apply

在JS中一切皆對象,做爲函數也不例外,而JavaScript函數帶有四個內置方法,它們是:

  • Function.prototype.apply()
  • Function.prototype.bind()(在ECMAScript 5(ES5)中引入)
  • Function.prototype.call()
  • Function.prototype.toString()

Function.prototype.toString()返回該函數的源代碼的字符串表示形式。

咱們已經討論過.call().apply()可以改變context。與Call 和 Apply不一樣,Bind自己不會調用該函數,它只能用於在調用該函數以前綁定上下文值和其餘參數。

(function introduce(name, interest) {
  console.log('Hi! 我是'+ name +' ,我喜歡'+ interest +'.');
    console.log('this' +'值爲'+this)
}).bind(window, '北宸', '南蓁')();
複製代碼

若是想對applybindcall有一個更深的瞭解能夠參考this、apply、call、bind

相關文章
相關標籤/搜索