企业🤖AI智能体构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
在`JavaScript`中,如果想要改变当前函数调用的上下文对象的时候,我们都会联想到`call、apply和bind`。 ~~~ var name = 'window name'; var obj = { name: 'call_me_R' }; function sayName(){ console.log(this.name); } sayName(); // window name sayName.call(obj); // call_me_R ~~~ ``` 有两种方法可以讲arguments转换成数组,其实原理是一样的。 Array.prototype.slice.call(arguments) [].slice.call(arguments) Array.from()`是个非常推荐的方法,其可以将所有类数组对象转换成数组。 ``` ## 三者的共同之处: 1. 都是用来改变函数的this对象的指向 2. 第一个参数都是this要指向的对象 3. 都可以利用后续参数进行传参 ## 说下区别: 1. 第二个参数开始不同,apply是传入带下标的集合,数组或者类数组,apply把它传给函数作为参数,call从第二个开始传入的参数是不固定的,都会传给函数作为参数。 2. call比apply的性能要好,平常可以多用call, call传入参数的格式正是内部所需要的格式, 3. 尤其是es6 引入了 Spread operator (延展操作符) 后,即使参数是数组,可以使用 call ~~~js let params = [1,2,3,4] xx.call(obj, ...params) ~~~ 4. `call`方法传参是传一个或者是多个参数,第一个参数是指定的对象,如开篇的`obj`。 ~~~js func.call(thisArg, arg1, arg2, ...) ~~~ 5. `apply`方法传参是传一个或两个参数,第一个参数是指定的对象,第二个参数是一个数组或者类数组对象。 ~~~js func.apply(thisArg, [argsArray]) ~~~ 6. `bind`方法传参是传一个或者多个参数,跟`call`方法传递参数一样。 ~~~js func.bind(this.thisArg, arg1, arg2, ...) ~~~ 简言之,`call`和`bind`传参一样;`apply`如果要传第二个参数的话,应该传递一个类数组。 ## 调用后是否立执行 **call和apply**在函数调用它们之后,会立即执行这个函数;而函数调用**bind**之后,会返回调用函数的引用,如果要执行的话,需要执行返回的函数引用。 变动下开篇的`demo`代码,会比较容易理解: ~~~js var name = 'window name'; var obj = { name: 'call_me_R' }; function sayName(){ console.log(this.name); } sayName(); // window name sayName.call(obj); // call_me_R sayName.apply(obj); // call_me_R console.log('---divided line---'); var _sayName = sayName.bind(obj); _sayName(); // call_me_R ~~~ `call, apply 和 bind`的区分点主要是上面的这两点 ## 手写call, apply, bind方法 #### call方法实现 在上面的了解中,我们很清楚了`call`的传参格式和调用执行方式,那么就有了下面的实现方法: ~~~js Function.prototype.call2 = function(context, ...args){ context = context || window; // 因为传递过来的context有可能是null context.fn = this; // 让fn的上下文为context const result = context.fn(...args); delete context.fn; return result; // 因为有可能this函数会有返回值return } ~~~ 我们来测试下: ~~~js var name = 'window name'; var obj = { name: 'call_me_R' }; // Function.prototype.call2 is here ... function sayName(a){ console.log(a + this.name); return this; } sayName(''); // window name var _this = sayName.call2(obj, 'hello '); // hello call_me_R console.log(_this); // {name: "call_me_R"} ~~~ #### apply方法实现 `apply`方法和`call`方法差不多,区分点是`apply`第二个参数是传递数组: ~~~ Function.prototype.apply2 = function(context, arr){ context = context || window; // 因为传递过来的context有可能是null context.fn = this; // 让fn的上下文为context arr = arr || []; // 对传进来的数组参数进行处理 const result = context.fn(...arr); // 相当于context.fn(arguments[1], arguments[2], ...) delete context.fn; return result; // 因为有可能this函数会有返回值return } ~~~ 同样的,我们来测试下: ~~~js var name = 'window name'; var obj = { name: 'call_me_R' }; // Function.prototype.apply2 is here ... function sayName(){ console.log((arguments[0] || '') + this.name); return this; } sayName(); // window name var _this = sayName.apply2(obj, ['hello ']); // hello call_me_R console.log(_this); // {name: "call_me_R"} ~~~ #### bind方法实现 `bind`的实现和上面的两种就有些差别,虽然和`call`传参相同,但是`bind`被调用后返回的是调用函数的指针。那么,这就说明`bind`内部是返回一个函数,思路打开了: ~~~js Function.prototype.bind2 = function(context, ...args){ var fn = this; return function () { // 这里不能使用箭头函数,不然参数arguments的指向就很尴尬了,指向父函数的参数 fn.call(context, ...args, ...arguments); } } ~~~ 测试一下: ~~~js var name = 'window name'; var obj = { name: 'call_me_R' }; // Function.prototype.bind2 is here ... function sayName(){ console.log((arguments[0] || '') + this.name + (arguments[1] || '')); } sayName(); // window name sayName.bind2(obj, 'hello ')(); // hello call_me_R sayName.bind2(obj, 'hello ')('!'); // hello call_me_R! ~~~ ## call的性能会比apply好 在我们平时的开发中其实不必关注call和apply的性能问题,但是可以尽可能的去用call,特别是es6的reset解构的支持,call基本可以代替apply,可以看出lodash源码里面并没有直接用Function.prototype.apply,而是在参数较少(1-3)个时采用call的方式调用(因为lodash里面没有超过4个参数的方法,PS如果一个函数的设计超过4个入参,那么这个函数就要考虑重构了) ## 参考文章 1. [谈谈JavaScript中的call、apply和bind](https://github.com/reng99/blogs/issues/29) 2. [call 和 apply 的区别是什么,哪个性能更好一些](https://muyiy.vip/question/js/48.html) 3. [call和apply的性能对比](https://github.com/noneven/__/issues/6)