美文网首页
深入学习js之call、apply、bind模拟实现

深入学习js之call、apply、bind模拟实现

作者: AlanLynn | 来源:发表于2019-03-01 16:58 被阅读0次

题外篇

如何改变this得指向,常见的四种操作如下

  • 使用call、apply、bind
  • 在执行函数内部使用let that = this
  • es6中使用箭头函数
  • 对象实例化 new操作

关于this的指向

在一个函数上下文中,this由调用者提供,由调用函数的方式来决定。如果调用者函数,被某一个对象所拥有,那么该函数在调用时,内部的this指向该对象。如果函数独立调用,那么该函数内部的this,则指向undefined。但是在非严格模式中,当this指向undefined时,它会被自动指向全局对象。

参考来自 波波老师

call

根据 MDN 的解释:

call()方法调用一个函数, 其具有一个指定的this值和分别地提供的参数(参数的列表)。

语法:

fun.call(thisArg, arg1, arg2, ...)

thisArg:fun函数运行时指定得this

this得值可能有如下几种可能:

  • 在严格模式下,传null,undefined or 不传,this默认指向window对象
  • 传其他函数的函数名称,如fn,this指向fn函数
  • 传其他对象,this指向这个对象

看个例子

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)的执行过程可以想象成如下步骤:

1、将fn复制到obj对象中,那么如下也就修改了fn中this的指向

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

obj.fn()

3、删除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的没一个值赋值给新的数组然后返回新数组。

结束语

大佬如果看到文中如有错误的地方欢迎指出支出,我会及时修改。

参考

http://es6.ruanyifeng.com/?search=spread&x=0&y=0

https://github.com/mqyqingfeng/Blog/issues/11

https://www.jianshu.com/p/d647aa6d1ae6

相关文章

网友评论

      本文标题:深入学习js之call、apply、bind模拟实现

      本文链接:https://www.haomeiwen.com/subject/lixjuqtx.html