了解js基础知识中的作用域和闭包以及闭包的一些应用场景,浅析函数柯里化
js基礎知識中的作用域和閉包
- 一、作用域
- 1、作用域、自由變量簡介
- (1)作用域定義
- (2)作用域實例演示
- (3)自由變量定義
- (4)自由變量實例演示
- 2、作用域鏈簡介
- (1)作用域鏈定義
- (2)作用域鏈實例演示
- 3、全局作用域、函數作用域和塊級作用域
- (1)全局作用域
- (2)函數作用域
- (3)塊級作用域(ES6新增)
- (4)var和let、const的區別
- 二、閉包
- 1、閉包是什么?
- (1)定義
- (2)本質
- (3)特性
- (4)優缺點
- (5)閉包的解決方法
- 2、一般如何產生閉包?
- (1)函數作為返回值被傳遞
- (2)函數作為參數被返回
- 3、閉包的應用場景
- (1)通過循環給頁面上多個dom節點綁定事件
- (2)做一個簡單的cache工具,實現閉包隱藏數據,只提供API
- (3)函數柯里化
- 三、寫在最后
作用域和閉包是前端再基礎不過的知識了!我們平常所寫程序中很多都不一定是平鋪的,有很多復雜的邏輯和函數以及模塊之間的聯系都會涉及到作用域和閉包。因此,對于前端來說,如果連作用域和閉包的關系都捋不清,那無形中總會寫出各式各樣的 bug ,這對于程序來說簡直是一個巨大的災難。
同時,在面試當中,也很容易被問到這一塊的知識,比如:
- this 的不同應用場景,如何取值?
- 手寫 apply 、 call 、 bind 函數。
- 實際開發中閉包的應用場景,舉例說明。
- ……
所以,了解作用域和閉包,對于前端來說是一項必備的技能。接下來開始講解作用域和閉包。
一、作用域
1、作用域、自由變量簡介
(1)作用域定義
先拋出定義:
- 作用域,就是當訪問一個變量時,即編譯器在執行這段代碼時,會首先從當前的作用域中查找是否有這個標識符,如果沒有找到,就會去父作用域查找,如果父作用域還沒有找到則繼續向上查找,直到全局作用域為止??衫斫鉃樵撋舷挛闹新暶鞯?strong>變量和聲明的作用范圍,可分為全局作用域、函數作用域和塊級作用域。
- 注: ES5 中只存在兩種作用域:全局作用域和函數作用域; ES6 新增了塊級作用域。
- 在 Javascript 中,我們將作用域定義為一套規則,這套規則用來管理引擎如何在當前作用域以及嵌套子作用域中根據標識符名稱進行變量(變量名和函數名)查找。
接下來我們用實例來了解作用域是什么?以及跟作用域相關的自由變量又是什么?
(2)作用域實例演示
先來看一段代碼。
let a = 0; function fn1(){let a1 = 10;;function fn2(){let a2 = 100;function fn3(){let a3 = 100;return a + a1 + a2 + a3;}fn3();}fn2(); }fn1();這段代碼中的作用域分為以下四個層級。
那所謂作用域是什么呢?作用域代表的是一個變量,或者某個變量的合法使用范圍。比如,上圖中的 (1) 區域,其中 a 在全局被定義了,所以它可以被 (1) 的整個區域所使用。再來看下 (2) 區域, (2) 區域在函數 fn1 里面定義,所以 a1 只能被 (2) 下的整個區域所使用,以此類推 (3) 和 (4) 也是如此, a2 只能被區域 (3) 所使用, a4 只能被區域 (4) 所使用。
所以,作用域就類似于1234區域的這幾個紅框,即變量的一個合法使用范圍,一旦這個變量跳出這個范圍去使用,就會報錯。
說完作用域,我們再來了解下自由變量。先來了解下自由變量的定義。
(3)自由變量定義
- 一個變量在當前作用域中沒有定義。
- 向上級作用域,一層一層依次查找,直到找到為止。
- 如果到全局作用域都沒找到,則報錯 xxx is not undefined 。
(4)自由變量實例演示
先來看一段代碼(區域4有改變)。
let a = 0; function fn1(){let a1 = 10;;function fn2(){let a2 = 100;function fn3(){let a3 = 100;return x + a + a1 + a2 + a3;}fn3();}fn2(); }fn1();我們定位到上圖的區域(4),區域(4) 想要返回 x + a + a1 + a2 + a3 , 但是它當前區域只有 a3 被定義了,其余四個變量都沒有被定義。所以除了 a3 之外, x、a、a1和a2 變量符合自由變量定義中的第一條規則,所以 x、a、a1 和 a2 這四個變量均為自由變量。
再繼續看, x、a、a1和a2 向上級作用域一層一層的尋找,最終 a 在 區域(1) 找到, a1 在 區域(2) 找到, a3 在 區域(3) 找到,所以, a、a1 和 a2 也滿足自由變量定義的第二條規則。
繼續看,其它變量都找到了,只有 x 搜索到全局變量都沒有找到對應的值,所以 x 滿足自由變量的第三條規則。
綜上所述,對于自由變量定義的三條規則中,只要變量滿足其中一個條件都可算是自由變量。
了解完作用域和自由變量,我們再來了解作用域鏈。
2、作用域鏈簡介
(1)作用域鏈定義
-
作用域鏈可以看成是將變量對象按順序連接起來的一條鏈子。
-
每個執行環境中的作用域都是不同的。
-
當我們引用變量時,會順著當前執行環境的作用域鏈,從作用域鏈的開頭開始,依次往上尋找對應的變量,直到找到作用域鏈的尾部,報錯 undefined 。
-
作用域鏈保證了變量的有序訪問。
注意:作用域鏈只能向上訪問,到 window 對象即被終止。
(2)作用域鏈實例演示
依然是上面那個例子,我們用一張圖來表示作用域鏈的訪問過程。
像上圖這樣,從第一步訪問到第二、三、四步,最終訪問不到則報錯 undefined 。這樣按順序一步一步的訪問,就像是一條鏈子,把變量對象按順序連接起來,這就是作用域鏈。
3、全局作用域、函數作用域和塊級作用域
(1)全局作用域
在程序中,定義一個變量,這個變量沒有受到任何約束,在任何區域都可以使用,比如window對象,document對象……
(2)函數作用域
在函數中,定義一個變量,這個變量只能在函數內使用,超過函數的范圍就會報錯。
(3)塊級作用域(ES6新增)
ES6 新增了塊級作用域。那什么叫做塊呢?可以理解為在任何有加大括號{}的區域都可以算是一個塊。比如:
舉例1:
if(true){ let x = 100; } console.log(x); //會報錯在上面這段代碼中, if 語句后面花括號{}部分就算是一個塊,塊級作用域只能在當前的模塊當中生效,所以,當if語句里面定義了 x 之后, x 只能在當前塊內訪問,不能跑出這個塊,一旦跑出這個塊以后,就不再生效。所以當打印 console.log(x) 的時候, x 并不在 if 語句的塊內,所以會報錯。
舉例2:
function f1(){let n = 5;if(true){let n = 10;}console.log(n); //10 }在上面這段代碼中, console.log(n) 中的 n 作用于 f1 函數當中,所以它會往 f1 函數這個塊找,而此時有小伙伴可能會疑惑說, if 語句的塊也有 n ,也屬于 f1 函數里面。事實上,對于塊級作用域來說,外層代碼塊不受內層代碼塊的影響,即外層干外層的事情,內層干內層的事情,所以 n 在外層,它會往外層找,與內層相互獨立,互不干擾。
(4)var和let、const的區別
了解完全局、函數和塊級作用域。我們來梳理下var和let、const的區別:
- var 是 ES5 語法, let 、 const 是 ES6 語法, var 有變量提升。
- var 和 let 是變量,可以修改;const 是常量,不可修改。
- let 和 const 有塊級作用域,而var沒有。
二、閉包
1、閉包是什么?
(1)定義
閉包,是指函數內部再嵌套函數,且在嵌套的函數內有權訪問另外一個函數作用域中的變量。
JavaScript 代碼的整個執行過程,分為兩個階段,代碼編譯階段與代碼執行階段。編譯階段由編譯器完成,將代碼翻譯成可執行代碼,這個階段作用域規則會確定。執行階段由JS引擎完成,主要任務是執行可執行的代碼,執行上下文在這個階段創建。
(2)本質
- 當前環境中存在指向父級作用域的引用
(3)特性
- 函數內再嵌套函數
- 內部函數可以引用外層的參數和變量
- 參數和變量不會被垃圾回收機制回收
(4)優缺點
- 優點:能夠實現封裝和緩存等。
- 缺點:①消耗內存;②使用不當會造成內存溢出。
(5)閉包的解決方法
- 在退出函數之前,將不使用的局部變量全部刪除。
2、一般如何產生閉包?
閉包是作用域應用的特殊情況,有兩種表現:
- 函數作為返回值被傳遞
- 函數作為參數被返回
(1)函數作為返回值被傳遞
function create(){let a = 100;return function(){console.log(a);} }let fn1 = create(); let a = 200;fn1(); //100從以上代碼中可以看到,當執行 fn1 時,即執行 create() 函數,之后程序會執行 create() 函數,在 create() 函數當中, a 的值為 100 ,且結果是要返回一個函數的值。此時 console.log(a) 中的 a 為自由變量,在當前函數中找不到 a 的值,則會繼續往上尋找,最終找到 a 的值為 100 ,返回結果 100 。
(2)函數作為參數被返回
function print(fn2){let a = 200;fn2(); }let a = 100; function fn2(){console.log(a); }print(fn2); //100從以上代碼中可以看到,當執行 print(fn2) 函數時, fn2 是作為函數傳遞,此時執行 print(fn2) 函數,先找到 a 變量的值為 200 ,之后執行 fn2 函數,
而 fn2 函數在 print 函數外部,所以此時 fn2 函數中得到a變量應該往當前定義的地方向上級作用域查找,而不是在執行的地方查找。所以 a 變量向上級查找找到了 a 的值為 100 ,最終輸出為 100 。
綜上,得出以下結論:
在閉包中,所有自由變量的查找,是在函數定義的地方向上級作用域查找,而不是在執行的地方進行查找!!
3、閉包的應用場景
在日常的使用中,閉包通常有以下幾種場景:
- 通過循環給頁面上多個 dom 節點綁定事件
- 做一個簡單的cache工具,實現閉包隱藏數據,只提供 API
- 函數柯里化
(1)通過循環給頁面上多個dom節點綁定事件
我們先來看一段代碼。
let i, a; for(i = 0; i < 10; i++){a = document.createElement('a');a.innerHTML = i + '<br>';a.addEventListener('click', function(e) {e.preventDefault();alert(i);});document.body.appendChild(a); }看完這段代碼不妨先思考一下,在網頁上呈現的形式是什么樣的?當點擊0~9的數字時,彈出來的數字是多少呢?我們來展示下結果。
我們可以發現,我們想要的結果其實是點擊0的時候彈出0,點擊1的時候彈出1,但是好像跟我們想象的似乎還有點落差。那問題出在哪里呢?
其實,當把這段代碼放在程序中時,很快就會被執行完,所以在for循環結束后,可能 a.addEventListener 還沒有被執行(即下面這段代碼),那 a.addEventListener 什么時候執行呢?那就是什么時候 click 什么時候執行。在我們還沒有 click 之前,整個 for 循環可能已經循環結束,所以等到我們點擊的時候,最終會打印出最后一個 for 執行時的結果:10 。
//不會立馬執行的函數 a.addEventListener('click', function(e) {e.preventDefault();alert(i); });所以,問題出現了,我們應該怎樣修改才能把它按照我們所想的進行顯示呢。具體代碼改動如下:
let a; for(let i = 0; i < 10; i++){a = createElement('a');a.innerHTML = i + '<br>';a.addEventListener('click', function(e) {e.preventDefault();alert(i);});document.body.appendChild(a); }改動后的效果展示如下圖。
通過修改,可以看到,最終成功輸出我們想要的結果。為什么呢?
因為把 i 放到 for 循環里面,并且用 let 定義,相當于把 let i = 0 放在 for 里面,定義了一個塊級作用域。每次 for 循環執行的時候,都會形成一個新的塊,每一個塊都相互獨立,執行到哪就顯示到哪。一旦執行到 alert(i) 時,它就會往它的塊級作用域里面尋找。
而如果把 i 放在全局作用域中, 全局作用域是針對所有的塊,不會去考慮到每一個塊的呈現形式是怎么樣的。所以,把 i 放在塊級作用域當中,讀取塊級作用域的內容,最終達到我們想要的效果。
(2)做一個簡單的cache工具,實現閉包隱藏數據,只提供API
我們先來寫一段代碼。
//閉包隱藏數據,只提供 API function createCache(){const data = {}; //閉包中的數據,被隱藏,不被外界訪問return{set: function(key, val){data[key] = val;},get: function(key, val){return data[key];}} }const c = createCache(); c.set('a', 100); console.log(c.get('a')); //100 console.log(c.delete('a')); //會報錯在這段代碼中,我們在函數 createCache() 中定義了 get 和 set 方法,也就是說,在 createCache() 這個函數里,只提供了 set 和 get 方法,不再提供其他方法。所以在下面的 c 調用中,它只能調用 set 和 get ,而調用不了 delete ,因為 delete 在函數 createCache() 里面并沒有提供出來,所以當 c 想要嘗試去調用的時候,會報錯。這樣達到了閉包的目的,只提供想要提供的 API ,不提供的一律獲取不了。
(3)函數柯里化
1)函數柯里化是什么?
函數柯里化是將一個接收多個參數的函數變為接收任意參數且最終返回一個函數的一種技術方式,其最終支持的是方法的連續調用,每次返回新的函數,在最終符合條件或者使用完所有的傳參時終止函數調用。
2)主要作用和特點
函數柯里化的主要作用和特點就是參數復用、提前返回和延遲執行。
2)舉例說明
比如:有一個add函數,用于返回所有參數的和,add(1, 2, 3, 4, 5)返回的是15,那么現在要將其變為類似 add(1)(2)(3)(4)(5) 或者 add(1)(2, 3, 4)(5) 的形式,并且功能相同,這就是柯里化想要達到的效果。
3)介紹柯里化的三種方式
在介紹柯里化的三種方式之前,先來了解下普通的add函數。
function add(){let sum = 0;let args = [...arguments];for(let i in args){sum += args[i];}return sum; }let res = add(1,2,3,4,5); console.log(res); //15普通的add()函數看著也沒有什么問題,但是一旦數據的類型各式各樣,就不是那么好處理了。于是引出柯里化來解決此類問題。
第一種add()函數柯里化方式
缺點:最后返回的結果是函數類型,但會被隱式轉化為字符串,調用 toString() 方法
function add1(){// 創建數組,用于存放之后接收的所有參數let args = [...arguments];function getArgs(){args.push(...arguments);return getArgs;}getArgs.toString = function(){return args.reduce((a,b) => {return a + b;})}return getArgs; }let res = add1(1)(2)(3)(4)(5); console.log(res); //f 15第二種add()函數柯里化方式
缺點:需要在最后再自調用一次,即不傳參調用表示已沒有參數了
function add2(){// 創建數組,用于存放之后接收的所有參數let args = [...arguments];return function(){if(arguments.length === 0){return args.reduce((a,b) => {return a + b;})}else{let _args = [...arguments];for(let i = 0; i < _args.length; i++){args.push(_args[i]);}return arguments.callee;}} }let res = add2(1)(2,3,4)(5)(); console.log(res); //15第三種add()函數柯里化方式
缺點:在剛開始傳參之前,設定總共需要傳入參數的個數
function add3(length){// slice(1)表示從第二個元素開始取值let args = [...arguments].slice(1);return function(){args = args.concat([...arguments]);if(arguments.length < length){return add3.apply(this, [length - arguments.length].concat(args));}else{return args.reduce((a,b) => a + b);}} }let res3 = add3(5); console.log(res3(1)(2, 3)(4)(5)); //15三、寫在最后
對于函數柯里化的內容我也還不是特別熟悉,寫的內容僅供參考,待后面有深入了解之后還會再繼續進行補充。
關于作用域、閉包以及閉包的一些應用場景就講到這里啦!如果有不理解或者有誤的地方也歡迎私聊我或加我微信指正~
- 公眾號:星期一研究室
- 微信:MondayLaboratory
創作不易,如果這篇文章對你有用,記得點個 Star 哦~
總結
以上是生活随笔為你收集整理的了解js基础知识中的作用域和闭包以及闭包的一些应用场景,浅析函数柯里化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 艳山姜的功效与作用、禁忌和食用方法
- 下一篇: 香菇干的功效与作用、禁忌和食用方法