搜尋
首頁web前端js教程深入理解javascript作用域與閉包_基礎知識

作用域

作用域是一個變數和函數的作用範圍,javascript中函數內宣告的所有變數在函數體內始終是可見的,在javascript中有全域作用域和局部作用域,但是沒有區塊級作用域,局部變數的優先權高於全域變量,透過幾個範例來了解下javascript中作用域的那些「潛規則」(這些也是在前端面試中經常問到的問題)。

1. 變數宣告提前
範例1:

var scope="global";
function scopeTest(){
  console.log(scope);
  var scope="local" 
}
scopeTest(); //undefined

此處的輸出是undefined,並沒有報錯,這是因為在前面我們提到的函數內的聲明在函數體內始終可見,上面的函數等效於:

var scope="global";
function scopeTest(){
  var scope;
  console.log(scope);
  scope="local" 
}
scopeTest(); //local

注意,如果忘記var,那麼變數就被宣告為全域變數了。

2. 沒有區塊級作用域

和其他我們常用的語言不同,在Javascript中沒有區塊級作用域:

function scopeTest() {
  var scope = {};
  if (scope instanceof Object) {
    var j = 1;
    for (var i = 0; i < 10; i++) {
      //console.log(i);
    }
    console.log(i); //输出10
  }
  console.log(j);//输出1

}

在javascript中變數的作用範圍是函數級的,即在函數中所有的變數在整個函數中都有定義,這也帶來了一些我們稍不注意就會碰到的「潛規則」:

var scope = "hello";
function scopeTest() {
  console.log(scope);//①
  var scope = "no";
  console.log(scope);//②
}

在①處輸出的值竟然是undefined,簡直喪心病狂啊,我們已經定義了全域變數的值啊,這地方不應該是hello嗎?其實,上面的程式碼等效於:

var scope = "hello";
function scopeTest() {
  var scope;
  console.log(scope);//①
  scope = "no";
  console.log(scope);//②
}

宣告提前、全域變數優先權低於局部變量,根據這兩條規則就不難理解為什麼輸出undefined了。

作用域鏈

在javascript中,每個函數都有自己的執行上下文環境,當程式碼在這個環境中執行時,會創建變數物件的作用域鏈,作用域鍊是一個物件列表或物件鏈,它保證了變數物件的有序存取。
作用域鏈的前端是當前代碼執行環境的變數對象,常被稱之為“活躍對象”,變數的查找會從第一個鏈的對象開始,如果對象包含變數屬性,那麼就停止查找,如果沒有就會繼續向上級作用域鏈查找,直到找到全域物件中:

作用域鏈的逐級查找,也會影響程式的效能,變數作用域鏈越長對效能影響越大,這也是我們盡量避免使用全域變數的一個主要原因。

閉包

基礎概念
作用域是理解閉包的一個前提,閉包是指在目前作用域內總是能存取外部作用域中的變數。

function createClosure(){
  var name = "jack";
  return {
    setStr:function(){
      name = "rose";
    },
    getStr:function(){
      return name + ":hello";
    }
  }
}
var builder = new createClosure();
builder.setStr();
console.log(builder.getStr()); //rose:hello

上面的範例在函數中傳回了兩個閉包,這兩個閉包都維持著對外部作用域的引用,因此不管在哪裡呼叫總是能夠存取外部函數中的變數。在一個函數內部定義的函數,會將外部函數的活躍物件加入自己的作用域鏈中,因此上面實例中透過內部函數能夠存取外部函數的屬性,這也是javascript模擬私有變數的一種方式。

注意:由於閉包會額外的附帶函數的作用域(內部匿名函數攜帶外部函數的作用域),因此,閉包會比其它函數多佔用些記憶體空間,過度的使用可能會導致記憶體佔用的增加。

閉包中的變數

使用閉包時,由於作用域鏈機制的影響,閉包只能取得內部函數的最後一個值,這引起的一個副作用就是如果內部函數在一個循環中,那麼變數的值始終為最後一個值。

  //该实例不太合理,有一定延迟因素,此处主要为了说明闭包循环中存在的问题
  function timeManage() {
    for (var i = 0; i < 5; i++) {
      setTimeout(function() {
        console.log(i);
      },1000)
    };
  }

上面的程式並沒有按照我們預期的輸入1-5的數字,而是5次全部輸出了5。再來看一個範例:

function createClosure(){
  var result = [];
  for (var i = 0; i < 5; i++) {
    result[i] = function(){
      return i;
    }
  }
  return result;
}

呼叫createClosure()[0]()回傳的是5,createClosure()[4]()回傳值仍然是5。透過以上兩個例子可以看出閉包在帶有循環的內部函數使用時存在的問題:因為每個函數的作用域鏈中都保存著對外部函數(timeManage、createClosure)的活躍對象,因此,他們都引用著同一變數i,當外部函數回傳時,此時的i值為5,所以內部的每個函數i的值也是5。
那麼如何解決這個問題呢?我們可以透過匿名包裹器(匿名自執行函數表達式)來強制傳回預期的結果:

function timeManage() {
  for (var i = 0; i < 5; i++) {
    (function(num) {
      setTimeout(function() {
        console.log(num);
      }, 1000);
    })(i);
  }
}

或在閉包匿名函數中再傳回一個匿名函數賦值:

function timeManage() {
  for (var i = 0; i < 10; i++) {
    setTimeout((function(e) {
      return function() {
        console.log(e);
      }
    })(i), 1000)
  }
}
//timeManager();输出1,2,3,4,5
function createClosure() {
  var result = [];
  for (var i = 0; i < 5; i++) {
    result[i] = function(num) {
      return function() {
        console.log(num);
      }
    }(i);
  }
  return result;
}
//createClosure()[1]()输出1;createClosure()[2]()输出2

无论是匿名包裹器还是通过嵌套匿名函数的方式,原理上都是由于函数是按值传递,因此会将变量i的值复制给实参num,在匿名函数的内部又创建了一个用于返回num的匿名函数,这样每个函数都有了一个num的副本,互不影响了。

闭包中的this

在闭包中使用this时要特别注意,稍微不慎可能会引起问题。通常我们理解this对象是运行时基于函数绑定的,全局函数中this对象就是window对象,而当函数作为对象中的一个方法调用时,this等于这个对象(TODO 关于this做一次整理)。由于匿名函数的作用域是全局性的,因此闭包的this通常指向全局对象window:

var scope = "global";
var object = {
  scope:"local",
  getScope:function(){
    return function(){
      return this.scope;
    }
  }
}

调用object.getScope()()返回值为global而不是我们预期的local,前面我们说过闭包中内部匿名函数会携带外部函数的作用域,那为什么没有取得外部函数的this呢?每个函数在被调用时,都会自动创建this和arguments,内部匿名函数在查找时,搜索到活跃对象中存在我们想要的变量,因此停止向外部函数中的查找,也就永远不可能直接访问外部函数中的变量了。总之,在闭包中函数作为某个对象的方法调用时,要特别注意,该方法内部匿名函数的this指向的是全局变量。

幸运的是我们可以很简单的解决这个问题,只需要把外部函数作用域的this存放到一个闭包能访问的变量里面即可:

var scope = "global";
var object = {
  scope:"local",
  getScope:function(){
    var that = this;
    return function(){
      return that.scope;
    }
  }
}
object.getScope()()返回值为local。

内存与性能

由于闭包中包含与函数运行期上下文相同的作用域链引用,因此,会产生一定的负面作用,当函数中活跃对象和运行期上下文销毁时,由于必要仍存在对活跃对象的引用,导致活跃对象无法销毁,这意味着闭包比普通函数占用更多的内存空间,在IE浏览器下还可能会导致内存泄漏的问题,如下:

 function bindEvent(){
  var target = document.getElementById("elem");
  target.onclick = function(){
    console.log(target.name);
  }
 }

上面例子中匿名函数对外部对象target产生一个引用,只要是匿名函数存在,这个引用就不会消失,外部函数的target对象也不会被销毁,这就产生了一个循环引用。解决方案是通过创建target.name副本减少对外部变量的循环引用以及手动重置对象:

 function bindEvent(){
  var target = document.getElementById("elem");
  var name = target.name;
  target.onclick = function(){
    console.log(name);
  }
  target = null;
 }

闭包中如果存在对外部变量的访问,无疑增加了标识符的查找路径,在一定的情况下,这也会造成性能方面的损失。解决此类问题的办法我们前面也曾提到过:尽量将外部变量存入到局部变量中,减少作用域链的查找长度。

总结:闭包不是javascript独有的特性,但是在javascript中有其独特的表现形式,使用闭包我们可以在javascript中定义一些私有变量,甚至模仿出块级作用域,但闭包在使用过程中,存在的问题我们也需要了解,这样才能避免不必要问题的出现。

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
JavaScript引擎:比較實施JavaScript引擎:比較實施Apr 13, 2025 am 12:05 AM

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

超越瀏覽器:現實世界中的JavaScript超越瀏覽器:現實世界中的JavaScriptApr 12, 2025 am 12:06 AM

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

使用Next.js(後端集成)構建多租戶SaaS應用程序使用Next.js(後端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:23 AM

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

如何使用Next.js(前端集成)構建多租戶SaaS應用程序如何使用Next.js(前端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:22 AM

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript:探索網絡語言的多功能性JavaScript:探索網絡語言的多功能性Apr 11, 2025 am 12:01 AM

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的演變:當前的趨勢和未來前景JavaScript的演變:當前的趨勢和未來前景Apr 10, 2025 am 09:33 AM

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

神秘的JavaScript:它的作用以及為什麼重要神秘的JavaScript:它的作用以及為什麼重要Apr 09, 2025 am 12:07 AM

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

Python還是JavaScript更好?Python還是JavaScript更好?Apr 06, 2025 am 12:14 AM

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。1.Python以简洁语法和丰富库生态著称,适用于数据分析和Web开发。2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

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

AI Clothes Remover

AI Clothes Remover

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

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

EditPlus 中文破解版

EditPlus 中文破解版

體積小,語法高亮,不支援程式碼提示功能

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用