js中宣告提升、作用域(鏈)、`this`關鍵字和箭頭函式

漲月薪發表於2019-04-02

1.關於宣告提前

  • js中,允許變數使用在宣告之前,不過此時為undefined
console.log(a); // undefined
var a = 1;
複製程式碼
  • 變數不管在哪裡宣告,都會在任意程式碼執行前處理。在es5 strict mode,賦值給未宣告的變數將報錯。
  • 顯式宣告:帶有關鍵字 var 的宣告,作用域就是當前執行上下文,即某個函式,或者全域性作用域(宣告在函式外,即變數會掛在在window物件上)
  • 隱式宣告:如果一個變數沒有使用var宣告,window便擁有了該屬性,因此這個變數的作用域不屬於某一個函式體,而是window物件。
function varscope(){
    foo = "I'm in function"; //直接賦值 沒有宣告
    console.log(foo);//I'm in function
}
varscope();
console.log(window.foo); //I'm in function
複製程式碼
  • 關於宣告提前的例子
function testOrder(arg) {
    console.log(arg); // arg是形參,不會被重新定義
    console.log(a); // 因為函式宣告比變數宣告優先順序高,所以這裡a是函式
    var arg = 'hello'; // var arg;變數宣告被忽略, arg = 'hello'被執行
    var a = 10; // var a;被忽視; a = 10被執行,a變成number
    function a() {
        console.log('fun');
    } // 被提升到作用域頂部
    console.log(a); // 輸出10
    console.log(arg); // 輸出hello
}; 
testOrder('hi');
/* 輸出:
hi 
function a() {
        console.log('fun');
    }
10 
hello 
*/
複製程式碼

2.關於作用域

  • 函式作用域

    函式作用域內,對外是封閉的,從外層的作用域無法直接訪問函式內部的作用域

    function bar() {
      var testValue = 'inner';
    }
    console.log(testValue);	// 報錯:ReferenceError: testValue is not defined
    複製程式碼

    通過 return 訪問函式內部變數:

    function bar(value) {
      var testValue = 'inner';
      return testValue + value;
    }
    console.log(bar('fun'));// "innerfun"
    複製程式碼

    通過 閉包 訪問函式內部變數:

    function bar(value) {
      var testValue = 'inner';
      var rusult = testValue + value;
      function innser() {
        return rusult;
      };
      return innser();
    }
    console.log(bar('fun'));		// "innerfun"
    複製程式碼
  • 立即執行函式作用域

    這是個很實用的函式,很多庫都用它分離全域性作用域,形成一個單獨的函式作用域;它能夠自動執行(function() { //... })()裡面包裹的內容,能夠很好地消除全域性變數的影響;

    <script type="text/javascript">
        (function() {
          var testValue = 123;
          var testFunc = function () { console.log('just test'); };
        })();
        console.log(window.testValue);		// undefined
        console.log(window.testFunc);		// undefined
    </script>
    複製程式碼
  • 塊級作用域

    在 ES6 之前,是沒有塊級作用域的概念的。

    for(var i = 0; i < 5; i++) {
      // ...
    }
    console.log(i)				// 5
    複製程式碼

    很明顯,用 var 關鍵字宣告的變數,在 for 迴圈之後仍然被儲存這個作用域裡;

    這可以說明: for() { }仍然在,全域性作用域裡,並沒有產生像函式作用域一樣的封閉效果;

    如果想要實現 塊級作用域 那麼我們需要用 let 關鍵字宣告

    for(let i = 0; i < 5; i++) {
      // ...
    }
    console.log(i)				// 報錯:ReferenceError: i is not defined
    複製程式碼

    在 for 迴圈執行完畢之後 i 變數就被釋放了,它已經消失了!!!

    同樣能形成塊級作用域的還有 const 關鍵字:

    if (true) {
      const a = 'inner';
    }
    console.log(a);				// 報錯:ReferenceError: a is not defined
    複製程式碼

    let 和 const 關鍵字,建立塊級作用域的條件是必須有一個 { } 包裹:

  • 詞法作用域

    當我們要使用宣告的變數時:JS引擎總會從最近的一個域,向外層域查詢

    testValue = 'outer';
    function afun() {
      var testValue = 'middle';
      console.log(testValue);// "middle"
      function innerFun() {
        var testValue = 'inner';
        console.log(testValue);// "inner"
      }
      return innerFun();
    }
    afun();
    console.log(testValue);	// "outer"
    複製程式碼

    當 JS 引擎查詢變數時,發現全域性的 testValue 離得更近一些,則取全域性的testValue的值即 outer

    var testValue = 'outer';
    function foo() {
      console.log(testValue);// "outer"
    }
    
    function bar() {
      var testValue = 'inner';
      foo();
    }
    bar();
    複製程式碼
  • 動態作用域

    動態作用域,作用域是基於呼叫棧的,而不是程式碼中的作用域巢狀;

    作用域巢狀,有詞法作用域一樣的特性,查詢變數時,總是尋找最近的作用域;

3.關於this關鍵字

在一個函式中,this總是指向當前函式的所有者物件,this總是在執行時才能確定其具體的指向, 也才能知道它的呼叫物件。

window.name = "window";
function f(){
    console.log(this.name);
}
f();//window

var obj = {name:'obj'};
f.call(obj); //obj
複製程式碼

在執行f()時,此時f()的呼叫者是window物件,因此輸出”window”

f.call(obj) 是把f()放在obj物件上執行,相當於obj.f(),此時f中的this就是obj,所以輸出的是”obj”

對比以下兩段程式碼:

var foo = "window";
var obj = {
    foo : "obj",
    getFoo : function(){
        return function(){
            return this.foo;
        };
    }
};
var f = obj.getFoo(); 
f(); //輸出'window'

/* 分析
執行var  f = obj.getFoo()返回的是一個匿名函式,相當於:
var f = function(){
     return this.foo;
}
f() 相當於window.f(), 因此f中的this指向的是window物件,this.foo相當於window.foo, 所以f()返回"window" 
*/
複製程式碼
var foo = "window";
var obj = {
    foo : "obj",
    getFoo : function(){
        var that = this;
        return function(){
            return that.foo;
        };
    }
};
var f = obj.getFoo();
f(); //輸出'obj'

/* 分析
執行var f = obj.getFoo() 同樣返回匿名函式,即:
var f = function(){
     return that.foo;
}
唯一不同的是f中的this變成了that, 要知道that是哪個物件之前,先確定f的作用域鏈:f->getFoo->window 並在該鏈條上查詢that,此時可以發現that指代的是getFoo中的this, getFoo中的this指向其執行時的呼叫者,從var f = obj.getFoo() 可知此時this指向的是obj物件,因此that.foo 就相當於obj.foo,所以f()返回"obj"
*/
複製程式碼

4.關於箭頭函式

箭頭函式有兩種格式:

var fn = x => x * x; //只包含一個表示式,連{ ... }和return都省略掉了

x => { //還有一種可以包含多條語句,這時候就不能省略{ ... }和return:
    if (x > 0) {
        return x * x;
    }
    else {
        return - x * x;
    }
}
複製程式碼

箭頭函式看上去是匿名函式的一種簡寫,但實際上,箭頭函式和匿名函式有個明顯的區別:箭頭函式內部的this是詞法作用域,由上下文確定。

對比以下兩個例子

var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = function () {
            return new Date().getFullYear() - this.birth; // this指向window或undefined
        };
        return fn();
    }
};
複製程式碼
//箭頭函式完全修復了this的指向,this總是指向詞法作用域,也就是外層呼叫者obj:
var obj = {
    birth: 1990,
    getAge: function () {
        var b = this.birth; // 1990
        var fn = () => new Date().getFullYear() - this.birth; // this指向obj物件
        return fn();
    }
};
obj.getAge(); // 25

//由於this在箭頭函式中已經按照詞法作用域繫結了,所以,用call()或者apply()呼叫箭頭函式時,無法對this進行繫結,即傳入的第一個引數被忽略:
var obj = {
    birth: 1990,
    getAge: function (year) {
        var b = this.birth; // 1990
        var fn = (y) => y - this.birth; // this.birth仍是1990
        return fn.call({birth:2000}, year);
    }
};
obj.getAge(2015); // 25
複製程式碼

箭頭函式與this結合例子

var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () { 
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1() //person1
person1.show1.call(person2) //person2

person1.show2() //window
person1.show2.call(person2) //window

person1.show3()() //window 
/*person1.show3是一個高階函式,它返回了一個函式,分步走的話,應該是這樣:
var func = person3.show()
func()
從而導致最終呼叫函式的執行環境是window,但並不是window物件呼叫了它。所以說,this總是指向呼叫該函式的物件,這句話還得補充一句:在全域性函式中,this等於window。
*/


person1.show3().call(person2)//person2 通過person2呼叫了最終的列印方法
person1.show3.call(person2)()//window 先通過person2呼叫了person1的高階函式,然後再在全域性環境中執行了該列印方法。

person1.show4()() //person1 箭頭函式體內的this物件,就是定義時所在的物件,而不是使用時所在的物件
person1.show4().call(person2) //person1 箭頭函式體內的this物件,就是定義時所在的物件,而不是使用時所在的物件,用person2去呼叫這個箭頭函式,它指向的還是person1。
person1.show4.call(person2)() //person2 箭頭函式的this指向的是誰呼叫箭頭函式的外層function,箭頭函式的this就是指向該物件,如果箭頭函式沒有外層函式,則指向window
複製程式碼

相關文章