call 理解了,apply和bind就都迎刃而解了,他们都是大同小异。在此对call
和apply
不做过多的定义性解释,先来看下调用了call后谁是那个被执行的方法,直接代码示例:
function fn1 () {
console.log(1);
};
function fn2 () {
console.log(2);
};
fn1.call(fn2);//1
执行fn1.call(fn2)
;控制台会打印1,这里可以说明fn1调用call后被执行的方法还是fn1。一定要弄清楚谁是这个被执行的方法,就是调用call的函数,而fn2现在的身份是替代window作为fn1的直接调用者,这是理解call和apply的关键,也可以运行下fn2.call(fn1)
;
再来个代码示例:
var obj1 = {
num : 20,
fn : function(n){
console.log(this.num+n);
}
};
var obj2 = {
num : 15,
fn : function(n){
console.log(this.num-n);
}
};
obj1.fn.call(obj2,10);//25
执行obj1.fn.call(obj2,10)
;控制台会打印25,call在此的作用其实很简单,就是在执行obj1.fn
的时候把这个fn的直接调用者由obj1变为obj2,obj1.fn(n)
内部的this经过call的作用指向了obj2,所以this.num
就是obj2.num
,10作为执行obj1.fn
时传入的参数,obj2.num
是15,因此打印出的值是15+10=25。
所以我们可以这样理解:call的作用是改变了那个被执行的方法(也就是调用call的那个方法)的直接调用者!而这个被执行的方法内部的this也会重新指向那个新的调用者,就是call方法所接收的第一个obj参数。还有两个特殊情况就是当这个obj参数为null或者undefined的时候,this会指向window。
Function.prototype.myCall = function (context) {
// 先判断调用myCall是不是一个函数
// 这里的this就是调用myCall的
if (typeof this !== 'function') {
throw new TypeError("Not a Function")
}
// 不传参数默认为window
context = context || window
// 保存this
context.fn = this
// 保存参数
let args = Array.from(arguments).slice(1)
//Array.from 把伪数组对象转为数组,然后调用 slice 方法,去掉第一个参数
// 调用函数
let result = context.fn(...args)
delete context.fn
return result
}
Function.prototype.myApply = function (context) {
// 判断this是不是函数
if (typeof this !== "function") {
throw new TypeError("Not a Function")
}
let result
// 默认是window
context = context || window
// 保存this
context.fn = this
// 是否传参
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
在实现手写bind方法的过程中,看了许多篇文章,答案给的都很统一,准确,但是不知其所以然,所以我们就好好剖析一下bind方法的实现过程。
我们先看一下bind函数做了什么:
bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
读到这里我们就发现,他和 apply , call 是不是很像,所以这里指定 this 功能,就可以借助 apply 去实现:
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
const self = this;
// 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
// 这里产生了闭包
const args = Array.from(arguments).slice(1)
return function () {
// 这个时候的 arguments 是指 myBind 返回的函数传入的参数
const bindArgs = Array.from(arguments)
// 合并
return self.apply(context, args.concat(bindArgs));
};
};
大家对这段代码应该都能看懂,实现原理和手写 call , apply 都很像,因为 bind 可以通过返回的函数传参,所以在 return 里面获取的 bindArgs 就是这个意思,然后最后通过 concat 把原来的参数和后来传进来的参数进行数组合并。
我们来看一下结果:
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
const test = man.myBind(person)
test(18)//zyj 18
现在重点来了,bind 区别于 call 和 apply 的地方在于它可以返回一个函数,然后把这个函数当作构造函数通过 new 操作符来创建对象。
我们来试一下:
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
const test = man.myBind(person)
const newTest = new test(18) // zyj 18
这是用的我们上面写的 myBind 函数是这个结果,那原生 bind 呢?
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
const test = man.bind(person)
const newTest = new test(18) // undefined 18
由上述代码可见,使用原生 bind 生成绑定函数后,通过 new 操作符调用该函数时,this.name 是一个 undefined,这其实很好理解,因为我们 new 了一个新的实例,那么构造函数里的 this 肯定指向的就是实例,而我们的代码逻辑中指向的始终都是 context ,也就是传进去的参数。
所以现在我们要加个判断逻辑:
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
const self = this;
// 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
// 这里产生了闭包
const args = Array.from(arguments).slice(1)
const theBind = function () {
const bindArgs = Array.from(arguments);
// 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
// 当作为普通函数时,将绑定函数的 this 指向 context 即可
// this instanceof fBound 的 this 就是绑定函数的调用者
return self.apply(
this instanceof theBind ? this : context,
args.concat(bindArgs)
);
};
return theBind;
};
现在这个效果我们也实现了,那我们的 myBind 函数就和其他的原生 bind 一样了吗?来看下面的代码:
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
man.prototype.sayHi = function() {
console.log('hello')
}
const test = man.myBind(person)
const newTest = new test(18) // undefined 18
newTest.sayHi()
如果 newTest 是我们 new 出来的 man 实例,那根据原型链的知识,定义在man的原型对象上的方法肯定会被继承下来,所以我们通过 newTest.sayHi 调用能正常输出 hello 么?
该版代码的改进思路在于,将返回的绑定函数的原型对象的 proto 属性,修改为原函数的原型对象。便可满足原有的继承关系。
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
const self = this;
// 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
// 这里产生了闭包
const args = Array.from(arguments).slice(1);
const theBind = function () {
const bindArgs = Array.from(arguments);
// 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
// 当作为普通函数时,将绑定函数的 this 指向 context 即可
// this instanceof fBound 的 this 就是绑定函数的调用者
return self.apply(
this instanceof theBind ? this : context,
args.concat(bindArgs)
);
};
theBind.prototype = Object.create(self.prototype)
return theBind;
};