函数原型方法(call 、apply、bind)
1.call
call方法使用一个指定的this值和单独给出的一个或者多个参数去调用一个函数。
简单来说,call方法的作用就是修改函数的this指向,将this指向call方法的第一个参数。假如我们有两个互不相干的对象。
let person = {
name: 'Jack',
sayName(){
console.log(this.name)
}
}
let person2 = {
name:'Tom'
}
person.sayName.call(person2)//Tom
应用call我们将sayName方法的this修改为了person2对象。看起来的效果就好像是我们在person2对象中又定义了一个和person中一模一样的sayName方法(这句话非常重要,他是我们手写实现call方法的关键和思路),实际上我们只是使用了call而已。
接下来我们手动实现一下call方法。
call方法被定义在函数对象的原型对象身上,参数就是指定的this和函数执行时的参数。这里直接给出代码,代码中对每一行都有详细的解释。
Function.prototype.call2 = function (context) {
//context不传或者传null的时候,默认指向window
var context = context || window;
//最关键的一步,将调用call方法的函数作为context的一个属性。
//以上面的person为例,这一步就是在person2中定义了一个和person一模一样的sayName方法。
context.fn = this;
//arguments为类数组对象,将保存在其中的参数提取出来形成正真正的数组
let args = [];
//从第二个参数开始,因为第一个参数是指定的this对象。
for(let i = 1, len = arguments.length; i < len; i++) {
//call方法接收的函数参数是单独的参数列表而非数组
args.push('arguments[' + i + ']');
}
//eval方法能够将字符串作为JavaScript代码执行。
//这也是上面为什么构建参数数组的时候使用args.push('arguments[' + i + ']')的原因
let result = eval('context.fn(' + args +')');
//context对象中是不应该有fn这个属性的,应记得删除
delete context.fn
return result;
}
该手写实现call的方法实际上采用的技巧就是,为参数对象添加新方法最后删除。例如
person.sayName.call2(person2)
实际上是在将person中的sayName方法作为fn属性放到person2中,然后调用这个属性对应的方法,最后删除这个属性。个人觉得call的用法和思路其实并不复杂,但是我起初理解起来还是花了很久的时间,如果你在学习的时候也觉得比较困难,个人建议仔细想想
context.fn = this
这行代码的作用,它是理解手写call思路的关键。
2.apply
apply的call的作用一模一样,和call唯一的区别就是apply接收函数的参数的时候,必须是以数组的形式。因此参考call的实现思路,只需要在获取参数的时候稍作修改即可。
Function.prototype.apply2 = function (context, arr) {
var context = context || window
context.fn = this
if (!arr){//没有传入参数的时候直接执行函数即可
context.fn()
}
var args = []
for (let i = 0; i < arr.length; i++) {
args.push('arr[' + i + ']')
}
let result = eval('context.fn(' + args + ')')
delete context.fn
return result
}
apply和call的手写思路只在处理参数的时候有区别,同时这也是这两者在使用时的区别。
3.bind
MDN对于bind的解释:bind()
方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
简单来说,bind返回一个函数的复制,该复制函数拥有指定的this值,也即bind方法的第一个参数。还是直接给出代码,代码中有详细的注释。
Function.prototype.bind2 = function (context) {
let that = this //this是调用bind方法的函数
//切割参数,去掉参数中的第一个参数(this)
let arr = Array.prototype.slice.call(arguments, 1)
//bind方法返回一个函数的复制
return function () {
//将参数类数组变为数组
let arr2 = Array.prototype.slice.call(arguments)
//将传递给bind方法和参数和直接传递给复制函数的参数组合起来,最后一起调用
let arrSum = arr.concat(arr2)
return that.apply(context, arrSum)//将函数的this修改为bind的参数
}
}
上面这个版本的手写代码是简易版的bind,支持我们上面所说的返回指定this的函数复制以及接收参数的功能。但是bind还有一个比较重要的功能,那就是bind返回的函数可能作为构造函数也就是被new关键字操作。
涉及到new的操作最关键的就是修改构造函数的this指向以及原型链的链接。这里需要进行一个判断,即判断是否使用了new操作,方法是查看返回函数的this,如果调用了new,则this指向一个由返回函数创建的实例对象,使用instanceof即可检测。如果this不是指向实例对象说明未调用new,按照简易版的规则执行即可。修改后可以得到支持new操作的代码:
Function.prototype.bind2 = function (context) {
let that = this //this是调用bind方法的函数
//切割参数,去掉参数中的第一个参数(this)
let arr = Array.prototype.slice.call(arguments, 1)
//bind方法返回一个函数的复制
newf = function () {
let arr2 = Array.prototype.slice.call(arguments)
let arrSum = arr.concat(arr2)
//调用new会使if中的条件成立,因为this会指向实例对象,此时将调用bind的函数的this指向实例对象
if (this instanceof newf) {
return that.apply(this, arrSum)
}else {
//未创建实例则按照原来的方式执行
return that.apply(context, arrSum)
}
}
return newf
}
现在的bind2支持构造函数形式的调用了,但是还有最后一个问题就是原型链的链接问题。被new创建的对象应该能够访问到构造函数的原型对象上的所有属性和方法,但是这个版本显然是不行的,因为我们对原型链没有进行任何操作(如果不放心可以自己试一试哦)。其实这一步操作也很简单,直接将newf的prototype指向调用bind的函数的prototype即可,但是我们知道直接修改原型对象会有许多弊端,因此这里还是利用一个空函数作为跳板来实现原型链的链接。
终于,历经千辛万苦完整版支持this指向修改,返回函数复制,并且支持以构造函数形式调用的bind方法完成了!(此处应有掌声)
Function.prototype.bind2 = function (context) {
let that = this //this是调用bind方法的函数
//切割参数,去掉参数中的第一个参数(this)
let arr = Array.prototype.slice.call(arguments, 1)
//利用o这个空函数作为跳板实现原型链的链接
o = function () {}
o.prototype = that.prototype
newf = function () {
let arr2 = Array.prototype.slice.call(arguments)
let arrSum = arr.concat(arr2)
//调用new会使if中的条件成立,因为this会指向实例对象,此时将调用bind的函数的this指向实例对象
if (this instanceof o) {
return that.apply(this, arrSum)
}else {
//未创建实例则按照原来的方式执行
return that.apply(context, arrSum)
}
}
newf.prototype = new o()
return newf
}
PS:如果看了上面这些还是不理解的话(因为我的水平实在是太菜了),强烈推荐b站蛋老师 的视频跟着一步一步写一下,遇到不明白的地方一定要暂停多想想,能有很多收获!