建 議 改 成:call,apply,bind 的 完 全 實 現 和 理 解

前言

春天到了,又到了交配,啊 ,不是。。又到了找工做的季節。相信不少朋友都會被問到過這樣的一個JS問題,如何實現call | apply | bind,不少朋友只會用可是不會寫,或者是死記硬背寫法,等到面試官提問的時候,支支吾吾講不清楚,今天我將教會你們徹底理解這個破題!es6

1.首先講講this

這是一個很方便,可是同時又容易出錯的屬性。面試

咱們只要記住4條規則就行了數組

1.1 純粹的函數調用

這個時候this指向window對象瀏覽器

let x = 'window';

function test() {

    let x = 'fn';

console.log(x);

}

test(); // window  
//注意這裏若是使用var的話,會是fn
//由於var沒有塊級做用域,函數內var會至關於在外面var
//也就是更改了window.x也就是this.x。

向上面這種簡單的你們都能理解,看看這個容易搞錯的app

var name = "zhangsan";

var obj = {

    name:"leelei",

    fn:function() { 

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

        x();

    }

}

obj.fn() // zhangsan

1.2 做爲對象的方法調用

這個時候的this就指向這個對象函數

function test() {

 console.log(this.x);

}

var obj = {};

o.x = 1;

o.m = test;

o.m(); // 1

1.3 做爲構造函數調用

  function Test() {

    this.x = 1;

  }

  var o = new Test();

 console.log(o.x); // 1

1.4 apply,call,bind調用

apply(),call()是函數對象的一個方法,它的做用是改變函數的調用對象,它的第一個參數就表示改變後的調用這個函數的對象。所以,this指的就是第一個參數。學習

bind()和他們相似,可是它執行後返回的仍是一個函數,而不是執行後的值。this指的也是第一個參數。this

2.實現call,apply

2.1 咱們現來看看怎麼使用

他的特性是把fn中的this指向第一個參數,當咱們使用的時候是這樣的。prototype

它實現了把 sayName中的this指向了 obj,即
this.nickName=>obj.nickNamecode

function sayName() {

    console.log(`my name is ${this.nickName}`);  

}

let obj = {nickName:"leelei"}

sayName.call(obj)  //my name is leelei

2.2 原理是什麼?

咱們能夠看看上面this的使用方法中的第二點,咱們若是把fn設置爲context的一個屬性,是否是fn的this就會指向context了呢?

context.property = fn;

let result = context.property();

delete context.property ;

return result;
  • 先把fn設置爲context的一個屬性
  • 而後執行這個方法,獲得結果result
  • 而後刪除這個屬性,若是不刪除,咱們就污染了咱們傳入的這個context對象,誰樂意乾乾淨淨進來,出去的時候帶了一坨屎啊。
  • 返回結果,完美

2.3 參數又怎麼搞阿?

call的用法是這樣的:除了第一個參數之外,其餘的參數全都是傳給fn

那麼藉助es6語法咱們能夠省下一大堆代碼,大體代碼以下

Function.prototype.mycall\= function(context,...args) {

context.fn= this;  //這裏的this指向調用該方法的實例,也就是fn.call()中的fn

let result = context.fn(...args);

delete context.fn;

return result;

}

完事了嗎?

固然沒有,做爲男人怎麼能夠那麼快完事兒?

2.4 若是咱們call方法傳入的第一個參數不是對象,那又如何對敵?

想一想knight會怎麼作?阿,不是,想一想call會怎麼作。

function sayName() {

    console.log(`my name is ${this.nickName}`);  

}

var sym = Symbol('halo') //ES6新增基礎類型,若是不懂,沒有瓜西!

sayName.call(sym) // my name is undefined

sayName.call('malegeji') // my name is undefined  

sayName.call(666) // my name is undefined  

sayName.call(true) // my name is undefined

sayName.call(null) // my name is undefined

sayName.call(undefined)  //my name isundefined

undefined表明什麼呢?

你能夠看看下面這個代碼

//對一個對象訪問它沒有的屬性值時會返回undefined

var obj = {};

obj.malegeji  //undefined

這個說明call內部,把咱們輸入的基礎類型都轉成了對象,那麼null和undefined也是如此嗎?他們根本就沒有本身的構造函數方法阿?那他們轉成了什麼?

洋蔥,轉成了洋蔥

好吧,實際上是window

如何驗證?咱們只要給個window的這個屬性賦值看一下就知道啦

window.nickName = "leelei"

  

function sayName() {

    console.log(\`my name is ${this.nickName}\`);  

}

var sym = Symbol('halo')

sayName.call(sym) // my name is undefined  

sayName.call('malegeji') // my name is undefined  

sayName.call(666) // my name is undefined  

sayName.call(true) // my name is undefined

sayName.call(null) // my name is leelei

sayName.call(undefined)  //my name isleelei

哦豁,驗證了咱們的想法~

搞清楚特性之後,咱們如今就能夠寫出一個和call一毛同樣表現的mycall了~

Function.prototype.mycall = function(context,...args) {

  if (typeof this !== 'function') {

    throw new TypeError('not funciton')

  }

  

  if(context == null || context == undefined) {

    context = window

  }else{

    context = Object(context);

  }

  context.fn = this; 

  let result = context.fn(...args);

  delete context.fn;

  return result;

};

3. 實現apply

apply和call其實大部分是同樣的,他們的惟一區別是什麼?

傳參格式不同

fn.call(context,arg1,arg2,arg3,...)

fn.apply(context,[arg1,arg2,arg3,...])

那麼,咱們能夠輕易地實現apply

Function.prototype.myapply = function(context,args) {

  if (typeof this !== 'function') {

    throw new TypeError('not funciton')

  }

  if(arguments.length>2){

    throw new Error("Incorrect parameter length")

  }

  

  if(context == null || context == undefined){

    context = window

  }else{

    context = Object(context);

  }

  context.fn = this;

  let result = context.fn(...args);

  delete context.fn;

  return result;

}

4. 實現bind

看這一部分以前,請先對構造函數有一個比較清晰的瞭解,否則能夠點贊而後關掉網頁了,固然也可再點個收藏

4.1 怎麼使用?

function sayName(age,sex) {

    console.log(`my name is ${this.nickName},I'm ${age} years old, ${sex}`);  

}

let obj = { nickName: "leelei" }

let bindFn = sayName.bind(obj)   //注意:使用bind後返回的是一個函數

bindFn();  //my name is leelei,I'm undefined years old, undefined

哎呀,忘了傳參數,怎麼傳呢?

第一種

let bindFn = sayName.bind(obj,18,'man')   //注意:使用bind後返回的是一個函數

bindFn();  //my name is leelei,I'm 18years old,man

第二種

let bindFn = sayName.bind(obj,18)   //注意:使用bind後返回的是一個函數

bindFn('man');  //my name is leelei,I'm 18years old,man

你能夠把除了第一個參數之外的參數隨意在綁定的時候傳入,或者在執行的時候傳入,這個也是一個函數柯里化的過程。

柯里化,英語:Currying是把接受多個參數的函數變換成接受一個單一參數(最初函數的第一個參數)的函數,而且返回接受餘下的參數並且返回結果的新函數的技術。

由於bind返回的是一個函數,當咱們把這個函數看成構造函數來使用,那又會怎樣呢?

//爲何我一用構造函數舉例會下意識命名爲foo | bar

function Foo(age, sex) {

  this.blog = "http://www.leelei.info"

  console.log(this.nickName);

  console.log(age);

  console.log(sex);

}

Foo.prototype.habit = "play lol in Zu'an and kill somebody's mom";

  

let bindFn = Foo.bind({ nickName: "leelei" }, 18, "man");

let bindFnInstance = new bindFn(); // undefined 18 'man'

  

console.log(bindFnInstance.blog); // http://www.leelei.info   

console.log(bindFnInstance.habit); //play lol in Zu'an and kill somebody's mom

  

let bindFnInstance2 = bindFn();  //普通調用,由於不是new運算符因此沒有返回

console.log(bindFnInstance2.habit); // Cannot read property 'habit' of undefined  

聰明的盲生,你發現什麼華點了嗎?

個人nickName怎麼是undefined阿,完了,全完了,我瀏覽器有問題,我先把谷歌卸了!

別急,實際上是由於當使用new操做符來構造綁定函數的時候,bind會忽略這個傳入的第一個參數,爲何?

由於構造函數Foo中的this會指向實例用於構造實例,(這個是new的特性,若是不明白能夠百度一下),那麼this指到實例bindFnInstance後就不能指到傳入的第一個參數了,那麼它的nickName就是bindFnInstance的nickName了,可是bindFnInstance說:」我他媽剛生成哪裏來的nickName阿「,因此最終就沒法訪問了嗷。

好的,咱們來總結一下這幾個特性嗷

  • 返回函數
  • 柯里化
  • new構造的時候忽略傳入的上下文對象,把this指向生成的實例

4.2 先實現第一個返回函數特性

那顆太簡單了嗷,鐵子,幹了奧裏給!

Function.prototype.mybind = function(context) {

  return function() {

    return fn.call(context);

  };

};

4.3 再來實現這個參數的分步傳入

Function.prototype.mybind = function(context, ...args) {

  return function(...args2) {

    return fn.call( context, ...args, ...args2);

  };

};

4.4 最後來實現這個new的指向判斷

Function.prototype.mybind = function(context, ...args) {

  const fn = this;

  function fBound(...args2) {

    return fn.call(this instanceof fBound ? this : context, ...args, ...args2);

  };

  fBound.prototype = Object.create(this.prototype);

  return fBound;

};
  • this instanceof fBound? this : context是幹什麼的?

你可能看過如何判斷數組代碼,arr instanceof Array,是否是感受很像?有麼有感受了?

  這個instanceof 能夠判斷 右邊這個構造函數是否出如今左邊這個對象的原型鏈上。

  按照寫法,咱們返回了fBound

  - 若是使用普通調用,那麼咱們這個this會指向window嘛(fBound中的this屬於第一部分提到的this的第一種用法),window和fBound有個毛關係?因此返回context做爲fn的第一個參數。

  - 若是使用的是new,那麼這個this指向的就是新的實例,新的實例的原型鏈確定有它的構造函數fBound阿,那麼就傳入this,也就是實例自己,而忽略context,其餘參數不變。

  • fBound.prototype = Object.create(this.prototype)是幹什麼的?

當咱們使用構造函數的時候,構造函數原型上的屬性,實例也可訪問,也就是這裏所表現的。

Foo.prototype.habit = "play lol in Zu'an and kill somebody's mom";  

  

console.log(bindFnInstance.habit); //play lol in Zu'an and kill somebody's mom

可是咱們返回的是fBound,fBound哪裏來的prototype.habit阿,因此咱們給他整上!

那能不能直接執行fBound.prototype =fn.prototype,將原函數的 prototype 賦值給 fBound 呢?

很明顯這樣的操做把 fBound 和 原函數的 prototype 強關聯起來了,若是fBound 函數的 prototype改動 將會影響到原函數的 prototype,因此能夠經過 fBound.prototype = Object.create(fn.prototype) ,以原函數的 prototype爲模板,生成一個新的實例對象,並賦值給fBound.prototype。

4.5 完事了嗎?

固然沒有!

還有一個須要注意的點

當咱們執行到 fBound.prototype = Object.create(fn.prototype) 時,若是fn.prototype是undefined可咋整,什麼狀況下會出現呢?

當咱們直接調用 Foo.prototype.bind 時候會出現,而且bong的一聲報了個大錯!

typeof Function.prototype === "function"  //true

因此咱們像前面call,apply那樣的判斷也限制不了  同時

Function.prototype.prototype  // undefined 

4.6 完事兒了嗎?

固然。。。沒有!

由於 Object.create() 和 bind 都是 ES5 規範提出的,若是不支持 bind, 那麼bind 的 polyfill 裏面天然不支持 Object.create()。因此咱們應該換個方法來實現,通常面試官到上面一步就足了。

最終究極終稿!

Function.prototype.mybind = function(context, ...args) {

  if (typeof this !== "function") {

    throw new TypeError("not funciton");

  }

  const fn = this;

  const fNop = function () {};

  function fBound(...args2) {

    return fn.call(this instanceof fBound ? this : context, ...args, ...args2);

  };

  if(fn.prototype){

    fNop.prototype =fn.prototype;

  }

 fBound.prototype = new fNop();

  return fBound;

};

5. 總結

總的來講call,apply,bind這三個方法涉及到了js的諸多方法,若是可以徹底理解的話,對於學習js會有很大幫助嗷~

若是有錯誤,請在評論區中指出,很是感謝!

順便打個廣告 leelei的我的博客最後祝你們拿到心儀的offer

相關文章
相關標籤/搜索