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

风流不在谈锋胜,袖手无言味最长。这篇文章主要讲述绑定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‘) // trueObject.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); // 4vegetables; // [‘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 exceededconcatOfArray(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 // 1var 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指向 person2person1.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,默认绑定,调用者是windowperson1.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(call()apply()bind()和它们的内部实现原理 + 箭头函数中的this)】因为没有自身的this,所以只能根据作用域链往上层查找,直到找到一个绑定了this的函数作用域,并指向调用该普通函数的对象。










    推荐阅读