【愣錘筆記】一篇小短文讓你完全搞懂this、call、apply和bind

跟我左手右手一塊兒慢動做,右手左手慢動做重複。額~貌似哪裏有點不對勁哎?讓我想一想,右手?左手?慢動做??重複???重播???不對不對,是左手call,右手apply,一塊兒來bind this。es6

額,這都能強扯一波,好吧,讓我吐血一波~~~提及了js中的this,確實是個有趣的話題,也是不少小夥伴一開始傻傻分不清的老命題了。算是老梗重提,再來聊聊this吧。api

關於this,首先要提的一點就是,它指向一個對象,具體指向誰是由函數運行時所處的上下文決定的。這是最重要的一個概念,也是理解js中this的關鍵。所謂的上下文,能夠理解爲函數運行時的環境,例如一個函數在全局中運行,那麼它的上下文就是這個全局對象,客戶端中這個global對象就是window;函數做爲對象的方法運行,那麼它的上下文就是該對象。數組

關於this的指向問題,咱們能夠大體分爲以下幾種情景來討論:

  • 函數做爲普通函數調用
  • ES5嚴格模式下的函數調用
  • 函數做爲對象的一個方法調用
  • 構造器中的this(也就是常說的類中的this,可是要搞清楚js是沒有類的,是基於原型委託實現的繼承,類只是你們習慣性的叫法)
(1)函數做爲普通函數調用:你們學習js,對函數應該是再熟悉不過了。函數但是js中的一等公民,人中呂布、馬中赤兔啊。

var name1 = 'hello this';
window.name2 = 'hello global';
function func () {
  console.log(this.name1); // 輸出:"hello this"
  console.log(this.name2); // 輸出:"hello global"
}
func();複製代碼

這裏的代碼你們天然一眼就知道結果了,結果寫在了上面的註釋裏。經過運行結果咱們知道,普通函數在全局調用中,this指向全局對象。這裏咱們定義了一個全局變量name1,和一個window的屬性name2,因此this.name1和this.name2如咱們的預期指向了這兩個值。值得一提的是:定義的全局變量,是被做爲全局對象window的屬性存在的哦。此時咱們打印看下window對象,看圖:瀏覽器


(2)ES5嚴格模式下的函數調用:this再也不指向全局對象,而是undefined。bash

function strictFunc () {
  'use strict'
  console.log(this)
  console.log(this.name)
}
strictFunc()複製代碼

咱們先看下運行結果:babel


能夠看到,this打印出來的值是undefined,而this.name會直接報錯。由此說明,嚴格模式下,this已經再也不指向全局對象,而是undefined值。引用undefinednull值的屬性會報Uncaught TypeError錯,這點咱們在平常開發中須要注意一下,以避免由於一個錯誤致使後面的程序直接掛掉(這是js單線程的緣由,一旦程序出錯,後面便不會再執行)。特別是咱們在拿到一些不是咱們決定的數據(例如後臺返回的)進行處理的時候,使用對象的屬性時最好判斷一下,這樣在極端狀況下,也能夠保證咱們的程序繼續跑下去,而不至於直接掛掉:app

obj && obj.name
// 而不是直接取值:
obj.name

或者用try/catch捕獲錯誤:
try {
    const { data } = await api.getArticleList()
} catch {

} finally {

}
複製代碼

(3)函數做爲對象的方法使用:this指向該對象函數

var obj = {
  name: 'xiaoming',
  getInfo () {
    return '姓名: ' + this.name;
  }
}
console.log(obj.getInfo()); // 姓名: xiaoming 複製代碼

當對象當屬性的值是一個函數時,咱們會稱這個函數是這個對象的一個方法。該方法中的this在運行時指向的是該對象。上面的例子的輸出結果也看的清清楚楚,然鵝,沒錯,就是鵝,現實有時候是會啪啪打臉的,打的響亮亮的、輕脆脆的、綠油油的~哎,我爲何要說綠油油,毛病。下面我簡單改寫一個上面的代碼:工具

// 仍是這個obj,仍是熟悉的味道
var obj = {
  name: 'xiaoming',
  getInfo () {
    return '姓名: ' + this.name;
  }
}
// 定義一個引入obj.getInfo的變量
var referenceGetInfo = obj.getInfo;
console.log(referenceGetInfo()); // 輸出:姓名:複製代碼

最終咱們沒有拿到預期的name值,打臉了吧,說好了的指向該對象的呢!果真咱們男人都是騙子,都是大豬蹄子!性能

這是爲何呢?咱們知道js分爲兩種數據類型:基本數據類型,如string、number、undefined、null等,引用類型,如object。而像數組、函數等,本質都是對象,因此都是引用類型。函數名只不過是指向該函數在內存中位置的一個引用。因此,這裏var referenceGetInfo = obj.getInfo在賦值以後,referenceGetInfo也只是該函數的一個引用。在看referenceGetInfo 的調用位置,是在全局中,因此是做爲普通函數調用的。由此this指向window,因此沒有值。能夠在getInfo函數中,增長以下驗證,結果必然是true

console.log(this === window)複製代碼

(4)構造器函數中的this:指向該構造器返回的新對象

提及構造器函數,可能感受會有些生硬,其實就是咱們常說的定義類時的那個函數。例如,下面這個最多見的一個類(構造器函數):

// 定義Person類
var Person = function (name, sex) {
  this.name = name;
  this.sex = sex;
}
// 定義Person類的原型對象
Person.prototype = {
  constructor: Person,
  getName: function () {
    return '我叫:' + this.name;
  },
  getSex: function () {
    return '性別:' + this.sex;
  }
}
// 實例化一個p1
var p1 = new Person('愣錘', '男');
// 調用p1的方法
console.log(p1.getName()); // 我叫:愣錘
console.log(p1.getSex()); // 性別:男複製代碼

構造器函數本是也是一個函數,若是直接調用該函數,那它和普通函數沒什麼區別。可是經過new調用以後,那它就成爲了構造器函數。構造器函數在實例化時會返回一個新建立的對象,並將this指向該對象。因此this.name的值是"愣錘"。另外這裏再提一點,若是你擔憂用戶使用類時忘記加new,能夠經過以下方式,強制使用new調用:

var Person = function (name, sex) {
  // 在構造器中增長以下這一行,其他不變
  if (!(this instanceof Person)) return new Person(name, sex);
  this.name = name;
  this.sex = sex;
}複製代碼

該行代碼判斷了當前的this是不是Person類的實例,若是不是則強制返回一個經過new初始化的類。覺得若是用戶忘記使用new初始化類,那麼此時的構造器函數是做爲普通函數調用的,this在非嚴格模式下指向window,確定不會是Person類的實例,因此咱們直接強制返回new初始化。這也是咱們在開發類庫時可使用的一個小技巧。

弄明白了js中的this的指向,下面咱們再聊聊如何改變this的指向。在js中,改變this指向方法,常見的有以下幾種:

  • Function.prototype.call()
  • Function.prototype.apply()
  • Function.prororype.bind()
  • 除此以外,還有eval()、with()等

(1)call()方法和apply()方法都是ES3中就存在的方法,能夠改變函數的this指向,二者的功能徹底同樣,因此這裏放在一塊兒說。惟一的區別是二者調用時傳入的參數不一樣,後面會仔細介紹。

// 仍是熟悉的味道,仍是那個obj
var obj = {
  name: 'xiaoming',
  getInfo (sex) {
    return '姓名: ' + this.name + '性別:' + this.sex || '未知';
  }
}
// 定義另外一個obj對象
var otherObj = {
  name: '狗子你變了,你不再是我認識的那個二狗了!'
}

console.log(obj.getInfo.call(otherObj, '女')); 
// 姓名: 狗子你變了,你不再是我認識的那個二狗了!性別:女複製代碼

咱們經過callobj.getInfo方法放在ohterObj這個對象執行,輸出了ohterObj.name的值,由此驗證了call能夠函數this的指向。call()方法接收多個參數: 

  • 第一個參數爲可選參數,即this指向的新的上下文對象。若是不傳該參數,則指向全局對象。若不傳入第一個參數且該方法(getInfo)使用嚴格模式,this值且undefined,和普通函數的嚴格模式同樣,從undefined上取值會報錯。
  • 後面的全部參數都是做爲參數傳遞給方法調用

apply()方法和call的功能同樣,只不過傳入的參數不同:

  • 第一個參數爲可選參數,和上面👆call的同樣
  • 第二個參數是一個參數數組/類數組,數組包含的全部參數都會做爲參數傳遞給該方法調用

用法很簡單,和call同樣就很少介紹了。可是這裏提到了類數組概念,說一下什麼是類數組,能夠理解爲自己不是數組,可是卻能夠像數組同樣擁有length屬性(例如函數的arguments對象)。咱們沒有確切的辦法判斷一個對象是否是類數組,因此這裏咱們只能使用js中的鴨子類型來判斷。何爲鴨子類型:若是它走起路來像鴨子,叫聲也像鴨子,咱們便認爲它就是鴨子。

鴨子類型是js中很重要的一個概念,由於咱們此時並不真正關心它是否是鴨子,咱們只是想聽到鴨子叫/或者看到鴨子走,即咱們要的只是它擁有鴨子的行爲,至於它是否是鴨子,無所謂呀!!!

因此只要一個對象能擁有數組的行爲,咱們就能夠把它做爲數組使用。下面引入underscore中的類數組判斷方法說明:

var isArrayLike = function(collection) {

var length = getLength(collection);
  return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};複製代碼

underscore.js中對類數組的判斷其實也是運用了鴨子類型的思想,即判斷若是該對象擁有length屬性且是number類型,而且length的值大於等於0小於等於一個數組的最大元素個數,那咱們就認定他是數組。

好了,有的稍微扯遠了。

下面繼續apply的實際運用場景,例如柯里化函數:

// 定義一個柯里化函數
var currying = function () {
  var arrPro = Array.prototype,
  fn = arrPro.shift.call(arguments),
  args = arrPro.slice.call(arguments);
    return function () {
    var _args = arrPro.slice.call(arguments);
    return fn.apply(fn, args.concat(_args));
  }
}
// 定義一個返回a+b的函數var add = function (a, b) {
  return a + b;
}
// 將這個求和函數進行柯里化,使其第一項的值恆爲5
var curryAdd = currying(add, 5);
var res = curryAdd(4);
console.log(res); // 9複製代碼

咱們在開發中apply方法和call方法是用的比較多的,例如這裏柯里化函數。特別是高階函數中,函數做爲值返回的時候,會常用apply這些方法來綁定函數運行時的上下文對象。

咱們再看一個更常見的函數節流吧:

// 去抖函數
function debounce (fn, delay) {
  var timer;
  return function () {
    var args = arguments;
    var _this = this;
    timer && clearTimeout(timer);
    timer = setTimeout(function () {
      fn.apply(_this, args);
    }, delay);
  }
}
// 調用,在瀏覽器窗口滾動的狀況下,debounce裏的函數並不會被頻繁觸發,而是滾動結束500ms後觸發
window.addEventListener('scroll', debounce(function () {
  console.log('window scroll');
}, 500), false);複製代碼

咱們在這個去抖函數裏,在返回的函數裏,使用裏定時器,而定時器的第一個參數是一個函數,因此造成裏一個局部的函數做用域。爲了能保證咱們的fn函數中的this的正確指向,咱們經過apply改變它的指向。

所謂去抖函數,在一個函數被頻繁調用的時候,若是這次調用距離上一次的時間小於咱們定下的delay 值,那麼取消本次調用。主要用來防止頻繁觸發的問題,從而提供程序運行性能。注意,上面只是一個函數去抖,真正在提高滾動性能的時候,咱們更多的是會將去抖和節流結合起了使用。此處更多地在於演示apply的運用場景,再也不多作節流去抖方面的說明。

call方法在v8的實現中,實際上是做爲apply方法的語法糖,由此,咱們能夠試着使用apply來模擬一個call方法(並不是v8源碼實現):

Function.prototype.call = function () {
  var ctx = Array.prototype.shift.apply(arguments);
  return this.apply(ctx, arguments);
}複製代碼

咱們知道call方法,第一個參數是上下文對象,因此咱們的第一件事就是取出參數中的第一個參數ctx,而後把剩餘的參數使用apply的方式調用。so,就是這樣。

(2)說完了call和apply,下面咱們再說一下ES5引入的新方法:Function.prototype.bind

該方法返回一個新的函數,並將該函數的this綁定到指定的上下文環境。接收多個參數:

  • 第一個參數爲this綁定到的新上下文環境
  • 後面的參數會做爲參數傳遞給該函數

用法很簡單,相信你們都會用:

// 仍是那個熟悉的狗子,哦不對,仍是那個熟悉的對象
var obj = {
  name: 'xiaoming',
  getInfo (sex, hobby) {
    return '姓名: ' + this.name + ', 性別:' + (sex || '未知') + hobby;
  }
}
// 另一個狗子,呸呸呸!另一個對象
var obj2 = {
  name: '我已經不是你認識的狗子了'
}
// 輸出:姓名: 我已經不是你認識的狗子了, 性別:男, 興趣:打球
var newGetInfo = obj.getInfo.bind(obj2, '男');console.log(newGetInfo('打球'));複製代碼

能夠看到,bind()後返回了一個新函數,並把第一個參數後面的參數傳遞給了obj.getInfo方法,在運行newGetInfo('打球')時,又繼續把參數傳遞給了obj.getInfo方法。是否是發現它自然支持了函數柯里化,是否是感受跟咱們上面的柯里化函數功能同樣?

可是bind方法,是es5引入的,在es3是不支持的。這時候可能會說了,es5已是主流了,你們也都已經大量使用es6及更高的語法,反正又babel等工具幫咱們轉換成es5的。沒錯,可是咱們仍是要了解其實現的,好比寫一個bind方法的profill。作到知其然,知其因此然。

// 若是自己支持bind方法,則使用原生的bind方法,不然咱們就實現一個使用
Function.prototype.bind = Function.prototype.bind || function () {
  var fn = this;  var ctx = arguments[0];
  var args = Array.prototype.slice.call(arguments, 1);
  return function () {
    var _args = Array.prototype.slice.call(arguments);
    return fn.apply(ctx, args.concat(_args));
  }
}複製代碼

講到這,相信已經能夠將this/call/apply方法搞清楚了。由此還引伸出更多的函數節流/去抖/柯里化/反柯里化,仍是能夠繼續深刻深究一下的。

相關文章
相關標籤/搜索