1、函數參數默認值中模糊的獨立做用域面試
我在ES6入門學習函數拓展這一篇博客中有記錄,當函數的參數使用默認值時,參數會在初始化過程當中產生一個獨立的做用域,初始化完成做用域會消失;若是不使用參數默認值,不會產生這個做用域;之因此要寫這篇博客是由於對這段代碼有所疑問:函數
var x = 1; function foo(x, y = function () {x = 2;}) { var x = 3; y(); console.log(x); }; foo();//3 foo(4);//3 console.log(x);//1
老實說,ES6入門中關於這個獨立做用域的描述十分抽象,當個人同事對於這個問題也提出疑問時,我發現本身確實不能很好的解釋這個問題,緣由很簡單,我也似懂非懂;對此我作了一些測試,並嘗試去模擬實現這個做用域,便於解釋給同事聽以及說服我本身。學習
爲何var x=3始終輸出3,爲何去掉var後始終輸出2,這個獨立的做用域究竟是怎麼回事?測試
若是你對於這個問題了如指掌,相關筆試題輕鬆解答,這篇文章就不那麼重要了;但若是你對這個做用域跟我同樣有一些疑慮,那能夠跟着個人思路來理一理,那麼本文開始。spa
2、ES6帶來的塊級做用域code
在改寫這段代碼前,有必要先把塊級做用域說清楚。對象
咱們都知道,在ES6以前JavaScript只存在全局做用域與函數做用域這兩類,更有趣的是當咱們使用var去聲明一個變量或者一個函數,本質上是在往window對象上新增屬性:blog
var name = "聽風是風"; var age = 26; window.name; //'聽風是風' window.age; //26
這天然是不太好的作法,咱們本想聲明幾個變量,結果本來乾淨的window對象被弄的一團糟,爲了讓變量聲明與window對象再也不有牽連,也是彌補變量提高等一些缺陷,ES6正式引入了let聲明。繼承
delete window.name; let name = "聽風是風"; window.name; //undefined
let還帶來了一個比較重要的概念,塊級做用域,當咱們在一個花括號中使用let去聲明一個變量,這個花括號就是一個塊級做用域,塊級做用域外無權訪問這個變量。ip
{ let x = 1; } console.log(x)//報錯,x未聲明
當你在這個塊級做用域外層再次聲明x時,外層做用域中的x與塊級做用域中的x就是不一樣的兩個x了,互不影響:
let x = 2; { let x = 1; console.log(x); //1 } console.log(x) //2 var y = 1; { let y = 2 } console.log(y) //1
但你不能夠在同層做用域中使用let聲明一個變量後再次var 或者再次let相同變量:
let x = 1; var x; //報錯,x已聲明 let y = 1; let y; //報錯,y已聲明 var z = 1; let z; //報錯,z已聲明
塊級做用域依舊存在做用域鏈,並非說你變成了塊級做用域就六親不認了,誰也別想用我塊級裏面的變量:
{ //父做用域 let x = 1; let y = 1; { //子做用域 console.log(x); //1 x = 2; let y = 2; console.log(y); //2 } console.log(x); //2 console.log(y);//1 }
上述代碼中子做用域中沒let x,父做用域仍是容許子做用域中訪問修改本身的x;父子做用域中都let y,那兩個做用域中的y就是徹底不相關的變量。
最後一點,不少概念都說,外(上)層做用域是無權訪問塊級做用域的變量,這句話其實有歧義,準確來講,是無權訪問塊級做用域中使用了let的變量,個人同事就誤會了這點:
{ let x = 1; var y = 2; z = 3; } console.log(y);//2 console.log(z);//3 console.log(x);//報錯,x未定義
let x確實產生了一個塊級做用域,但你只能限制外層訪問產生塊級做用域的x,我y用的var,z直接就全局,大家抓週樹人跟我魯迅有什麼關係?這點千萬要理解清楚。
介紹let可能花了點時間,明明是介紹函數參數默認值的做用域,怎麼聊到let了。這是由於我在給同事說個人推測時,我發現他對於let存在部分誤解,因此在理解個人思路上也花了一些時間。
3、關於函數參數默認值獨立做用域的推測與個人代碼模擬思路
1.改寫函數參數
咱們都知道,函數的參數其實等同於在函數內部聲明瞭一個局部變量,只是這個變量在函數調用時能與傳遞的參數一一對應進行賦值:
function fn(x) { console.log(x); }; fn(1); //等用於 function fn() { //函數內部聲明瞭一個變量,傳遞的值會賦予給它 var x = 1; }; fn()
因此第一步,我將文章開頭那段代碼中的函數進行改寫,將形參改寫進函數內部:
function foo() { var x; var y = function () { x = 2; }; var x = 3; y(); console.log(x); };
2.模擬形參的獨立做用域
改寫後有個問題,此時形參與函數內部代碼處於同一層做用域,這與咱們得知的概念不太相符,概念傳達的意思是,函數參數使用默認值,會擁有獨立的做用域,因此咱們用一個花括號將函數內代碼隔離起來:
function foo() { var x; var y = function () { x = 2; }; { var x = 3; y(); console.log(x); } };
其次,由文章開頭的代碼結果咱們已經得知,var x =3這一行代碼,若是帶了var ,函數體內x變量就與參數內的x互不影響了,永遠輸出3;若是把var去掉呢,就能繼承並修改參數中的變量x了,此時x始終輸出2,這個效果能夠本身複製文章開頭的原代碼測試。
我在上文介紹let塊級做用域時有提到塊級做用域也是有做用域鏈的;父子塊級做用域,若是子做用域本身let一個父做用域已聲明的變量,那麼二者就互不影響,若是子不聲明這個變量,仍是能夠繼承使用和修改父做用域的此變量。這個狀況不就是示例代碼的除去var和不除去var效果嗎,只是咱們還缺個塊級做用域才能知足這個條件,因此我將var x =3前面的var修改爲了let,整個代碼修改完畢:
function foo() { //父做用域 var x; var y = function () { x = 2; }; { // 子塊級做用域 let x = 3; y(); console.log(x); } };
你確定要問,我爲何要把var改成let?並非我根據結論強行倒推理,我在斷點時發現了一個問題,帶var的狀況:
注意觀察右邊Scope的變化,當斷點跑到var x = 3時,顯示在block(塊級做用域)下x是undefined,而後被賦值成了3,最後斷點跑到console時,也是輸出了block做用域下的x,並且在block做用域和local做用域中分別存在2個變量x,以下圖:
函數內部明明沒用let,也就是說,函數執行時,隱性建立了一個塊級做用域包裹住了函數體內代碼。當我把var去掉時,再看截圖:
能夠看到,當去掉var時,整個代碼執行完,全程都不存在block做用域,並且從頭至尾都只有local做用域下的一個x。
由此我推斷var是產生塊級做用域的緣由,因此將x變量前的var改成了let。
3.模擬代碼測試階段:
咱們最終修改後的代碼就是這樣:
var x = 1; function foo() { var x; var y = function () { x = 2; }; { let x = 3; y(); console.log(x); } }; foo(); //3 foo(4); //3 console.log(x); //1
帶var分別輸出3 3 1,咱們把var 改爲了let,也是輸出3 3 1。去var輸出2 2 1,咱們把let去掉也是輸出2 2 1,效果如出一轍。
咱們對比了修改先後,代碼執行時scope的變化,是如出一轍的,能夠說模擬還算成功。
4.最終模擬版本
而後我又發現了一個改寫的大問題:
function fn(x=x){ }; fn();//報錯
這段代碼是會報錯的,它會提示你,x未聲明就使用了,這是let聲明常見的錯誤。可是若是按照我前面說的將形參移到函數體內用var聲明,那就不會報錯了:
function fn(){ var x = x; }; fn()//不報錯 function fn(){ let x = x; }; fn()//報錯
因此我上面的初始代碼改寫後的最終版本是這樣:
var x = 1; function foo() { let x; let y = function () { x = 2; }; { let x = 3; y(); console.log(x); } }; foo(); //3 foo(4); //3 console.log(x); //1
這是執行效果圖,仔細觀察能夠發現scope變化以及執行結果與沒改以前同樣,只是我以爲這樣改寫更爲嚴謹。
4、最終結論與我的推測
因此我獲得的最終結論是,並非函數形參使用了默認值會產生獨立的做用域,而是函數形參使用了默認值時,會讓函數體內的var聲明隱性產生一個塊級做用域,從而變相致使了函數參數所在做用域被隔離。不使用參數默認值或函數體內不使用var聲明不會產生此做用域。
個人改寫模擬思路是這樣:
第一步,形參若是用了默認值,將形參移到函數體內並用let聲明它們;
第二步,若是此時沒報錯,再用花括號將本來的函數體代碼包裹起來,再將花括號中的var聲明修改爲let聲明。
function fn(x, y = x) { let x = 1; console.log(x); }; //第一步: function fn() { let x; let y = x; let x = 1; console.log(x); };
好比上述這段代碼,形參移動到函數體內其實你就已經會報錯了,x變量被反覆申明瞭,因此就不必再用花括號包裹執行體代碼了。
我大概總結出瞭如下幾個規律(能夠按照個人思路改寫,方便理解):
1.當函數形參聲明瞭x,函數體內不能使用let再次聲明x,不然會報錯,緣由參照函數改寫步驟1。
var x = 1; function fn(x){ let x =1;//報錯 }; fn();
2.當函數形參聲明瞭x,函數體內再次使用var聲明x時,函數體內會隱性建立一個塊級做用域,這個做用域會包裹執行體代碼,也變相致使參數有了一個獨立的做用域,此時兩個x互不影響,緣由參照函數改寫步驟2。
function fn(x =1){ var x =2; console.log(x);//2 }; fn();
3.當函數形參聲明瞭x,函數體內未使用var或者let去聲明x,函數體內能夠直接修改和使用參數x的,此時共用的是同一個變量x,塊級做用域也存在做用域鏈。
var x =2; function fn(y = x){ x =3; console.log(y);//2 }; fn(); x//3
4.當函數形參未聲明x,可是參數內又有參數默認值使用了x,此時會從全局做用域繼承x。
var x = 1; function fn(y=x){ console.log(y);//1 }; fn();
那麼到這裏,我大概模擬了函數參數默認值時產生獨立做用域的過程,同時按照個人理解去解釋了它。也許個人推測與底層代碼實現有所誤差,可是這個模擬過程可以很直觀的去推測正確的執行結果。
我寫這篇文章也是爲了兩個目的,第一若是在面試中遇到,我能更好的解釋它,而不是似懂非懂;其次,在平常開發中使用函數參數默認值時,我能更清晰的寫出符合我預期結果的代碼,此時的你應該也能作到這兩點了。
本文中全部的代碼都是可測的,如有問題,或者更好的推測歡迎留言討論。
那麼就寫到這裏了,端午節快樂!