一次筆試引發的關於setTimeout的this的思考

JayJunG發表於2018-12-21

之前對於setTimeout的this指向理解一直迷迷糊糊,在專案實踐中也沒有遇到相關問題,在面試時也沒有被過關於這個問題,所以得過且過,直到最近的一次筆試碰到了令自己困惑的問題才去深入的瞭解了。也再一次提醒自己對於知識點的理解真的應該細緻到位,不能停留在表面。

在《高程3》中關於setTimeout的this的描述是:超時呼叫的程式碼都是在全域性作用域中執行的,因此函式中this的值在非嚴格模式下指向window物件在嚴格模式下是undefined,那為什麼是這樣呢,我們可以簡單的理解為setTimeout為window物件下的一個方法,本文僅討論非嚴格模式下的情況。依據高程三的結論,如果真正理解了,我們可以搞定遇到的百分之90的問題,如:

setTimeout(console.log(this),0)//window
複製程式碼

let obj = {
    print : function () {
        setTimeout(function () {
            console.log('setTimeout:'+this);
        },0);
    }
}; 
obj.print() //setTimeout: window複製程式碼

function say() {
            console.log('setTimeout:'+this);
        }let obj = {
    print : function () {
        setTimeout(say,0);
    }
}; 
obj.print() //setTimeout: window複製程式碼

無論是直接引用、通過物件方法呼叫還是函式引用都很容易理解,有時候,我們會遇到兩個this的情況,如下,一個是setTimeout呼叫環境中的this,一個是延遲執行函式中的this,這個時候需要注意區別,我們可以理解為,setTimeout中的第一個引數就是一個單純的函式的引用而已,它的指向跟我們一般的函式呼叫時一樣取決於被呼叫時所處的環境。

let obj = {
      say : function () {
            console.log(this);  //延遲執行函式中的this
        },
    print : function () {
        setTimeout(this.say,0); //setTimeout呼叫環境中的this,指向呼叫者即obj
    }
}; 
obj.print() //setTimeout: window複製程式碼

我們換種寫法讓上面程式碼中setTimeout呼叫環境中的this指向window,此時函式執行就不會有什麼效果了:

let obj = {
      say : function () {
            console.log(this);  //延遲執行函式中的this
        },
      print : function () {
        setTimeout(this.say,0); //setTimeout呼叫環境中的this,指向呼叫者即obj
    }
}; 
let func = obj.print;
func() 複製程式碼

下面再看:

var a = 1;
function func(){
        let a = 2;
        setTimeout(function(){
            console.log(a);
            console.log(this.a);
    },0) 
}
func(); //輸出2 1複製程式碼

var a = 1;
function func(){
       // let a = 2;
        setTimeout(function(){
            console.log(a);
            console.log(this.a);
    },0) 
}
func(); //輸出1 1複製程式碼

可見,在沒有使用this時,在setTimeout超時呼叫中變數是跟正常函式呼叫時沿著定義時的作用域向上查詢的。

那麼,當以字串形式執行又是怎麼樣呢

var a = 2
function say(a){
  console.log(a)
}
function test(){
  let a = 1;
  setTimeout("say(a)",0)
}
test() //2

var a = 2
function test(){
  let a = 1;
  function say(a){
    console.log(a)
  }
  setTimeout("say(a)",0)
}
test()  //say is not defined複製程式碼

可見,當把say方法移到test內部時報錯say is not defined,原因是以字串形式執行時javascript內部實際上呼叫了eval(),而eval的執行環境是全域性作用域window,全域性作用域沒有say方法所以報錯。

將引數直接以賦值形式傳進去則不會報錯:

var a = 2;
function say(a){
    console.log(a)
}
function test(){
    let a = 1;
    setTimeout("say('hhhh')",0)
  }
test() //hhhh複製程式碼

現在看看結合es6的箭頭函式時this指向是怎麼樣的,大家都知道,由於箭頭函式不繫結this, 它會捕獲其所在(即定義的位置)上下文的this值, 作為自己的this值,在setTimeout中情況亦是如此。

let obj = {
    name :  "jay",
    print : function () {
        setTimeout(() => {
            console.log(this.name)
    },0);
    }
}; 
obj.print() //jay複製程式碼

如何改變setTimeout的this指向

前面的討論其實已經有兩種答案了,即利用中間變數引用外面的this和應用箭頭函式

方法一
let obj = {
    name :  "jay",
    print : function () {
           let that = this;
        setTimeout(function() {
            console.log(that.name)
    },0);
    }
}; 方法二
let obj = {
    name :  "jay",
    print : function () {
        setTimeout(() => {
            console.log(this.name)
    },0);
    }
}; 複製程式碼

還有一種方法是應用bind方法:

方法三
var name = "window";
 function say(){
  console.log(this.name);
}
let obj = {
  name : "jay",
  print : function(){
    setTimeout(say.bind(this),0)
  }
}
obj.print(); //jay

複製程式碼

setTimeout引數傳遞問題

1.setTimeout(function,milliseconds,param1,param2,...);  param1,param2,...是可選項,用於給function提供額外的引數,但是注意,該特性在IE9及之前的IE不能使用!!

function say(name) {
   console.log(name)
 }s
etTimeout(say,0,'jay')複製程式碼

2.字串形式傳參:

function say(a,b) {
   console.log(a+b)
 } 
//let name = "jay"
setTimeout( "say(3,4)",3000) //三秒後輸出7複製程式碼

注意事項

儘量避免setTimeout第一個引數為字串,setTimeout允許講一個字串作為第一個引數,js內部將會呼叫eval()函式用來動態執行一段字串指令碼,eval()具有許多不可預見的危險性,eval的效率是非常低的,執行一段程式碼需要先將字串轉換為可執行程式碼,也就是比平常多了一步,並且可能隱式建立全域性變數。

setTimeout遞迴呼叫時注意記得應用clearTimeout清除,以避免無限遞迴造成記憶體洩漏。



相關文章