最近群里有人发了下面这题:

实现一个函数,运算结果可以满足如下预期结果:

add(1)(2) // 3
add(1, 2, 3)(10) // 16
add(1)(2)(3)(4)(5) // 15

对于一个好奇的切图仔来说,忍不住动手尝试了一下,看到题目首先想到的是会用到高阶函数以及 Array.prototype.reduce()

高阶函数(Higher-order function):高阶函数的意思是它接收另一个函数作为参数。在 javascript 中,函数是一等公民,允许函数作为参数或者返回值传递。

得到了下面这个解法:

function add() {var args = Array.prototype.slice.call(arguments);return function() {var arg2 = Array.prototype.slice.call(arguments);return args.concat(arg2).reduce(function(a, b){return a + b;});}
}

验证了一下,发现错了:

add(1)(2) // 3
add(1, 2)(3) // 6
add(1)(2)(3) // Uncaught TypeError: add(...)(...) is not a function(…)

上面的解法,只有在 add()() 情形下是正确的。而当链式操作的参数多于两个或者少于两个的时候,无法返回结果。

而这个也是这题的一个难点所在,add()的时候,如何既返回一个值又返回一个函数以供后续继续调用?

后来经过高人指点,通过重写函数的 valueOf 方法或者 toString 方法,可以得到其中一种解法:

function add () {var args = Array.prototype.slice.call(arguments);var fn = function () {var arg_fn = Array.prototype.slice.call(arguments);return add.apply(null, args.concat(arg_fn));}fn.valueOf = function () {return args.reduce(function(a, b) {return a + b;})}return fn;
}

嗯?第一眼看到这个解法的时候,我是懵逼的。因为我感觉 fn.valueOf() 从头到尾都没有被调用过,但是验证了下结果:

add(1) // 1
add(1,2)(3) //6
add(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,有如下几种原始类型:

  • Number
  • String
  • Boolean
  • Undefined
  • Null

在 JavaScript 进行对比或者各种运算的时候会把对象转换成这些类型,从而进行后续的操作,下面逐一说明:

String 类型转换

在某个操作或者运算需要字符串而该对象又不是字符串的时候,会触发该对象的 String 转换,会将非字符串的类型尝试自动转为 String 类型。系统内部会自动调用 toString 函数。举个例子:

var obj = {name: 'Coco'};
var str = '123' + obj;
console.log(str);  // 123[object Object]

转换规则:

  1. 如果 toString 方法存在并且返回原始类型,返回 toString 的结果。
  2. 如果 toString 方法不存在或者返回的不是原始类型,调用 valueOf 方法,如果 valueOf 方法存在,并且返回原始类型数据,返回 valueOf 的结果。
  3. 其他情况,抛出错误。

上面的例子实际上是:

var obj = {name: 'Coco'};
var str = '123' + obj.toString();

其中,obj.toString() 的值为 "[object Object]"

假设是数组:

var arr = [1, 2];
var str = '123' + arr;console.log(str); // 1231,2

上面 + arr ,由于这里是个字符串加操作,后面的 arr 需要转化为一个字符串类型,所以其实是调用了 + arr.toString() 。

但是,我们可以自己改写对象的 toStringvalueOf 方法:

var obj = {toString: function() {console.log('调用了 obj.toString');return {};},valueOf: function() {console.log('调用了 obj.valueOf')return '110';}
}alert(obj);
// 调用了 obj.toString
// 调用了 obj.valueOf
// 110

上面 alert(obj + '1') ,obj 会自动调用自己的 obj.toString() 方法转化为原始类型,如果我们不重写它的 toString 方法,将输出 [object Object]1 ,这里我们重写了 toString ,而且返回了一个原始类型字符串 111 ,所以最终 alert 出了 1111。

上面的转化规则写了,toString 方法需要存在并且返回原始类型,那么如果返回的不是一个原始类型,则会去继续寻找对象的 valueOf 方法:

下面我们尝试证明如果在一个对象尝试转换为字符串的过程中,如果 toString() 方法不可用的时候,会发生什么。

这个时候系统会再去调用 valueOf() 方法,下面我们改写对象的 toString 和 valueOf

var obj = {toString: function() {console.log('调用了 obj.toString');return {};},valueOf: function() {console.log('调用了 obj.valueOf')return '110';}
}alert(obj);
// 调用了 obj.toString
// 调用了 obj.valueOf
// 110

从结果可以看到,当 toString 不可用的时候,系统会再尝试 valueOf 方法,如果 valueOf 方法存在,并且返回原始类型(String、Number、Boolean)数据,返回valueOf的结果。

那么如果,toString 和 valueOf 返回的都不是原始类型呢?看下面这个例子:

var obj = {toString: function() {console.log('调用了 obj.toString');return {};},valueOf: function() {console.log('调用了 obj.valueOf')return {};}
}alert(obj);
// 调用了 obj.toString
// 调用了 obj.valueOf
// Uncaught TypeError: Cannot convert object to primitive value

可以发现,如果 toString 和 valueOf 方法均不可用的情况下,系统会直接返回一个错误。

添加于 2017-03-07:在查证了 ECMAScript5 官方文档后,发现上面的描述有一点问题,Object 类型转换为 String 类型的转换规则远比上面复杂。转换规则为:1.设原始值为调用 ToPrimitive 的结果;2.返回 ToString(原始值) 。关于 ToPrimitive 和 ToString 的规则可以看看官方文档:ECMAScript5 — ToString

Number 类型转换

上面描述的是 String 类型的转换,很多时候也会发生 Number 类型的转换:

  • 调用 Number() 函数,强制进行 Number 类型转换
  • 调用 Math.sqrt() 这类参数需要 Number 类型的方法
  • obj == 1 ,进行对比的时候
  • obj + 1 , 进行运算的时候

与 String 类型转换相似,但是 Number 类型刚好反过来,先查询自身的 valueOf 方法,再查询自己 toString 方法:

  1. 如果 valueOf 存在,且返回原始类型数据,返回 valueOf 的结果。
  2. 如果 toString 存在,且返回原始类型数据,返回 toString 的结果。
  3. 其他情况,抛出错误。

按照上述步骤,分别尝试一下:

var obj = {valueOf: function() {console.log('调用 valueOf');return 5;}
}console.log(obj + 1);
// 调用 valueOf
// 6
var obj = {valueOf: function() {console.log('调用 valueOf');return {};},toString: function() {console.log('调用 toString');return 10;}
}console.log(obj + 1);
// 调用 valueOf
// 调用 toString
// 11
var obj = {valueOf: function() {console.log('调用 valueOf');return {};},toString: function() {console.log('调用 toString');return {};}
}console.log(obj + 1);
// 调用 valueOf
// 调用 toString
// Uncaught TypeError: Cannot convert object to primitive value

Boolean 转换

什么时候会进行布尔转换呢:

  • 布尔比较时
  • if(obj) , while(obj) 等判断时

简单来说,除了下述 6 个值转换结果为 false,其他全部为 true:

  • undefined
  • null
  • -0
  • 0或+0
  • NaN
  • ”(空字符串)
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false

Function 转换

好,最后回到我们一开始的题目,来讲讲函数的转换。

我们定义一个函数如下:

function test() {var a = 1;console.log(1);
}

如果我们仅仅是调用 test 而不是 test() ,看看会发生什么?

可以看到,这里把我们定义的 test 函数的重新打印了一遍,其实,这里自行调用了函数的 valueOf 方法:

我们改写一下 test 函数的 valueOf 方法。

test.valueOf = function() {console.log('调用 valueOf 方法');return 2;
}test;
// 输出如下:
// 调用 valueOf 方法
// 2

与 Number 转换类似,如果函数的 valueOf 方法返回的不是一个原始类型,会继续找到它的 toString 方法:

test.valueOf = function() {console.log('调用 valueOf 方法');return {};
}test.toString= function() {console.log('调用 toString 方法');return 3;
}test;
// 输出如下:
// 调用 valueOf 方法
// 调用 toString 方法
// 3

破题

再看回我正文开头那题的答案,正是运用了函数会自行调用 valueOf 方法这个技巧,并改写了该方法。我们稍作改变,变形如下:

function add () {console.log('进入add');var args = Array.prototype.slice.call(arguments);var fn = function () {var arg_fn = Array.prototype.slice.call(arguments);console.log('调用fn');return add.apply(null, args.concat(arg_fn));}fn.valueOf = function () {console.log('调用valueOf');return args.reduce(function(a, b) {return a + b;})}return fn;
}

当调用一次 add 的时候,实际是是返回 fn 这个 function,实际是也就是返回 fn.valueOf();

add(1);
// 输出如下:
// 进入add
// 调用valueOf
// 1

其实也就是相当于:

[1].reduce(function(a, b) {return a + b;
})
// 1

当链式调用两次的时候:

add(1)(2);
// 输出如下:
// 进入add
// 调用fn
// 进入add
// 调用valueOf
// 3

当链式调用三次的时候:

add(1)(2)(3);
// 输出如下:
// 进入add
// 调用fn
// 进入add
// 调用fn
// 进入add
// 调用valueOf
// 6

可以看到,这里其实有一种循环。只有最后一次调用才真正调用到 valueOf,而之前的操作都是合并参数,递归调用本身,由于最后一次调用返回的是一个 fn 函数,所以最终调用了函数的 fn.valueOf,并且利用了 reduce 方法对所有参数求和。

除了改写 valueOf 方法,也可以改写 toString 方法,所以,如果你喜欢,下面这样也可以:

function add () {var args = Array.prototype.slice.call(arguments);var fn = function () {var arg_fn = Array.prototype.slice.call(arguments);return add.apply(null, args.concat(arg_fn));}fn.toString = function() {return args.reduce(function(a, b) {return a + b;})}return fn;
}

这里有个规律,如果只改写 valueOf() 或是 toString() 其中一个,会优先调用被改写了的方法,而如果两个同时改写,则会像 Number 类型转换规则一样,优先查询 valueOf() 方法,在 valueOf() 方法返回的是非原始类型的情况下再查询 toString() 方法。

后记

像阮一峰老师所说的,“炫耀从来不是我写作的动机,好奇才是”。本文行文过程也是我自己学习的一个过程,过程中我也遇到了很多困惑,所以即便查阅了官方文档及大量的文章,但是错误及疏漏仍然在所难免,欢迎指正及给出更好的方法。

对于类型转换,最好还是看看 ECMAScript 规范,拒绝成为伸手党,自己多尝试。另外评论处有很多人提出了自己的疑问,值得一看。

到此本文结束,如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

from: http://developer.51cto.com/art/201703/534335.htm

一道面试题引发的对JavaScript类型转换的思考相关推荐

  1. java yang模型_一道面试题引发的对Java内存模型的一点疑问

    一道面试题引发的对Java内存模型的一点疑问 问题描述如上图所示程序,按道理,子线程会通过 num++ 操作破坏 while 循环的条件,从而终止循环,执行最后的输出操作.但在我的多次运行中,偶尔会出 ...

  2. 一道概率题引发对考研数学复习的思考

    一道概率题引发对考研数学复习的思考 知识回顾 如题,今天上午我兴致勃勃地拿出2006年数学一做了起来.做前面还算顺利,直到最后那道概率题,关于极大似然估计的题目,我的内心:艹了,这怎么跟之前做的不一样 ...

  3. 一道笔试题引发的Promise笔记

    前言 近来参加校招笔试,发现有好几道关于Promise的题目.然而我都没有了解过.所以,这篇文章以网易笔试的一道题开始,记录关于Promise的那些事. 文章地址:http://lsxj615.com ...

  4. 一道面试题引发的关于程序设计的想法

    申明:这是在看到园子里两个帖子关于两道面试编程题之后个人的一点想法 面试题一: 大厅里有100盏灯,每盏灯都编了号码,分别为1-100.每盏灯由一个开关来控制.(开关按一下,灯亮,再按一下灯灭.开关的 ...

  5. 一道面试题引发的“血案”

    2019独角兽企业重金招聘Python工程师标准>>> 前言 es6之前,js的作用域只有两种,全局作用域和函数作用域,没有像C和java那样的块级作用域,于是对于学了C或者java ...

  6. 一道HashSet面试题引发的蝴蝶效应

    没错,我又借着"面试题"的名头来搞事情了,今天要说的是 HashSet ,而这确实是一个实际面试中遇到的问题.当时的场景大概是这样的,面试官在了解了你的知识广度以后,决心来考察一番 ...

  7. JavaScript中关于call函数的一道面试题

    来看一道面试题: ``` function fn1() {console.log(1);} function fn2() {console.log(2);} fn1.call(fn2);  //分析: ...

  8. 「一道面试题」输入URL到渲染全面梳理中-页面渲染篇

    前置知识 此文是一道面试题,又不仅仅是一道面试题,不过这道题共分了三篇来说,嗯..可想而知 接上文,上文我们讲了网络通信的部分,详细请看「一道面试题」输入URL到渲染全面梳理上-网络通信篇, 那么该说 ...

  9. h5 bootstrap 小程序模板_一道面试题小程序与H5的区别

    抛砖 此文是一道面试题,又不仅仅是一道面试题 面试题,在各个技术社区里都是一个永不落伍的话题,好像大多数人临面试前都会狂刷面试题,恨不得把所有面试题都看一遍,要说有用没,当然有用,因为大部分面试题确实 ...

最新文章

  1. nodejs获取ASP.Net WebAPI(IIS Windows验证)
  2. 一点一滴培养你的领导气质
  3. boost::safe_numerics模块实现混合类型产生令人惊讶的结果的测试程序
  4. 消控中心人员配置_关于2018年度环创中心楼宇物业综合管理考评情况的通报
  5. 初步使用计算机说课,初步认识计算机说课稿
  6. 一份所有中国人都应该听的歌单,你听过几首?
  7. TP5 急速上手 语法规则
  8. can使能上拉 gpio_单片机GPIO输入电压不可过大,最好使能上拉
  9. Starling实现的硬皮翻书效果
  10. python基础入门第0天
  11. hdu 2553 N皇后问题
  12. python字符串转化列表_Python列表到字符串的转换
  13. python的数值类型和运算符_Python全栈工程师(数值类型、运算符)
  14. codeforces346e
  15. Android 图片处理方法大全
  16. 计算机英语词汇_通信人必备英语词汇大全
  17. python三大神器之fabric
  18. Atitit 高性能架构法艾提拉著作 目录 1. 前期可以立即使用的技术 2 2. 分离法 3 2.1. Web db分离 3 2.2. 读写分离 4 2.3. CDN加速技术 4 2.4. 动静分
  19. 线程池,是时候做个了结了!
  20. 李开复创办创新工场的发言稿及访谈

热门文章

  1. 【科技金融】互金风控大数据盘点
  2. var_export
  3. 元宇宙iwemeta:互联网行业年底清算,税收优惠门槛抬高,阿里巴巴多交41亿税款
  4. 以太坊完整工作原理和运行机制!
  5. 从腾讯朋友圈揭秘内部AI部门竞争关系,谁能像微信当年一样熬出头? By 微胖2017年11月10日 09:06 撰文 | 宇多田 在腾讯合作伙伴大会上,腾讯首席运营官任宇昕提出的「AI in All」
  6. 这样用组图创作内容,能让你的文章被转发
  7. 信息检索与数据挖掘的常用加权技术。
  8. 管中窥豹,物联网之我见
  9. Java 8 - 正确高效的使用并行流
  10. 白话Elasticsearch73_ES生产集群中的索引管理01