前言
也再学学这两个函数吧,万一以后面试问到了呢?
正文
这两个函数的作用主要是以另一个上下文允许函数,并且可以传递参数。
挂载在Function.prototype
Function.prototype.apply
有两个参数
context
指定函数执行的上下文,传入undefined
和null
会指定全局对象,基本类型会被包装。
args
参数数组
可以返回任意的值,取决于改变上下文函数的返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var obj = { a:1, getA:function(){ return this.a; } }
console.log(obj.getA());
var ctxObj = { a:100 };
console.log(obj.getA.apply(ctxObj,[]));
|
手写一个apply函数
以apply2来命名我们的实现,挂载在Function.prototype
上
1 2 3
| Function.prototype.apply2 = function(){ }
|
首先我们需要处理入参的问题,这里可以直接标明参数,也可以不标明参数
因为JavaScript函数内有一个内置对象arguments
,储存着全部的入参,是一个类似数组的对象。
这里直接标明入参即可,因为参数是固定的。只有两个
1 2 3
| Function.prototype.apply2 = function(context,args){ }
|
现在先不管参数,先解决如何以context
为上下文呢?
这时候聪明的同学肯定想到了
1 2 3 4
| Function.prototype.apply2 = function(context,args){ this.apply(context); }
|
也没那么难嘛(叉腰)
哈哈哈哈,上面只是一个玩笑,
那么如何才能以改上下文呢,
其实很简单,只要把函数挂载在context
上,再以context.fn
的形式调用,就可以使用context
为上下文调用函数了。
1 2 3 4 5 6
| Function.prototype.apply2 = function(context,args){ context.fn = this; context.fn(); }
|
ok,其实这个函数我们已经解决一大半的,现在我们来解决参数问题。
参数有一个很大的问题就是长度不一定。
肯定又有聪明的同学想到了,我可以用展开这个数组!
1 2 3 4 5 6 7 8
| Function.prototype.apply2 = function(context,args){ context.fn = this; var result = context.fn(...args); return result; }
|
当然这并不是不可以,但是要注意,
有些浏览器并不支持解构和展开,比如我电脑上的ie11。(话说这东西真的卡,开个控制台给我闪退了…)
所以,正确的打开方式是使用eval
函数来传参并运行。
1 2 3 4 5 6 7 8 9 10 11 12
| Function.prototype.apply2 = function(context,args){ var argumentList = []; context.fn = this; for(var i = 0; i < args.length; i++){ argumentList.push(args[i]); } var result = eval("context.fn(" + argumentList.join(",") + ")"); return result; }
|
这里重点就是通过数组的join
方法拼接参数。
很多人以为到这里以为eval这么写应该就没什么大问题了,
只能说这波你在第一层,而用户在第五层。
如果我们的参数内存在对象的话,就会出现下面的情况。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| Function.prototype.apply2 = function(context,args){ var argumentList = []; context.fn = this; for(var i = 0; i < args.length; i++){ argumentList.push(args[i]); } var str = "context.fn(" + argumentList.join(",") + ")" console.log(str); var result = eval(str); return result; }
function fn(o) { return this.value + o.name; } var ctx = { value:24 }
fn.apply2(ctx,[{name:'lwf'}]);
|
当我们把对象和一个字符串相加时,会调用对象的toString()
方法返回字符串来相加,
导致打印了context.fn([object Object])
如何避免这种情况,也就是我们不能直接的把值连接起来,而是应该把变量连接起来
1 2 3 4 5 6 7 8 9 10 11 12 13
| Function.prototype.apply2 = function(context,args){ var argumentList = []; context.fn = this; for(var i = 0; i < args.length; i++){ argumentList.push("args[" + i + "]"); } var result = eval("context.fn(" + argumentList.join(",") + ")"); return result; }
|
现在,我们就写完了大部分的功能,也可以正确运行前面的例子了
1 2 3 4 5 6 7 8 9
| function fn(o) { return this.value + o.name; }
var ctx = { value:24 }
fn.apply2(ctx,[{name:'lwf'}]);
|
但是这时候用户很不乖,上下文传了个null或者undefined进来,完蛋,报错。
所以我们要检测传入参数的合法性。根据MDN
上对于apply参数的定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Function.prototype.apply2 = function(context,args){ if(context === null || context === undefined){ context = window; }else{ context = Object(context); } args = args || []; var argumentList = []; context.fn = this; for(var i = 0; i < args.length; i++){ argumentList.push("args[" + i + "]"); } var result = eval("context.fn(" + argumentList.join(",") + ")"); return result; }
|
至此,基本的步骤都已经完成,但是我们的上下文对象上多了一个fn的属性,我们要把它删除,
如果fn本来就有值的话该如何处理呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| Function.prototype.apply2 = function(context,args){ if(context === null || context === undefined){ context = window; }else{ context = Object(context); } args = args || []; var argumentList = []; context.fn = this; for(var i = 0; i < args.length; i++){ argumentList.push("args[" + i + "]"); } var result = eval("context.fn(" + argumentList.join(",") + ")"); delete context.fn; return result; }
|
解决的办法时找到一个一定没有被使用的属性进行挂载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| Function.prototype.apply2 = function(context,args){ if(context === null || context === undefined){ context = window; }else{ context = Object(context); } args = args || []; var argumentList = []; var n = 0; while(context['fn' + n] !== undefined){ n++; } context['fn' + n] = this; for(var i = 0; i < args.length; i++){ argumentList.push("args[" + i + "]"); } var result = eval("context.fn" + n + "(" + argumentList.join(",") + ")"); delete context['fn' + n]; return result; }
|
至此,整个函数就已经完成了。
Function.prototype.call
有n个参数,n>=1
context
指定函数执行的上下文
arg1,arg2,...,argN
传入执行函数的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| var obj = { a:1, getA:function(){ return this.a; } }
console.log(obj.getA());
var ctxObj = { a:100 };
console.log(obj.getA.call(ctxObj));
|
手写一个call函数
call和apply的实现其实差不多,只要修改下参数的组合方式即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| Function.prototype.call2 = function(){ var context = arguments[0]; if(context === null || context === undefined){ context = window; }else{ context = Object(context); } var argumentList = []; var n = 0; while(context['fn' + n] !== undefined){ n++; } context['fn' + n] = this; for(var i = 1; i < arguments.length; i++){ argumentList.push("arguments[" + i + "]"); } var result = eval("context.fn" + n + "(" + argumentList.join(",") + ")"); delete context['fn' + n]; return result; }
|
后记
这首歌挺好听的~