绑定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()
的参数应当是一个对象。若留空
、null
、undefined
则是全局对象。
若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的主要内容,如果未能解决你的问题,请参考以下文章
绑定this:call()apply()bind()和它们的内部实现原理 + 箭头函数中的this