都2020年了,你應該知道如何手寫Call、Apply、Bind了吧

導讀

做爲面試中面試官最寵愛的一個問題,在這裏進行一個詳細的介紹,你們重點要放在理解,而不是背。 寫的很差或不對的地方,請你們積極指出,好了,話很少說,咱們「圓規正轉」javascript

先說一下三者的區別
共同點就是修改this指向,不一樣點就是
1.call()和apply()是馬上執行的, 而bind()是返回了一個函數
2.call則能夠傳遞多個參數,第一個參數和apply同樣,是用來替換的對象,後邊是參數列表。
3.apply最多隻能有兩個參數——新this對象和一個數組argArray

複製代碼

1、手寫實現Call

1.call主要都作了些什麼。

  • 更改this指向
  • 函數馬上執行

2.簡單實現

Function.prototype.myCall = function(context) {
  context.fn = this;
  context.fn();
}

const obj = {
  value: 'hdove'
}

function fn() {
  console.log(this.value);
}

fn.myCall(obj); // hdove
複製代碼

3.出現的問題

  • 沒法傳值
  • 若是fn()有返回值的話,myCall 以後獲取不到
function fn() {
  return this.value;
}

console.log(fn.myCall(obj)); // undefined

複製代碼
  • call其實就是更改this指向,指向一個Object,若是用戶傳的是基本類型又或者乾脆就不傳呢?
  • myCall執行以後,obj會一直綁着fn()

4.通通解決

Function.prototype.myCall = function(context) {
  // 1.判斷有沒有傳入要綁定的對象,沒有默認是window,若是是基本類型的話經過Object()方法進行轉換(解決問題3)
  var context = Object(context) || window;
  
  /** 在指向的對象obj上新建一個fn屬性,值爲this,也就是fn() 至關於obj變成了 { value: 'hdove', fn: function fn() { console.log(this.value); } } */
  context.fn = this;
  
  // 2.保存返回值
  let result = '';
  
  // 3.取出傳遞的參數 第一個參數是this, 下面是三種截取除第一個參數以外剩餘參數的方法(解決問題1)
  const args = [...arguments].slice(1);
  //const args = Array.prototype.slice.call(arguments, 1);
  //const args = Array.from(arguments).slice(1);
  
  // 4.執行這個方法,並傳入參數 ...是es6的語法用來展開數組
  result = context.fn(...args);
  
  //5.刪除該屬性(解決問題4)
  delete context.fn;
  
  //6.返回 (解決問題2)
  return result;
}


const obj = {
  value: 'hdove'
}

function fn(name, age) {
  return  {
      value: this.value,
      name,
      age
  }
}

fn.myCall(obj, 'LJ', 25); // {value: "hdove", name: "LJ", age: 25}

複製代碼

2、手動實現Apply

實現了call其實也就間接實現了apply,只不過就是傳遞的參數不一樣

Function.prototype.myApply = function(context, args) {
  var context = Object(context) || window;
  
  context.fn = this;
  
  let result = '';
  
  //4. 判斷有沒有傳入args
  if(!args) {
    result = context.fn();
  }else {
    result = context.fn(...args);
  }
  
  delete context.fn;
  
  return result;
}


const obj = {
  value: 'hdove'
}

function fn(name, age) {
  return  {
      value: this.value,
      name,
      age
  }
}

fn.myApply(obj, ['LJ', 25]); // {value: "hdove", name: "LJ", age: 25}

複製代碼

3、實現Bind

bind() 方法建立一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其他參數將做爲新函數的參數,供調用時使用(MDN)

複製代碼

1.bind特性

  • 指定this
  • 返回一個函數
  • 傳遞參數並柯里化

2.簡單實現

Function.prototype.myBind = function(context) {
    const self = this;
    
    return function() {
        self.apply(context);
    }
}

const obj = {
  value: 'hdove'
}

function fn() {
    console.log(this.value);
}

var bindFn = fn.myBind(obj);

bindFn(); // 'hdove;


複製代碼

3.優化

相比於call、apply,我我的以爲bind的實現邏輯更加複雜,須要考慮的東西不少,在這裏分開進行優化。java

3.1 調用bind是個啥玩意?

在這裏咱們須要進行一下判斷,判斷調用bind的是否是一個函數,不是的話就要拋出錯誤。es6

Function.prototype.myBind = function(context) {

    if (typeof this !== "function") {
        throw new Error("不是一個函數");
    }
    const self = this;
    
    return function() {
        self.apply(context);
    }
}

複製代碼

3.2 傳遞參數

咱們看下面這段代碼web

Function.prototype.myBind = function(context) {

    if (typeof this !== "function") {
        throw new Error("不是一個函數");
    }
    const self = this;
    
    return function() {
        self.apply(context);
    }
}

const obj = {
  value: 'hdove'
}

function fn(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);
}

var bindFn = fn.myBind(obj, LJ, 25);

bindFn(); // 'hdove' undefined undefined


複製代碼

很明顯,第一個優化的地方就是傳遞參數,咱們來改造下面試

Function.prototype.myBind = function(context) {

    if (typeof this !== "function") {
        throw new Error("不是一個函數");
    }
    
    const self = this;
    
    // 第一個參數是this,截取掉
    const args = [...arguments].slice(1);
    
    return function() {
        /**
            這裏咱們其實便可以使用apply又可使用call來更改this的指向
            使用apply的目的其實就是由於args是一個數組,更符合apply的條件
        */
        return self.apply(context, args);
    }
}

const obj = {
  value: 'hdove'
}

function fn(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);
}

var bindFn = fn.myBind(obj, 'LJ', 25);

bindFn(); // 'hdove' 'LJ' 25
 
複製代碼

想在看起來沒什麼問題,可是咱們這樣傳一下參數數組

var bindFn = fn.myBind(obj, 'LJ');

bindFn(25); // 'hdove' 'LJ' undefined

複製代碼

咱們發現後面傳遞的參數丟了,這裏就須要使用柯里化來解決這個問題bash

Function.prototype.myBind = function(context) {

    if (typeof this !== "function") {
        throw new Error("不是一個函數");
    }
    
    const self = this;
    
    // 第一個參數是this,截取掉
    const args1 = [...arguments].slice(1);
    
    return function() {
        // 獲取調用時傳入的參數
        const args2 = [...arguments];
        return self.apply(context, args1.concat(args2));
    }
}

const obj = {
  value: 'hdove'
}

function fn(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);
}

var bindFn = fn.myBind(obj, 'LJ');

bindFn(25); // 'hdove' 'LJ' 25

複製代碼

3.3this丟失

其實bind還具備一個特性就是 做爲構造函數使用的綁定函數,意思就是這個綁定函數能夠當成構造函數使用,能夠調用new操做符去建立一個實例,當咱們使用new操做符以後,this其實不是指向咱們指定的對象,而是指向new出來的這個實例的構造函數,不過提供的參數列表仍然會插入到構造函數調用時的參數列表以前。咱們簡單實現一下。app

Function.prototype.myBind = function(context) {
    if (typeof this !== "function") {
        throw new Error("不是一個函數");
    }
    const self = this;
    const args1 = [...arguments].slice(1);
    
    const bindFn = function() {
        const args2 = [...arguments];
        
        /**
            這裏咱們經過打印this,咱們能夠看出來。
            當這個綁定函數被當作普通函數調用的時候,this實際上是指向window。
            而當作構造函數使用的時候,倒是指向這個實例,因此this instanceof bindFn爲true,這個實例能夠獲取到fn()裏面的值。
            
            咱們能夠再fn裏面添加一個屬性test.
            若是按照以前的寫法 打印出來的是undefined,正好驗證了咱們上面所說的this指向的問題。
            因此解決方法就是添加驗證,判斷當前this
            若是 this instanceof bindFn 說明這是new出來的實例,指向這個實例, 不然指向context
        */
        console.log(this);
        
        return self.apply(this instanceof bindFn ? this : context, args1.concat(args2));
    }
    
    return bindFn;
}

const obj = {
  value: 'hdove'
}

function fn(name, age) {
    this.test = '我是測試數據';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

var bindFn = fn.myBind(obj, 'LJ');

var newBind = new bindFn(25);

console.log(newBind.test); // undefined

複製代碼

3.4綁定原型

咱們都知道每個構造函數,都會有一個原型對象(prototype),來添加額外的屬性。函數

function fn(name, age) {
    this.test = '我是測試數據';
}

fn.prototype.pro = '原型數據';

var bindFn = fn.myBind(obj, 'LJ', 25);

var newBind = new bindFn();

console.log(bindObj.pro); // undefined

複製代碼

由於咱們沒有綁定原型,因此會出現undefined,咱們簡單綁定一下post

Function.prototype.myBind = function(context) {
    if (typeof this !== "function") {
        throw new Error("不是一個函數");
    }
    const self = this;
    const args1 = [...arguments].slice(1);
    
    const bindFn = function() {
        const args2 = [...arguments];
        return self.apply(this instanceof bindFn ? this : context, args1.concat(args2));
    }
    
    // 綁定原型
    bindFn.prototype = self.prototype;
    
    return bindFn;
}

function fn(name, age) {
    this.test = '我是測試數據';
}

fn.prototype.pro = '原型數據';

var bindFn = fn.myBind(obj, 'LJ', 25);

var newBind = new bindFn();

console.log(bindObj.pro); // "原型數據"

複製代碼

可是這樣會出現這樣一個問題

function fn(name, age) {
    this.test = '我是測試數據';
}

fn.prototype.pro = '原型數據';

var bindFn = fn.myBind(obj, 'LJ');

var bindObj = new bindFn();

bindObj.__proto__.pro = '篡改原型數據';

console.log(bindObj.__proto__ === fn.prototype); // true

console.log(bindObj.pro); // "篡改原型數據"

console.log(fn.prototype.pro); // "篡改原型數據"

當咱們修改bindObj的原型的時候,fn的原型也一塊兒修改了
這實際上是由於 bindObj.__proto__ === fn.prototype
咱們在修改bindObj的同時也間接修改了fn

複製代碼

解決方法其實很簡單,建立一個新方法proFn(),來進行原型綁定,也就是實現繼承的幾種方式中的原型式繼承,而後咱們把這個新方法的實例對象綁定到咱們的綁定函數的原型中

Function.prototype.myBind = function(context) {
    if (typeof this !== "function") {
        throw new Error("不是一個函數");
    }
    const self = this;
    const args1 = [...arguments].slice(1);
    
    const bindFn = function() {
      const args2 = [...arguments];
       
        return self.apply(this instanceof bindFn ? this : context, args1.concat(args2));
    }
    
    // 綁定原型
    
    function proFn() {}  //建立新方法
    proFn.prototype = self.prototype; //繼承原型
    bindFn.prototype = new proFn(); //綁定原型
    
    return bindFn;
}

function fn(name, age) {
    this.test = '我是測試數據';
}

fn.prototype.pro = '原型數據';

var bindFn = fn.myBind(obj, 'LJ', 25);

var newBind = new bindFn();

console.log(bindObj.__proto__ === fn.prototype); // false

console.log(bindObj.pro); // "篡改原型數據"

console.log(fn.prototype.pro); // "原型數據"

複製代碼

4、面試

這些東西實際上是面試中比較容易考到的問題,你們不要想着去背,背下來實際上是沒什麼用處的,容易會被問倒,重點仍是在於理解,理解了也就能夠垂手可得的寫出來了。但願這篇文章會給你們帶來收穫,那怕是一點點。在這裏,提早給你們夥拜個早年,鼠年幸運,跳槽順利,漲薪順利,哈哈。

5.推薦閱讀

相關文章
相關標籤/搜索