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

Hdove發表於2020-01-10

導讀

作為面試中面試官最寵愛的一個問題,在這裡進行一個詳細的介紹,大家重點要放在理解,而不是背。 寫的不好或不對的地方,請大家積極指出,好了,話不多說,我們“圓規正轉”


先說一下三者的區別
共同點就是修改this指向,不同點就是
1.call()和apply()是立刻執行的, 而bind()是返回了一個函式
2.call則可以傳遞多個引數,第一個引數和apply一樣,是用來替換的物件,後邊是引數列表。
3.apply最多隻能有兩個引數——新this物件和一個陣列argArray

複製程式碼

一、手寫實現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}

複製程式碼

二、手動實現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}

複製程式碼

三、實現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的實現邏輯更加複雜,需要考慮的東西很多,在這裡分開進行優化。

3.1 呼叫bind是個啥玩意?

在這裡我們需要進行一下判斷,判斷呼叫bind的是不是一個函式,不是的話就要丟擲錯誤。


Function.prototype.myBind = function(context) {

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

複製程式碼

3.2 傳遞引數

我們看下面這段程式碼


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

複製程式碼

我們發現後面傳遞的引數丟了,這裡就需要使用柯里化來解決這個問題


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出來的這個例項的建構函式,不過提供的引數列表仍然會插入到建構函式呼叫時的引數列表之前。我們簡單實現一下。


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,我們簡單繫結一下

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); // "原型資料"

複製程式碼

四、面試

這些東西其實是面試中比較容易考到的問題,大家不要想著去背,背下來其實是沒什麼用處的,容易會被問倒,重點還是在於理解,理解了也就可以輕而易舉的寫出來了。希望這篇文章會給大家帶來收穫,那怕是一點點。在這裡,提前給大傢伙拜個早年,鼠年幸運,跳槽順利,漲薪順利,哈哈。

5.推薦閱讀

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

相關文章