zoukankan      html  css  js  c++  java
  • 绑定this:call()、apply()、bind()和它们的内部实现原理 + 箭头函数中的this

    this的灵活性让编程困难了许多,因此需要一些方法把this给固定下来。

    Function.prototype.call(thisValue, arg1, arg2, ...)

    函数实例call可以指定函数内部this的指向。

    var obj = {};
    
    var f = function () {
      return this;
    };
    
    f() === window // true   全局环境下运行(不指定this指向),this指向window
    f.call(obj) === obj // true  用call将this指定到obj,在obj作用域运行
    

    call()的参数应当是一个对象。若留空nullundefined则是全局对象。
    若call的参数是原始值,那么原始值会自动转换成包装对象后传入call。

    var f = function () {
      return this;
    };
    
    f.call(5) // Number {[[PrimitiveValue]]: 5}
    // 5不是对象,会被自动转成包装对象(Number的实例),绑定f内部的this。
    

    call方法第一个参数之后的参数则是函数调用时所需的参数。

    function add(a, b) {
      return a + b;
    }
    
    add.call(this, 1, 2) // 3
    

    call()还可以用来调用对象的原生方法。

    var obj = {};
    obj.hasOwnProperty('toString') // false
    
    // 覆盖掉继承的 hasOwnProperty 方法
    obj.hasOwnProperty = function () {
      return true;
    };
    obj.hasOwnProperty('toString') // true
    
    Object.prototype.hasOwnProperty.call(obj, 'toString') // false
    //call方法将hasOwnProperty方法在Object上的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果。
    

    Call()的具体实现

    Function.prototype.imitateCall = function (context) {
        // 赋值作用域参数,如果没有则默认为 window,即访问全局作用域对象
        context = context || window;    
        // 绑定调用函数(.call之前的方法即this,前面提到过调用call方法会调用一遍自身,所以这里要存下来)
        context.invokeFn = this;    
        // 截取要传入的的参数
        let args = [...arguments].slice(1);
        // 执行调用函数,记录拿取返回值
        let result = context.invokFn(...args);
        // 销毁调用函数,以免作用域污染
        Reflect.deleteProperty(context, 'invokFn');
        return result
    }
    

    Function.prototype.apply(thisValue, [arg1, arg2, ...])

    apply()call()的作用一样,唯一的区别是它接收一个数组作为函数执行时的参数。
    传入数组的所有成员依次作为参数,传入原函数。原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加。

    function f(x, y){
      console.log(x + y);
    }
    
    f.call(null, 1, 1) // 2
    f.apply(null, [1, 1]) // 2
    

    利用好这个特性完成进行一些有趣的任务。
    1、合并两个数组

    var vegetables = ['parsnip', 'potato'];
    var moreVegs = ['celery', 'beetroot'];
    
    // 将第二个数组融合进第一个数组
    // 相当于 vegetables.push('celery', 'beetroot');
    Array.prototype.push.apply(vegetables, moreVegs);
    // 4
    
    vegetables;
    // ['parsnip', 'potato', 'celery', 'beetroot']
    

    当第二个数组(如示例中的 moreVegs )太大时不要使用这个方法来合并数组,因为一个函数能够接受的参数个数是有限制的。不同的引擎有不同的限制,JS核心限制在 65535,有些引擎会抛出异常,有些不抛出异常但丢失多余参数。
    如何解决呢?方法就是将参数数组切块后循环传入目标方法

    function concatOfArray(arr1, arr2) {
        var QUANTUM = 32768;
        for (var i = 0, len = arr2.length; i < len; i += QUANTUM) {
            Array.prototype.push.apply(
                arr1, 
                arr2.slice(i, Math.min(i + QUANTUM, len) )
            );
        }
        return arr1;
    }
    
    // 验证代码
    var arr1 = [-3, -2, -1];
    var arr2 = [];
    for(var i = 0; i < 1000000; i++) {
        arr2.push(i);
    }
    
    Array.prototype.push.apply(arr1, arr2);
    // Uncaught RangeError: Maximum call stack size exceeded
    
    concatOfArray(arr1, arr2);
    // (1000003) [-3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]
    

    2、获取数组中的最大值和最小值

    var numbers = [5, 458 , 120 , -215 ]; 
    Math.max.apply(Math, numbers);   //458    
    Math.max.call(Math, 5, 458 , 120 , -215); //458
    
    // ES6
    Math.max.call(Math, ...numbers); // 458
    

    实现

    Function.prototype.imitateApply = function (context) {
        // 赋值作用域参数,如果没有则默认为 window,即访问全局作用域对象
        context = context || window
        // 绑定调用函数(.call之前的方法即this,前面提到过调用call方法会调用一遍自身,所以这里要存下来)
        context.invokFn = this
        // 执行调用函数,需要对是否有参数做判断,记录拿取返回值
        let result
        if (arguments[1]) {
            result = context.invokFn(...arguments[1])
        } else {
            result = context.invokFn()
        }
        // 销毁调用函数,以免作用域污染
        Reflect.deleteProperty(context, 'invokFn')
        return result
    }
    

    Function.prototype.bind()

    bind()与上面两个不一样,它把this绑定到某个对象后,会返回一个新函数
    每调用一次bind,就会生成一个新函数。

    var counter = {
      count: 0,
      inc: function () {
        this.count++;
      }
    };
    
    var func = counter.inc.bind(counter);
    func();
    counter.count // 1
    
    var obj = {
      count: 100
    };
    var func = counter.inc.bind(obj);
    func();
    obj.count // 101
    

    bind()也可以接受更多参数,作为被绑定函数的参数。

    var add = function (x, y) {
      return x * this.m + y * this.n;
    }
    
    var obj = {
      m: 2,
      n: 2
    };
    
    var newAdd = add.bind(obj, 5);
    newAdd(5) // 20
    // bind()方法除了绑定this对象,还将add()函数的第一个参数x绑定成5,然后返回一个新函数newAdd(),这个函数只要再接受一个参数y就能运行了。
    

    因为每调用一次bind,就会生成一个新函数。所以下面这种写法不行。

    element.addEventListener('click', o.m.bind(o));
    element.removeEventListener('click', o.m.bind(o));
    
    // Should be
    var listener = o.m.bind(o);
    element.addEventListener('click', listener);
    element.removeEventListener('click', listener);
    

    多次bind是无效的,只会保留第一次bind的结果。因为bind() 的实现相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次及以后的 bind 是无法生效的。例子如下:

    const people1 = {
        age: 18
    }
    
    const people2 = {
        age: 19
    }
    
    const people3 = {
        age: 20
    }
    
    const girl = {
        getAge: function() {
            return this.age
        }
    }
    
    const callFn = girl.getAge.bind(people1)
    const callFn1 = girl.getAge.bind(people1).bind(people2)
    const callFn2 = girl.getAge.bind(people1).bind(people2).bind(people3)
    
    console.log(callFn(), callFn1(), callFn2())
    // 18 18 18
    

    实现

    Function.prototype.imitateBind = function (context) {
        // 获取绑定时的传参
    	let args = [...arguments].slice(1),
            // 定义中转构造函数,用于通过原型连接绑定后的函数和调用bind的函数
            F = function () {},
            // 记录调用函数,生成闭包,用于返回函数被调用时执行
            self = this,
            // 定义返回(绑定)函数
            bound = function () {
                // 合并参数,绑定时和调用时分别传入的
                let finalArgs = [...args, ...arguments]
                
                // 改变作用域,注:aplly/call是立即执行函数,即绑定会直接调用
                // 这里之所以要使用instanceof做判断,是要区分是不是new xxx()调用的bind方法
                return self.call((this instanceof F ? this : context), ...finalArgs)
            }
        
        // 将调用函数的原型赋值到中转函数的原型上
        F.prototype = self.prototype
        // 通过原型的方式继承调用函数的原型
        bound.prototype = new F()
        
        return bound
    }
    

    箭头函数

    箭头函数没有this。如果访问this,它会从外部获取。(在作用域中逐级寻找)
    箭头函数的this无法通过bind,call,apply来直接修改(可以间接修改)。
    改变作用域中this的指向可以改变箭头函数的this

    eg. function closure(){()=>{//code }},在此例中,我们通过改变封包环境closure.bind(another)(),来改变箭头函数this的指向。
    

    例1:

    let user = {
      firstName: "Ilya",
      sayHi() {
        let arrow = () => alert(this.firstName);
        arrow();
      }
    };
    
    user.sayHi(); // Ilya
    

    例2:

    /**
     * 非严格模式
     */
    
    var name = 'window'
    
    var person1 = {
      name: 'person1',
      show1: function () {
        console.log(this.name)
      },
      show2: () => console.log(this.name),
      show3: function () {
        return function () {
          console.log(this.name)
        }
      },
      show4: function () {
        return () => console.log(this.name)
      }
    }
    var person2 = { name: 'person2' }
    
    person1.show1() // person1,隐式绑定,this指向调用者 person1 
    person1.show1.call(person2) // person2,显式绑定,this指向 person2
    
    person1.show2() // window,箭头函数绑定,this指向外层作用域,即全局作用域
    person1.show2.call(person2) // window,箭头函数绑定,this指向外层作用域,即全局作用域
    
    person1.show3()() // window,默认绑定,这是一个高阶函数,调用者是window
    				  // 类似于`var func = person1.show3()` 执行`func()`
    person1.show3().call(person2) // person2,显式绑定,this指向 person2
    person1.show3.call(person2)() // window,默认绑定,调用者是window
    
    person1.show4()() // person1,箭头函数绑定,this指向外层作用域,即person1函数作用域
    person1.show4().call(person2) // person1,箭头函数绑定,
    							  // this指向外层作用域,即person1函数作用域
    person1.show4.call(person2)() // person2
    

    最后一个person1.show4.call(person2)()有点复杂,我们来一层一层的剥开。

    1、首先是var func1 = person1.show4.call(person2),这是显式绑定,调用者是person2,show4函数指向的是person2。
    2、然后是func1(),箭头函数绑定,this指向外层作用域,即person2函数作用域
    首先要说明的是,箭头函数绑定中,this指向外层作用域,并不一定是第一层,也不一定是第二层。

    因为没有自身的this,所以只能根据作用域链往上层查找,直到找到一个绑定了this的函数作用域,并指向调用该普通函数的对象。

  • 相关阅读:
    python Flask JQuery使用说明
    sqlserve 数据类型具体解释
    赵雅智_ListView_SimpleAdapter
    HDU 1018 Big Number (log函数求数的位数)
    cocos2d函数
    BZOJ 3514 Codechef MARCH14 GERALD07加强版 Link-Cut-Tree+划分树
    QQ好友列表数据模型封装
    【Codeforces】512C Fox and Dinner
    spring中操作mysql数据库
    【读书笔记】iOS-Xcode-模拟器操作的一些快捷键
  • 原文地址:https://www.cnblogs.com/Nullc/p/12876893.html
Copyright © 2011-2022 走看看