call、apply和bind都是对this值得改变,那三者有什么不同呢?本篇通过模拟三者代码的形式来讲解。
1、call
W3school中对call()方法的定义是:它可以用来调用所有者对象作为参数的方法。通过 call(),能够使用属于另一个对象的方法。 我们举个例子:
var foo = {
name: 'Jack'
}
function bar() {
console.log(this.name);
}
bar.call(foo);
由上可以看出call()函数主要做了两件事,第一个是改变this的指向,将bar中的this指向了foo,第二个是执行了bar方法。 接下来我们来模拟call方法: 第一步:实现简单的call方法: 上面的例子可以改造为:
var foo = {
name: 'Jack',
bar: function() {
console.log(this.name);
}
}
foo.bar();
输出的结果和上述的一模一样,唯一的不同就是foo中多了一个bar的属性,那我们执行完后删除这个属性就可以了。
Function.prototype.myCall = function(Context) {
Context.fn = this; // 此时的this指向的是Function,即调用者
Context.fn();
delete Context.fn;
}
var foo = {
name: 'Jack'
}
function bar() {
console.log(this.name);
}
bar.myCall(foo);
结果和call一致。 第二步:因为call可以带参数,所以我们接下来实现这个 看一下原版的:
var foo = {
name: 'Jack'
}
function bar(age, goods) {
console.log(this.name);
console.log(age); // 10
console.log(goods); // 自行车
}
bar.call(foo, 10, '自行车');
因为参数是不固定的,所以可以想到arguments。怎么将arguments传入对象中,这就是个难题,高兴的是ES6给了我们方法:解构赋值
Function.prototype.myCall = function(Context) {
Context.fn = this; // 此时的this指向的是Function,即调用者
var args = [...arguments].slice(1);
Context.fn(...args);
delete Context.fn;
}
var foo = {
name: 'Jack'
}
function bar(age, goods) {
console.log(this.name);
console.log(age); // 10
console.log(goods); // 自行车
}
bar.myCall(foo, 10, '自行车');
是不是感觉特别简单,但是call是ES3的方法,解构赋值是ES6的方法,感觉有点欺负它,那我们就需要重新想一个。这个方法也是查看了资料后才找到的,自己能力还是有限啊! 可以运用eval(),eval() 函数可计算某个字符串,并执行其中的的 JavaScript 代码。
Function.prototype.myCall = function(Context) {
Context.fn = this; // 此时的this指向的是Function,即调用者
var args = [];
for (var i = 1; i < arguments.length; i++>) {
args.push('arguments[+' i '+]');
}
eval('Context.fn(+' args '+)');
delete Context.fn;
}
var foo = {
name: 'Jack'
}
function bar(age, goods) {
console.log(this.name);
console.log(age); // 10
console.log(goods); // 自行车
}
bar.myCall(foo, 10, '自行车');
感觉很完美了,但是依旧有两小点需要注意:
- 当this为null时
- 当函数有返回值时 我们先来看第一点:
var name = 'Jack';
function bar() {
console.log(this.name); // Jack
}
bar.call(null)
当this为null时,默认走向window 第二点:
var foo = {
name: 'Jack'
}
function bar() {
return {
age: 1,
goods: '自行车'
}
}
bar.call(foo); // {age: 1, goods: "自行车"}
当函数有返回值时,结果就是这个返回值。 所以我们对自己模拟的方法进行最后的优化:
Function.prototype.myCall = function(Context) {
var Context = Context ? Context : window;
Context.fn = this; // 此时的this指向的是Function,即调用者
var args = [];
for (var i = 1; i < arguments.length; i++) {
args.push('arguments['+ i +']');
}
var result = eval('Context.fn('+ args +')');
delete Context.fn;
return result;
}
2、apply
call() 和 apply() 之间的区别,不同之处是: call() 方法分别接受参数。 apply() 方法接受数组形式的参数。 所以call和apply只是接受参数的不同,思路还是和call一样,这次就不重复了直接贴代码: 运用解构赋值:
Function.prototype.myApply = function(Context) {
var Context = Context ? Context : window;
Context.fn = this; // 此时的this指向的是Function,即调用者
var args = arguments[1]?arguments[1]:[];
var result = Context.fn(...args);
delete Context.fn;
return result;
}
运用eval:
Function.prototype.myApply = function(Context) {
var Context = Context ? Context : window;
Context.fn = this; // 此时的this指向的是Function,即调用者
var args = [];
var arguments = arguments[1]?arguments[1]:[];
for (var i = 0; i < arguments.length; i++) {
args.push('arguments['+ i +']');
}
var result = eval('Context.fn('+ args +')');
delete Context.fn;
return result;
}
var foo = {
name: 'Jack'
}
function bar() {
console.log(this.name);
}
bar.myApply(foo);
3、bind
bind() 方法会创建一个新函数,当这个新函数被调用时,它的this值是传递给bind()的第一个参数, 它的参数是bind()的其他参数和其原本的参数。 所以bind执行两个步骤:
- 返回一个新函数
- 可以传入参数 我们按照分析call()函数一样的思路来分析bind(),首先看一下原先的bind:
var foo = {
name: 'Jack'
}
function bar() {
console.log(this.name);
}
var bindFoo = bar.bind(foo);
bindFoo(); // Jack
所以可以看出bar.bind(foo);返回一个新的函数,当这个函数执行时,才返回其中的结果,那我们先模拟一下这个:
Function.prototype.myBind = function(Context) {
var self = this;
return function() {
return self.apply(Context);
}
}
var foo = {
name: 'Jack'
}
function bar() {
console.log(this.name);
}
var bindFoo = bar.myBind(foo);
bindFoo(); // Jack
结果一样,说明这一步模拟的没有问题。那我们进行下一步的模拟,因为bind也是可以携带参数的,携带参数的方式和call相同:
var foo = {
name: 'Jack'
}
function bar(age, goods) {
console.log(this.name);
console.log(age);
console.log(goods);
}
var bindFoo = bar.bind(foo, 1, '1');
bindFoo(); // Jack 1 1
模拟板升级:
Function.prototype.myBind = function(Context) {
var self = this;
var args = [].slice.call(arguments, 1);
return function() {
return self.apply(Context, args);
}
}
很完美,但是bind有个特点被忽视了,因为bind返回一个新的函数,那我们将返回的函数里面传参,会有什么效果,我们看一下:
var foo = {
name: 'Jack'
}
function bar(age, goods) {
console.log(this.name);
console.log(age);
console.log(goods);
}
var bindFoo = bar.bind(foo, 1);
bindFoo('1'); // Jack 1 1
可以看出bind可以只传入age,然后再从返回的新函数中传入goods,那我们需要把上述模拟进行升级:
Function.prototype.myBind = function(Context) {
var self = this;
var args = [].slice.call(arguments, 1);
return function() {
var bindArgs = [].slice.call(arguments);
return self.apply(Context, args.concat(bindArgs));
}
}
思路其实很简单,就是将两个arguments进行合并。 本以为这样就结束了,但是MDN提到了bind的另外一个特点:绑定函数自动适应于使用 new 操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的 this 就会被忽略。不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前。什么意思呢?用代码演示一下:
var foo = {
name: 'Jack'
}
function bar(age, goods) {
console.log(this.name);
console.log(age);
console.log(goods);
}
bar.prototype.friends = 'a';
var BindFoo = bar.bind(foo, 1);
var bindFoo = new BindFoo('1'); // undefined 1 1
console.log(bindFoo.friends); // a
this.name竟然输出undefined,那是因为new后,BindFoo中this的指向改变了,指向了bindFoo,而BindFoo实际是bar函数,并且bindFoo没有value属性,所以就输出了undefined,通过instanceof就可以看出来,bindFoo是BindFoo的实例,也是bar的实例。
Function.prototype.myBind = function(Context) {
var self = this;
var args = [].slice.call(arguments, 1);
var cacheFn = function() {};
var bindFun = function() {
var bindArgs = [].slice.call(arguments);
return self.apply(this instanceof cacheFn? this : Context, args.concat(bindArgs));
}
cacheFn.prototype = this.prototype;
bindFun.prototype = new cacheFn();
return bindFun;
}
我们进行分步讲解: 1、为什么要判断this instanceof bindFun? 之前也说到,当将bind返回后函数当做构造函数时,bindFoo即是BindFoo的实例也是bar的实例,BindFoo即为返回来的函数,在我们模拟的代码中就是bindFun这个函数,并且当new之后this指向的是实例,所以用this instanceof bindFun判断的实际就是函数前有没有new这个关键词。 2、为什么要继承this的原型? 这是为了继承bar原型上的属性。 最后一步,健壮模拟的bind,判断传过来的this是否为函数,也是最终版:
Function.prototype.myBind = function(Context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var args = [].slice.call(arguments, 1);
var cacheFn = function() {};
var bindFun = function() {
var bindArgs = [].slice.call(arguments);
return self.apply(this instanceof cacheFn? this : Context, args.concat(bindArgs));
}
cacheFn.prototype = this.prototype;
bindFun.prototype = new cacheFn();
return bindFun;
}