简单模拟实现javascript中的call、apply、bind方法

更新日期: 2021-04-15阅读量: 422标签: 方法

    隐式丢失

    由于模拟实现中有运用到隐式丢失, 所以在这还是先介绍一下。

    隐式丢失是一种常见的this绑定问题, 是指: 被隐式绑定的函数会丢失掉绑定的对象, 而最终应用到默认绑定。说人话就是: 本来属于隐式绑定(obj.xxx this指向obj)的情况最终却应用默认绑定(this指向全局对象)。

    常见的隐式丢失情况1: 引用传递

    var a = 'window'
    function foo() {
        console.log(this.a)
    }
    var obj = {
        a: 'obj',
        foo: foo
    }
    
    obj.foo() // 'obj' 此时 this => obj
    var lose = obj.foo
    lose()  // 'window' 此时 this => window
    

    常见的隐式丢失情况2: 作为回调函数被传入

    var a = 'window'
    function foo() {
        console.log(this.a)
    }
    var obj = {
        a: 'obj',
        foo: foo
    }
    
    function lose(callback) {
        callback()
    }
    
    lose(obj.foo)  // 'window' 此时 this => window
    
    
    // ================   分割线  ===============
    var t = 'window'
    function bar() {
        console.log(this.t)
    }
    
    setTimeout(bar, 1000)   // 'window'
    

    对于这个我总结的认为(不知对错): 在排除显式绑定后, 无论怎样做值传递,只要最后是被不带任何修饰的调用, 那么就会应用到默认绑定

    进一步的得到整个实现的关键原理: 无论怎么做值传递, 最终调用的方式决定了this的指向


    硬绑定

    直观的描述硬绑定就是: 一旦给一个函数显式的指定完this之后无论以后怎么调用它, 它的this的指向将不会再被改变

    硬绑定的实现解决了隐式丢失带来的问题, bind函数的实现利用就是硬绑定的原理

    // 解决隐式丢失
    var a = 'window'
    function foo() {
        console.log(this.a)
    }
    var obj = {
        a: 'obj',
        foo: foo
    }
    
    function lose(callback) {
        callback()
    }
    
    lose(obj.foo)   // 'window'
    
    var fixTheProblem = obj.foo.bind(obj)
    lose(fixTheProblem) // 'obj'

    实现及原理分析

    模拟实现call

    // 模拟实现call
    Function.prototype._call = function ($this, ...parms) {     // ...parms此时是rest运算符, 用于接收所有传入的实参并返回一个含有这些实参的数组
        /* 
            this将会指向调用_call方法的那个函数对象   this一定会是个函数
            ** 这一步十分关键 **  => 然后临时的将这个对象储存到我们指定的$this(context)对象中
        */
        $this['caller'] = this
        //$this['caller'](...parms)
    
        // 这种写法会比上面那种写法清晰
        $this.caller(...parms) // ...parms此时是spread运算符, 用于将数组中的元素解构出来给caller函数传入实参
        /* 
            为了更清楚, 采用下面更明确的写法而不是注释掉的
                1. $this.caller是我们要改变this指向的原函数
                2. 但是由于它现在是$this.caller调用, 应用的是隐式绑定的规则
                3. 所以this成功指向$this
        */
        delete $this['caller']  // 这是一个临时属性不能破坏人为绑定对象的原有结构, 所以用完之后需要删掉
    }
    

    模拟实现apply

    // 模拟实现apply  ** 与_call的实现几乎一致, 主要差别只在传参的方法/类型上 **
    Function.prototype._apply = function ($this, parmsArr) {    // 根据原版apply  第二个参数传入的是一个数组
        $this['caller'] = this
        $this['caller'](...parmsArr) // ...parmsArr此时是spread运算符, 用于将数组中的元素解构出来给caller函数传入实参
        delete $this['caller']
    }
    

    既然_call与_apply之前的相似度(耦合度)这么高, 那我们可以进一步对它们(的相同代码)进行抽离

    function interface4CallAndApply(caller, $this, parmsOrParmArr) {
        $this['caller'] = caller
        $this['caller'](...parmsOrParmArr)
        delete $this['caller']
    }
    
    
    Function.prototype._call = function ($this, ...parms) {
        var funcCaller = this
        interface4CallAndApply(funcCaller, $this, parms)
    }
    
    
    Function.prototype._apply = function ($this, parmsArr) {
        var funcCaller = this
        interface4CallAndApply(funcCaller, $this, parmsArr)
    }
    

    一个我认为能够较好展示_call 和 _apply实现原理的例子

    var myName = 'window'
    var obj = {
        myName: 'Fitz',
        sayName() {
            console.log(this.myName)
        }
    }
    
    var foo = obj.sayName
    
    var bar = {
        myName: 'bar',
        foo
    }
    
    bar.foo()

    模拟实现bind

    // 使用硬绑定原理模拟实现bind
    Function.prototype._bind = function ($this, ...parms) {
        $bindCaller = this  // 保存调用_bind函数的对象   注意: 该对象是个函数
        // 根据原生bind函数的返回值: 是一个函数
        return function () { // 用rest运算符替代arguments去收集传入的实参
            return $bindCaller._apply($this, parms)
        }
    }
    

    一个能够展现硬绑定原理的例子

    function hardBind(fn) {
        var caller = this
        var parms = [].slice.call(arguments, 1)
        return function bound() {
            parms = [...parms, ...arguments]
            fn.apply(caller, parms) // apply可以接受一个伪数组而不必一定是数组
        }
    }
    
    
    var myName = 'window'
    function foo() {
        console.log(this.myName)
    }
    var obj = {
        myName: 'obj',
        foo: foo,
        hardBind: hardBind
    }
    
    // 正常情况下
    foo()   // 'window'
    obj.foo()   // 'obj'
    
    var hb = hardBind(foo)
    // 可以看到一旦硬绑定后无论最终怎么调用都不能改变this指向
    hb()    // 'window'
    obj.hb = hb // 给obj添加该方法用于测试
    obj.hb()    // 'window'
    
    // 在加深一下印象
    var hb2 = obj.hardBind(foo)
    hb2()   // 'obj'    // 这里调用this本该指向window

    总体实现(纯净版/没有注释)

    function interface4CallAndApply(caller, $this, parmsOrParmArr) {
        $this['caller'] = caller
        $this['caller'](...parmsOrParmArr)
        delete $this['caller']
    }
    
    
    Function.prototype._call = function ($this, ...parms) {
        var funcCaller = this
        interface4CallAndApply(funcCaller, $this, parms)
    }
    
    
    Function.prototype._apply = function ($this, parmsArr) {
        var funcCaller = this
        interface4CallAndApply(funcCaller, $this, parmsArr)
    }
    
    
    Function.prototype._bind = function ($this, ...parms) {
        $bindCaller = this
        return function () {
            return $bindCaller._apply($this, parms)
        }
    }
    
    
    
    // ============ 测试 ===============
    var foo = {
        name: 'foo',
        sayHello: function (a, b) {
            console.log(`hello, get the parms => ${a} and ${b}`)
        }
    }
    
    var bar = {
        name: 'bar'
    }
    
    foo.sayHello._call(bar, 'Fitz', 'smart')
    foo.sayHello._apply(bar, ['Fitz', 'smart'])
    
    var baz = foo.sayHello._bind(bar, 'Fitz', 'smart')
    baz()
    
    var testHardBind = foo.sayHello._bind(bar, 'hard', 'bind')
    testHardBind._call(Object.create(null))   // hello, get the parms => hard and bind 测试_bind的硬绑定
    站长推荐

    1.云服务推荐: 国内主流云服务商,各类云产品的最新活动,优惠券领取。地址:阿里云腾讯云华为云

    链接: https://www.fly63.com/article/detial/10223

    浅析 JavaScript 中的方法链

    方法链是一种流行的编程方法,可以帮助你写出更简洁易读的代码。在本文中我们一起学习 JavaScript 中的方法链是什么,以及它是怎样工作的。另外我们还会探讨如何使用方法链接来提高代码的质量和可读性。

    js中Element.getBoundingClientRect()方法

    Element.getBoundingClientRect()方法返回元素的大小及其相对于视口的位置。返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合, 即:是与该元素相关的CSS 边框集合

    vue cached、camelize方法理解

    驼峰转换的主要原理,主要是replace方法的使用(以下内容主要针对当前例子,详细使用见:W3school:JavaScript replace() 方法或MDN:String.prototype.replace())

    JS中toFixed()方法的四舍五入问题解决方法

    最近发现JS当中toFixed()方法存在一些问题,采用原生的Number对象的原型对象上的toFixed()方法时,规则并不是所谓的四舍五入

    常用原生JS方法总结(兼容性写法)

    经常会用到原生JS来写前端。。。但是原生JS的一些方法在适应各个浏览器的时候写法有的也不怎么一样的,一下的方法都是包裹在一个EventUtil对象里面的,直接采用对象字面量定义方法了

    javascript中怎么定义静态方法?

    JavaScript中直接定义在构造函数上的方法和属性是静态的, 定义在构造函数的原型和实例上的方法和属性是非静态的。

    什么是javascript的私有方法?

    私有方法是只有父类可以访问的方法和属性,他与静态方法一致,只是表现形式不一样。构造器中的var变量和参数都是私有方法。私有成员由构造函数生成。构造函数的普通变量和参数成为私有成员。

    如何判断一个原生方法是否被重写

    有的脚本会重写该方法,那么如何判断这个方法是否被重写了呢?浏览器根据 ECMScript 标准,为我们提供了很多原生的方法。但有的脚本会重写该方法,那么如何判断这个方法是否被重写了呢?

    javascript中的split方法怎么用?

    split()方法可以利用字符串的子字符串的作为分隔符将字符串分割为字符串数组,并返回此数组,本文给大家介绍javascript split 方法。split() 方法用于把一个字符串分割成字符串数组。

    前端百题_竟然有五种方式实现flat方法

    不知道老铁们有没有遇到过一道面试题:如何将一个多维数组展开成一个一维数组?当时我遇到的时候还不了解flat这个神奇的方法,用了最传统的解决方法进行解决。

    点击更多...

    内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!