绑定this:call()apply()bind()和它们的内部实现原理 + 箭头函数中的this

Posted NullCream

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了绑定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的函数作用域,并指向调用该普通函数的对象。










以上是关于绑定this:call()apply()bind()和它们的内部实现原理 + 箭头函数中的this的主要内容,如果未能解决你的问题,请参考以下文章

JS-this

轻松搞定this绑定方法 call apply bind

call apply bind的区别

绑定this:call()apply()bind()和它们的内部实现原理 + 箭头函数中的this

绑定this:call()apply()bind()和它们的内部实现原理 + 箭头函数中的this

函数方法:call() apply() bind() 自定义绑定对象