聊一下JS中的做用域scope和閉包closure

先上幾道面試題練練手

var bb = 1;
    function aa(bb) {
      bb = 2;
      alert(bb);
    }
    aa(bb);
    alert(bb);
var a="undefined";
    var b="false";
    var c="";
    function assert(aVar){
        if(aVar)     
            alert(true);
        else  
            alert(false);
    }
    assert(a);
    assert(b);
    assert(c);
function Foo() {
        var i = 0;
        return function() {
            console.log(i++);
        };
    }
    Foo();
    var f1 = Foo(), f2 = Foo();
    
    f1();
    f1();
    f2();
var foo = true;
    if (foo) {
        let bar = foo * 2;
        bar = something( bar );
        console.log( bar );
    }
    console.log( bar );
var foo = true;
    if (foo) {
        var a = 2;
        const b = 3; //僅存在於if的{}內
        a = 3;
        b = 4; // 出錯,值不能修改
    }
    console.log( a ); // 3
    console.log( b ); // ReferenceError!

閉包的深度遞進

在JavaScript中,做用域是基於函數來界定的。也就是說屬於一個函數內部的代碼,函數內部以及內部嵌套的代碼均可以訪問函數的變量。面試

順便講講常見的兩種error,ReferenceError和TypeError。如上圖,若是在bar裏使用了d,那麼通過查詢都沒查到,那麼就會報一個ReferenceError;若是bar裏使用了b,可是沒有正確引用,如b.abc(),這會致使TypeError。閉包

嚴格的說,在JavaScript也存在塊級做用域。以下面幾種狀況:app

  • with異步

var obj = {a: 2, b: 2, c: 2};
    with (obj) { //均做用於obj上
      a = 5;
      b = 5;
      c = 5;  
    }
  • let函數

let是ES6新增的定義變量的方法,其定義的變量僅存在於最近的{}以內。以下this

var foo = true;
    if (foo) {
        let bar = foo * 2;
        bar = something( bar );
        console.log( bar );
    }
    console.log( bar ); // ReferenceError
  • constcode

與let同樣,惟一不一樣的是const定義的變量值不能修改。以下:對象

var foo = true;
    if (foo) {
      var a = 2;
      const b = 3; //僅存在於if的{}內
      a = 3;
      b = 4; // 出錯,值不能修改
    }
    console.log( a ); // 3
    console.log( b ); // ReferenceError!

瞭解這些了後,咱們來聊聊閉包。什麼叫閉包?簡單的說就是一個函數內嵌套另外一個函數,這就會造成一個閉包。這樣提及來可能比較抽象,那麼咱們就舉例說明。可是在距離以前,咱們再複習下這句話,來,跟着大聲讀一遍,「不管函數是在哪裏調用,也不管函數是如何調用的,其肯定的詞法做用域永遠都是在函數被聲明的時候肯定下來的」。
來,下面咱們看一個經典的閉包的例子:ip

for (var i=1; i<=9; i++) {
         setTimeout( function timer(){
         console.log( i );
         },1000 );
     }

運行的結果是啥捏?你可能期待每隔一秒出來一、二、3...10。那麼試一下,按F12,打開console,將代碼粘貼,回車!咦???等一下,擦擦眼睛,怎麼會運行了10次10捏?這是腫麼回事呢?咋眼睛還很差使了呢?不要着急,等我給你忽悠!
如今,再看看上面的代碼,因爲setTimeout是異步的,那麼在真正的1000ms結束前,其實10次循環都已經結束了。咱們能夠將代碼分紅兩部分分紅兩部分,一部分處理i++,另外一部分處理setTimeout函數。那麼上面的代碼等同於下面的:作用域

// 第一個部分
    i++; 
    i++; // 總共作10次
              
    // 第二個部分
    setTimeout(function() {
        console.log(i);
    }, 1000);
    
    setTimeout(function() {
       console.log(i);
    }, 1000); // 總共作10次

看到這裏,相信你已經明白了爲何是上面的運行結果了吧。那麼,咱們來找找如何解決這個問題,讓它運行如咱們所料!

由於setTimeout中的匿名函數沒有將i做爲參數傳入來固定這個變量的值,讓其保留下來, 而是直接引用了外部做用域中的i, 所以i變化時,也影響到了匿名函數。其實要讓它運行的跟咱們料想的同樣很簡單,只須要將setTimeout函數定義在一個單獨的做用域裏並將i傳進來便可。以下:

for (var i=1; i<=9; i++) {
         (function(){
          var j = i;
          setTimeout( function timer(){
               console.log( j );
          }, 1000 );
         })();
     }

不要激動,勇敢的去試一下,結果確定如你所料。那麼再看一個實現方案:

for (var i=1; i<=9; i++) {
         (function(j){
             setTimeout( function timer(){
                 console.log( j );
             }, 1000 );
         })( i );
     }

啊,竟然這麼簡單啊,你確定在這麼想了!那麼,看一個更優雅的實現方案:

for (let i=1; i<=9; i++) {
         setTimeout( function timer(){
             console.log( i );
         }, 1000 );
     }

咦?!腫麼回事呢?是否是出錯了,不着急,我這裏也出錯了。這是由於let須要在strict mode中執行。具體如何使用strict mode模式,自行谷歌吧

再整理一些面試題吧

var x = 1;
    var y = 0;
    var z = 0;
    function add(n){n=n+1;}
    y = add(x);
    function add(n){n=n+3;}
    z = add(x);
    console.log(x,y,z);
    //兩個函數沒有返回值,打印1 undefined undefined
function getAge() {
        var y = new Date().getFullYear();
        return y - this.birth;
    }
    
    var xiaoming = {
        name: '小明',
        birth: 1990,
        age: getAge
    };
    
    xiaoming.age(); // 25, 正常結果
    getAge(); // NaN

單獨調用函數getAge怎麼返回了NaN?請注意,咱們已經進入到了JavaScript的一個大坑裏。JavaScript的函數內部若是調用了this,那麼這個this到底指向誰?

答案是,視狀況而定!若是以對象的方法形式調用,好比xiaoming.age(),該函數的this指向被調用的對象,也就是xiaoming,這是符合咱們預期的。

若是單獨調用函數,好比getAge(),此時,該函數的this指向全局對象,也就是window。
坑爹啊!

var xiaoming = {
        name: '小明',
        birth: 1990,
        age: function () {
            var that = this; // 在方法內部一開始就捕獲this
            function getAgeFromBirth() {
                var y = new Date().getFullYear();
                return y - that.birth; // 用that而不是this
            }
            return getAgeFromBirth();
        }
    };
    
    xiaoming.age(); // 25
function getAge() {
        var y = new Date().getFullYear();
        return y - this.birth;
    }
    
    var xiaoming = {
        name: '小明',
        birth: 1990,
        age: getAge
    };
    
    xiaoming.age(); // 25
    getAge.apply(xiaoming, []); // 25, this指向xiaoming, 參數爲空

另外一個與apply()相似的方法是call(),惟一區別是:

  • apply()把參數打包成Array再傳入;

  • call()把參數按順序傳入。

function foo() {
        var x = 'Hello, ' + y;
        alert(x);//hello,undefined
        var y = 'Bob';
    }
    foo();
相關文章
相關標籤/搜索