聊聊JS裡的this

shooterRao發表於2018-02-28

前言

在我剛開始學js和寫js時,以及在工作中,我都被this這傢伙困擾過,迷惑過。經過我查閱書籍和反覆實踐,終於大致搞懂了關於this這個機制,其實,它並不難,還挺有意思的。下面我就來總結和解析下什麼是this

什麼是this?為什麼要用this?

'this'是JS中一個機制,也是一個關鍵字,它被自動定義在所有函式的作用域中。

在設計API中,使用'this'會以一種更加優雅的方式來傳遞一個物件的引用,使得API設計更加簡潔,擴充性更強。例如在物件導向程式設計中,"this"的使用會非常頻繁。

我開始對this的誤解

誤解一:this指向函式自身

demo:

function fn() {
  this.count++;
}
fn.count = 0;
fn();
fn();
console.log(fn.count);// 0
複製程式碼

看完上面那段程式碼,如果你感到驚訝的話,不要慌,我當時也是這種感覺,哈哈,this就是這樣,對初學者迷惑極大,Follow me,讓我帶你逐步擊退這些迷惑。

事實上,上面程式碼中,函式fn內部的this指向是全域性物件window或者global(Node.js中),並沒有指向函式fn自身。為什麼this指向全域性物件去了?因為非嚴格模式下全域性作用域中函式被獨立呼叫時,它的this預設指向(繫結)window或者global;在嚴格模式中,它的this為undefined

注意:在全域性物件中,瀏覽器執行環境的是window,Node環境中是global

誤解二:this指向函式的作用域

丟擲個demo:

function fn() {
  var a = 1;
  this.b();// 這裡this指向的是window
}
function b() {
  console.log(this.a);// 這個this指向的也是window
}
fn();// 報錯a is not defined
複製程式碼

上面程式碼開始是希望函式b能通過this來訪問函式fn作用域變數a,但是事實並不能。函式b裡的this不會在詞法作用域中查到函式fn裡的變數a,它們的this均指向window。如果想要函式b訪問函式fn裡的a變數,可以使用閉包解決

function fn() {
  var a = 1;
  function b() {
    console.log(a)
  }
  b();
}
fn();// 1
複製程式碼

揭開this這一機制的真實面紗

總的來說,你只要牢記這兩點:

  • this不指向函式自身,也不指向函式作用域
  • this是在函式被呼叫時發生的繫結,它的(繫結)指向完全取決於函式在哪裡被呼叫,並不是在編寫時繫結

其中,第一點已經舉例證明,現在來細說第二點。

函式被呼叫時,this的繫結規則(重點)

預設繫結

當函式被獨立呼叫時,也就是直接函式名( ),沒有new啊,call,apply,作為物件的方法呼叫這些,函式裡的this會被預設繫結到全域性物件(在非嚴格模式下),也就是上文中兩個demo,裡面的函式的this均指向全域性物件。

給上文誤解一裡demo加點料加深理解:

var count = 0;
function fn() {
  this.count++;// this指向window
}
fn.count = 0;
fn();// 實際呼叫的是window.fn()
fn();
console.log(fn.count);// 0
console.log(count);// 2 實際呼叫的是window.count
複製程式碼

隱式繫結

這條規則具體是這樣的,函式如果被某個物件(舉例obj物件)擁有,且作為obj物件的方法呼叫時,函式裡面的this就是obj物件。

舉個demo:

function fn () {
  console.log(this.a);
}
var obj = {
  a: 1,
  fn: fn
}
obj.fn();// 1

// 或者
var obj = {
  a: 1,
  fn: function() {
    console.log(this.a);
  }
}
obj.fn();// 1
複製程式碼

這個規則會有幾種常見的“怪現象”,舉個demo,在回撥函式中:

function cb(fn) {
  fn&&fn();// 看,呼叫位置在這!
}
function fn () {
  console.log(this.a);
}
var a = 2;// window的
var obj = {
  a: 1,
  fn: fn
};

cb(obj.fn);// 2

複製程式碼

為什麼會這樣?其實,是醬紫的,函式fn並沒有作為obj物件的方法被呼叫,而是通過obj物件傳入給函式cb,作為回撥進行呼叫,也就是直接fn()呼叫了,觸發了上面的預設繫結,this被繫結到window物件中。

那麼,有沒有方法讓函式fn訪問obj物件的屬性a呢?答案是有的,如下:

function cb(fn) {
  fn&&fn();// 看,呼叫位置在這!
}
function fn () {
  console.log(this.a);
}
var a = 2;// window的
var obj = {
  a: 1,
  fn: fn
};

cb(obj.fn.bind(obj));// 1

複製程式碼

哈哈,終於可以獲取obj物件的屬性a啦,關於bind繫結,就要說說有關顯式繫結了。

顯式繫結

js提供了call,apply以及ES5提供的bind方法給我們來強制繫結函式的this。

demo:

function fn() {
  console.log(this.a);
};
var obj = {a: 1};
fn.call(obj);// 1
複製程式碼

callapply的區別是呼叫函式時,傳參的方法不同。

demo:

var obj = {
  a: 1
};
function fn(params) {
  console.log(this.a,params)
};
fn.call(obj,2); // call傳參是一個個傳
fn.apply(obj,[2]); // apply傳參是傳一個陣列
複製程式碼

說說bindbind會返回一個硬編碼的新函式,它會把你指定的引數設定為this的上下文並呼叫原始函式(來自:《你不知道的JavaScript》)

在上文隱式繫結的末尾,就用了bind來繫結this實現訪問obj物件裡的a屬性。

你可以用call或者apply來簡單模擬實現bind方法。

if(!Function.prototype.bind) {
  Function.prototype.bind = function(context) {
    var self = this,args = Array.prototype.slice.call(arguments, 1),;
    return function() {
      self.apply(context, args.concat(Array.prototype.slice.call(arguments)))
    }
  }
}
複製程式碼

new繫結

在使用new來呼叫函式,會執行下面的操作

  1. 建立一個全新的物件
  2. 這個新物件會被執行[[Prototype]]連線
  3. 這個新物件會指向(繫結)到函式呼叫的this
  4. 如果函式沒有返回其他物件,那麼new函式呼叫會自動返回這個新物件

demo:

function fn(a) {
  this.a = a;
}
var obj = new fn(1);
obj.a // 1

// 以上程式碼,可以這麼理解
var obj = {};// 建立新物件
obj.__proto__ = fn.prototype;// 關聯原型鏈
fn.call(obj);// 繫結this;
複製程式碼

關於箭頭函式裡的this

箭頭函式裡this,不採用上面的4種原則,而是根據外層(函式或者全域性)作用域來決定this,它會繼承外層函式呼叫的this繫結。

箭頭函式能解決什麼有關this的問題?

舉個demo:

var a = 123;
var obj = {
  a: 1,
  fn: function() {
    // 若obj.fn(), 這個作用域this指向obj; 
    return function() {
      // 這種情況下,這個閉包裡的this是無法訪問外部作用域的this
      console.log(this.a);// this指向全域性
    }
  }
}
obj.fn()();// 123

// ES5
var obj = {
  a: 1,
  fn: function() {
    // 用self儲存當前this的引用;
    var self = this;
    return function() {
      console.log(self.a);// 1
    }
  }
}
obj.fn()();// 1

// 使用箭頭函式
var obj = {
  a: 1,
  fn: function() {
    return () => {
      // 箭頭繼承了外層函式的this的繫結
      console.log(this.a);
    }
  }
}
obj.fn()();// 1
複製程式碼

箭頭函式書寫簡潔,語義清晰明瞭。

還有在setTimeout回撥函式中,同樣可以使用

function Fn(a) {
  this.a = a;
  setTimeout(() => {
    console.log(this.a);
  },0)
};
new Fn(1);// 1
複製程式碼

還有,在React開發中,用箭頭函式或者bind可以解決函式中this的繫結問題。

小結

這篇文章的內容都是我在讀《你不知道的JavaScript》和《高程3》中總結出來的,加上一些自己的理解。如果有不對的地方,煩請指出,一起討論,如果對你有幫助,我很開心^_^

相關文章