前端戰五渣學JavaScript——call、apply以及bind

寫這篇博客以前,我想先說下今天(2019年3月28日)一直關注的一件事吧(出於湊熱鬧的心情——尷尬)。在昨天,全球最大交友網站Github上悄然出現一個名爲996.ICU的文檔項目,整個項目沒有代碼,只是列了一些《勞動法》的條款和最近代表實行996工做制的公司。原本覺得是一個小打小鬧的抱怨,結果今天中午再看的時候star數已經有30k以上,而且issues達到5000+。下午更是勢如破竹,在Github的star排行榜上,一路過五關斬六將,截止目前,這個出現不到24小時的項目,坐擁63k的star,而且排行榜第21名。爲何一個這麼簡單的項目會異軍突起,伴着屠榜的架勢,一發不可收拾。也許這只是觸動了被強行996工做的朋友們,以及無休止的加班沒有回報的程序員們心中那最敏感的神經,可能迫於生計問題,現實生活中只能忍氣吞聲,但當出現一個虛擬的世界可讓你盡情發泄的時候,心中的苦水傾瀉而出,造就了這個怪異的項目。咱們不是不能接受996,是要實行996工做制公司得付的出相應的報酬,這讓員工感受本身的付出是有回報的,既沒有相應的酬勞,又沒有本身的時間,怨氣只會越攢越多。咱們如今能作什麼:1、儘可能不去996的公司,讓996的公司無人可招;2、提升本身的技術水平,讓本身擁有議價的主導權,非要實行996,能談出你能夠接受的薪酬。以上是我我的見解,不喜勿噴。(仍是那句。。。錢給到位,住公司都行)javascript

What is this?

What is this?這是什麼?this是什麼?(黑人問號臉)
今天的主題(😍?)是call、apply以及bind,這裏這個以及我以爲用的很好,後面我會解釋爲何不把bindcall、apply歸爲一類。前端

this對象是在運行時基於函數的執行環境綁定的(拋開箭頭函數)
當函數被做爲某個對象的方法調用時,this等於那個對象
this等於最後調用函數的對象java

讓咱們來for example ⬇️node

var name = 'Jack Sparrow';

function sayWhoAmI() {
  console.log(this.name)
}

sayWhoAmI(); // Jack Sparrow

var onePiece = {
  name: 'Monkey·D·Luffy',
  sayWhoAmI: function () {
    console.log(this.name)
  }
};

onePiece.sayWhoAmI(); // Monkey·D·Luffy
複製代碼

上面的代碼咱們能夠看出,無論定義在哪的sayWhoAmI()方法,函數體是同樣的,onePiece.sayWhoAmI()根據上面說的能夠理解:
∵(由於,下同)調用方法的最後那個對象就是onePiece
∴(因此,下同)thisonePiecethis.name就是onePiece.name
可是爲何全局定義的sayWhoAmI方法輸出的是Jack Sparrow,那我換種寫法可能你們就明白了 ⬇️git

var name = 'Jack Sparrow';

function sayWhoAmI() {
  console.log(this.name)
}

- sayWhoAmI(); // Jack Sparrow
+ window.sayWhoAmI(); // Jack Sparrow
複製代碼

這樣是否是清晰明瞭了
∵ 在全局聲明的變量或者函數,都是在window或者globle這個對象裏的
∴ 在window全局下聲明的sayWhoAmI能夠輸出同是window全局下聲明的name程序員

小進階

簡單的咱們已經明白了,如今咱們來看看加入return的方法,我以爲算是有點難度的了,大佬請飄過 ⬇️github

var area = 'East Ocean';

var onePiece = {
  area: 'New World',
  tellMeWhereAreYou: function () {
    return function () {
      console.log(this.area);
    }
  }
};

onePiece.tellMeWhereAreYou()(); // East Ocean
// 若是看不懂這裏爲何執行兩次,或者不明白爲何輸出的全局變量
// 那我引入一箇中間變量,讓過程多一步就能看懂了
var grandLine = onePiece.tellMeWhereAreYou();
// 這時候的 grandLine = function() { console.log(this.area); },等於onePiece.tellMeWhereAreYou();返回的函數
// 由於grandLine是一個全局變量,因此this.area返回的是East Ocean
grandLine(); // East Ocean
複製代碼

上面我以爲用了言簡意賅的方法解釋了一下這個問題,由於這個涉及到閉包的知識,以及函數的活動對象,不明白的能夠看個人另外一篇博客《前端戰五渣學JavaScript——閉包》,若是還不懂,還想更深刻的瞭解能夠自行翻閱《JavaScript高級程序設計》有關閉包的7.2章節,弄明白7.2章節中的兩張圖。web

那麼如今問題來了,我怎麼才能讓這個函數輸出我對象內部的area: 'New World' ⬇️面試

var area = 'East Ocean';

var onePiece = {
  area: 'New World',
  tellMeWhereAreYou: function () {
    var that = this;
    // 咱們經過聲明一個變量來保存this所指向的對象,而後再閉包中,就是返回的函數中使用
    // 一個典型的閉包結構就完成了
    return function () {
      console.log(that.area);
    }
  }
};

onePiece.tellMeWhereAreYou()(); // New World
複製代碼

可能你們以前工做中會用到中間變量來保存this的這種方法,並且我感受也不難,那我就跳過了。數組

咱們如今應該大致搞明白了this指向的問題了。可是咱們就是變態,咱們有病,咱們終於搞明白了this的指向問題,那咱們如今又想改變this指向,😜人生到處是艱難啊

這時候咱們就須要用到標題中提到的callapply

Apply nothing and just call me

call()方法與apply()方法的做用相同,它們的區別僅在於接收參數的方式不一樣。————————《JavaScript高級程序設計》

書裏面說的很清楚,它們兩個的做用是同樣的,只是接收參數的方式不一樣,那到底有什麼區別呢,聽我我細細道來

瘋狂打call

call()方法能夠指定一個this的值(第一個參數),而且分別傳入參數(第一個參數後面的就是須要傳入函數的參數,須要一個一個傳)

call()方法到底有什麼用呢,天然是解決咱們剛纔提出來的改變this指向,怎麼用呢???⬇️

var first = '大黑刀·夜',
    second = '二代鬼徹',
    third = '初代鬼徹',
    fourth = '時雨';

var zoro = {
  first: '和道一文字',
  second: '三代鬼徹',
  third: '雪走',
  fourth: '秋水'
};

function sayYourWeapon(num, num2) {
  console.log(`這是我${num}獲得的刀"${this[num]}"`)
  console.log(`這是我${num2}獲得的刀"${this[num2]}"`)
}

sayYourWeapon('first', 'third'); // 這是我first獲得的刀"大黑刀·夜";這是我third獲得的刀"初代鬼徹"
sayYourWeapon.call(zoro, 'first', 'fourth'); // 這是我first獲得的刀"和道一文字";這是我fourth獲得的刀"秋水"
複製代碼

上面這段代碼很明顯的改變了this的指向,若是我直接調用sayYourWeapon()必然輸出的是全局全局變量firstthird的值,而我後面經過sayYourWeapon.call(zoro, 'first', 'fourth')中的call()方法
∵ 改變了函數中的this值,就是傳入的zoro,把this值從全局對象改爲了zoro對象
∴ 後面輸出的也都是對象zoro中的'first', 'fourth'的值

apply全部配置

apply()方法能夠指定一個this的值(第一個參數),而且傳入參數數組(參數須要在一個數組或者類數組中)

咱們應該已是知道了call()方法怎麼用了,那咱們熟悉apply()就簡單多了,咱們能夠把上面的例子改一下⬇️

var first = '大黑刀·夜',
  second = '二代鬼徹',
  third = '初代鬼徹',
  fourth = '時雨';

var zoro = {
  first: '和道一文字',
  second: '三代鬼徹',
  third: '雪走',
  fourth: '秋水'
};

function sayYourWeapon(num, num2) {
  console.log(`這是我${num}獲得的刀"${this[num]}"`)
  console.log(`這是我${num2}獲得的刀"${this[num2]}"`)
}

sayYourWeapon('first', 'third'); // 這是我first獲得的刀"大黑刀·夜";這是我third獲得的刀"初代鬼徹"
- sayYourWeapon.call(zoro, 'first', 'fourth'); // 這是我first獲得的刀"和道一文字";這是我fourth獲得的刀"秋水"
+ sayYourWeapon.apply(zoro, ['first', 'fourth']); // 這是我first獲得的刀"和道一文字";這是我fourth獲得的刀"秋水"
複製代碼

能夠看到,我全篇就只是把call改爲了apply,而且把以前'first', 'fourth'這麼傳進去的參數改爲了['first', 'fourth']一個數組。若是咱們是在一個函數當中使用,那咱們還能夠直接使用arguments這個類數組對象⬇️

var first = '大黑刀·夜',
    second = '二代鬼徹',
    third = '初代鬼徹',
    fourth = '時雨';

  var zoro = {
    first: '和道一文字',
    second: '三代鬼徹',
    third: '雪走',
    fourth: '秋水'
  };

  function sayYourWeapon(num, num2) {
    console.log(`這是我${num}獲得的刀"${this[num]}"`)
    console.log(`這是我${num2}獲得的刀"${this[num2]}"`)
  }

  function mySayYourWeapon(num, num2) {
    sayYourWeapon.apply(zoro, arguments) // 咱們本身聲明一個函數,而且在裏面調用apply,這是咱們只須要傳入arguments這個參數,而不須要想call那樣一個一個傳進去了
  }

  sayYourWeapon('first', 'fourth'); // 這是我first獲得的刀"大黑刀·夜";這是我fourth獲得的刀"時雨"
  mySayYourWeapon('first', 'fourth'); // 這是我first獲得的刀"和道一文字";這是我fourth獲得的刀"秋水"
複製代碼

羈bind祕密

文章開頭我說過這樣一句話⬇️

call、apply以及bind,這裏這個以及我以爲用的很好

如今咱們就來聊聊這個‘以及’的內涵
我爲何說‘以及’呢,由於bindcall、apply這兩個方法的使用有一丟丟的不同。上面咱們一個函數調用.call()或者.apply()方法,方法會當即執行,若是函數有返回值會得到返回值,可是bind不同
bind()方法不會當即執行目標函數,而是返回一個原函數的拷貝,而且擁有指定this值和初始函數(爲何是指定的,固然是咱們本身傳進去的啦)

什麼叫原函數的拷貝呢,那咱們先來看一下⬇️

function a() {}

console.log(typeof a.bind() === 'function'); // 返回是true,先證實a.bind()是一個函數
console.log(a.bind()); // 輸出function a() {},跟原函數同樣
console.log(a.bind() == a); // false
console.log(a.bind() === a); // false 不論是 === 仍是 == 都是false,證實是拷貝出來一份而不是原先的那個函數
複製代碼

上面解釋了‘原函數的拷貝’這個問題,那接下來咱們看看bind()怎麼使用

結印準備

bind()方法在傳參上跟call是同樣的,第一個參數是須要綁定的對象,後面一次傳入函數須要的參數,以下⬇️

var name = 'Jack Sparrow';

var onePiece = {
  name: 'Monkey·D·Luffy'
};

function sayWhoAmI() {
  console.log(this.name)
}

var mySayWhoAmI = sayWhoAmI.bind(onePiece)

sayWhoAmI(); // Jack Sparrow
mySayWhoAmI(); // Monkey·D·Luffy
複製代碼

一個簡單的實現,原本輸出的是全局變量'Jack Sparrow',後來通過bind之後綁定上了對象onePiece,因此輸出的就是對象onePiece中的nodeMonkey·D·Luffy。

那咱們須要傳參的時候怎麼辦 ⬇️

var first = '大黑刀·夜',
  second = '二代鬼徹',
  third = '初代鬼徹',
  fourth = '時雨';

var zoro = {
  first: '和道一文字',
  second: '三代鬼徹',
  third: '雪走',
  fourth: '秋水'
};

function sayYourWeapon(num, num2) {
  console.log(`這是我${num}獲得的刀"${this[num]}"`)
  console.log(`這是我${num2}獲得的刀"${this[num2]}"`)
}

// 既然咱們知道bind是返回一個函數,那咱們聲明一個變量來接這個函數會看的直觀一些
var mySayYourWeapon = sayYourWeapon.bind(zoro, 'first', 'fourth'); // 傳入初始參數
var hisSayYourWeapon = sayYourWeapon.bind(zoro); // 只傳入目標對象

sayYourWeapon('first', 'third');
mySayYourWeapon(); // 由於咱們當時bind綁定函數的時候已經傳入了目標對象zoro和指定的參數,因此這裏就不須要傳參數了
hisSayYourWeapon( 'first', 'fourth'); // 固然咱們開始bind綁定函數的時候不傳入,在調用的時候再傳入參數也是能夠的
複製代碼

上面的代碼咱們能夠發現mySayYourWeaponhisSayYourWeaponbind的時候一個傳入了初始的參數,一個沒有傳入,可是後續調用的時候能夠再傳

既然是初始化參數,那咱們就能夠預設參數一個,而後再傳一個——————偏函數(不知道本身理解的對不對,可是確定是有這麼個功能,不懂的能夠移步MDN web docs的Function.prototype.bind中的偏函數

印結完了,該出招了

影子模仿術

默認你們到這裏已經知道怎麼使用bind了,那咱們接下來須要挑戰的就是,本身手寫一個bind方法,這個能夠幫助咱們更清楚的理解bind方法是怎麼運做的,而且面試的時候也可能會被問到哦~
下面咱們來看從MDN web docs 的Function.prototype.bind中複製過來的實現,添加了我本身的理解和註釋,但願你們能看懂⬇️

// 判斷當前環境的Function對象的原型上有沒有bind這個方法,若是沒有,那咱們就本身添加一個
if (!Function.prototype.bind) {
  /** * 添加bind方法 * @param oThis 目標對象 * @returns {function(): *} 返回的拷貝函數 */
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      // closest thing possible to the ECMAScript 5
      // 最接近ECMAScript 5的實現(貌似是這個意思)
      // internal IsCallable function
      // 內部IsCallable函數(🙄什麼鬼)
      // 若是當前this對象不是function,就拋出錯誤,由於只有function才須要實現bind這個方法。。。畢竟是返回函數
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    // 聲明變量aArgs保存arguments中除了第一個參數的其餘參數的數組,由於第一個參數不是函數須要的參數,而是須要綁定的目標對象
    // 這塊就用到了call的方法,由於arguments是類數組對象,沒有slice這個方法,因此只能從Array那call過來一個使用
    var aArgs = Array.prototype.slice.call(arguments, 1);
    // 保存原先的this對象,是在調用bind的時候沒有傳入目標對象,那就使用原先的this對象
    var fToBind = this;
    // 聲明空函數,在下面的原型中可使用
    var fNOP = function() {};
    // 須要放回的拷貝函數的本體,從最後的return也知道,最後是返回的fBound這個方法
    var fBound  = function() {
        // this instanceof fBound === true時,說明返回的fBound被當作new的構造函數調用
        // 下面就涉及到剛纔說的是bind時初始化參數,仍是bind之後調用的時候再傳入參數
        return fToBind.apply(
          // 判斷原始this對象是否是fBound的實例,或者說this的原型鏈上有沒有fBound
          this instanceof fBound
            // 若是有,就使用原始的this 
          ? this
            // 若是沒有,就使用如今的傳入的this對象
          : oThis,
          // 獲取調用時(fBound)的傳參.bind 返回的函數入參每每是這麼傳遞的
          // 這一步就是爲了保障在bind時候沒有傳入參數的時候,調用時候傳入的參數能使用上
          aArgs.concat(Array.prototype.slice.call(arguments)));
      };

    // 維護原型關係
    // 判斷原始this對象上有沒有prototype
    if (this.prototype) {
      // Function.prototype doesn't have a prototype property
      // 若是原始this對象上有prototype 就把fNOP的prototype改爲this.prototype,fNOP就繼承自原始this了
      fNOP.prototype = this.prototype;
    }
    // 下行的代碼使fBound.prototype是fNOP的實例,所以
    // 返回的fBound若做爲new的構造函數,new生成的新對象做爲this傳入fBound,新對象的__proto__就是fNOP的實例
    // 既然fNOP是繼承自原始this對象的,那這裏的這一步就是讓拷貝函數也擁有原始this對象的prototype,繼承自同一個地方,師出同門
    fBound.prototype = new fNOP();
    // 最後返回被拷貝出來的函數
    return fBound;
  };
}
複製代碼

上面的代碼中有我添加的註釋,方便你們能更好的理解,理解了上面的代碼之後,bind方法算是瞭解的差很少了,其餘實現原理上摸清楚了
可能上面的代碼註釋有點多,看着很費勁,下面貼出沒有註釋的代碼,方便你們複製粘貼調試

if (!Function.prototype.bind) {
  Function.prototype.bind = function(oThis) {
    if (typeof this !== 'function') {
      throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
    }
    var aArgs = Array.prototype.slice.call(arguments, 1);
    var fToBind = this;
    var fNOP = function() {};
    var fBound  = function() {
        return fToBind.apply(this instanceof fBound ? this : oThis, aArgs.concat(Array.prototype.slice.call(arguments)));
    };
    if (this.prototype) {
      fNOP.prototype = this.prototype;
    }
    fBound.prototype = new fNOP();
    return fBound;
  };
}
複製代碼

這麼看來代碼還不算不少就實現了bind方法

人的夢想,是不會完結的,沒錯吧?

可能 996.ICU 起不到本質上的做用,可是讓咱們知道有一羣可愛的人跟咱們同樣在爲生計奔波勞累着,讓咱們知道咱們的圈子不小,只是沒到團結的時候,敢折騰就不賴,人必定要夢想,趁着年輕,萬一實現了呢。

帶病寫博客。。。

病

年輕嘛,就是幹!

ps:博客能夠技術分享,也當記錄生活了,之後看見的話,沒準會說「當時是否是傻」,可是如今感受perfect


我是前端戰五渣,一個前端界的小學生。

相關文章
相關標籤/搜索