目錄
上下文(Context)和作用域(Scope)
變數作用域
什麼是this上下文
執行環境(execution context)
作用域链(The Scope Chain)
闭包" >闭包
Call和Apply
ES6中的箭头函数
小结
首頁 web前端 js教程 深入理解JavaScript中的作用域和上下文(圖)

深入理解JavaScript中的作用域和上下文(圖)

Mar 09, 2017 pm 02:14 PM

JavaScript對於作用域(Scope)和上下文(Context)的實作是這門語言的一個非常獨到的地方,部分歸功於其獨特的靈活性。 函數可以接收不同的上下文和作用域。這些概念為JavaScript中的許多強大的設計模式提供了堅實的基礎。 然而這也概念也非常容易帶給開發人員困惑。為此,本文將全面的剖析這些概念,並闡述不同的設計模式是如何利用它們的。

上下文(Context)和作用域(Scope)

首先需要知道的是,上下文和作用域是兩個完全不同的概念。多年來,我發現很多開發者會混淆這兩個概念(包括我自己), 錯誤的將兩個概念混淆了。平心而論,這些年來很多術語都被混亂的使用了。

函數的每次呼叫都有與之緊密相關的作用域和上下文。從根本上來說,作用域是基於函​​數的,而上下文是基於物件的。 換句話說,作用域涉及到所被呼叫函數中的變數訪問,並且不同的呼叫場景是不一樣的。上下文始終是this關鍵字的值, 它是對擁有(控制)目前所執行程式碼的物件的參考。

變數作用域

一個變數可以定義在局部或全域作用域中,這建立了在執行時間(runtime)期間變數的存取性的不同作用域範圍。 任何被定義的全域變量,意味著它需要在函數體的外部被聲明,並且存活於整個運行時(runtime),並且在任何作用域中都可以被存取。 在ES6之前,局部變數只能存在於函數體中,並且函數的每次呼叫它們都擁有不同的作用域範圍。 局部變數只能在其被呼叫期的作用域範圍內被賦值、檢索、操縱。

需要注意,在ES6之前,JavaScript不支援區塊級作用域,這表示在if語句、switch語句、for迴圈、while迴圈中無法支援區塊級作用域。 也就是說,ES6之前的JavaScript並不能建構類似Java中的那樣的區塊級作用域(變數不能在語句區塊外被存取)。但是, 從ES6開始,你可以透過let關鍵字來定義變量,它修正了var關鍵字的缺點,能夠讓你像Java語言一樣定義變量,並且支援區塊級作用域。看兩個例子:在

ES6之前,我們使用var關鍵字定義變數:

function func() {
  if (true) {
    var tmp = 123;
  }
  console.log(tmp); // 123
}
登入後複製

之所以能夠訪問,是因為var#關鍵字宣告的變數有一個變數提升的過程。而在ES6場景,建議使用let關鍵字定義變數:

function func() {
  if (true) {
    let tmp = 123;
  }
  console.log(tmp); // ReferenceError: tmp is not defined
}
登入後複製

這種方式,能夠避免很多錯誤。

什麼是this上下文

上下文通常取決於函數是如何被呼叫的。當一個函數被當作物件中的一個方法被呼叫的時候,this被設定為呼叫該方法的物件上:

var obj = {
    foo: function(){
        alert(this === obj);    
    }
};

obj.foo(); // true
登入後複製

這個準則也適用於當呼叫函數時使用new操作符來建立物件的實例的情況下。在這種情況下,在函數的作用域內部this的值被設定為新建立的實例:

function foo(){
    alert(this);
}

new foo() // foo
foo() // window
登入後複製

當呼叫一個為綁定函數時,this預設是全域上下文,在瀏覽器中它指向window物件。需要注意的是,ES5引入了嚴格模式的概念, 如果啟用了嚴格模式,此時上下文預設為undefined

執行環境(execution context)

JavaScript是一個單執行緒語言,意味著同一時間只能執行一個任務。當JavaScript解釋器初始化執行程式碼時, 它會先預設進入全域執行環境(execution context),從此刻開始,函數的每次呼叫都會建立一個新的執行環境。

這裡會經常引起新手的困惑,這裡提到了一個新的術語-執行環境(execution context),它定義了變數或函數有權存取的其他數據,決定了它們各自的行為。 它更偏向作用域的作用,而不是我們前面討論的上下文(Context)。請務必仔細的區分執行環境和上下文這兩個概念(註:英文容易造成混淆)。 說實話,這是一個非常糟糕的命名約定,但是它是ECMAScript規範制定的,你還是遵守吧。

每個函數都有自己的執行環境。當執行流進入函數時,函數的環境就會被推入一個環境堆疊中(execution stack)。在函數執行完後,堆疊將其環境彈出, 把控制權傳回給先前的執行環境。 ECMAScript程式中的執行流程正是由這個便利的機制所控制。

执行环境可以分为创建和执行两个阶段。在创建阶段,解析器首先会创建一个变量对象(variable object,也称为活动对象 activation object), 它由定义在执行环境中的变量、函数声明、和参数组成。在这个阶段,作用域链会被初始化,this的值也会被最终确定。 在执行阶段,代码被解释执行。

每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。 需要知道,我们无法手动访问这个对象,只有解析器才能访问它。

作用域链(The Scope Chain)

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。 作用域链包含了在环境栈中的每个执行环境对应的变量对象。通过作用域链,可以决定变量的访问和标识符的解析。 注意,全局执行环境的变量对象始终都是作用域链的最后一个对象。我们来看一个例子:

var color = "blue";

function changeColor(){
  var anotherColor = "red";

  function swapColors(){
    var tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;

    // 这里可以访问color, anotherColor, 和 tempColor
  }

  // 这里可以访问color 和 anotherColor,但是不能访问 tempColor
  swapColors();
}

changeColor();

// 这里只能访问color
console.log("Color is now " + color);
登入後複製

上述代码一共包括三个执行环境:全局环境、changeColor()的局部环境、swapColors()的局部环境。 上述程序的作用域链如下图所示:

scope chain example

从上图发现。内部环境可以通过作用域链访问所有的外部环境,但是外部环境不能访问内部环境中的任何变量和函数。 这些环境之间的联系是线性的、有次序的。

对于标识符解析(变量名或函数名搜索)是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始, 然后逐级地向后(全局执行环境)回溯,直到找到标识符为止。

闭包是指有权访问另一函数作用域中的变量的函数。换句话说,在函数内定义一个嵌套的函数时,就构成了一个闭包, 它允许嵌套函数访问外层函数的变量。通过返回嵌套函数,允许你维护对外部函数中局部变量、参数、和内函数声明的访问。 这种封装允许你在外部作用域中隐藏和保护执行环境,并且暴露公共接口,进而通过公共接口执行进一步的操作。可以看个简单的例子:

function foo(){
    var localVariable = 'private variable';
    return function bar(){
        return localVariable;
    }
}

var getLocalVariable = foo();
getLocalVariable() // private variable
登入後複製

模块模式最流行的闭包类型之一,它允许你模拟公共的、私有的、和特权成员:

var Module = (function(){
    var privateProperty = 'foo';

    function privateMethod(args){
        // do something
    }

    return {

        publicProperty: '',

        publicMethod: function(args){
            // do something
        },

        privilegedMethod: function(args){
            return privateMethod(args);
        }
    };
})();
登入後複製

模块类似于一个单例对象。由于在上面的代码中我们利用了(function() { ... })();的匿名函数形式,因此当编译器解析它的时候会立即执行。 在闭包的执行上下文的外部唯一可以访问的对象是位于返回对象中的公共方法和属性。然而,因为执行上下文被保存的缘故, 所有的私有属性和方法将一直存在于应用的整个生命周期,这意味着我们只有通过公共方法才可以与它们交互。

另一种类型的闭包被称为立即执行的函数表达式(IIFE)。其实它很简单,只不过是一个在全局环境中自执行的匿名函数而已:

(function(window){

    var foo, bar;

    function private(){
        // do something
    }

    window.Module = {

        public: function(){
            // do something 
        }
    };

})(this);
登入後複製

对于保护全局命名空间免受变量污染而言,这种表达式非常有用,它通过构建函数作用域的形式将变量与全局命名空间隔离, 并通过闭包的形式让它们存在于整个运行时(runtime)。在很多的应用和框架中,这种封装源代码的方式用处非常的流行, 通常都是通过暴露一个单一的全局接口的方式与外部进行交互。

Call和Apply

这两个方法内建在所有的函数中(它们是Function对象的原型方法),允许你在自定义上下文中执行函数。 不同点在于,call函数需要参数列表,而apply函数需要你提供一个参数数组。如下:

var o = {};

function f(a, b) {
  return a + b;
}

// 将函数f作为o的方法,实际上就是重新设置函数f的上下文
f.call(o, 1, 2);    // 3
f.apply(o, [1, 2]); // 3
登入後複製

两个结果是相同的,函数f在对象o的上下文中被调用,并提供了两个相同的参数12

在ES5中引入了Function.prototype.bind方法,用于控制函数的执行上下文,它会返回一个新的函数, 并且这个新函数会被永久的绑定到bind方法的第一个参数所指定的对象上,无论该函数被如何使用。 它通过闭包将函数引导到正确的上下文中。对于低版本浏览器,我们可以简单的对它进行实现如下(polyfill):

if(!('bind' in Function.prototype)){
    Function.prototype.bind = function(){
        var fn = this, 
            context = arguments[0], 
            args = Array.prototype.slice.call(arguments, 1);
        return function(){
            return fn.apply(context, args.concat(arguments));
        }
    }
}
登入後複製

bind()方法通常被用在上下文丢失的场景下,例如面向对象和事件处理。之所以要这么做, 是因为节点的addEventListener方法总是为事件处理器所绑定的节点的上下文中执行回调函数, 这就是它应该表现的那样。但是,如果你想要使用高级的面向对象技术,或需要你的回调函数成为某个方法的实例, 你将需要手动调整上下文。这就是bind方法所带来的便利之处:

function MyClass(){
    this.element = document.createElement('p');
    this.element.addEventListener('click', this.onClick.bind(this), false);
}

MyClass.prototype.onClick = function(e){
    // do something
};
登入後複製

回顾上面bind方法的源代码,你可能会注意到有两次调用涉及到了Arrayslice方法:

Array.prototype.slice.call(arguments, 1);
[].slice.call(arguments);
登入後複製

我们知道,arguments对象并不是一个真正的数组,而是一个类数组对象,虽然具有length属性,并且值也能够被索引, 但是它们不支持原生的数组方法,例如slicepush。但是,由于它们具有和数组类似的行为,数组的方法能够被调用和劫持, 因此我们可以通过类似于上面代码的方式达到这个目的,其核心是利用call方法。

这种调用其他对象方法的技术也可以被应用到面向对象中,我们可以在JavaScript中模拟经典的继承方式:

MyClass.prototype.init = function(){
    // call the superclass init method in the context of the "MyClass" instance
    MySuperClass.prototype.init.apply(this, arguments);
}
登入後複製

也就是利用callapply在子类(MyClass)的实例中调用超类(MySuperClass)的方法。

ES6中的箭头函数

ES6中的箭头函数可以作为Function.prototype.bind()的替代品。和普通函数不同,箭头函数没有它自己的this值, 它的this值继承自外围作用域。

对于普通函数而言,它总会自动接收一个this值,this的指向取决于它调用的方式。我们来看一个例子:

var obj = {

  // ...

  addAll: function (pieces) {
    var self = this;
    _.each(pieces, function (piece) {
      self.add(piece);
    });
  },

  // ...

}
登入後複製

在上面的例子中,最直接的想法是直接使用this.add(piece),但不幸的是,在JavaScript中你不能这么做, 因为each的回调函数并未从外层继承this值。在该回调函数中,this的值为windowundefined, 因此,我们使用临时变量self来将外部的this值导入内部。我们还有两种方法解决这个问题:

使用ES5中的bind()方法

var obj = {

  // ...

  addAll: function (pieces) {
    _.each(pieces, function (piece) {
      this.add(piece);
    }.bind(this));
  },

  // ...

}
登入後複製

使用ES6中的箭头函数

var obj = {

  // ...

  addAll: function (pieces) {
    _.each(pieces, piece => this.add(piece));
  },

  // ...

}
登入後複製

在ES6版本中,addAll方法从它的调用者处获得了this值,内部函数是一个箭头函数,所以它集成了外部作用域的this值。

注意:对回调函数而言,在浏览器中,回调函数中的thiswindowundefined(严格模式),而在Node.js中, 回调函数的thisglobal。实例代码如下:

function hello(a, callback) {
  callback(a);
}

hello('weiwei', function(a) {
  console.log(this === global); // true
  console.log(a); // weiwei
});
登入後複製

小结

在你学习高级的设计模式之前,理解这些概念非常的重要,因为作用域和上下文在现代JavaScript中扮演着的最基本的角色。 无论我们谈论的是闭包、面向对象、继承、或者是各种原生实现,上下文和作用域都在其中扮演着至关重要的角色。 如果你的目标是精通JavaScript语言,并且深入的理解它的各个组成,那么作用域和上下文便是你的起点。


以上是深入理解JavaScript中的作用域和上下文(圖)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

c語言中typedef struct的用法 c語言中typedef struct的用法 May 09, 2024 am 10:15 AM

typedef struct 在 C 語言中用於建立結構體類型別名,簡化結構體使用。它透過指定結構體別名將一個新的資料類型作為現有結構體的別名。優點包括增強可讀性、程式碼重複使用和類型檢查。注意:在使用別名前必須定義結構體,別名在程式中必須唯一且僅在其宣告的作用域內有效。

java中的variable expected怎麼解決 java中的variable expected怎麼解決 May 07, 2024 am 02:48 AM

Java 中的變數期望值異常可以透過以下方法解決:初始化變數;使用預設值;使用 null 值;使用檢查和賦值;了解局部變數的作用域。

js中閉包的優缺點 js中閉包的優缺點 May 10, 2024 am 04:39 AM

JavaScript 閉包的優點包括維持變數作用域、實作模組化程式碼、延遲執行和事件處理;缺點包括記憶體洩漏、增加了複雜性、效能開銷和作用域鏈影響。

c++中的include什麼意思 c++中的include什麼意思 May 09, 2024 am 01:45 AM

C++ 中的 #include 預處理器指令將外部來源檔案的內容插入到目前原始檔案中,以複製其內容到目前原始檔案的相應位置。主要用於包含頭文件,這些頭文件包含程式碼中所需的聲明,例如 #include <iostream> 是包含標準輸入/輸出函數。

C++ 智慧指標:全面剖析其生命週期 C++ 智慧指標:全面剖析其生命週期 May 09, 2024 am 11:06 AM

C++智慧指標的生命週期:建立:分配記憶體時建立智慧指標。所有權轉移:透過移動操作轉移所有權。釋放:智慧指標離開作用域或被明確釋放時釋放記憶體。物件銷毀:所指向物件被銷毀時,智慧型指標成為無效指標。

c++中函數的定義和呼叫可以巢狀嗎 c++中函數的定義和呼叫可以巢狀嗎 May 06, 2024 pm 06:36 PM

可以。 C++ 允許函數巢狀定義和呼叫。外部函數可定義內建函數,內部函數可在作用域內直接呼叫。巢狀函數增強了封裝性、可重複用性和作用域控制。但內部函數無法直接存取外部函數的局部變量,且傳回值類型需與外部函數宣告一致,內部函數不能自遞歸。

vue中let和var的區別 vue中let和var的區別 May 08, 2024 pm 04:21 PM

在 Vue 中,let 和 var 宣告變數時在作用域上存在差異:作用域:var 具有全域作用域,let 具有區塊級作用域。區塊級作用域:var 不會建立區塊級作用域,let 建立區塊級作用域。重新宣告:var 允許在同一作用域內重新宣告變數,let 不允許。

C++ 智慧指標:從基礎到高級 C++ 智慧指標:從基礎到高級 May 09, 2024 pm 09:27 PM

智慧指針是C++專用指針,能夠自動釋放堆記憶體對象,避免記憶體錯誤。類型包括:unique_ptr:獨佔所有權,指向單一物件。 shared_ptr:共享所有權,允許多個指標同時管理物件。 weak_ptr:弱引用,不增加引用計數,避免循環引用。使用方法:使用std命名空間的make_unique、make_shared和make_weak建立智慧指標。智慧型指標在作用域結束時自動釋放物件記憶體。進階用法:可以使用自訂刪除器控制物件釋放方式。智慧型指標可有效管理動態數組,防止記憶體洩漏。

See all articles