首页 > 编程笔记
JS call()、apply()和bind()的区别
JavaScript 中的函数也是对象,相当于调用了 Function() 构造函数所创建的对象,所以每个函数都继承了 Function.prototype 对象中的属性和方法,其中有3个重要的方法:call()、apply()和bind(),这3种方法与 this 有关系。
例如使用 call() 改变 this 指向,代码如下:
如果直接调用该函数 sum("a","b"),则会返回 NaN,因为这样调用函数,里边的 this 指向的是全局对象,而全局对象中并没有 a 和 b 这两个属性,所以其结果是两个 undefined 相加。
后面定义了 obj 对象,里边有 a 和 b 属性,然后通过调用 sum() 原型对象中的 call() 方法,把 obj 作为函数的 this 传递进去,这样就能成功地访问这两个属性,然后返回了正确的结果3。
使用 call() 方法还可以实现链式调用构造函数。假如有两个构造函数,它们有同样的初始化代码,那么可以把它们抽离成一个公共的构造函数,再在这两个构造函数中通过 call() 来调用这个公共的构造函数,call() 中的 this 分别设置为两个构造函数的 this,这样它们还是指向各自新创建的对象。
例如有两个创建消息对象的构造函数,一个用于创建文本消息,另一个用于创建表情消息,它们都有 message 消息内容和 sender 发送者属性,但是有不同的 msgType 消息类别属性,用于区分消息,此时就可以把初始化 message 和 sender 的代码抽离成公共的构造函数,然后各自初始化 msgType,代码如下:
例如,数组中的 push() 方法接收多个参数,把这些参数作为新的元素追加到数组中,此时就可以使用 apply() 方法,把一个数组追加到当前数组中,代码如下:
在 ES6 中,还可以使用 spread 扩展运算符进行同样的操作:
另外在学习 Array-like 类数组的结构时,应知道它不能直接使用数组中的方法,因为它与数组对象本身没有继承关系,但是通过 apply() 或 call() 可以间接地调用数组中的方法。
例如使用数组中的 push() 方法还可以给类数组结构添加新元素,而且更重要的是,它还能自动增长类数组中的 length 属性的值,代码如下:
同理,pop() 也可以在类数组结构中用于删除它里边的属性,并自动减少 length 属性的值。
其他的方法例如 forEach()、map() 等也可以如此调用,下方示例演示了如何使用 forEach() 遍历类数组结构,因为这里只给 forEach() 传递了一个回调函数作为参数,所以下例使用 call() 来演示它的用法,代码如下:
a
b
c
使用数组中的 slice() 方法还能把类数组转换为普通数组的形式,只需忽略 slice() 方法的参数,这种使用方法在 rest 运算符出现以前非常普遍,用于把函数中的 arguments 类数组结构转换为数组,然后就可以使用数组中的方法来操作参数了,代码如下:
1
2
3
其他的方法,例如 shift()、unshift()、reverse()、includes() 等,也都可以使用 call() 或 apply() 应用到类数组对象中。
下方示例展示了使用 bind() 给函数绑定 this 指向的过程,代码如下:
后面使用了 bind() 方法,把 obj 作为 this 绑定到了 f() 函数中,之后再调用它就可以访问 obj 中 a 属性的值了。
在这个例子中,可以看到并没有使用 bind() 给 f() 传递参数,这样后边再调用的时候需要手动传递参数。不过,也可以在使用 bind() 的时候给函数传递参数,除了传递全部参数之外,还可以只传递一部分参数,后续参数在调用的时候再进行传递,这种使用 bind() 传递了部分参数的函数称为部分传递参数函数(Partially Applied Function)。
例如,假设有一个构建文件路径字符串的函数,接收目录和文件名两个参数,如果目录是确定的,则可以使用 bind() 把目录参数确定好,然后在返回的新函数中传递文件名参数,代码如下:
不过,利用 bind() 和 apply() 可以把任何一个函数转换为柯里化的形式,代码如下:
代码中的 func.length 用于获取函数参数的数量,因为函数本身也是对象,它内部有这个属性。在使用返回的新函数时,能够以完全柯里化的形式调用,也能以部分柯里化的方式调用,代码如下:
1. call()
在 JS 中,函数中的 call() 方法用于调用该函数,它接收两个参数,第1个用于设置函数内部 this 的指向,第2个参数是一个变长参数,接收多个逗号分隔的参数并传递给原函数。例如使用 call() 改变 this 指向,代码如下:
function sum(prop1,prop2){ return this[prop1]+this[prop2]; } const obj={a:1,b:2}; const result=sum.call(obj,"a","b"); result; //3示例中首先定义了普通函数 sum(),它用于给对象中的两个属性进行求和,两个参数为进行求和计算的属性名,因为属性名是使用变量动态表示的,所以这里使用了
[]
访问对象中的属性。如果直接调用该函数 sum("a","b"),则会返回 NaN,因为这样调用函数,里边的 this 指向的是全局对象,而全局对象中并没有 a 和 b 这两个属性,所以其结果是两个 undefined 相加。
后面定义了 obj 对象,里边有 a 和 b 属性,然后通过调用 sum() 原型对象中的 call() 方法,把 obj 作为函数的 this 传递进去,这样就能成功地访问这两个属性,然后返回了正确的结果3。
使用 call() 方法还可以实现链式调用构造函数。假如有两个构造函数,它们有同样的初始化代码,那么可以把它们抽离成一个公共的构造函数,再在这两个构造函数中通过 call() 来调用这个公共的构造函数,call() 中的 this 分别设置为两个构造函数的 this,这样它们还是指向各自新创建的对象。
例如有两个创建消息对象的构造函数,一个用于创建文本消息,另一个用于创建表情消息,它们都有 message 消息内容和 sender 发送者属性,但是有不同的 msgType 消息类别属性,用于区分消息,此时就可以把初始化 message 和 sender 的代码抽离成公共的构造函数,然后各自初始化 msgType,代码如下:
function Message(message, sender){ this.message = message; this.sender = sender; } function TextMessage(message, sender){ Message.cal1(this, message, sender); this.msgType = "文本消息"; } function EmojMessage(message, sender){ Message.cal1(this, message, sender); this.msgType = "表情消息"; } const txtMsg = new TextMessage("你好", "张三"); const emjMsg = new EmojMessage("^_^","李四"); console.log(txtMsg.message, txtMsg.msgType); console.log(emjMsg.message, emjMsg.msgType); //你好 文本消息 //^_^表情消息可以看到,Message() 构造函数中的 message 和 sender 属性,以及 TextMessage() 和 EmojMessage() 构造函数中的 msgType 属性都正确地赋给了新创建的对象。
2. apply()
JS 中的 apply() 方法与 call() 方法的作用几乎一模一样,但是 apply() 的第2个参数接收的是一个数组,而不是变长参数,通过这个特性,可以把接收多个参数的函数转换成使用一个数组接收参数的函数。例如,数组中的 push() 方法接收多个参数,把这些参数作为新的元素追加到数组中,此时就可以使用 apply() 方法,把一个数组追加到当前数组中,代码如下:
const arr1=[1,2,3]; const arr2=[4,5,6]; arr1.push.apply(arr1,arr2); arr1; //[1,2,3,4,5,6]
在 ES6 中,还可以使用 spread 扩展运算符进行同样的操作:
arr1.push(...arr2)不过在一些旧的 JavaScript 的代码中,还是可以看到很多使用 apply() 的方式,这里只需知道它的用法就可以了,其他的应用场景跟 call() 保持一致。
另外在学习 Array-like 类数组的结构时,应知道它不能直接使用数组中的方法,因为它与数组对象本身没有继承关系,但是通过 apply() 或 call() 可以间接地调用数组中的方法。
例如使用数组中的 push() 方法还可以给类数组结构添加新元素,而且更重要的是,它还能自动增长类数组中的 length 属性的值,代码如下:
let arrLike={0:"a",1:"b",2:"c",length:3}; Array.prototype.push.apply(arrLike,["d","e","f"]); arrLike; //{0:"a",1:"b",2:"c",3:"d",4:"e",5:"f",length:6}
同理,pop() 也可以在类数组结构中用于删除它里边的属性,并自动减少 length 属性的值。
其他的方法例如 forEach()、map() 等也可以如此调用,下方示例演示了如何使用 forEach() 遍历类数组结构,因为这里只给 forEach() 传递了一个回调函数作为参数,所以下例使用 call() 来演示它的用法,代码如下:
let arrLike={0:"a",1:"b",2:"c",length:3}; Array.prototype.forEach.call(arrLike,v=>console.log(v));输出结果为
a
b
c
使用数组中的 slice() 方法还能把类数组转换为普通数组的形式,只需忽略 slice() 方法的参数,这种使用方法在 rest 运算符出现以前非常普遍,用于把函数中的 arguments 类数组结构转换为数组,然后就可以使用数组中的方法来操作参数了,代码如下:
function f(){ const args=Array.prototype.slice.apply(arguments); args.forEach(arg=>{ console.log(arg); }) } f(1,2,3);输出结果如下:
1
2
3
其他的方法,例如 shift()、unshift()、reverse()、includes() 等,也都可以使用 call() 或 apply() 应用到类数组对象中。
3. bind()
在 JS 中,bind() 与 call() 类似,用于给函数绑定 this,并通过变长参数给函数传递参数,不同之处在于,使用 bind() 会创建并返回一个新的函数,这个函数并不会立即被执行,而是需要在合适的地方进行调用。下方示例展示了使用 bind() 给函数绑定 this 指向的过程,代码如下:
const obj={ a:1, f(b){ return this.a+b; }, }; const f=obj.f; console.log(f(10)); //NaN const boundF=f.bind(obj); console.log(boundF(10)); //11代码中使用常量 f 保存了 obj 对象中的 f() 方法的引用,直接调用它会丢失 this 对 obj 的指向,所以 f 中的 this.a 会变成 undefined,其结果就成了 NaN,按之前判断 this 的原则,在调用 f(10) 时,左侧没有内容,因为它的 this 指向的就是全局对象,而全局对象里并没有 a 这个变量。
后面使用了 bind() 方法,把 obj 作为 this 绑定到了 f() 函数中,之后再调用它就可以访问 obj 中 a 属性的值了。
在这个例子中,可以看到并没有使用 bind() 给 f() 传递参数,这样后边再调用的时候需要手动传递参数。不过,也可以在使用 bind() 的时候给函数传递参数,除了传递全部参数之外,还可以只传递一部分参数,后续参数在调用的时候再进行传递,这种使用 bind() 传递了部分参数的函数称为部分传递参数函数(Partially Applied Function)。
例如,假设有一个构建文件路径字符串的函数,接收目录和文件名两个参数,如果目录是确定的,则可以使用 bind() 把目录参数确定好,然后在返回的新函数中传递文件名参数,代码如下:
function buildPath(dir,fileName){ return `${dir}/${fileName}`; } const usr=buildPath.bind(null, "/usr"); console.log(usr("image.jpg")); ///usr/image.jpg这种用法和柯里化类似,只是柯里化需要在函数内部返回一系列接收1个参数的子函数,并且可以捕获内部的状态,而使用 bind() 则只能保存参数,且只有在最后调用新创建的函数时,函数中的代码才会被执行。
不过,利用 bind() 和 apply() 可以把任何一个函数转换为柯里化的形式,代码如下:
function curry(func){ return function_curry(...args){ if(args.length>=func.length){ return func.apply(null,args); }else{ return_curry.bind(null,...args); } }; }curry() 接收一个函数作为参数,并返回柯里化后的新函数,新函数的执行过程如下:
- 如果接收的参数数量大于或等于原函数中参数的数量,即参数已经传递完毕,则直接返回最后执行的结果。
- 如果数量小于原函数中参数的数量,则使用 bind() 创建一个新函数,并加上新传递的参数。
- 重复第 ① 步。
代码中的 func.length 用于获取函数参数的数量,因为函数本身也是对象,它内部有这个属性。在使用返回的新函数时,能够以完全柯里化的形式调用,也能以部分柯里化的方式调用,代码如下:
function add(a,b,c){ return a+b+c; } const addCurry=curry(add); console.log(addCurry(2)(4)(10)); //16 console.log(addCurry(1,3)(6)); //10 console.log(addCurry(4)(5,7)); //16