setTimeout系列(1)----弄清setTimeout中this的指向問題及解決方案

準備對setTimeout函數的使用做一個歸納總結,主要 涉及以下幾個問題:

1. this 的指向問題。
2. setTimeout中變量作用域的問題。
3. setTimeout中第三個參數的問題。
4. setTimeout中延時時間寫0的問題。

本篇主要講第一個問題:setTimeout中this指向的問題,及解決方案。

首先,記住這兩段話,它們非常的關鍵和重要:
“《JavaScript高級程序設計》第二版中,寫到:“超時調用的代碼都是在全局作用域中執行的,因此函數中this的值在非嚴格模式下指向window對象,在嚴格模式下是undefined”。

“我們說,setTimeout中有兩個this。第一,調用環境下的this,稱之爲第一個this;第二,把延遲執行函數中的this稱之爲第二個this;第一個this的指向是需要根據上下文來確定的,默認爲window;第二個this就是指向window。”

下面通過一些列子來說明:

一、在setTimeout中回調對象方法。

let obj = {
    a: 1,
    name: 'A.L',
    init: function () {
        console.log('this in init: ', this);
    }
}
obj.getName = function () {
    console.log('this in getName: ', this.name);
}

// 這兩句調用時絕對ok的
// no problem-------------------------
obj.init();   //obj
obj.getName();   //alice

當我們需求變成了,延遲1秒鐘顯示名字,於是:
1.2

//problem--------------------
setTimeout(obj.init, 1000); 	//window
setTimeout(obj.getName, 1000); 	//undefine

問題出現了!對象方法中的this變成了全局變量windows.

1.2代碼可以分解爲如下1.3,套用第一段話,超時調用的代碼都是在全局作用域中執行的,因此函數中this的值在非嚴格模式下指向window對象。所以這裏兩個延時回調函數中的this,都是window。

setTimeout(function () {
        				console.log('this in init: ', this);
   				 }, 1000); 	//window
setTimeout(function () {
    					console.log('this in getName: ', this.name);
				}, 1000); 	//undefine

還可以再繼續改造代碼:

function f1() {
       console.log('this in init: ', this);
}
function f2() {
    	console.log('this in getName: ', this.name);
}
setTimeout(f1, 1000); 	//window
setTimeout(f2, 1000); 	//undefine

好,下面來看解決方案:
在這裏插入圖片描述
對應鏈接:https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout

1. 方法1,wrapper function:

setTimeout(function () {
    obj.init()   //保留了obj對init()方法的調用。
}, 1500);

setTimeout(function () {
     obj.getName()  //保留了obj對getName()方法的調用。
 }, 1500);

2. 方法2,熟悉的箭頭函數,專治this指向不固定問題
在這裏箭頭函數作用不明顯,大致意思跟方法1中funtion的用途一樣。一會我會在【原型中使用setTimeout的例子】中做講解。

setTimeout(() => {
    obj.init()
}, 1000);

setTimeout(() => {
    obj.getName()
}, 1000);

3. 方法3,call, apply, bind,改變方法中this的指向

setTimeout(obj.init.call(obj), 1000);
setTimeout(obj.init.apply(obj), 1000);
setTimeout(obj.init.bind(obj)(), 1000);

setTimeout(obj.getName.call(obj), 1000);
setTimeout(obj.getName.apply(obj), 1000);
setTimeout(obj.getName.bind(obj)(), 1000);

對 call,apply,bind 不熟悉的童鞋,我後續會再總結篇這方面的文章。

4、方法4,當然還有最土的方法,用變量that來暫存this。
這個例子貌似也不怎麼用得上。會在【原型中使用setTimeout的例子】中講解。

二、在原型方法中調用setTimeout方法

定義了一個構造函數,Animal。

// code 2.1
function Animal(name) {
    this.name = name;
}
Animal.prototype.type = "All";
Animal.prototype.bark = function () {
    console.log('in barking....this: ', this, );
}
Animal.prototype.eat = function () {
    console.log('in eating...this: ', this);
    this.bark();
}

let a1 = new Animal();
a1.eat();            //in eating...this: Animal

需求變成了,改造Animal.prototype.eat()方法,讓其能在1秒鐘後正常調用this.bark()輸出’in barking…this: Animal;

於是,寫成如下2.2,出問題了this打印出來是window:

// code 2.2
 Animal.prototype.eat = function () {
    setTimeout(this.bark, 1000);    //回調函數內部的this: window
}

想起之前說的用1.1方法1 wrapper function的方法來解決,也不行了啊:

// code 2.3
Animal.prototype.eat = function () {
    setTimeout(function () {
        this.bark()       //這裏的this: window
    }, 1000);
}

想想這是爲什麼?因爲這裏的延時回調函數中對bark()的調用是this指針,而之前的例子是obj.init()沒有this指向的問題。

再想想重要的兩段話,
“超時調用的代碼都是在全局作用域中執行的,因此函數中this的值在非嚴格模式下指向window對象”,
“setTimeout中有兩個this。第一,調用環境下的this,稱之爲第一個this;第二,把延遲執行函數中的this稱之爲第二個this;第一個this的指向是需要根據上下文來確定的,默認爲window;第二個this就是指向window。”
是不是看出點啥。
延時回調函數裏的this,永遠指向window。

那我們繼續改代碼,改成如下,用箭頭函數來解決這個this問題:

// code 2.4
Animal.prototype.eat = function () {
    setTimeout(() => {
        this.bark()  //這裏的this: 實例對象
    }, 1000);
}

阮一峯es6中說到,箭頭函數中沒有自己的this的,而箭頭函數會默認使用父級函數作用域的this。
setTimeout()中的this,也是它作用域上下文中的this。那不就是調用eat函數
的Animal的實例呀。所以這裏的this,就是真正正確的指向,Animal的實例對象了。

那麼,只要能改變this的指向,就能讓this.bark()正確調用,還有哪些方法能實現。不就是1.3方法3中描述的call, apply, bind 三種方法。

Animal.prototype.eat = function () {
    setTimeout(this.bark.call(this), 1000);   //這裏的第一個this: 實例對象,第二個this,也是實例對象
};
// 或者這樣
Animal.prototype.eat = function () {
    setTimeout((function () {
        this.bark();          //這裏的this: 實例對象
    }).call(this), 1000);
};

最後,就說說那個土方法,暫存this到that,同樣能實現this的正確指向:

Animal.prototype.eat = function () {
    let that = this;
    setTimeout(function () {
        that.bark();
    }, 1000);
}

//----------------------------------------------------------------------------------
需要的話,還可以看看下面的例子,加深印象:
1. 示例1:

function foo(){
	setTimeout(function(){
		console.log(this);
	},100);
}

var obj ={a:1};
foo.call(obj);      		//window

2. 示例2:

function foo(){
	setTimeout(()=>{
		console.log(this);
	},100);
}
var obj ={a:1};
foo.call(obj);  		 //Object{a:1}

3.示例3:

function foo(){
	setTimeout(()=>{
		console.log(this);
	},100);
}
foo();    //window

foo()是被Window調用的,foo()函數作用域中的this也是windows。箭頭函數跟父級函數共享this。所以,執行結果是window.

4.示例4:

setTimeout(function(){
	console.log(this);
},100);

setTimeout中的匿名函數,沒有其它對象調用它。所以它的默認調用對象就是Window.

最後,文章上有什麼不妥的地方,希望大家指正,歡迎探討。

參考:
關於箭頭函數:

  1. https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions
  2. 箭頭函數在對象中的this指向及適用環境
    http://www.cnblogs.com/githubzy/p/5780135.html
  3. 在定義對象內部的新方法時,如何使用箭頭函數?https://segmentfault.com/q/1010000006944383
  4. 談談setTimeout的作用域以及this的指向問題
    http://www.cnblogs.com/hutaoer/p/3423782.html
  5. https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout
  6. ES6中setTimeout函數的執行上下文
    https://blog.csdn.net/liwusen/article/details/56278944?utm_source=blogxgwz0
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章