Javascript的作用域和閉包知多少?
本文轉載自微信公眾號「前端萬有引力」,作者一川。轉載本文請聯系前端萬有引力公眾號。
1寫在前面
在Javascript編程中,閉包是個相當重要且難以理解的概念,并且與作用域知識是密切相關的。那么:
- Javascript中的作用域是什么意思呢?
- 閉包會在哪些場景中使用呢?
- 通過定時器循環輸出自增的數字,通過JS代碼如何實現呢?
2作用域
作用域:指的是變量的作用范圍,即變量能夠被訪問到的范圍。
es5之前只有:全局作用域和函數作用域
es6之后新增:塊級作用域
2.1 全局作用域
在Javascript中,全局變量是掛載到window頂級對象下的變量,因此在網頁中的任何位置都可以進行使用,并且可以訪問到這個全局變量。
- function getName(){
- var name = "inner";
- console.log(name);//"inner"
- }
- getName();
- console.log(name);//name is not defined
使用全局變量的缺點是:定義很多全局變量的時候,會容易引起變量命名的沖突。因此定義全局變量的時候,要注意作用域的訪問范圍。
2.2 函數作用域
函數作用域:除了函數內部能夠進行訪問外,其他地方都是不能夠訪問的。且此函數執行完畢后,定義在函數作用域的局部變量也會被銷毀。
- function getName(){
- var name = "inner";
- console.log(name);//"inner"
- }
- getName();
- console.log(name);//name is not defined
2.3 塊級作用域
塊級作用域最直接的表現是:新增的let關鍵字,使用let關鍵字定義的變量只能在塊級作用域中被訪問。這是因為其具有暫時性死區的特點:這個變量在定義之前是不能夠被使用的。
在Javascript中,if語句、try...catch...、switch以及for等語句后面{...}里面所包括的都是塊級作用域。
- console.log(name);//name is not defined
- if(true){
- let name = "yichuan";
- console.log(name);//"yichuan"
- }
- console.log(name);//name is not defined
3閉包
在《Javscript高級程序設計》中是這樣定義閉包的:閉包指有權訪問另外一個函數作用域中的變量的函數。
在MDN中是這樣定義的:一個函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者說函數被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個內層函數中訪問到其外層函數的作用域。在 JavaScript 中,每當創建一個函數,閉包就會在函數創建的同時被創建出來。
對于我們理解,其實閉包就是一個可以訪問其它函數內部變量的函數。
通常情況下,函數內部變量是無法在函數外部進行訪問的,因此使用閉包的作用就是突破這種限制,使得具備了能夠在函數外部訪問此函數內部變量的功能。
- function func1(){
- var name = "yichuan";
- return function(){
- console.log(name);
- }
- }
- func1();//此處返回的是一個function函數
- var result = func1();//使用result接收這個return出來的函數
- result();//1
我們看上面的代碼其實就已經具有閉包的特點,就是可以在func函數外部去訪問name的值。
我們分析下閉包產生的原因,這就需要先理解什么是作用域鏈。
作用域鏈:當訪問一個變量時,代碼解釋器會首先在當前作用域進行查找,如果沒有查找到,就會去父級作用域進行查找,知道找到該變量或者不存在父級作用域中。
- var name = "outer";
- console.log(name);//outer
- function func1(){
- var name = "inner";
- console.log(name);//inner
- function func2(){
- var name = "self";
- console.log(name);//self
- }
- function func3(){
- console.log(name);//inner
- }
- func2();
- func3();
- }
- function func4(){
- function func5(){
- console.log(name);//outer
- }
- }
- func1();
- func4();
我們看到,在函數func2中使用name變量,他會現在func2中查找到name變量并使用它的值。在函數func3中沒有name變量,它就會沿著作用域鏈向上查找到func1函數中的name變量并使用。在函數func5中,查找并沒有name變量,它就會去func4函數中查找也沒有,就接著去全局變量查找到name變量并使用。
閉包產生本質就是:當前環境中存在指向父級作用域的引用。
- function func1(){
- var name = "inner";
- function func2(){
- console.log(name);//inner
- }
- return func2;
- }
- var result = func1();
- result();
是不是只有返回函數的形式才能說產生了閉包呢?當然不是呀,只要讓父級作用域的引用存在即可。
- var func2;
- function func1(){
- var name = "yichuan";
- func2 = function(){
- console.log(name);
- }
- }
- func1();
- func2();//"yichuan"
所以閉包有哪些表現形式呢?
- 直接返回一個函數
- 在定時器、事件監聽、ajax請求、web workers或者任何異步中只要使用到了回調函數,實際上就是在使用閉包
- 作為函數參數傳遞的形式
- 立即執行函數IIFE,創建了閉包,保存了全局作用域window和當前函數作用域,因此可以輸出全局的變量
- //定時器
- setTimeout(function(){
- console.log("yichuan");
- },1000);
- //事件監聽
- const container = document.getElementById("app");
- container.addEventListener("click",function(){
- console.log("event listener");
- });
- //函數參數傳遞
- var name = "yichuan";
- function func1(){
- var name = "onechuan";
- function func2(){
- console.log(name);
- }
- func3(func2);
- }
- function func3(func){
- func();//閉包
- }
- func1();//輸出"onechuan"
- //立即執行函數
- var name = "yihchuan";
- (function(){
- console.log(name);//"yichuan"
- })();
那么,我們應該如何應用閉包解決循環問題呢?
4面試中常考的循環打印問題
例如:在循環中是否是期望的,打印出來的是1、2、3、4、5呢?
- for(var i = 1; i <= 5; i++){
- setTimeout(function(){
- console.log(i);
- },0);
- }
答案是否定的,最終打印出來是五個6。這是為什么呢?
這是因為:setTimeout是宏任務,由于js中單線程EventLoop機制,在主線程同步任務執行完畢后才會去執行宏任務,因此循環結束后setTimeout中的回調才會依次執行。而setTimeout函數也是一種閉包,沿著作用域鏈往上找它的父級作用域就是window,而變量i是window上的全局變量。等開始執行setTimeout函數之前變量i就已經變成了6了,因此最后輸出的數值就是5個6。
那么如何解決呢?
- //使用立即執行函數
- for(var i = 1; i <= 5; i++){
- (function(j){
- setTimeout(function(){
- console.log(j);
- },0);
- })(i)
- }
- //使用es6中let
- for(let i = 1; i <= 5; i++){
- setTimeout(function(){
- console.log(i);
- },0);
- }
還有個很難想到的方法,就是使用setTimeout的隱藏的第三個參數。
- for(var i = 1; i <= 5; i++){
- setTimeout(function(){
- console.log(i);
- },0,i);
- }
附加參數,一旦定時器到期,它們會作為參數傳遞給function。但是得注意:IE9 及更早的 IE 瀏覽器不支持向回調函數傳遞額外參數(第一種語法)。
5參考文章
《Javascript核心原理精講》
《MDN》
《Javascript高級程序設計》
6寫在后面
閉包的使用在日常的javascript編程中經常出現,使用的場景比較多且復雜,需要讀者仔細分析。本篇文章講到了關于作用域和閉包的知識,其中作用域根據變量的作用范圍分為:全局作用域、函數作用域以及塊級作用域。而閉包就是一個可以訪問其它函數內部變量的函數,在解決for循環打印的問題可以使用立即執行函數、setTimeout的第三參數、es6中的let關鍵字解決。
注意:閉包在實際開發要注意不要濫用,容易引起內存泄漏。