前言 也再学学这两个函数吧,万一以后面试问到了呢?
正文 这两个函数的作用主要是以另一个上下文允许函数,并且可以传递参数。
挂载在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 }
后记 这首歌挺好听的~