我在 五种绑定策略完全弄懂this 一文中,咱们提到call,apply,bind
属于显示绑定,这三个方法都能直接修改this指向。其中call与apply比较特殊,它们在修改this的同时还会直接执行方法,而bind只是返回一个修改完this的boundFunction
并未执行,那么今天咱们来说讲若是经过JavaScript模拟实现call与apply方法。javascript
除了都能改变this指向并执行函数,call与apply惟一区别在于参数不一样,具体以下:html
var fn = function (arg1, arg2) { // do something }; fn.call(this, arg1, arg2); // 参数散列 fn.apply(this, [arg1, arg2]) // 参数使用数组包裹
call第一参数为this指向,后续散列参数均为函数调用所需形参,而在apply中这些参数被包裹在一个数组中。java
call与apply在平常开发中很是实用,咱们在此列举几个实用的例子。git
检验数据类型:github
function type(obj) { var regexp = /\s(\w+)\]/; var result = regexp.exec(Object.prototype.toString.call(obj))[1]; return result; }; console.log(type([123]));//Array console.log(type('123'));//String console.log(type(123));//Number console.log(type(null));//Null console.log(type(undefined));//Undefined
数组取最大/小值:数组
var arr = [11, 1, 0, 2, 3, 5]; // 取最大 var max1 = Math.max.call(null, ...arr); var max2 = Math.max.apply(null, arr); // 取最小 var min1 = Math.min.call(null, ...arr); var min2 = Math.min.apply(null, arr); console.log(max1); //11 console.log(max2); //11 console.log(min1); //0 console.log(min2); //0
函数arguments类数组操做:markdown
var fn = function () { var arr = Array.prototype.slice.call(arguments); console.log(arr); //[1, 2, 3, 4] }; fn(1, 2, 3, 4);
关于这两个方法实用简单说到这里,毕竟本文的核心主旨是手动实现call与apply方法,咱们接着说。app
咱们从一个简单的例子解析call方法函数
var name = '时间跳跃'; var obj = { name: '听风是风' }; function fn() { console.log(this.name); }; fn(); //时间跳跃 fn.call(obj); //听风是风
在这个例子中,call方法主要作了两件事:this
this
指向,好比fn()
默认指向window
,因此输出时间跳跃
fn
先说第一步改变this
怎么实现,其实很简单,只要将方法fn
添加成对象obj
的属性不就行了。因此咱们能够这样:
//模拟call方法 Function.prototype.call_ = function (obj) { obj.fn = this; // 此时this就是函数fn obj.fn(); // 执行fn delete obj.fn; //删除fn }; fn.call_(obj); // 听风是风
注意,这里的call_
是咱们模拟的call
方法,咱们来解释模拟方法中作了什么。
Function.prototype.call_
的形式绑定了call_
方法,因此全部函数均可以直接访问call_
。fn.call_
属于this隐式绑定,因此在执行时call_
时内部this
指向fn
,这里的obj.fn = this
就是将方法fn
赋予成了obj
的一条属性。obj
如今已经有了fn
方法,执行obj.fn
,由于隐式绑定的问题,fn
内部的this
指向obj
,因此输出了听风是风
。delete
删除了obj
上的fn
方法,毕竟执行完不删除会致使obj
上的属性愈来愈多。咱们成功改变了this指向并执行了方法,但仍有一个问题待解决,call_
没法接受参数。
其实也不难,咱们知道函数有一个arguments
属性,代指函数接收的全部参数,它是一个类数组,好比下方例子:
Function.prototype.call_ = function (obj) { console.log(arguments); }; fn.call_(obj, 1, 2, 3);// [{name:'听风是风'},1,2,3...]
很明显arguments
第一位参数是咱们须要让this指向的对象,因此从下标1开始才是真正的函数参数,这里就得对arguments
进行加工,将下标1以后的参数剪切出来。
有同窗确定就想到了arguments.splice
,前面说了arguments并不是数组,因此不支持Array
方法。不要紧,不是还有Array.prototype.slice.call(arguments)
吗,转一次数组再用。很遗憾,咱们如今是在模拟call
方法,也不行。那就用最保险的for
循环吧,以下:
Function.prototype.call_ = function (obj) { var args = []; // 注意i从1开始 for (var i = 1, len = arguments.length; i < len; i++) { args.push(arguments[i]); }; console.log(args);// [1, 2, 3] }; fn.call_(obj, 1, 2, 3);
数组也不能直接做为参数传递给函数,有同窗可能想到array.join
字符拼接方法,这也存在一个问题,好比咱们是但愿传递参数1 2 3
三个参数进去,但通过join
方法拼接,它会变成一个参数"1,2,3"
,函数此时接受的就只有一个参数了。
因此这里咱们不得不借用恶魔方法eval,看个简单的例子:
var fn = function (a, b, c) { console.log(a + b + c); }; var arr = [1, 2, 3]; fn(1, 2, 3);//6 eval("fn(" + arr + ")");//6
你必定有疑问,为何这里数组arr
都不分割一下,fn
在执行时又如何分割数组呢?其实eval
在执行时会将变量转为字符串,这里隐性执行了arr.toString()
。来看个有趣的对比:
console.log([1, 2, 3].toString()); //"1,2,3" console.log([1, 2, 3].join(',')); //"1,2,3"
能够看出`eval
帮咱们作了数组处理,这里就不须要再使用join
方法了,所以eval("fn(" + arr + ")")
能够当作eval("fn(1,2,3)")
。
咱们整理下上面的思路,改写后的模拟方法就是这样:
var name = '时间跳跃'; var obj = { name: '听风是风' }; function fn(a, b, c) { console.log(a + b + c + this.name); }; //模拟call方法 Function.prototype.call_ = function (obj) { var args = []; // 注意i从1开始 for (var i = 1, len = arguments.length; i < len; i++) { args.push(arguments[i]); }; obj.fn = this; // 此时this就是函数fn eval("obj.fn(" + args + ")"); // 执行fn delete obj.fn; //删除fn }; fn.call_(obj, "个人", "名字", "是");
能够了吗?很遗憾,这段代码会报错。由于咱们传递的后三个参数都是字符串。在args.push(arguments[i])
这一步咱们提早将字符串进行了解析,这就致使eval
在执行时,表达式变成了eval("obj.fn(个人,名字,是)");
设想一下咱们普通调用函数的形式是这样obj.fn("个人","名字","是")
,因此对于eval而言就像传递了三个没加引号的字符串,没法进行解析。
不信咱们能够传递三个数字,好比:
fn.call_(obj, 1,2,3); // 6听风是风
由于数字无论加不加引号,做为函数参数都是可解析的,而字符串不加引号,那就被认为是一个变量,而不存在个人
这样的变量,天然就报错了。
怎么办呢?其实咱们能够在args.push(arguments[i])
这里先不急着解析,改写成这样:
args.push("arguments[" + i + "]");
遍历完成的数组args最终就是这个样子["arguments[1]","arguments[2]","arguments[3]"]
,当执行eval
时,arguments[1]
此时确实是做为一个变量存在不会报错,因而被eval
解析成了一个真正的字符传递给了函数。
因此改写后的call_
应该是这样:
var name = '时间跳跃'; var obj = { name: '听风是风' }; function fn(a, b, c) { console.log(a + b + c + this.name); }; //模拟call方法 Function.prototype.call_ = function (obj) { var args = []; // 注意i从1开始 for (var i = 1, len = arguments.length; i < len; i++) { args.push("arguments[" + i + "]"); }; obj.fn = this; // 此时this就是函数fn eval("obj.fn(" + args + ")"); // 执行fn delete obj.fn; //删除fn }; fn.call_(obj, "个人", "名字", "是"); // 个人名字是听风是风
咱们知道,当call
第一个参数为undefined
或者null
时,this默认指向window
,因此上面的方法还不够完美,咱们进行最后一次改写,考虑传递参数是不是有效对象:
var name = '时间跳跃'; var obj = { name: '听风是风' }; function fn(a, b, c) { console.log(a + b + c + this.name); }; //模拟call方法 Function.prototype.call_ = function (obj) { //判断是否为null或者undefined,同时考虑传递参数不是对象状况 obj = obj ? Object(obj) : window; var args = []; // 注意i从1开始 for (var i = 1, len = arguments.length; i < len; i++) { args.push("arguments[" + i + "]"); }; obj.fn = this; // 此时this就是函数fn eval("obj.fn(" + args + ")"); // 执行fn delete obj.fn; //删除fn }; fn.call_(obj, "个人", "名字", "是"); // 个人名字是听风是风 fn.call_(null, "个人", "名字", "是"); // 个人名字是时间跳跃 fn.call_(undefined, "个人", "名字", "是"); // 个人名字是时间跳跃
那么到这里,对于call
方法的模拟就完成了。
apply
方法由于接受的参数是一个数组,因此模拟起来就更简单了,理解了call
实现,咱们就直接上代码:
var name = '时间跳跃'; var obj = { name: '听风是风' }; function fn(a, b, c) { console.log(a + b + c + this.name); }; //模拟call方法 Function.prototype.apply_ = function (obj, arr) { obj = obj ? Object(obj) : window; obj.fn = this; if (!arr) { obj.fn(); } else { var args = []; // 注意这里的i从0开始 for (var i = 0, len = arr.length; i < len; i++) { args.push("arr[" + i + "]"); }; eval("obj.fn(" + args + ")"); // 执行fn }; delete obj.fn; //删除fn }; fn.apply_(obj, ["个人", "名字", "是"]); // 个人名字是听风是风 fn.apply_(null, ["个人", "名字", "是"]); // 个人名字是时间跳跃 fn.apply_(undefined, ["个人", "名字", "是"]); // 个人名字是时间跳跃
上述代码总有些繁杂,咱们来总结下这两个方法:
// call模拟 Function.prototype.call_ = function (obj) { //判断是否为null或者undefined,同时考虑传递参数不是对象状况 obj = obj ? Object(obj) : window; var args = []; // 注意i从1开始 for (var i = 1, len = arguments.length; i < len; i++) { args.push("arguments[" + i + "]"); }; obj.fn = this; // 此时this就是函数fn var result = eval("obj.fn(" + args + ")"); // 执行fn delete obj.fn; //删除fn return result; }; // apply模拟 Function.prototype.apply_ = function (obj, arr) { obj = obj ? Object(obj) : window; obj.fn = this; var result; if (!arr) { result = obj.fn(); } else { var args = []; // 注意这里的i从0开始 for (var i = 0, len = arr.length; i < len; i++) { args.push("arr[" + i + "]"); }; result = eval("obj.fn(" + args + ")"); // 执行fn }; delete obj.fn; //删除fn return result; };
若是容许使用ES6,使用拓展运算符会简单不少,实现以下:
// ES6 call Function.prototype.call_ = function (obj) { obj = obj ? Object(obj) : window; obj.fn = this; // 利用拓展运算符直接将arguments转为数组 let args = [...arguments].slice(1); let result = obj.fn(...args); delete obj.fn return result; }; // ES6 apply Function.prototype.apply_ = function (obj, arr) { obj = obj ? Object(obj) : window; obj.fn = this; let result; if (!arr) { result = obj.fn(); } else { result = obj.fn(...arr); }; delete obj.fn return result; };
那么到这里,关于call与apply模拟实现所有结束。
这篇文章也是第一篇我使用markdown书写的文章,为了统同样式,我也专门修改了博客样式。