關於函式中this指向的問題
箭頭函式有幾個使用注意點。
(1)函式體內的this
物件,就是定義時所在的物件,而不是使用時所在的物件。
(2)不可以當作建構函式,也就是說,不可以使用new
命令,否則會丟擲一個錯誤。
(3)不可以使用arguments
物件,該物件在函式體內不存在。如果要用,可以用Rest引數代替。
(4)不可以使用yield
命令,因此箭頭函式不能用作Generator函式。
上面四點中,第一點尤其值得注意。this
物件的指向是可變的,但是在箭頭函式中,它是固定的。
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
上面程式碼中,setTimeout
的引數是一個箭頭函式,這個箭頭函式的定義生效是在foo
函式生成時,而它的真正執行要等到100毫秒後。如果是普通函式,執行時this
應該指向全域性物件window
,這時應該輸出21
。但是,箭頭函式導致this
總是指向函式定義生效時所在的物件(本例是{id:
42}
),所以輸出的是42
。
箭頭函式可以讓setTimeout
裡面的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);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
上面程式碼中,Timer
函式內部設定了兩個定時器,分別使用了箭頭函式和普通函式。前者的this
繫結定義時所在的作用域(即Timer
函式),後者的this
指向執行時所在的作用域(即全域性物件)。所以,3100毫秒之後,timer.s1
被更新了3次,而timer.s2
一次都沒更新。
箭頭函式可以讓this
指向固定化,這種特性很有利於封裝回調函式。下面是一個例子,DOM事件的回撥函式封裝在一個物件裡面。
var handler = {
id: '123456',
init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id);
}
};
上面程式碼的init
方法中,使用了箭頭函式,這導致這個箭頭函式裡面的this
,總是指向handler
物件。否則,回撥函式執行時,this.doSomething
這一行會報錯,因為此時this
指向document
物件。
this
指向的固定化,並不是因為箭頭函式內部有繫結this
的機制,實際原因是箭頭函式根本沒有自己的this
,導致內部的this
就是外層程式碼塊的this
。正是因為它沒有this
,所以也就不能用作建構函式。
所以,箭頭函式轉成ES5的程式碼如下。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
上面程式碼中,轉換後的ES5版本清楚地說明了,箭頭函式裡面根本沒有自己的this
,而是引用外層的this
。
請問下面的程式碼之中有幾個this
?
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
上面程式碼之中,只有一個this
,就是函式foo
的this
,所以t1
、t2
、t3
都輸出同樣的結果。因為所有的內層函式都是箭頭函式,都沒有自己的this
,它們的this
其實都是最外層foo
函式的this
。
除了this
,以下三個變數在箭頭函式之中也是不存在的,指向外層函式的對應變數:arguments
、super
、new.target
。
function foo() {
setTimeout(() => {
console.log('args:', arguments);
}, 100);
}
foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
上面程式碼中,箭頭函式內部的變數arguments
,其實是函式foo
的arguments
變數。
另外,由於箭頭函式沒有自己的this
,所以當然也就不能用call()
、apply()
、bind()
這些方法去改變this
的指向。
(function() {
return [
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });
// ['outer']
上面程式碼中,箭頭函式沒有自己的this
,所以bind
方法無效,內部的this
指向外部的this
。
長期以來,JavaScript語言的this
物件一直是一個令人頭痛的問題,在物件方法中使用this
,必須非常小心。箭頭函式”繫結”this
,很大程度上解決了這個困擾。