1.非箭頭函數下的 this
var obj = { x: 0, f1: function () { console.log(this.x); } } var f1 = obj.f1; var x = 1; obj.f1(); //0 f1(); //1
上面代碼中,雖然 obj.f1 和 f1 指向的是同一個函數,但是執行的結果卻不一樣。這種差異的原因,就在於函數體內使用了 this 關鍵字。我們都知道,this 指的是函數運行時所在的環境。對於 obj.f1() 來說,f1 運行在 obj 環境下,所以this 指向obj ;對於 f1() 來說,f1() 運行在全局環境,所以 this 指向全局環境。因此,兩者的運行結果不一樣。
現在我們來想想,為什么會這樣呢?為什么obj.f1() 就是在 obj 環境執行,而 var f1 = obj.f1 之后,f1() 就是在全局環境執行呢?
那就要從內存里面的數據結構說起了,我們一步一步來看。
var obj = { x: 0, f1: function () { console.log(this.x); } }
上面代碼中,將一個對象賦值給變量 obj ,javascript 引擎會先在內存中,生成一個對象,然后再將這個對象的內存地址賦值給變量 obj。也就是說,變量obj 是一個地址。后面如果要讀取 obj.x,引擎首先從 obj 拿到內存地址,然后再從該地址讀出原始的對象,返回 x 屬性。
如上圖所示,對象的每一個屬性名都對應一個屬性描述對象,屬性的值保存在屬性描述對象的 value 屬性里面。
屬性的值為函數時,引擎會將函數單獨保存在內存中,然后再將函數的地址賦值給 f1 屬性的 value 屬性。由於函數是一個單獨的值,所以它可以在不同的環境(上下文)執行。
我們可以在函數內部,引用當前環境的其他變量。那么,是怎樣在函數內部獲得當前的運行環境的呢?答案就是 this ,this 的設計目的就是在函數體內部,指代函數當前的運行環境。
現在我們可以回答一開始的問題了, obj.f1() 是通過 obj 里存的地址找到 f1, 所以是在 obj 環境下執行的;而 var f1 = obj.f1 之后,f1() 直接指向函數本身,所以 f1() 就是在全局環境執行.
2.箭頭函數中的this
箭頭函數不會創建自己的this,它只會從自己的作用域鏈的上一層繼承this。箭頭函數導致 this 總是指向函數定義生效時所在的對象。
function Timer(){ this.s1 = 0; this.s2 = 0; //箭頭函數 setInterval(() => this.s1++, 1000); //普通函數 setInterval(function (){ this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() => console.log('s1: ', timer.s1), 3100); //s1: 3 setTimeout(() => console.log('s2: ',timer.s2), 3100); //s2: 0
上面代碼中, Timer 函數內部設置了兩個定時器,分別使用了箭頭函數和普通函數。箭頭函數的 this 綁定定義時所在的作用域(即 Timer 函數),普通函數的 this 指向運行時所在的作用域(即全局對象)。var timer = new Timer() 這條語句執行之后,再過1000毫秒,箭頭函數定義生效,this總是指向函數定義生效時所在對象,即timer;對於普通函數,因為setTimeout()
調用的代碼運行在與所在函數完全分離的執行環境上,這會導致 this 指向 window 對象。所以3100毫秒之后,timer.s1 被更新了3次,而 timer.s2 一次都沒更新。
箭頭函數中,this 對象的指向是固定的。這種指向的固定化,實際是因為箭頭函數根本沒有自己的 this ,導致內部的 this 就是外層代碼塊的 this 。正是因為它沒有this,所以也就不能用作構造函數。
var name = 'window'; var A = { name : 'A', show: () => { console.log(this.name); } } A.show(); //window
如上面代碼中的箭頭函數,即 show 函數內沒有 this,其內部的 this 就是外層代碼塊的 this,因為沒有其他函數的包裹,所以最外層代碼塊的this指向的就是window對象。所以最后會輸出window。
那么,怎樣改成永遠綁定A呢?
var name = 'window'; var A = { name: 'A', show: function() { var s = () => console.log(this.name); return s; } } var show = A.show(); show(); //A var B = { name: B } show.call(B); //A show.call(); //A
這樣就做到了永遠指向 A 對象,我們再來分析一下:
原代碼中 show 即是箭頭函數,沒有自己的 this;修改之后的代碼中,箭頭函數所在的作用域是在 show 函數內,show 是普通函數,有自己的 this,在函數調用時生效。A.show() 指向的對象是 A。所以箭頭函數 s 中的 this 就是指向 A 了。