ES6函數參數默認值做用域的模擬原理實現與我的的一些推測

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();

那麼到這裏,我大概模擬了函數參數默認值時產生獨立做用域的過程,同時按照個人理解去解釋了它。也許個人推測與底層代碼實現有所誤差,可是這個模擬過程可以很直觀的去推測正確的執行結果。

我寫這篇文章也是爲了兩個目的,第一若是在面試中遇到,我能更好的解釋它,而不是似懂非懂;其次,在平常開發中使用函數參數默認值時,我能更清晰的寫出符合我預期結果的代碼,此時的你應該也能作到這兩點了。

本文中全部的代碼都是可測的,如有問題,或者更好的推測歡迎留言討論。

那麼就寫到這裏了,端午節快樂!

相關文章
相關標籤/搜索