apply,call和bind的基本介绍
语法
fun.call(thisArg, param1, param2, ...)
fun.apply(thisArg, [param1,param2,...])
fun.bind(thisArg, param1, param2, ...)
返回值
all/apply: 返回fun执行的结果
bind: 返回fun的拷贝,并拥有指定的this值和初始参数
参数
thisArg(可选)
- fun的this指向thisArg对象。
- 在非严格模式下,thisArg指定null,undefined,fun中this指向window对象。
- 在严格模式下,fun的this指向undefined
- 值是原始值(比如:数字,字符串,布尔值)的this会指向该原始值的自动包装对象,如:String,Number,Boolean。
param1,param2(可选): 传给fun的参数。
如果param不传或为 null/undefined,则表示不需要传入任何参数.
apply第二个参数为数组,数组内的值为传给fun的参数。
call与apply
在JavaScript中,call和apply都是为了改变函数执行的上下文而存在的,也就是
为了改变函数体内部的this的指向。
JavaScript的一大特点是函数存在「定义是上下文」和「运行是上下文」以及上下文是可以被改变的。
为何要改变执行上下文?举一个生活中的小例子:平时没时间做饭的我,周末想给孩子炖个腌笃鲜尝尝。但是没有适合的锅,而我又不想出去买。所以就问邻居借了一个锅来用,这样既达到了目的,又节省了开支,一举两得。
改变执行上下文也是一样的,A 对象有一个方法,而 B 对象因为某种原因,也需要用到同样的方法,那么这时候我们是单独为 B 对象扩展一个方法呢,还是借用一下 A 对象的方法呢?当然是借用 A 对象的啦,既完成了需求,又减少了内存的占用。
另外,它们的写法也很类似,调用call和apply的对象必须包含一个函数Function。
function fruit() {}
fruit.prototype = {
color: 'red',
say: function() {
console.log("my color is " + this.color);
}
}
var apple = new fruit();
apple.say(); //my color is red
这个时候如果我们又想重新定义一个banana={color: "yellow"};
,我们不想重新定义一个say方法,那么这个时候我们就可以使用call和apply方法:
banner = {
color: "yellow"
};
apple.say.call(banana); //my color is yellow
apple.say.apply(banana); //my color is yellow
所以从上面可以看出,call和apply是为了动态改变this而出现的,当一个对象没有某个方法的时候(本例子中banner对象没有say方法),但是其他对象有某个方法(本例子中apple中有say方法),我们就可以借助call和apply用其他对象的方法来实现。
apply和call的区别
apply和call的作用是完全一样的,只是传递的参数不一样而已。例如有一个函数:
var func = function(arg1,arg2){
};
就可以通过下面的方式调。
func.call(this,arg1,arg2);
func.apply(this,[arg1,arg2]);
其中this是你想指定的上下文,它可以是任何JavaScript对象,call把参数按照顺序传递进去,而apply是把参数放在数组里再传进去。
apply和call该用哪个呢?
- 参数数据、顺序确定就用call,参数数量/殊勋不确定的话就用apply
- 考虑可读性:参数数量不多就用call,参数数量比较多的话,把参数整合成数组,使用apply。
- 参数集合已经是一个数组的情况,用apply,比如上下文的获取数组最大值/最小值。
参数数量/顺序不确定的话就用apply,比如以下示例:
const obj = {
age: 24,
name: 'linKGe'
}
const obj2 = {
age: 27
}
callObj(obj, handle);
callObj(obj2, handle);
//根据某些条件决定要传递参数的数量,以及顺序
function callObj(thisAge,fn) {
let params = [];
if(thisAge.name) {
params.push(thisAge.name);
}
if(thisAge.age) {
params.push(thisAge.age);
}
fn.apply(thisAge,params)// 数量和顺序不确定,不能使用call
}
function handle(...params) {
console.log('params',params);
}
结果:
params [ 'linKGe', 24 ]
params [ 27 ]
call和apply的应用场景
下面会分别列举 call 和 apply 的一些使用场景。声明:例子中没有哪个场景是必须用 call 或者必须用 apply 的,只是个人习惯这么用而已。
1.call的使用场景
1.1 对象的继承
function superClass () {
this.a = 1;
this.print = function () {
console.log(this.a);
}
}
function subClass () {
superClass.call(this);
this.print();
}
subClass(); //1
subClass 通过 call 方法,继承了 superClass 的 print 方法和 a 变量。此外,subClass 还可以扩展自己的其他方法。
1.2 借用方法
如果一个类数组想使用Array原型上的方法,可以使用:
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
这样,domNodes 就可以使用 Array 下的所有方法了。
2.apply应用场景
apply获取数组最大值和最小值
apply直接传递数组做要调用方法的参数,也省一步展开数组,比如使用Math.max、Math.min
来获取数组的最大值和最小值。
const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6
面试题
定义一个 log 方法,让它可以代理 console.log 方法,常见的解决方法是:
function log(msg) {
console.log(msg);
}
log(1); //1
log(1,2); //1
上面方法可以解决最基本的需求,但是当传入参数的个数是不确定的时候,上面的方法就失效了,这个时候就可以考虑使用 apply 或者 call,注意这里传入多少个参数是不确定的,所以使用apply是最好的,方法如下:
function log(){
console.log.apply(console, arguments);
};
log(1); //1
log(1,2); //1 2
接下来的要求是给每一个 log 消息添加一个"(app)"的前辍,比如:
log("hello world"); //(app)hello world
这个时候想到arguments是个伪数组,通过Array.prototype.slice.call
可以转成标准的数组,再使用数组的unshift方法。
function log() {
let arg = Array.prototype.slice.call(arguments);
arg.unshift('(app)');
console.log.apply(console,arg);
}
log('hello world'); //(app) hello world
bind
在学习bind之前我们先来看一下这道题题目:
var altwrite = document.write;
altwrite("hello");
结果报错:Uncaught TypeError: Illegal invocation
,altwrite()函数改变了this的指向global或者window对象,导致执行的时候提示非法调用异常,正确的方案就是使用bind()方法。
altwrite.bind(document)('hello');
当然也可以使用call()方法。
altwrite.call(document,'hello');
绑定函数
bind()最简单的方法就是绑定函数,使这个函数无论怎么调用都有同样的this值,常见的错误就像上面的例子一样,将方法从对象中拿出来,然后调用,并且希望this指向原来的对象。如果不做特殊处理,一般会丢失原来的对象。使用bind()方法能够很好的解决这个问题:
this.num = 9;
var mymodule = {
num: 81,
getNum: function() {
console.log(this.num);
}
};
mymodule.getNum(); //81 //this是mymodule
var getNum = mymodule.getNum;
getNum(); //9 //这时候this是window
var boundGetNum = getNum.bind(mymodule);
boundGetNum(); // 81
bind() 方法与apply和call相似,也是可以改变函数体内this的指向。
bind()方法会创建一个新的函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入bind()方法的第一个参数作为this,传入bind()方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。
直接来看看具体如何使用,在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它。 像这样:
var foo = {
bar : 1,
eventBind: function(){
$('.someClass').on('click',function(event) {
/* Act on the event */
console.log(this.bar); //1
}.bind(this));
}
}
在上述代码里,bind() 创建了一个函数,当这个click事件绑定在被调用的时候,它的 this 关键词会被设置成被传入的值(这里指调用bind()时传入的参数)。因此,这里我们传入想要的上下文 this(其实就是 foo ),到 bind() 函数中。然后,当回调函数被执行的时候, this 便指向 foo 对象。再来一个简单的例子:
var bar = function(){
console.log(this.x);
}
var foo = {
x:3
}
bar(); // undefined
var func = bar.bind(foo);
func(); // 3
这里我们创建了一个新的函数func,当使用bind()创建一个绑定函数之后,它被执行的时候,它的this会被设置成foo,而不是像我们调用bar()时全局作用域。
偏函数(Partial Functions)
Partial Function也叫Partial Application,这里截取一段关于偏函数的定义:
Partial application can be described as taking a function that accepts some number of arguments, binding values to one or more of those arguments, and returning a new function that only accepts the remaining, un-bound arguments.
可以将部分应用程序描述为采用一个接受一些参数的函数,将值绑定到这些参数中的一个或多个,然后返回一个仅接受其余未绑定参数的新函数。
这是一个很好的特性,使用bind()我们设定函数的预定义参数,然后调用的时候传入其他参数即可:
function list() {
return Array.prototype.slice.call(arguments);
}
var list1 = list(1, 2, 3); // [1, 2, 3]
// 预定义参数37
var leadingThirtysevenList = list.bind(undefined, 37);
var list2 = leadingThirtysevenList(); // [37]
var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]
和setTimeout一起使用
function Bloomer() {
this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
// 1秒后调用declare函数
Bloomer.prototype.bloom = function() {
window.setTimeout(this.declare.bind(this), 100);
};
Bloomer.prototype.declare = function() {
console.log('我有 ' + this.petalCount + ' 朵花瓣!');
};
var bloo = new Bloomer();
bloo.bloom(); //我有 5 朵花瓣!
注意:对于事件处理函数和setInterval方法也可以使用上面的方法
绑定函数和构造函数
绑定函数也适用于使用new操作符来构造目标函数的实例,当使用绑定函数来构造实例,注意:this会被忽略,但是传入的参数仍然可用。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function() {
console.log(this.x + ',' + this.y);
};
var p = new Point(1, 2);
p.toString(); // '1,2'
var emptyObj = {};
var YAxisPoint = Point.bind(emptyObj, 0/*x*/);
// 实现中的例子不支持,
// 原生bind支持:
var YAxisPoint = Point.bind(null, 0/*x*/);
var axisPoint = new YAxisPoint(5);
axisPoint.toString(); // '0,5'
axisPoint instanceof Point; // true
axisPoint instanceof YAxisPoint; // true
new Point(17, 42) instanceof YAxisPoint; // true
捷径
bind()也可以为需要特定this值的函数创造捷径。
例如要将一个类数组对象转换为真正的数组:
var slice = Array.prototype.slice;
slice.call(arguments);
如果使用bind()的话,情况变得更加简单。
var unboundSlice = Array.prototype.slice;
var slice = Function.protorype.call.bind(unboundSlice);
//...
slice(arguments);
面试题--bind的应用场景
1. 保存函数参数:
首先来看一下这一道经典的面试题:
for (var i = 1; i <= 5; i++) {
setTimeout(function test() {
console.log(i) // 依次输出:6 6 6 6 6
}, i * 1000);
}
造成这个现象的原因是等到setTimeout异步执行时,i已经变成6了。
那么如何使它输出:1,2,3,4,5呢?
- 可以使用闭包保存变量
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function () {
console.log('闭包:', i); // 依次输出:1 2 3 4 5
}, i * 1000);
}(i));
}
- bind
for (var i = 1; i <= 5; i++) {
// 缓存参数
setTimeout(function (i) {
console.log('bind', i) // 依次输出:1 2 3 4 5
}.bind(null, i), i * 1000);
}
实际山这里也是用了闭包,我们知道bind会返回一个函数,这个函数也是闭包。
它保存了函数的this指向、初始参数,每次i的变更都会被bind的闭包存起来,所以输出1-5.
具体细节可从下面的手写bind深入研究。
- let
用let声明i也可以输出1-5;因为let是块级作用域,所以每次都会创建一个新的变量,所以setTimeout每次读的值都是不同的。
参考:
https://segmentfault.com/a/1190000018270750
https://www.imooc.com/article/290456
转载: