apply、call和 bind 方法详解
1. 三者的简单比较
先看一个简单地例子:
let person = {
name: '张三',
age: '20',
say(arg1, arg2) {
console.log(`我叫${this.name},年龄${this.age},喜欢${arg1}、${arg2}`);
}
}
person.say('唱歌', '跳舞');
let ada = {
name: '阿大',
age: '30',
}
person.say.apply(ada, ['书法', '绘画']);
person.say.call(ada, '书法', '绘画');
person.say.bind(ada)('书法', '绘画');
person.say.bind(ada, '书法')('绘画');
// 我叫张三,年龄20,喜欢唱歌,跳舞
// 我叫阿大,年龄30,喜欢书法,绘画
// 我叫阿大,年龄30,喜欢书法,绘画
// 我叫阿大,年龄30,喜欢书法,绘画
// 我叫阿大,年龄30,喜欢书法,绘画
由上例可得出以下结论:
- apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
- apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
- apply 、 call 、bind 三者都可以利用后续参数传参:
- apply 接受一个数组(或者类数组对象)作为参数输入
- call 接受一系列的单独变量
- bind 情况比较复杂,既可以在 this 后面指定初始参数,又可以通过 bind 返回的函数传递参数,但都只能传递一系列的单独变量
- bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。
2. apply、call
apply() 方法的作用和 call() 方法类似,可以放在一起进行比较理解
2.1 语法:
fun.apply(thisArg, [argsArray]) fun.call(thisArg, arg1, arg2, ...)
如果这个函数处于非严格模式下,则指定为 null
或 undefined
时,会自动替换为指向全局对象(严格模式下则不会替换),原始值会被包装。
返回值:使用提供的 this
值和参数,调用原函数的返回值。若该方法没有返回值,则返回 undefined
。
2.2 示例
// 合并数组
var array1 = [12 , "foo" , {name:"Joe"} , -2458];
var array2 = ["Doe" , 555 , 100];
Array.prototype.push.apply(array1, array2);
// array1 值为 [12 , "foo" , {name:"Joe"} , -2458 , "Doe" , 555 , 100]
// 最大值
var numbers = [5, 458 , 120 , -215 ];
var maxInNumbers = Math.max.apply(Math, numbers), //458
maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458
// 调用父构造器
function Product(name, price) {
this.name = name;
this.price = price;
}
function Food(name, price) {
Product.call(this, name, price);
this.category = 'food';
}
function Toy(name, price) {
Product.apply(this, [name, price]);
this.category = 'toy';
}
2.3 手写 apply、call
手写 call 函数:
Function.prototype.myCall = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window;
// 非常关键:改变 this 的作用域
context.fn = this;
const args = [...arguments].slice(1);
const result = context.fn(...args);
// 必须删除,会给 context 增加方法 fn
delete context.fn;
return result;
}
手写 apply 函数:
Function.prototype.myApply = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
context = context || window;
context.fn = this;
let result;
// 处理参数和 call 有区别
if (arguments[1]) {
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
delete context.fn;
return result;
}
3. bind
3.1 语法:
function.bind(thisArg[,arg1[,arg2[, ...]]])
bind()
方法会创建一个新函数,称为绑定函数(exotic function object,又称怪异函数对象)。
调用绑定函数时:
- this:
thisArg
(bind的第一个参数)作为this
参数传递给目标函数的值。如果使用new
运算符构造绑定函数,则忽略该值。当使用bind
在setTimeout
中创建一个函数(作为回调提供)时,作为thisArg
传递的任何原始值都将转换为object
。 - 参数:传入
bind()
方法的参数列表 arg1, arg2, … 加上绑定函数运行时本身的参数,按照顺序作为原函数的参数
function fn(a, b, c) {
console.log(a, b, c);
}
var fn1 = fn.bind(null, 'Dot');
fn('A', 'B', 'C'); // A B C
fn1('A', 'B', 'C'); // Dot A B
fn1('B', 'C'); // Dot B C
fn.call(null, 'Dot'); // Dot undefined undefined
3.2 使用
3.2.1 替代保存this
通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它。 像这样:
let obj = {
a: 1,
eventBind: function() {
let that = this;
setTimeout(function() {
console.log(that.a);
}, 500);
}
}
obj.eventBind();
使用 bind() 可以更加优雅的解决这个问题:
let obj = {
a: 1,
eventBind: function() {
// let that = this;
setTimeout(function() {
console.log(this.a);
}.bind(this), 500);
}
}
obj.eventBind();
3.2.2 偏函数
bind()
的另一个最简单的用法是使一个函数拥有预设的初始参数,这也是一种函数柯里化。
function addArguments(arg1, arg2) {
return arg1 + arg2
}
// 创建一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37);
var result1 = addThirtySeven(5);
// 37 + 5 = 42
var result2 = addThirtySeven(6, 10);
// 37 + 6 = 42 ,第二个参数被忽略
3.2.3 作为构造函数使用的绑定函数
警告 :这部分演示了 JavaScript 的能力并且记录了
bind()
的超前用法。以下展示的方法并不是最佳的解决方案,且可能不应该用在任何生产环境中。
前文说过,如果使用new
运算符构造绑定函数,原来提供的 this
就会被忽略,不过提供的参数列表仍然会插入到构造函数调用时的参数列表之前
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return this.x + ',' + this.y;
};
var p = new Point(1, 2);
p.toString(); // '1,2'
var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0 /*x*/ );
// 以下这行代码在 polyfill 不支持,
// 在原生的bind方法运行没问题:
//(译注:polyfill的bind方法如果加上把bind的第一个参数,即新绑定的this执行Object()来包装为对象,Object(null)则是{},那么也可以支持)
// var YAxisPoint = Point.bind(null, 0 /*x*/ );
// this 被忽略
console.log(emptyObj); //{}
var axisPoint = new YAxisPoint(5);
console.log(axisPoint.toString()); // '0,5'
console.log(axisPoint instanceof Point); // true
console.log(axisPoint instanceof YAxisPoint); // true
console.log(new Point(17, 42) instanceof YAxisPoint); // true
// this 被忽略
console.log(emptyObj); //{}
// 仍然能作为一个普通函数来调用(通常来说应避免这种情况)
YAxisPoint(13);
console.log(emptyObj); // { x: 0, y: 13 }
3.4 bind 与 apply、call 连用
简单来说,bind()
方法的主要作用就是将函数绑定至某个对象。
var slice = Array.prototype.slice;
var arr = [1, 2, 'name', 'age'];
console.log(slice.call(arr, 1)); // [ 2, 'name', 'age' ]
console.log(slice.apply(arr, [1])); // [ 2, 'name', 'age' ]
// 用 bind()可以使这个过程变得简单
var slice1 = Function.prototype.call.bind(slice);
var slice2 = Function.prototype.apply.bind(slice);
console.log(slice1(arr, 1)); // [ 2, 'name', 'age' ]
console.log(slice2(arr, [1])); // [ 2, 'name', 'age' ]
3.4 手写 bind
先来简单实现 bind()
方法:
Function.prototype.myBind = function(context){
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var args = Array.prototype.slice.call(arguments, 1),
fToBind = this;
return function(){
return self.apply(context, args.concat(Array.prototype.slice.call(arguments)));
};
};
bind 还可以作为构造函数使用,这种情况就比较复杂,先以简单的 myBind
进行测试:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
return this.x + ',' + this.y;
};
var p = new Point(1, 2);
p.toString(); // '1,2'
var emptyObj = {};
var YAxisPoint = Point.myBind(emptyObj, 0 /*x*/ );
var axisPoint = new YAxisPoint(5);
console.log(emptyObj); // { x: 0, y: 5 }
console.log(axisPoint); // {}
console.log(axisPoint instanceof Point); // false
console.log(axisPoint instanceof YAxisPoint); // true
从代码中,可以发现两个问题:
new YAxisPoint(5)
的this
指向有问题,此时this
指向emptyObj
,而不是我们预想的axisPoint
;- 原型链有问题,
axisPoint
应是Point
的实例。
先解决问题1,代码修改如下:
Function.prototype.myBind = function(context) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var args = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fBound = function() {
// this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
return fToBind.apply(this instanceof fBound ?
this :
context,
// 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
args.concat(Array.prototype.slice.call(arguments)));
};
return fBound;
};
解释一下为什么要进行 this instanceof fBound
判断:
-
明白 new 运算符的原理,new 关键字会进行如下的操作:
-
创建一个空的简单JavaScript对象(即
{}
); -
链接该对象(即设置该对象的构造函数)到另一个对象 ;
以
var a = new b()
为例,此时:a._proto = b.prototype;
-
将步骤1新创建的对象作为
this
的上下文 ; -
如果该函数没有返回对象,则返回
this
。
-
-
instanceof
运算符用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置
再进行测试:
// ...
console.log(emptyObj); // {}
console.log(axisPoint); // fBound { x: 0, y: 5 }
console.log(axisPoint instanceof Point); // false
console.log(axisPoint instanceof YAxisPoint); // true
可以看出,此时 this
指向 axisPoint
,但是原型链依然有问题:
《JavaScript Web Application》一书中对
bind()
的实现:通过设置一个中转构造函数F,使绑定后的函数与调用bind()的函数处于同一原型链上,用new操作符调用绑定后的函数,返回的对象也能正常使用instanceof
最终代码如下:
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function() {},
fBound = function() {
// this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
return fToBind.apply(this instanceof fBound
? this
: oThis,
// 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 维护原型关系
if (this.prototype) {
// 当执行Function.prototype.bind()时, this为Function.prototype
// this.prototype(即Function.prototype.prototype)为undefined,所以需进行判断
fNOP.prototype = this.prototype;
}
// 下行的代码使fBound.prototype是fNOP的实例,因此
// 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
fBound.prototype = new fNOP();
return fBound;
};
再次测试:
// ...
console.log(emptyObj); // {}
console.log(axisPoint); // '0,5'
console.log(axisPoint instanceof Point); // Point { x: 0, y: 5 }
console.log(axisPoint instanceof YAxisPoint); // true
PS:原型链相关内容参考我的另一篇文章 JS 对象详解(原型链、继承)。
参考文献