1. 程式人生 > >【THE LAST TIME】this:call、apply、bind

【THE LAST TIME】this:call、apply、bind

前言

The last time, I have learned

【THE LAST TIME】一直是我想寫的一個系列,旨在厚積薄發,重溫前端。

也是給自己的查缺補漏和技術分享。

歡迎大家多多評論指點吐槽。

系列文章均首發於公眾號【全棧前端精選】,筆者文章集合詳見Nealyang/personalBlog。目錄皆為暫定

講道理,這篇文章有些拿捏不好尺度。準確的說,這篇文章講解的內容基本算是基礎的基礎了,但是往往這種基礎類的文章很難在囉嗦和詳細中把持好。文中道不到的地方還望各位評論多多補充指正。

THE LAST TIME 系列

  • 【THE LAST TIME】徹底吃透 JavaScript 執行機制

This

相信使用過 JavaScript 庫做過開發的同學對 this 都不會陌生。雖然在開發中 this 是非常非常常見的,但是想真正吃透 this,其實還是有些不容易的。包括對於一些有經驗的開發者來說,也都要駐足琢磨琢磨~ 包括想寫清楚 this 呢,其實還得聊一聊 JavaScript 的作用域和詞法

This 的誤解一:this 指向他自己

function foo(num) {
  console.log("foo:"+num);
  this.count++;
}

foo.count = 0;

for(var i = 0;i<10;i++){
    foo(i);
}

console.log(foo.count);

通過執行上面的程式碼我們可以看到,foo函式的確是被呼叫了十次,但是this.count似乎並沒有加到foo.count上。也就是說,函式中的this.count並不是foo.count。

This 的誤解二:this 指向他的作用域

另一種對this的誤解是它不知怎麼的指向函式的作用域,其實從某種意義上來說他是正確的,但是從另一種意義上來說,這的確是一種誤解。

明確的說,this不會以任何方式指向函式的詞法作用域,作用域好像是一個將所有可用識別符號作為屬性的物件,這從內部來說他是對的,但是JavaScript程式碼不能訪問這個作用域“物件”,因為它是引擎內部的實現

function foo() {
    var a = 2;
    this.bar();
}

function bar() {
    console.log( this.a );
}

foo(); //undefined

全域性環境中的 This

既然是全域性環境,我們當然需要去明確下宿主環境這個概念。簡而言之,一門語言在執行的時候需要一個環境,而這個環境的就叫做宿主環境。對於 JavaScript 而言,宿主環境最為常見的就是 web 瀏覽器。

如上所說,我們也可以知道環境不是唯一的,也就是 JavaScript 程式碼不僅僅可以在瀏覽器中跑,也能在其他提供了宿主環境的程式裡面跑。另一個最為常見的就是 Node 了,同樣作為宿主環境node 也有自己的 JavaScript 引擎:v8.

  • 瀏覽器中,在全域性範圍內,this 等價於 window 物件
  • 瀏覽器中,用 var 宣告一個變數等價於給 this 或者 window 新增屬性
  • 如果你在宣告一個變數的時候沒有使用var或者let(ECMAScript 6),你就是在給全域性的this新增或者改變屬性值
  • 在 node 環境裡,如果使用 REPL 來執行程式,那麼 this 就等於 global
  • 在 node 環境中,如果是執行一個 js 指令碼,那麼 this 並不指向 global 而是module.exports{}
  • 在node環境裡,在全域性範圍內,如果你用REPL執行一個指令碼檔案,用var宣告一個變數並不會和在瀏覽器裡面一樣將這個變數新增給this
  • 如果你不是用REPL執行指令碼檔案,而是直接執行程式碼,結果和在瀏覽器裡面是一樣的
  • node環境裡,用REPL執行指令碼檔案的時候,如果在宣告變數的時候沒有使用var或者let,這個變數會自動新增到global物件,但是不會自動新增給this物件。如果是直接執行程式碼,則會同時新增給globalthis

這一塊程式碼比較簡單,我們不用碼說話,改為用圖說話吧!

函式、方法中的 This

很多文章中會將函式和方法區分開,但是我覺得。。。沒必要啊,咱就看誰點了如花這位菇涼就行

當一個函式被呼叫的時候,會建立一個活動記錄,也成為執行環境。這個記錄包含函式是從何處(call-stack)被呼叫的,函式是 如何 被呼叫的,被傳遞了什麼引數等資訊。這個記錄的屬性之一,就是在函式執行期間將被使用的this引用。

函式中的 this 是多變的,但是規則是不變的。

你問這個函式:”老妹~ oh,不,函式!誰點的你?“

”是他!!!“

那麼,this 就指向那個傢伙!再學術化一些,所以!一般情況下!this不是在編譯的時候決定的,而是在執行的時候繫結的上下文執行環境。this 與宣告無關!

function foo() {
    console.log( this.a );
}

var a = 2;

foo(); // 2

記住上面說的,誰點的我!!! => foo() = windwo.foo(),所以其中this 執行的是 window 物件,自然而然的打印出來 2.

需要注意的是,對於嚴格模式來說,預設繫結全域性物件是不合法的,this被置為undefined。

function foo() {
    console.log( this.a );
}

var obj2 = {
    a: 42,
    foo: foo
};

var obj1 = {
    a: 2,
    obj2: obj2
};

obj1.obj2.foo(); // 42

雖然這位 xx 被點的多了。。。但是,我們只問點他的那個人,也就是 ojb2,所以 this.a輸出的是 42.

注意,我這裡的點!不是你想的那個點哦,是執行時~

建構函式中的 This

恩。。。這,就是從良了

還是如上文說到的,this,我們不看在哪定義,而是看執行時。所謂的建構函式,就是關鍵字new打頭!

誰給我 new,我跟誰

其實內部完成了如下事情:

  • 一個新的物件會被建立
  • 這個新建立的物件會被接入原型鏈
  • 這個新建立的物件會被設定為函式呼叫的this繫結
  • 除非函式返回一個他自己的其他物件,這個被new呼叫的函式將自動返回一個新建立的物件
foo = "bar";
function testThis(){
  this.foo = 'foo';
}
console.log(this.foo);
new testThis();
console.log(this.foo);
console.log(new testThis().foo)//自行嘗試

call、apply、bind 中的 this

恩。。。這就是被包了

在很多書中,call、apply、bind 被稱之為 this 的強繫結。說白了,誰出力,我跟誰。那至於這三者的區別和實現以及原理呢,咱們下文說!

function dialogue () {
  console.log (`I am ${this.heroName}`);
}
const hero = {
  heroName: 'Batman',
};
dialogue.call(hero)//I am Batman

上面的dialogue.call(hero)等價於dialogue.apply(hero)``dialogue.bind(hero)().

其實也就是我明確的指定這個 this 是什麼玩意兒!

箭頭函式中的 this

箭頭函式的 this 和 JavaScript 中的函式有些不同。箭頭函式會永久地捕獲 this值,阻止 apply或 call後續更改它。

let obj = {
  name: "Nealyang",
  func: (a,b) => {
      console.log(this.name,a,b);
  }
};
obj.func(1,2); // 1 2
let func = obj.func;
func(1,2); //   1 2
let func_ = func.bind(obj);
func_(1,2);//  1 2
func(1,2);//   1 2
func.call(obj,1,2);// 1 2
func.apply(obj,[1,2]);//  1 2

箭頭函式內的 this值無法明確設定。此外,使用 call 、 apply或 bind等方法給 this傳值,箭頭函式會忽略。箭頭函式引用的是箭頭函式在建立時設定的 this值。

箭頭函式也不能用作建構函式。因此,我們也不能在箭頭函式內給 this設定屬性。

class 中的 this

雖然 JavaScript 是否是一個面向物件的語言至今還存在一些爭議。這裡我們也不去爭論。但是我們都知道,類,是 JavaScript 應用程式中非常重要的一個部分。

類通常包含一個 constructor , this可以指向任何新建立的物件。

不過在作為方法時,如果該方法作為普通函式被呼叫, this也可以指向任何其他值。與方法一樣,類也可能失去對接收器的跟蹤。

class Hero {
  constructor(heroName) {
    this.heroName = heroName;
  }
  dialogue() {
    console.log(`I am ${this.heroName}`)
  }
}
const batman = new Hero("Batman");
batman.dialogue();

建構函式裡的 this指向新建立的 類例項。當我們呼叫 batman.dialogue()時, dialogue()作為方法被呼叫, batman是它的接收器。

但是如果我們將 dialogue()方法的引用儲存起來,並稍後將其作為函式呼叫,我們會丟失該方法的接收器,此時 this引數指向 undefined 。

const say = batman.dialogue;
say();

出現錯誤的原因是JavaScript 類是隱式的執行在嚴格模式下的。我們是在沒有任何自動繫結的情況下呼叫 say()函式的。要解決這個問題,我們需要手動使用 bind()將 dialogue()函式與 batman繫結在一起。

const say = batman.dialogue.bind(batman);
say();

this 的原理

咳咳,技術文章,咱們嚴肅點

我們都說,this指的是函式執行時所在的環境。但是為什麼呢?

我們都知道,JavaScript 的一個物件的賦值是將地址賦值給變數的。引擎在讀取變數的時候其實就是要了個地址然後再從原地址讀出來物件。那麼如果物件裡屬性也是引用型別的話(比如 function),當然也是如此!

而JavaScript 允許函式體內部,引用當前環境的其他變數,而這個變數是由執行環境提供的。由於函式又可以在不同的執行環境執行,所以需要個機制來給函式提供執行環境!而這個機制,也就是我們說到心在的 this。this的初衷也就是在函式內部使用,代指當前的執行環境。

var f = function () {
  console.log(this.x);
}

var x = 1;
var obj = {
  f: f,
  x: 2,
};

// 單獨執行
f() // 1

// obj 環境執行
obj.f() // 2

obj.foo()是通過obj找到foo,所以就是在obj環境執行。一旦var foo = obj.foo,變數foo就直接指向函式本身,所以foo()就變成在全域性環境執行.

總結

  • 函式是否在new中呼叫,如果是的話this繫結的是新建立的物件
var bar = new Foo();
  • 函式是否通過call、apply或者其他硬性呼叫,如果是的話,this繫結的是指定的物件
var bar = foo.call(obj);
  • 函式是否在某一個上下文物件中呼叫,如果是的話,this繫結的是那個上下文物件
var bar = obj.foo();
  • 如果都不是的話,使用預設繫結,如果在嚴格模式下,就繫結到undefined,注意這裡是方法裡面的嚴格宣告。否則繫結到全域性物件
var bar = foo();

小試牛刀

var number = 2;
var obj = {
  number: 4,
  /*匿名函式自調*/
  fn1: (function() {
    var number;
    this.number *= 2; //4

    number = number * 2; //NaN
    number = 3;
    return function() {
      var num = this.number;
      this.number *= 2; //6
      console.log(num);
      number *= 3; //9
      alert(number);
    };
  })(),

  db2: function() {
    this.number *= 2;
  }
};

var fn1 = obj.fn1;

alert(number);

fn1();

obj.fn1();

alert(window.number);

alert(obj.number);

評論區留下你的答案吧~

call & applay

上文中已經提到了 callapplybind,在 MDN 中定義的 apply 如下:

apply() 方法呼叫一個函式, 其具有一個指定的this值,以及作為一個數組(或類似陣列的物件)提供的引數

語法:

fun.apply(thisArg, [argsArray])

  • thisArg:在 fun 函式執行時指定的 this 值。需要注意的是,指定的 this 值並不一定是該函式執行時真正的 this 值,如果這個函式處於非嚴格模式下,則指定為 null 或 undefined 時會自動指向全域性物件(瀏覽器中就是window物件),同時值為原始值(數字,字串,布林值)的 this 會指向該原始值的自動包裝物件。
  • argsArray:一個數組或者類陣列物件,其中的陣列元素將作為單獨的引數傳給 fun 函式。如果該引數的值為null 或 undefined,則表示不需要傳入任何引數。從ECMAScript 5 開始可以使用類陣列物件。瀏覽器相容性請參閱本文底部內容。

如上概念 apply 類似.區別就是 apply 和 call 傳入的第二個引數型別不同。

call 的語法為:

fun.call(thisArg[, arg1[, arg2[, ...]]])

需要注意的是:

  • 呼叫 call 的物件,必須是個函式 Function
  • call 的第一個引數,是一個物件。 Function 的呼叫者,將會指向這個物件。如果不傳,則預設為全域性物件 window。
  • 第二個引數開始,可以接收任意個引數。每個引數會對映到相應位置的 Function 的引數上。但是如果將所有的引數作為陣列傳入,它們會作為一個整體對映到 Function 對應的第一個引數上,之後引數都為空。

apply 的語法為:

Function.apply(obj[,argArray])

需要注意的是:

  • 它的呼叫者必須是函式 Function,並且只接收兩個引數
  • 第二個引數,必須是陣列或者類陣列,它們會被轉換成類陣列,傳入 Function 中,並且會被對映到 Function 對應的引數上。這也是 call 和 apply 之間,很重要的一個區別。

記憶技巧:apply,a 開頭,array,所以第二引數需要傳遞資料。

請問!什麼是類陣列?

核心理念

借!

對,就是借。舉個栗子!我沒有女朋友,週末。。。額,不,我沒有摩托車