call、apply、bind的使用、作用、区别、原理、实现
众所周知这仨老哥的作用都是改变作用域,但是很多博客只是简单介绍了作用和区别,关于内部具体什么逻辑都是一知半解,这样不知其理,使用起来很容易出现一些意想不到的bug,浪费很多时间成本,这里记录下来 也方便自己回顾查找。
一、使用方法
call
fun.call(thisArg,arg1,arg2,…) //thisArg为想要指向的对象,arg1,arg2等为原函数参数
apply
fun.apply(thisArg,[argsArray]) //thisArg为想要指向的对象, argsArray为原函数参数数组
bind
fun.bind(thisArg,arg1,arg2,…) //thisArg为想要指向的对象,arg1,arg2等为原函数参数
二、作用及使用场景
1.主要用于在调用函数时改变函数内部的this指向。
function Product(name, price) {
this.name = name
this.price = price
}
function Food(name, price) {
// 调用Product的call方法并将当前作用域对象this传入替换掉Product内部的this作用域对象
console.log(this) //打印的是Food对象,也就是new构造的一个Food对象
// Product.call(this, name, price)
Product.apply(this,[name,price])
this.category = 'food'
}
let tempFood = new Food('cheese', 5)
console.log('输出:' + tempFood.name, tempFood.price, tempFood.category)
// 输出:cheese 5 food
2.bind方法可以用于实现柯里化 1
3.可以快捷获取数组最大最小值
Math.max.apply(Math, [1,3,5,6,4])
4.可以帮助对象实现他不具备的属性或方法,如arguments就是一个伪数组,他自己并不具备数组的方法,所以需要借助Array原型来实现数组函数。例如:Array.prototype.shift.call(arguments)
注意:有很多博客将call、apply的作用解释为方法共享,窃以为共享这个词并不准确,共享指的是双方都有这个能力,但是实际上改变的指向者并不会实际拥有这个函数。我认为“借用”这个词更符合,我现在借给你用一次我的函数,用完你要马上还给我。只有bind类似于共享的概念,但是bind是返回了一个改变了指向的全新函数
let arrObj = {
'a': 'test',
'b': 111
}
arrObj.push('666') //报错 对象没有数组的push方法
Array.prototype.push.call(arrObj, '666') //Array的原型链中把push方法借给arrObj使用一次
console.log(arrObj) //{0: '666', a: 'test', b: 111, length: 1}
所以通过这个例子,我们得知要实现这个功能,其实不只是这几个老哥可以干,还有继承途径可以实现:
Object.setPrototypeOf(arrObj, Array.prototype); //将Array的原型继承给arrObj
arrObj.push('666')
arrObj.__proto__ = Array.prototype;
arrObj.push('666')
三、区别
1.call和apply是替换作用域、传递参数后,立即执行这个函数;bind不会立即执行,而是返回一个改变了this指向的待使用的新函数,想立即使用就在后面加上()调用就好。
2.call和apply唯一的区别就是参数的传递方式不同,call方法第二个参数开始是原函数的参数列表,而apply方法第二个参数就是原函数的参数列表,是一个数组
四、原理
4.1 作用域问题
要知道原理,首先要了解js的作用域问题,我们先来看一下:
- 在浏览器里,在全局范围内this 指向window对象
- 在函数中,this永远指向最后调用他的那个对象,如果方法赋值给了另一个对象,就会改变this的指向
- 构造函数中,this指向new出来的那个新的对象
- call、apply、bind中的this被强绑定在指定的那个对象上;
- 箭头函数中this比较特殊,箭头函数this为父作用域的this,不是调用时的this.要知道前四种方式,都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,声明的时候就确定了下来;
4.2 实现原理
所以根据this指向的原理,实现call、apply、bind的思路,就是把默认的window上下文改为指定的对象,然后给这个对象赋值一个函数,执行这个函数后删除这个函数。
实现
call函数实现
Function.prototype.myCall = function (context) {
// 准备1:判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 准备2:判断 context 是否传入,如果未传入则设置为 window
var context = context || window
// 给 context 添加一个属性
// getValue.call(a, 'pp', '24') => a.fn = getValue
// 1.将调用函数设为对象的方法
context.fn = this
// 将 context 后面的参数取出来
var args = [...arguments].slice(1)
// getValue.call(a, 'pp', '24') => a.fn('pp', '24')
// 调用函数
var result = context.fn(...args)
// 删除 fn
delete context.fn
return result
}
apply函数实现
apply相比于call 实现上的差别只是第二个参数是一个数组 所以差别不大
Function.prototype.myApply = function(context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 判断 context 是否存在,如果未传入则为 window
context = context || window;
let result = null;
// 1.将函数设为对象的方法
context.fn = this;
// 2.调用方法
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
// 3.将属性删除
delete context.fn;
return result;
};
bind函数实现
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 因为返回了一个函数,我们可以 new F(),所以需要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
1.柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
2.这是一种对函数参数的’缓存’
3.让函数变的更灵活,让函数的粒度更小
4.可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能 ↩︎