• JS进阶第一篇:手写call apply bind


    手写call apply bind

    深入理解 call 方法

    call 理解了,apply和bind就都迎刃而解了,他们都是大同小异。在此对callapply不做过多的定义性解释,先来看下调用了call后谁是那个被执行的方法,直接代码示例:

    function fn1 () {
        console.log(1);
    };
    function fn2 () {
        console.log(2);
    };
    fn1.call(fn2);//1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    执行fn1.call(fn2);控制台会打印1,这里可以说明fn1调用call后被执行的方法还是fn1。一定要弄清楚谁是这个被执行的方法,就是调用call的函数,而fn2现在的身份是替代window作为fn1的直接调用者,这是理解call和apply的关键,也可以运行下fn2.call(fn1);
    再来个代码示例:

    var obj1 = {
        num : 20,
        fn : function(n){
            console.log(this.num+n);
        }
    };
    var obj2 = {
        num : 15,
        fn : function(n){
            console.log(this.num-n);
        }
    };
    obj1.fn.call(obj2,10);//25
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    执行obj1.fn.call(obj2,10);控制台会打印25,call在此的作用其实很简单,就是在执行obj1.fn的时候把这个fn的直接调用者由obj1变为obj2,obj1.fn(n)内部的this经过call的作用指向了obj2,所以this.num就是obj2.num,10作为执行obj1.fn时传入的参数,obj2.num是15,因此打印出的值是15+10=25。
    所以我们可以这样理解:call的作用是改变了那个被执行的方法(也就是调用call的那个方法)的直接调用者!而这个被执行的方法内部的this也会重新指向那个新的调用者,就是call方法所接收的第一个obj参数。还有两个特殊情况就是当这个obj参数为null或者undefined的时候,this会指向window。

    手写call

    Function.prototype.myCall = function (context) {
          // 先判断调用myCall是不是一个函数
          // 这里的this就是调用myCall的
          if (typeof this !== 'function') {
            throw new TypeError("Not a Function")
          }
     
          // 不传参数默认为window
          context = context || window
     
          // 保存this
          context.fn = this
     
          // 保存参数
          let args = Array.from(arguments).slice(1)
          //Array.from 把伪数组对象转为数组,然后调用 slice 方法,去掉第一个参数
     
          // 调用函数
          let result = context.fn(...args)
     
          delete context.fn
     
          return result
     
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    手写apply

    Function.prototype.myApply = function (context) {
          // 判断this是不是函数
          if (typeof this !== "function") {
            throw new TypeError("Not a Function")
          }
     
          let result
     
          // 默认是window
          context = context || window
     
          // 保存this
          context.fn = this
     
          // 是否传参
          if (arguments[1]) {
            result = context.fn(...arguments[1])
          } else {
            result = context.fn()
          }
          delete context.fn
     
          return result
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    手写bind

    在实现手写bind方法的过程中,看了许多篇文章,答案给的都很统一,准确,但是不知其所以然,所以我们就好好剖析一下bind方法的实现过程。

    我们先看一下bind函数做了什么:

    bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
    
    • 1

    读到这里我们就发现,他和 apply , call 是不是很像,所以这里指定 this 功能,就可以借助 apply 去实现:

    Function.prototype.myBind = function (context) {
        // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
        const self = this;
        // 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
        // 这里产生了闭包
        const args = Array.from(arguments).slice(1)
        return function () {
            // 这个时候的 arguments 是指 myBind 返回的函数传入的参数
            const bindArgs = Array.from(arguments)
            // 合并
            return self.apply(context, args.concat(bindArgs));
        };
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    大家对这段代码应该都能看懂,实现原理和手写 call , apply 都很像,因为 bind 可以通过返回的函数传参,所以在 return 里面获取的 bindArgs 就是这个意思,然后最后通过 concat 把原来的参数和后来传进来的参数进行数组合并。

    我们来看一下结果:

    const person = {
        name: 'zyj'
    }
     
    function man(age) {
        console.log(this.name);
        console.log(age)
    }
     
    const test = man.myBind(person)
    test(18)//zyj 18
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    现在重点来了,bind 区别于 call 和 apply 的地方在于它可以返回一个函数,然后把这个函数当作构造函数通过 new 操作符来创建对象。

    我们来试一下:

    const person = {
        name: 'zyj'
    }
     
    function man(age) {
        console.log(this.name);
        console.log(age)
    }
     
    const test = man.myBind(person)
    const newTest = new test(18) // zyj 18
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这是用的我们上面写的 myBind 函数是这个结果,那原生 bind 呢?

    const person = {
        name: 'zyj'
    }
     
    function man(age) {
        console.log(this.name);
        console.log(age)
    }
     
    const test = man.bind(person)
    const newTest = new test(18) // undefined 18
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    由上述代码可见,使用原生 bind 生成绑定函数后,通过 new 操作符调用该函数时,this.name 是一个 undefined,这其实很好理解,因为我们 new 了一个新的实例,那么构造函数里的 this 肯定指向的就是实例,而我们的代码逻辑中指向的始终都是 context ,也就是传进去的参数。
    
    • 1

    所以现在我们要加个判断逻辑:

    Function.prototype.myBind = function (context) {
        // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
        const self = this;
        // 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
        // 这里产生了闭包
        const args = Array.from(arguments).slice(1)
        const theBind = function () {
            const bindArgs = Array.from(arguments);
        
            // 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
            // 当作为普通函数时,将绑定函数的 this 指向 context 即可
            // this instanceof fBound 的 this 就是绑定函数的调用者
            return self.apply(
              this instanceof theBind ? this : context,
              args.concat(bindArgs)
            );
          };
          return theBind;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    现在这个效果我们也实现了,那我们的 myBind 函数就和其他的原生 bind 一样了吗?来看下面的代码:

    const person = {
        name: 'zyj'
    }
    function man(age) {
        console.log(this.name);
        console.log(age)
    }
    man.prototype.sayHi = function() {
        console.log('hello')
    }
    const test = man.myBind(person)
    const newTest = new test(18) // undefined 18
    newTest.sayHi()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    如果 newTest 是我们 new 出来的 man 实例,那根据原型链的知识,定义在man的原型对象上的方法肯定会被继承下来,所以我们通过 newTest.sayHi 调用能正常输出 hello 么?

    alt

    该版代码的改进思路在于,将返回的绑定函数的原型对象的 proto 属性,修改为原函数的原型对象。便可满足原有的继承关系。

    Function.prototype.myBind = function (context) {
        // 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
        const self = this;
        // 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
        // 这里产生了闭包
        const args = Array.from(arguments).slice(1);
        const theBind = function () {
            const bindArgs = Array.from(arguments);
        
            // 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
            // 当作为普通函数时,将绑定函数的 this 指向 context 即可
            // this instanceof fBound 的 this 就是绑定函数的调用者
            return self.apply(
              this instanceof theBind ? this : context,
              args.concat(bindArgs)
            );
          };
          theBind.prototype = Object.create(self.prototype)
          return theBind;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
  • 相关阅读:
    Windows服务器被入侵后如何实现排查工作,应该从那几方面入手,排查什么内容
    Citus 分布式 PostgreSQL 集群 - SQL Reference(创建和修改分布式表 DDL)
    基于STM32程序万年历液晶1602显示-proteus仿真-源程序
    【微服务】SpringCloud微服务续约源码解析
    Kotlin - 协程 Coroutine
    Unity3D工程作为库内嵌到安卓原生开发指南
    面试说:聊聊JavaScript中的数据类型
    车载诊断新驱动——远程诊断
    el-table 动态合并单元格和给某一行添加颜色
    [USACO11MAR] Brownie Slicing G题解(二分+二维前缀和+矩阵分割)
  • 原文地址:https://blog.csdn.net/qq_49900295/article/details/128077131