重學 this 關鍵字的用法

  • 2019 年 11 月 23 日
  • 筆記

為什麼要學習this關鍵字

1. 面試會問啊!總有一些面試官喜歡問你一段不可能這麼寫的程式碼。看一道經典且古老的面試題(學完本文後,文末會有一道更複雜的面試題等著你哦!)

程式碼如下:

let a = 5;    let obj = {      a : 10,      foo: function(){        console.log(this.a)      }    }        let bar = obj.foo    obj.foo()     bar()

2. 我在讀 Events 的 lib/events 源碼的時候發現多次用到call關鍵字,看來有必要搞懂 this 與 call 相關的所有內容。

其中幾句程式碼是這樣寫的

   // 場景1:    function EventEmitter() {      EventEmitter.init.call(this);    }        // 場景2:    return this.listener.call(this.target);        // 場景3:    return listenerCount.call(emitter, type);

3.箭頭函數使用不當報錯,在封裝 Node.js 的一個 ORM 映射框架 Sequelize 時,封裝表關聯關係,由於使用箭頭函數造成了讀到的上下文發生變化,不是想要的 model 資訊,而是指向了全局 。

4. call 關鍵字在寫程式碼過程中還是比較常用的,有時候我們常常會使用 call 關鍵字來指定某個函數運行時的上下文,有時候還使用 call 關鍵字實現繼承。

程式碼例子如下:

var person = {    "name": "koala"};function changeJob(company, work) {    this.company = company;    this.work    = work;};  changeJob.call(person, '百度', '程式設計師');console.log(person.work); // '程式設計師'

文章概覽圖

文章會同步到GitHub,部落格地址為:https://github.com/koala-coding/goodBlog

函數調用

JS(ES5)裡面有三種函數調用形式:

func(p1, p2) obj.child.method(p1, p2)func.call(context, p1, p2) // 這裡先不講 apply

好多初學者都只用到過前兩種情況,而且認為前兩者優於第三者。直到幾天前想系統複習一下this關鍵字,找this相關的各種資料,在知乎看到了一個關於this的討論。說第三種形式才是正常的調用形式。

func.call(context,p1,p2)

其它兩種都是語法糖,可以等價的變為 call形式。func(p1,p2)等價於func.call(undefined,p1,p2);

obj.child.method(p1,p2)等價於obj.child.method.call(obj.child,p1,p2);這麼看我們的函數調用只有一種形式:

func.call(context,p1,p2)

這時候是不是就知道this是什麼了,就是上面的context。回到我開篇提到的面試題。

let a = 5;let obj = {  a : 10,  foo: function(){    console.log(this.a)  }}  let bar = obj.fooobj.foo() bar()
  • obj.foo() 轉化為call的形式就是obj.foo.call(obj)

所以this指向了obj

  • bar() 轉化為call的形式就是bar.call() 由於沒有傳 context,所以 this 就是 undefined,如果是在瀏覽器中最後給你一個默認的 this——window 對象。如果是在 Node.js 環境中運行 this——globel對象。在瀏覽器中運行結果為5 在 Node.js 環境中為 undefined。

Node.js 環境下指向全局的this關鍵字說明(你可能不知道)

為什麼在瀏覽器或者前端環境可以直接正常輸出值,而在 Node.js 環境中輸出的卻是 undefined。看一下這段程式碼你可能就懂了。

(function(exports, require, module, __filename, __dirname) {    {    // 模組的程式碼    // 所以那整個程式碼應該在這裡吧    var a = 10;    function A(){        a = 5;        console.log(a);        console.log(this.a);    }    // const haha = new A();    A();    }});

先說一下 Node.js 環境下在運行某個 js 模組程式碼時候發生了什麼,Node.js 在執行程式碼之前會使用一個程式碼封裝器進行封裝,例如下面所示:

(function(exports, require, module, __filename, __dirname) {    {    // 模組的程式碼    // 所以那整個程式碼應該在這裡吧    }});

這段程式碼在 Node.js 環境下輸出結果為 5,undefined是不是就能理解了。這裡面的this是默認綁定指向全局,當輸出this.a的時候,全局應該指向這個閉包的最外層。所以輸出結果式是undefined。

[]語法中的this關鍵字

function fn (){ console.log(this) }var arr = [fn, fn2]arr[0]() // 這裡面的 this 又是什麼呢?

我們可以把 arr0 想像為arr.0( ),雖然後者的語法錯了,但是形式與轉換程式碼里的 obj.child.method(p1, p2) 對應上了,於是就可以愉快的轉換了:

        arr[0]() 假想為    arr.0()然後轉換為 arr.0.call(arr)那麼裡面的 this 就是 arr 了

this綁定原則

默認綁定

默認綁定是函數針對的獨立調用的時候,不帶任何修飾的函數引用進行調用,非嚴格模式下 this 指向全局對象(瀏覽器下指向 Window,Node.js 環境是 Global ),嚴格模式下,this 綁定到 undefined ,嚴格模式不允許this指向全局對象。

var  a = 'hello'  var obj = {    a: 'koala',    foo: function() {        console.log(this.a)    }}  var  bar = obj.foo  bar()              // 瀏覽器中輸出: "hello"

這段程式碼, bar()就是默認綁定,函數調用的時候,前面沒有任何修飾調用,也可以用之前的 call函數調用形式理解,所以輸出結果是 hello

默認綁定的另一種情況

在函數中以函數作為參數傳遞,例如 setTimeOutsetInterval等,這些函數中傳遞的函數中的 this指向,在非嚴格模式指向的是全局對象。

例子:

var name = 'koala';var person2 = {    name: '程式設計師成長指北',    sayHi: sayHi}  function sayHi(){    console.log('Hello,', this.name);}  setTimeout(function(){    person2.sayHi();},200);// 輸出結果 Hello,koala

隱式綁定

判斷 this 隱式綁定的基本標準:函數調用的時候是否在上下文中調用,或者說是否某個對象調用函數。

例子:

var a = 'koala'  var obj = {    a: '程式設計師成長指北',    foo: function() {        console.log(this.a)    }}obj.foo()       // 瀏覽器中輸出: "程式設計師成長指北"

foo 方法是作為對象的屬性調用的,那麼此時 foo 方法執行時,this 指向 obj 對象。

隱式綁定的另一種情況

當有多層對象嵌套調用某個函數的時候,如 對象.對象.函數,this 指向的是最後一層對象。

例子:

function sayHi(){    console.log('Hello,', this.name);}var person2 = {    name: '程式設計師成長指北',    sayHi: sayHi}var person1 = {    name: 'koala',    friend: person2}person1.friend.sayHi();  // 輸出結果為 Hello, 程式設計師成長指北

看完這個例子,是不是也就懂了隱式調用的這種情況。

顯式綁定

顯式綁定,通過函數call apply bind 可以修改函數this的指向。call 與 apply 方法都是掛載在 Function 原型下的方法,所有的函數都能使用。

call 和 apply 的區別

  1. call和apply的第一個參數會綁定到函數體的this上,如果 不傳參數,例如 fun.call(),非嚴格模式,this默認還是綁定到全局對象
  2. call函數接收的是一個參數列表,apply函數接收的是一個參數數組。
unc.call(thisArg, arg1, arg2, ...)        // call 用法func.apply(thisArg, [arg1, arg2, ...])     // apply 用法

看程式碼例子:

var person = {    "name": "koala"};function changeJob(company, work) {    this.company = company;    this.work    = work;};  changeJob.call(person, '百度', '程式設計師');console.log(person.work); // '程式設計師'  changeJob.apply(person, ['百度', '測試']);console.log(person.work); // '測試'

call和apply的注意點

這兩個方法在調用的時候,如果我們傳入數字或者字元串,這兩個方法會把傳入的參數轉成對象類型。

例子:

var number = 1, string = '程式設計師成長指北';function getThisType () {    var number = 3;    console.log('this指向內容',this);    console.log(typeof this);}getThisType.call(number);getThisType.apply(string); // 輸出結果// this指向內容 [Number: 1]// object// this指向內容 [String: '程式設計師成長指北']// object

bind函數

bind 方法 會創建一個新函數。當這個新函數被調用時,bind() 的第一個參數將作為它運行時的 this,之後的一序列參數將會在傳遞的實參前傳入作為它的參數。(定義內容來自於 MDN )

func.bind(thisArg[, arg1[, arg2[, ...]]])    // bind 用法

例子:

var publicAccounts = {    name: '程式設計師成長指北',    author: 'koala',    subscribe: function(subscriber) {        console.log(subscriber + this.name)    }}  publicAccounts.subscribe('小紅')   // 輸出結果: "小紅 程式設計師成長指北"  var subscribe1 = publicAccounts.subscribe.bind({ name: 'Node成長指北', author: '考拉' }, '小明 ')subscribe1()       // 輸出結果: "小明 Node成長指北"

new 綁定

使用new調用函數的時候,會執行怎樣的流程:

  1. 創建一個空對象
  2. 將空對象的 proto 指向原對象的 prototype
  3. 執行構造函數中的程式碼
  4. 返回這個新對象

例子:

function study(name){    this.name = name;  }var studyDay = new study('koala');console.log(studyDay);console.log('Hello,', studyDay.name);// 輸出結果// study { name: 'koala' }// hello,koala

newstudy('koala')的時候,會改變this指向,將 this指向指定到了studyDay對象。注意:如果創建新的對象,構造函數不傳值的話,新對象中的屬性不會有值,但是新的對象中會有這個屬性。

手動實現一個new創建對象程式碼(多種實現方式哦)

function New(func) {    var res = {};    if (func.prototype !== null) {        res.__proto__ = func.prototype;    }    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));    if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {        return ret;    }    return res;}var obj = New(A, 1, 2);// equals tovar obj = new A(1, 2);

this綁定優先順序

上面介紹了 this 的四種綁定規則,但是一段程式碼有時候會同時應用多種規則,這時候 this 應該如何指向呢?其實它們也是有一個先後順序的,具體規則如下:

new綁定 > 顯式綁定 > 隱式綁定 > 默認綁定

箭頭函數中的 this

箭頭函數

在講箭頭函數中的 this 之前,先講一下箭頭函數。

定義

MDN:箭頭函數表達式的語法比函數表達式更短,並且不綁定自己的this,arguments,super或 new.target。這些函數表達式最適合用於非方法函數(non-method functions),並且它們不能用作構造函數。

  • 箭頭函數中沒有 arguments

常規函數可以直接拿到 arguments 屬性,但是在箭頭函數中如果使用 arguments 屬性,拿到的是箭頭函數外層函數的 arguments 屬性。

例子:

function constant() {    return () => arguments[0]}  let result = constant(1);console.log(result()); // 1

如果我們就是要訪問箭頭函數的參數呢?

你可以通過 ES6 中 命名參數 或者 rest 參數的形式訪問參數

let nums = (...nums) => nums;
  • 箭頭函數沒有構造函數

箭頭函數與正常的函數不同,箭頭函數沒有構造函數 constructor,因為沒有構造函數,所以也不能使用 new 來調用,如果我們直接使用 new 調用箭頭函數,會報錯。

例子:

let fun = ()=>{}let funNew = new fun(); // 報錯內容 TypeError: fun is not a constructor
  • 箭頭函數沒有原型

原型 prototype 是函數的一個屬性,但是對於箭頭函數沒有它。

例子:

let fun = ()=>{}console.loh(fun.prototype); // undefined
  • 箭頭函數中沒有 super

上面說了沒有原型,連原型都沒有,自然也不能通過 super 來訪問原型的屬性,所以箭頭函數也是沒有 super 的,不過跟 this、arguments、new.target 一樣,這些值由外圍最近一層非箭頭函數決定。

  • 箭頭函數中沒有自己的this

箭頭函數中沒有自己的 this,箭頭函數中的 this 不能用 call()、apply()、bind() 這些方法改變 this 的指向,箭頭函數中的 this 直接指向的是 調用函數的上一層運行時

let a = 'kaola'  let obj = {    a: '程式設計師成長指北',    foo: () => {        console.log(this.a)    }}  obj.foo()             // 輸出結果: "koala"

看完輸出結果,怕大家有疑問還是分析一下,前面我說的箭頭函數中this直接指向的是 調用函數的上一層運行時,這段程式碼 obj.foo在調用的時候如果是不使用箭頭函數this應該指向的是 obj ,但是使用了箭頭函數,往上一層查找,指向的就是全局了,所以輸出結果是 koala

自執行函數

什麼是自執行函數?自執行函數在我們在程式碼只能夠定義後,無需調用,會自動執行。開發過程中有時間測試某一小段程式碼報錯會使用。程式碼例子如下:

(function(){    console.log('程式設計師成長指北')})()

或者

(function(){    console.log('程式設計師成長指北')}())

但是如果使用了箭頭函數簡化一下就只能使用第一種情況了。使用第二種情況簡化會報錯。

(() => {    console.log('程式設計師成長指北')})()

this應用場景

應用場景其實就是開篇說到的為什麼寫這篇文章,再重複一下。

  1. 面試官他考!
  2. 看源碼總看見,有時候想確認一下當前的上下文指向。為什麼源碼中用的多,大家可以想想這個問題。
  3. 我們寫程式碼也會用,經常會出現用 call 指向某個對象的上下文,或者實現繼承等等。

學後小練習

學到這裡是不是發現開篇那道面試題有點簡單,已經不能滿足你目前對於 this 關鍵字的知識儲備。好的,我們來一道複雜點的面試題。

程式碼如下:

var length = 10;function fn() {    console.log(this.length);}  var obj = {  length: 5,  method: function(fn) {    fn();    arguments[0]();  }};  obj.method(fn, 1);//輸出是什麼?

這段程式碼的輸出結果是: 10,2

認真讀文章的應該都能正確的答出答案,每一個細節文章中都講了,我在這就不具體分析,如果不懂可以再讀文章,或者直接加我好友我們一起討論,kaola 是一個樂於分享的人,期待與你共同進步。

聲明:任何形式轉載都請聯繫本人,如有問題也感謝您的指出和建議哦。

參考文章

  • MDN中this關鍵字的講解 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this
  • 知乎的一個關於this討論 :https://www.zhihu.com/question/19636194
  • 阮一峰老師的ES6書籍中箭頭函數內容
  • 書籍《你不知道的javascript》