準備對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.
最後,文章上有什麼不妥的地方,希望大家指正,歡迎探討。
參考:
關於箭頭函數:
- https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Functions/Arrow_functions
- 箭頭函數在對象中的this指向及適用環境
http://www.cnblogs.com/githubzy/p/5780135.html - 在定義對象內部的新方法時,如何使用箭頭函數?https://segmentfault.com/q/1010000006944383
- 談談setTimeout的作用域以及this的指向問題
http://www.cnblogs.com/hutaoer/p/3423782.html - https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout
- ES6中setTimeout函數的執行上下文
https://blog.csdn.net/liwusen/article/details/56278944?utm_source=blogxgwz0