读《不一样的javascript》之手写bind,call,apply 函数及fn.call.call....call(fn3)问题

《不一样的javascript》系列

  1. this指向问题
  2. 手写三大显式绑定函数

文章目录


前言

上一篇讲this指向时候讲到四大绑定时说到:call…这三个可以函数显示绑定一个对象到一个函数的this上,这篇主要讲一下三者区别以及手写他们。至于你问为啥要手写它呢,别问,问就是在某个面试上遇到过 。:)


提示:以下是本篇文章正文内容,下面案例可供参考

一、call、apply、bind是什么,有何用,区别是什么?

1. 语法

可以参考这篇文章或者看MDN文档

2. 相同点

  • 三个的作用都是把一个对象绑定到一个执行函数的this上(运行时函数才有this)
  • 如果第一个参数是null,undefined 非严格模式转换为window对象,严格模式为null,undefined
  • 如果第一个参数是数字或者字符串等非对象,在非严格模式下转为对应的字面量对象,严格模式不会。
  • 第一个参数没有 非严格模式 window 严格模式undefined
		// 'use strict'
        var obj = {
            a: 1
        }

        function person() {
            console.log(this);
        }
        var a = 3;
        person.call()//window
        person.call(obj) //obj
        person.call(null) //window  strict null 
        person.call(undefined) //window strict undefined
        person.call('11') //String{"11"} strict '11'
        person.call(11) //Number{11} strict 11

        person.apply(null) //window
        person.apply(undefined) //window
        person.apply(11) //Number{11}

        var fn = person.bind(null) //window
        fn()
        var fn2 = person.bind(undefined) //window
        fn2()
        var fn3 = person.bind(11) //Number{11}
        fn3()

3. 不同点

  • bind 与call,apply 的区别是bind不会执行该函数,只改变this指向,其余参数不做要求。
  • call apply 都执行该函数,区别在于apply 除了第一个参数 剩余参数只能是一个数组或者类数组里面包含若干参数
  • 而call为若干个参数列表

4. 疑惑点

  • 上面说bind 在非严格模式下null,undefined转换window 但是在MDN文档

调用绑定函数时作为 this 参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。当使用 bind 在 setTimeout 中创建一个函数(作为回调提供)时,作为 thisArg 传递的任何原始值都将转换为 object。如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。

与我下面测试不符,有知道可告知吾。

	function person() {
            console.log(this);
        }
		var obj2 = {
            fn4: function() {
                console.log(this);//obj2
                setTimeout(() => {
                    var fn6 = person.bind(null);
                    fn6() //window
                }, 100)
            }
        }

       obj2.fn4()

5. 用途

这个就有点多了,略

二、手写

有了上面的准备开始写吧,还记得上一篇隐式绑定吗?

1. apply

  /*
         * 不做函数监测的判断
         * 实现apply的类型功能
         * 利用隐式绑定
         * 非严格模式
         * 不判断类数组
         */
        Function.prototype.myApply = function(context, args) {
            args = args ? args : []//参数为空
            if (context === null || context === undefined) {
                context = window 
                // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
            } else {
                context = Object(context) 
                // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
            }
            var fn = this //this就是你要改变的函数
            const key = Symbol() //给context新增一个独一无二的属性以免覆盖原有属性
            context[key] = fn //相当于context 增加fn方法,让context拥有这个函数

            const res = context[key](...args) 
            //通过隐式绑定的方式调用函数 相当于obj.fn() , ...args es6语法                

            delete context[key] //删除添加的属性     
            console.log(context[key]);
            return res //返回函数调用的返回值

        }
        var obj = {
            a: 2
        }
        var a = 3;
        var fn = function() {
            console.log(this);
        }
        fn.myApply(obj) //obj
        fn.myApply(null) //window
        fn.myApply(11) //Number{11}

2. call

唯一区别第一行,注意两个…的区别

Function.prototype.mybind = function(context, ...args) {
            args = args ? args : []
            if (context === null || context === undefined) {
                context = window // 指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
            } else {
                context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
            }

            var fn = this //就是你要改变的函数

            const key = Symbol() //给context新增一个独一无二的属性以免覆盖原有属性
            context[key] = fn //相当于context 增加fn方法,让context拥有这个函数

            const res = context[key](...args) //通过隐式绑定的方式调用函数 相当于obj.fn() , ...args es6语法把类数组转化为数组                

            delete context[key] //删除添加的属性     
            console.log(context[key]);
            return res //返回函数调用的返回值

        }
		function testRest(...args) { 
		//rest运算,剩余运算符(the rest operator)用于解构数组和对象
            console.log(args); //[1,2,3,4]
            console.log(...args); 
            //1,2,3,4扩展运算符:拆解数组,将一个数组转为用逗号分隔的参数序列

        }
        testRest(1, 2, 3, 4)

3. bind

  • 这里注意一点就是bind是可以new的,new的优先级是大于bind的
  • 有的教程里面把不绑定new 的对象,这与优先级违背了。
	function person() {
            console.log(this); //person
        }
        var a = 3
        var obj = {
            a: 2
        }
        var newPerson = person.bind(obj)
        var np = new newPerson();
        console.log(np); //person new优先级

        var newFn = person.bind(obj);
        newFn() //{a:2}

        Function.prototype.myBind = function(context, ...args) {
            const fn = this
            args = args ? args : []
            return function newFn(...secondArgs) { //二次传参问题
                if (this instanceof newFn) { //解决new的问题
                    return new fn(...args, ...secondArgs)//如果是new 就返回一个对象
                }
                return fn.apply(context, [...args, ...secondArgs])
            }
        }

        var newPerson2 = person.myBind(obj)
        var np2 = new newPerson2();
        console.log(np2); //person new优先级

        var newFn2 = person.myBind(obj);
        newFn2() //{a:2}

4. 总结一下

  1. 核心:利用隐式绑定。
  2. 上面的代码只是实现了一部分,距离真正的实现还远着。
  3. 上面代码参考与(我这小脑瓜子可实现不了,侵删)
    https://juejin.cn/post/6844903891092389901#heading-12
    https://juejin.cn/post/6844903906279964686#heading-22
    https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/

三、做道题

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

        var obj2 = {
            name: '233'
        }
        fn.call(obj2) 
        fn.call(fn)
        fn.call.call(function() {
                    console.log(this, 1)
                }) 
        fn.call.call.call.call(function() {
                console.log(this)
            }, obj2) 

会输出什么呢?
你可能觉得很乱
那我换一下

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

        var obj2 = {
            name: '233'
        }

        function fn3() {
            console.log(this, 'fn3');
        }


        console.log(fn.prototype);
        fn.call(obj2) //obj2
        fn.call(fn)
        fn.call.call(fn3) //window fn3
        fn.call.call.call.call(fn3, obj2) // obj fn3

先说个结论:

  1. fn.call.call.call.call.call…call(fn3)
    结果fn3()//window
  2. fn.call.call.call.call.call…call(fn3,obj)
    结果fn3()//obj

原型链不懂得可以看一下

  • 第一点 fn.call.call.call.call会是什么
    你可以console.log(fn.prototype);里面找call然后在找call 一直找下去
  • 第二点 Function.prototype.call意味什么?就是意味着只要是函数都有call ,call也是函数它也有call,第一点可以看到
  • 第三点 fn.call…最终起作用得只有最后两个call ,fn已经没用了,最后就是call.call()如下面。
  • 把fn 比作对象(它也确实是一个对象,以原型链来说)fn.call…相当于一个对象里一个子属性对象得子属性对象…
		//这里改为对象会好点
		// function fx() {
        //     var fx = function() {
        //         var fx = function() {
        //             console.log('1');
        //         }
        //     }
        // }
        // console.log(fx.fx.fx());

第四点:如我们上面手写一样,fn.call(context,args) 内部是隐式绑定conext.fn(args)

所以

  1. fn.call.call…call = calll
  2. fn.call.call(fn3) = call.call(fn3)
  3. 内部就是 context => fn3 fn=>call
  4. conext.fn(args) => fn3.call(args) args为空绑定到window
  5. fn.call.call(fn3,obj) => fn3.call(obj) obj绑定到fn3上

总结:
call数>=2 看最后call参数 call(fn3,obj)

  • 只有fn3 则执行fn3() 绑定window到fn3上
  • fn3,obj 则执行fn3() 绑定obj到fn3上
上一篇:RPC(远程过程调用协议)


下一篇:大爽Python入门教程 4-3 函数传参 形参、实参 default、*args、**kwargs