最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 改变 this 指向、深入理解 call/apply/bind 的原理

    正文概述 掘金(iconLee)   2021-05-17   479

    前言

    在“多数情况下”,this 遵循的指向机制。在另外一些情况下 this 是不遵循这个机制的。改变 this 的指向,我们主要有两条路:

    • 通过改变书写代码的方式做到(比如箭头函数)。
    • 显式地调用一些方法来帮忙(比如call/apply/bind)。

    改变书写代码的方式,进而改变 this 的指向

    唱反调的箭头函数

    var n = 1
    
    var obj = {
      n: 2,
      // 声明位置
      sayN: () => {
          console.log(this.n)
      }
    }
    
    // 调用位置
    obj.sayN() // 1
    

    当我们将普通函数改写为箭头函数时,箭头函数的 this 会在书写阶段(即声明位置)就绑定到它父作用域的 this 上。无论后续我们如何调用它,都无法再为它指定目标对象 —— 因为箭头函数的 this 指向是静态的,“一次便是一生”

    构造函数里的 this

    function Person(name) {
      this.name = name
      console.log(this)
    }
    var person = new Person('icon') // Person {name: "icon"}
    

    构造函数里面的 this 会绑定到我们 new 出来的这个对象上

    显式地调用一些方法来帮忙

    考虑到实际开发中我们改变 this 指向的场景非常多,所以这三种方法的使用在面试中考察的频率也比较高。最常见的考法,是问询三种方法的使用及区别。但很多时候,为了能够进一步试探你对 this 相关概念理解和掌握的深度, 面试官会考察你 call、apply 和 bind 的实现机制,甚至可能会要求你手写代码

    因此,针对 call、 apply 和 bind,我们不仅要会用、会辨析,更要对其原理知根知底。接下来,我们将这三种方法的考察方式汇聚到两道题里面,大家若能掌握这两个问题,就可以做到举一反三,知一解百。

    基本问答题:call、apply 和 bind 是干嘛的?如何使用?它们之间有哪些区别?

    改变 this 指向、深入理解 call/apply/bind 的原理 结合这张图来说明,会清楚得多

    call、apply 和 bind,都是用来改变函数的 this 指向的。

    call、apply 和 bind 之间的区别比较大,前两者在改变 this 指向的同时,也会把目标函数给执行掉;后者则只负责改造 this,不作任何执行操作

    call 和 apply 之间的区别,则体现在对入参的要求上。前者只需要将目标函数的入参逐个传入即可,后者则希望入参以数组形式被传入

    进阶编码题:模拟实现一个 call/apply/bind 方法

    call 方法的模拟

    在实现 call 方法之前,我们先来看一个 call 的调用示范:

    var me = {
      name: 'icon'
    }
    
    function showName() {
      console.log(this.name)
    }
    
    showName.call(me) // icon
    

    结合 call 表现出的特性,我们首先至少能想到以下两点:

    • call 是可以被所有的函数继承的,所以 call 方法应该被定义在 Function.prototype 上
    • call 方法做了两件事:
      • 改变 this 的指向,将 this 绑到第一个入参指定的的对象上去;
      • 根据输入的参数,执行函数。

    结合这两点,我们一步一步来实现 call 方法。首先,改变 this 的指向

    showName 在 call 方法调用后,表现得就像是 me 这个对象的一个方法一样。

    所以我们最直接的一个联想是,如果能把 showName 直接塞进 me 对象里就好了,像这样:

    var me = {
      name: 'icon',
      showName: function() {
        console.log(this.name)
      }
    }
    
    me.showName()
    

    但是这样做有一个问题,因为在 call 方法里,me 是一个入参:

    showName.call(me) // icon
    

    用户在传入 me 这个对象的时候, 想做的仅仅是让 call 把 showName 里的 this 给改掉,而不想给 me 对象新增一个 showName 方法。所以说我们在执行完 me.showName 之后,还要记得把它给删掉。遵循这个思路,我们来模拟一下 call 方法(注意看注释):

    Function.prototype.myCall = function(context) {
        // 1: 把函数挂到目标对象上(这里的 this 就是我们要改造的的那个函数)
        context.func = this
        // 2: 执行函数
        context.func()
        // 3: 删除 1 中挂到目标对象上的函数,把目标对象”完璧归赵”
        delete context.func
    }
    

    试试

    var me = {
      name: 'icon'
    }
    
    function showName() {
      console.log(this.name)
    }
    
    showName.myCall(me) // icon
    

    到这里,我们已经实现了 改变 this 的指向 这个功能点。现在我们的 myCall 还需要具备读取函数入参的能力,类比于 call 的这种调用形式:

    var me = {
      name: 'icon'
    }
    
    function showFullName(surName) {
      console.log(`${this.name} ${surName}`)
    }
    
    showFullName.call(me, 'lee') // icon lee
    

    读取函数入参,具体来说其实是读取 call 方法的第二个到最后一个入参。要做到这一点,我们可以借助ES6数组的扩展符

    // '...'这个扩展运算符可以帮助我们把一系列的入参变为数组
    function readArr(...args) {
        console.log(args)
    }
    readArr(1,2,3) // [1,2,3]
    

    我们把这个逻辑用到我们的 myCall 方法里:

    Function.prototype.myCall = function(context, ...args) {
        console.log('入参是', args)
        context.func = this
        context.func()
        delete context.func
    }
    

    就能通过 args 这个数组拿到我们想要的入参了。把 args 数组代表的目标入参重新展开,传入目标方法里,就大功告成了:

    Function.prototype.myCall = function(context, ...args) {
        context.func = this
        // 执行函数,利用扩展运算符将数组展开
        context.func(...args)
        delete context.func
    }
    

    现在我们来测试一下功能完备的 myCall 方法:

    Function.prototype.myCall = function(context, ...args) {
        context.func = this
        context.func(...args)
        delete context.func
    }
    
    var me = {
      name: 'icon'
    }
    
    function showFullName(surName) {
      console.log(`${this.name} ${surName}`)
    }
    
    showFullName.myCall(me, 'lee') // icon lee
    

    以上,我们就成功模拟了一个 call 方法出来。

    基于这个最基本的 call 思路,大家还可以为这个方法作能力扩充:

    比如如果我们第一个参数传了 null 怎么办?是不是可以默认给它指到 window 去?函数如果是有返回值的话怎么办?是不是新开一个 result 变量存储一下这个值,最后 return 出来就可以了?等等—— 这些都是小事儿。

    当面试官问你 “如何模拟 call 方法的实现的时候”,他最想听的其实就楼上这两个核心功能点的实现思路,其它的,都是锦上添花

    基于对 call 方法的理解,写出一个 apply 方法(更改读取参数的形式) 和 bind 方法(延迟目标函数执行的时机)不是什么难事,只需要大家在上面这段代码的基础上作改造即可。

    apply方法的模拟

    apply的实现和call非常相似,区别只是在参数的处理上。这里就不说多了,直接上代码(注意看注释):

    Function.prototype.myApply = function(context, args){
        // 1: 判断当前传参是否是数组
        if(args && !(args instanceof Array)){
            throw new TypeError('呀呀呀,参数必须是数组哦')
        }
        // 2: 上面说的 如果是null默认指向window
        context = context || window
        // 3: 把函数挂到目标对象上(这里的 this 就是我们要改造的的那个函数)
        context.func = this
        // 4: 执行函数并且存储上面说的 返回值
        const result = context.func(args ? [...args] : '')
        // 5: 删除 1 中挂到目标对象上的函数,把目标对象”完璧归赵”
        delete context.func;
        // 6: 返回结果值
        return result;
    }
    

    bind方法的模拟

    bind的实现稍微麻烦点,因为需要返回一个函数,需要判断些边界条件。这里就不说多了,直接上代码(注意看注释):

    Function.prototype.myBind = function (context, ...args) {
        // 1: 保存下当前 this(这里的 this 就是我们要改造的的那个函数)
        const _this = this;
        // 2: 返回一个函数
        return function F() {
          // 3: 因为返回了一个函数,除了直接调用还可以 new F(),所以需要判断分开走
          // 4: new 的方式
          if (_this instanceof F) {
            return new _this(...args, ...arguments);
          }
          // 5: 直接调用,这里选择了 apply 的方式实现但是对于参数需要注意以下情况:因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(…arguments);
          return _this.apply(context, args.concat(...arguments));
        }
    }
    

    起源地 » 改变 this 指向、深入理解 call/apply/bind 的原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元