深刻學習js之call、apply、bind模擬實現

題外篇git

如何改變this得指向,常見的四種操做以下es6

  • 使用call、apply、bind
  • 在執行函數內部使用let that = this
  • es6中使用箭頭函數
  • 對象實例化 new操做

關於this的指向github

在一個函數上下文中,this由調用者提供,由調用函數的方式來決定。若是調用者函數,被某一個對象所擁有,那麼該函數在調用時,內部的this指向該對象。若是函數獨立調用,那麼該函數內部的this,則指向undefined。可是在非嚴格模式中,當this指向undefined時,它會被自動指向全局對象。數組

參考來自 波波老師bash

call

根據 MDN 的解釋:

call()方法調用一個函數, 其具備一個指定的this值和分別地提供的參數(參數的列表)。閉包

語法:app

fun.call(thisArg, arg1, arg2, ...)
複製代碼

thisArg:fun函數運行時指定得this函數

this得值可能有以下幾種可能:測試

  • 在嚴格模式下,傳null,undefined or 不傳,this默認指向window對象
  • 傳其餘函數的函數名稱,如fn,this指向fn函數
  • 傳其餘對象,this指向這個對象

看個例子ui

let obj = {
    val:'call'
}
function fn () {
    console.log(this.val,'testCall');
}
fn.call(obj) //'call','testCall'
複製代碼

模擬初版

先考慮能夠正常執行,後面的傳參暫時不考慮

琢磨一下上面例子的代碼執行過程

call()在執行過程當中,咱們想象一下它大概會經歷一些幾個階段(真實原理不作介紹)

  • fn方法複製到obj對象中
  • 改變fn函數的this指向
  • fn函數執行
  • fnobj對象刪除

分析: 那麼咱們在模擬代碼的場景下 fn.call(obj)的執行過程能夠想象成以下步驟:

一、將fn複製到obj對象中,那麼以下也就修改了fn中this的指向

obj = {
   val:'call',
   fn:function(){
       console.log(this.val,'testCall')
  }
  
二、 執行fn()

obj.fn()

三、刪除fn這個key

delete obj.fn

複製代碼
模擬開始
Function.prototype.call2 = function(args){

    //此時的args就是 上面的obj
    //1,此時使用this來獲取調用call的方法
    args.fn = this;
    
    //第二步 調用執行fn()
    args.fn();
    
    //第三步 刪除方法
    delete args.fn
    
}

//測試下
let obj = {
    val:'call2'
}
function fn () {
    console.log(this.val,'testCall2');
}
fn.call2(obj) //'call2','testCall2'
複製代碼

模擬二版

MDN文檔上介紹過,call能夠接受多個參數,那麼在第二版的時候咱們加上入參這個功能

栗子

let obj = {
    val:'call'
}
function fn (name) {
    console.log(this.val,name);
}
fn.call(obj,'alan') //'call','alan'
複製代碼

分析:

  • 跟第一階段相比就是多了一個傳參,有疑惑的地方,可能不知道穿幾個參數,不慌,能夠從Arguments中獲取第二個開始到最後結束的參數就好了
模擬開始
// 第二版
Function.prototype.call2 = function(...args) {

    //利用es6的 rest 來獲取函數的傳參,以及傳入thisArg;
    let [thisArg,...arr] = args ;
    
    // 獲取調用的函數方法
    thisArg.fn = this;
    
    // 用解構執行函數
    thisArg.fn(...arr)
    
    //刪除
    delete thisArg.fn
    
}

let obj = {
    val:'call'
}

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

fn.call2(obj,'alan') //'call','alan'

複製代碼

解釋:

  • es6rest (形式爲...變量名),這樣能夠獲得一個數組,即args此時爲數組,那麼上文中的thisArg就是傳遞的第一個參數。
  • fn(..arr)使用了es6 spread ,他就比如是reset的逆運算,這樣操做之後無論傳遞了幾個參數均可以正常處理

模擬第三版

文章開頭介紹過,若是在嚴格模式下,傳null,undefinedor 不傳,thisArg默認指向window對象,還有一種場景若是fn方法有返回值的狀況。

栗子 1

var val = 'call'
function fn () {
    console.log(this.val);
}
fn.call() //'call'
fn.call(null);//'call'
fn.call(undefind);//'call'
複製代碼

分析: 若是不傳值或傳null等值,處理起來不算麻煩,稍微在咱們原來的版本上作一些修改就好,看以下代碼

// 3.1
Function.prototype.call2 = function(...args) {
    let thisArg,arr = [];
    if(args.length === 0 || !args[0]){
        thisArg = window;
    } else{
        //利用es6的解構來獲取函數的傳參,以及傳入thisArg;
        [thisArg,...arr] = args ;
    }
   
    // 獲取調用的函數方法
    thisArg.fn = this;
    // 用解構執行函數
    thisArg.fn(...arr)
    //刪除
    delete thisArg.fn
}
fn.call2() //'call'
fn.call2(null);//'call'
fn.call2(undefind);//'call'
複製代碼

栗子2

let obj = {
    val:'call'
}
function fn (name) {
    console.log(this.val,name);
    return {
        val:this.val,
        name:name
    }
}
fn.call(obj,'alan') //'call','alan'
//
{
    val:'call',
    name:'alan'
}

複製代碼

終極版本

// 3.2
Function.prototype.call2 = function(...args) {
    let thisArg,arr = [];
    if(args.length === 0 || !args[0]){
        thisArg = window;
    } else{
        //利用es6的解構來獲取函數的傳參,以及傳入thisArg;
        [thisArg,...arr] = args ;
    }
   
    // 獲取調用的函數方法
    thisArg.fn = this;
    // 用解構執行函數
    let result = thisArg.fn(...arr)
    //刪除
    delete thisArg.fn
    return result
}

let obj = {
    val:'call'
}
function fn (name) {
    console.log(this.val,name);
    return {
        val:this.val,
        name:name
    }
}
fn.call2(obj,'alan') //'call','alan'
//
{
    val:'call',
    name:'alan'
}
複製代碼

apply

apply的實現方式跟call基本類似,就是在傳參上,apply接受的是數組,直接就貼一下代碼

Function.prototype.apply2 = function(thisArg,arr) {

    if(!thisArg){
        thisArg = window;
    } 
   
    // 獲取調用的函數方法
    thisArg.fn = this;
    // 用解構執行函數
    let result = thisArg.fn(...arr)
    //刪除
    delete thisArg.fn
    
    return result
}
let obj = {
    val:'apply'
}
function fn (name) {
    console.log(this.val,name);
    return {
        val:this.val,
        name:name
    }
}
fn.apply2(obj,['alan']) //'apply','alan'
複製代碼

bind

根據 MDN 的解釋:

bind()方法建立一個新的函數,在調用時設置this關鍵字爲提供的值。將給定參數列表做爲原函數的參數序列的前若干項。

語法:

fun.bind(thisArg,arg1,arg2......)
複製代碼
  • bind()方法會建立一個新的函數,通常叫綁定函數

  • 能夠接受參數,這個地方注意,它能夠在bind的時候接受參數,同時bind()返回的新函數也能夠接受參數

栗子

var obj = {
    val: 'bind'
};

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

// 返回了一個函數
var bindObj = fn.bind(obj); 

bindObj(); // bind
複製代碼

模擬初版

照舊,暫時不考慮傳參

分析:

  • bindObj()的執行結果跟使用call同樣的,不一樣的是它須要調用返回的方法bindObj

琢磨上述代碼執行過程,這個時候咱們對比一下call的模擬來看

  • bindObj像是call模擬過程當中的fn,然後bindObj()就像是fn()
  • bind返回的函數,咱們能夠想象成call()調用只有返回的函數而不會執行,只是apply(),call()是當即執行,而bind須要再次調用執行
模擬開始
Function.prototype.bind2 = function (args) {

  //經過this拿到調用方法
  let that  = this;
  
  //使用一個閉包來存儲call方法的結果
  return function () {
      return that.call(args);
  }

}

var obj = {
    val: 'bind'
};

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

// 返回了一個函數
var bindObj = fn.bind2(obj); 

bindObj(); // bind

複製代碼

模擬第二版

考慮下傳參的場景,開頭介紹過,傳參有兩種場景

栗子

let obj = {
    val:'bind'
};
function fn(name,sex){
    let o = {
        val:this.val,
        name:name,
        sex:sex
    }
    console.log(o)
}
let bindObj = fn.bind(obj,'alan'); 

bindObj('man'); //{ val: 'bind', name: 'alan', sex: 'man' }
複製代碼

栗子分析:

  • 首先bind的時候接受了一個參數name,同時返回了一個函數
  • 執行放回的函數的時候傳入了第二個參數sex

模擬分析:

  • 首先考慮bind方法傳參的場景,咱們能夠借用以前在call函數中的方法,使用es6 rest的方法。獲取從第二個開始到結束的全部參數
  • 考慮bind返回的函數傳參,能夠在寫的時候,將bind傳參跟後續的傳參合並

模擬開發

// 2.1
Function.prototype.bind2 = function (args) {
    //經過this拿到調用方法
    let that = this;
    
    // 獲取bind2函數從第二個參數到最後一個參數
    let allArgs = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 這個時候的arguments是指bind返回的函數傳入的參數
        var bindArgs = Array.prototype.slice.call(arguments);
        return that.apply(args, allArgs.concat(bindArgs));
    }

}
//2.2 es6實現
Function.prototype.bind2 = function (...args) {
  //利用es6的 rest 來獲取函數的傳參,以及傳入thisArg;allArgs就是第二個參數到最後一個參數的數組
  let [thisArg,...allArgs] = args ;
  let that = this;
  return function (...bindArgs) {
      return that.apply(thisArg, allArgs.concat(bindArgs));
  }

}
let obj = {
    val:'bind'
};
function fn(name,sex){
    let o = {
        val:this.val,
        name:name,
        sex:sex
    }
    console.log(o)
}
let bindObj = fn.bind2(obj,'alan'); 
bindObj('man'); //{ val: 'bind', name: 'alan', sex: 'man' }
複製代碼

說明

  • Array.prototype.slice.call(arguments)是如何將arguments轉換成數組的,首先調用call以後,this就指向了arguments,或許咱們能夠假象一下slice的內部實現是:建立一個新的數組,而後循環遍歷this,將this的沒一個值賦值給新的數組而後返回新數組。

結束語

大佬若是看到文中若有錯誤的地方歡迎指出,我及時修正。

參考

es6.ruanyifeng.com/?search=spr…

github.com/mqyqingfeng…

www.jianshu.com/p/d647aa6d1…

相關文章
相關標籤/搜索