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