最近群里有人发了一道题:实现一个函数,运行结果可以达到如下预期结果:add(1)(2)//3add(1,2,3)(10)//16add(1)(2)(3)(4)(5)//15对于一个好奇的figurecutboy,忍不住试了一下,第一个看到标题我想到的最重要的是使用高阶函数和Array.prototype.reduce()。高阶函数:高阶函数意味着它接收另一个函数作为参数。在javascript中,函数是一等公民,允许函数作为参数或返回值传递。得到这个解决方案:functionadd(){varargs=Array.prototype.slice.call(arguments);returnfunction(){vararg2=Array.prototype.slice.call(arguments);返回args.concat(arg2).reduce(function(a,b){returna+b;});}}验证了一下,发现不对:add(1)(2)//3add(1,2)(3)//6add(1)(2)(3)//UncaughtTypeError:add(...)(...)isnotafunction(...)上述解决方案仅在add()()的情况下正确。并且当链式运算的参数大于两个或小于两个时,不能返回结果。而这也是本题的一个难点。add()时,如何既返回一个值又返回一个函数供后续调用?后来经高手指点,重写函数的valueOf方法或toString方法,得到其中一种解决方案:varfn=function(){vararg_fn=Array.prototype.slice.call(arguments);返回add.apply(null,args.concat(arg_fn));}fn.valueOf=function(){returnargs.reduce(function(a,b){returna+b;})}returnfn;嗯?当***第一次看到这个解决方案的时候,我惊呆了。因为感觉fn.valueOf()从头到尾都没有被调用过,而是验证了结果:add(1)//1add(1,2)(3)//6add(1)(2)(3)(4)(5)//15神奇地正确!那么谜底就在上面的fn.valueOf=function(){}。为什么会这样?这个方法在函数中的什么时候执行?并一步步听我说。valueOf和toString首先简单了解一下这两个方法:Object.prototype.valueOf()用MDN的话说,valueOf()方法返回指定对象的原始值。JavaScript调用valueOf()方法将对象转换为基本类型的值(数字、字符串和布尔值)。但我们很少需要自己调用这个函数,valueOf方法通常由JavaScript自动调用。把上面这句话牢记在心,下面我们就来详细说说所谓的自动调用是什么意思。Object.prototype.toString()toString()方法返回表示对象的字符串。每个对象都有一个toString()方法,当对象被表示为文字值时或当对象以需要字符串的方式被引用时,该方法会自动调用。请记住,valueOf()和toString()在某些情况下会调用自己。原始类型好了,我们来打个基础。我们先了解下javascript的几种原始类型。除了Object和Symbol之外,还有如下几种原始类型:NumberStringBooleanUndefinedNullObject在JavaScript进行比较或者各种操作的时候会被转换成这些类型,从而进行后续的操作,下面一一介绍:String类型转换当操作或计算需要字符串但对象不是字符串时,会触发对象的字符串转换,非字符串类型会尝试自动转换为字符串类型。系统会自动调用toString函数。例如:varobj={name:'Coco'};varstr='123'+obj;控制台日志(海峡);//123[objectObject]转换规则:如果toString方法存在且返回原类型,则返回toString的结果。如果toString方法不存在或返回非原始类型,则调用valueOf方法。如果valueOf方法存在并返回原始类型数据,则返回valueOf的结果。否则,将抛出错误。上面的例子其实就是:varobj={name:'Coco'};varstr='123'+obj.toString();其中,obj.toString()的值为“[objectObject]”。假设是一个数组:vararr=[1,2];varstr='123'+arr;控制台日志(海峡);//1231,上面的2+arr,由于这是一个字符串加法操作,后面的arr需要转为字符串类型,所以实际上调用了+arr.toString()。但是,我们可以自己重写对象的toString和valueOf方法:varobj={toString:function(){console.log('obj.toStringwascalled');返回{};},valueOf:function(){控制台。日志('调用obj.valueOf')返回'110';}}alert(obj);//调用obj.toString//调用obj.valueOf//上面的110alert(obj+'1'),obj会自动调用自己的obj.toString()方法将其转换为原始类型。如果我们不重写它的toString方法,它会输出[objectObject]1。这里我们重写了toString,返回了一个原始类型的字符串111,所以最后的alert是1111。上面的转换规则写的是toString方法需要存在并返回原始类型,那么如果返回的不是原始类型,则继续寻找对象的valueOf方法:让我们尝试证明如果一个对象试图转换为字符串,如果toString()方法不可用会发生什么。这时系统会再次调用valueOf()方法。接下来,我们重写对象的toString和valueOf:varobj={toString:function(){console.log('obj.toStringwascalled');返回{};},valueOf:function(){console.log('calledobj.valueOf')return'110';}}alert(obj);//calledobj.toString//calledobj.valueOf//110从结果可以看出,当toString不可用时,系统会再次尝试valueOf方法。如果valueOf方法存在并返回原始类型(String、Number、Boolean)数据,则返回valueOf的结果。那么,如果toString和valueOf都不返回原始类型怎么办?看下面的例子:varobj={toString:function(){console.log('obj.toStringwascalled');返回{};},valueOf:function(){console.log('obj.valueOf被调用')return{};}}alert(obj);//calledobj.toString//calledobj.valueOf//UncaughtTypeError:Cannotconvertobjecttoprimitivevaluecanbefound,ifnottoStringnorvalueOfmethodsareavailable在这种情况下,系统返回一个直接报错。添加于2017-03-07:查看ECMAScript5官方文档后,发现上述描述有问题。Object类型转String类型的转换规则远比上述复杂。转换规则为:1.设置原始值作为调用ToPrimitive的结果;2.返回ToString(原始值)。ToPrimitive和ToString的规则可以参考官方文档:ECMAScript5——ToStringNumber类型转换上面介绍了String类型的转换,很多时候也会发生Number类型的转换:调用Number()函数来强制进行Number类型转换并调用Math.sqrt()的参数需要Number类型的方法obj==1。比较时,使用obj+1。计算类似于String类型转换,Number类型正好相反。先查询自己的valueOf方法,再查询自己的toString方法:如果valueOf存在且返回原始类型数据,则返回valueOf的结果。如果toString存在并且返回原始类型数据,则返回toString的结果。否则,将抛出错误。按照上述步骤进行尝试:varobj={valueOf:function(){console.log('callvalueOf');返回5;}}console.log(obj+1);//调用valueOf//6varobj={valueOf:function(){console.log('callvalueOf');返回{};},toString:function(){console.log('calltoString');返回10;}}console.log(obj+1);//调用valueOf//调用toString//11varobj={valueOf:function(){console.log('callvalueOf');返回{};},toString:function(){console.log('CalltoString');返回{};}}console.log(obj+1);//CallvalueOf//CalltoString//UncaughtTypeError:CannotconvertobjecttoprimitivevalueBoolean何时执行布尔转换:Boolean比较if(obj),while(obj)和其他判断,简单来说,除了下面6个值转换结果为false,其他都为true:undefinednull-00or+0NaN”(emptystring)Boolean(undefined)//falseBoolean(null)//falseBoolean(0)//falseBoolean(NaN)//falseBoolean('')//false函数被转换了,***回到我们原来的话题,说说函数转换我们定义一个函数如下:functiontest(){vara=1;console.log(1);}如果我们只调用test而不是test()会发生什么?可以看到,这里转载了我们定义的测试函数。其实这里调用了函数的valueOf方法:我们重写测试函数的valueOf方法。test.valueOf=function(){console.log('调用valueOf方法');return2;}test;//输出结果如下://调用valueOf方法//2类似于Number转换,如果函数的valueOf方法返回Notaprimitivetype,会继续寻找其toString方法:test.valueOf=function(){console.log('调用valueOf方法');return{};}test.toString=function(){console.log('调用toString方法');return3;}test;//输出结果如下://调用valueOf方法//调用toString方法//3断题回头看我正文开头那题的答案,是将由其自身调用valueOf方法的函数,并覆盖该方法。我们稍微改一下,改造一下:functionadd(){console.log('enteradd');varargs=Array.prototype.slice.call(arguments);varfn=function(){vararg_fn=Array.prototype.slice.call(arguments);console.log('callfn');返回add.apply(null,args.concat(arg_fn));}fn.valueOf=function(){console.log('callvalueOf');returnargs.reduce(function(a,b){returna+b;})}returnfn;}调用一次add时,实际上返回的是函数fn,实际上返回的是fn.valueOf();add(1);//输出如下://enteradd//callvalueOf//1其实等价于:[1].reduce(function(a,b){returna+b;})//1当chain被调用了两次:add(1)(2);//输出如下://enteradd//callfn//enteradd//callvalueOf//3当chain被调用三次时:add(1)(2)(3);//输出如下://enteradd//callfn//enteradd//callfn//enteradd//callvalueOf//可以看到6,这里是其实是有循环的。只有最后一次调用真正调用了valueOf,而前面的操作都是组合参数,递归调用自己,因为最后一次调用返回的是一个fn函数,所以最后调用的是函数的fn.valueOf,并使用reduce方法求和所有争论。除了重写valueOf方法之外,您还可以重写toString方法,因此如果您愿意,以下内容也可以使用:functionadd(){varargs=Array.prototype.slice.call(arguments);varfn=function(){vararg_fn=Array.prototype.slice.call(arguments);返回add.apply(null,args.concat(arg_fn));}fn.toString=function(){returnargs.reduce(function(a,b){returna+b;})}returnfn;}这里有个规则,如果只有一个valueOf()或toString()重写时,会先调用重写的方法,如果同时重写两者,则和Number类型的转换规则一样,先查询valueOf()方法,toString()方法是当valueOf()方法返回非原始类型时查询。后记正如阮一峰先生所说,“炫耀从来不是我写作的动力,好奇心才是。”写这篇文章的过程,也是我自己学习的过程。过程中也遇到了很多困惑,所以即使查阅了官方文档和大量文章,错误和疏漏还是在所难免。欢迎指正,给出更好的方法。类型转换,最好看看ECMAScript规范,拒绝当送外卖党,自己多尝试。另外,评论中也有不少人提出了自己的疑问,值得一读。本文到此结束。有什么问题或者建议可以多交流。原创文章文笔有限,知识匮乏。如果文章中有任何不准确的地方,请告诉我。
