一道面试题引发的对javascript类型转换的思考


Posted in Javascript onMarch 06, 2017

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

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

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 类型转换

在某个操作或者运算需要字符串的时候,会触发 Object 的 String 转换,举个例子:

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

转换规则:

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

上面的例子实际上是:

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.toString()

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

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

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

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

下面我们尝试证明如果 toString() 方法不可用的时候系统会调用 valueOf() 方法,下面我们改写对象的 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 方法均不可用的情况下,系统会直接返回一个错误。

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() 其中一个,会优先调用被改写了的方法,而如果两个同时改写,则会像 String 转换规则一样,优先查询 valueOf() 方法,在 valueOf() 方法返回的是非原始类型的情况下再查询 toString() 方法。

如果你能认真读完,相信会有所收获。

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,同时也希望多多支持三水点靠木!

Javascript 相关文章推荐
Jquery数独游戏解析(一)-页面布局
Nov 05 Javascript
IE与FF下javascript获取网页及窗口大小的区别详解
Jan 14 Javascript
JS+CSS实现Li列表隔行换色效果的方法
Feb 16 Javascript
JavaScript点击按钮后弹出透明浮动层的方法
May 11 Javascript
对JavaScript客户端应用编程的一些建议
Jun 24 Javascript
JS函数修改html的元素内容,及修改属性内容的方法
Oct 28 Javascript
Angular页面间切换及传值的4种方法
Nov 04 Javascript
js replace()去除代码中空格的实例
Feb 14 Javascript
解决Vue 通过下表修改数组,页面不渲染的问题
Mar 08 Javascript
vue input 输入校验字母数字组合且长度小于30的实现代码
May 16 Javascript
如何在微信小程序中使用骨架屏的步骤
Jun 12 Javascript
vue+element-ui JYAdmin后台管理系统模板解析
Jul 28 Javascript
JavaScript函数参数的传递方式详解
Mar 06 #Javascript
jQuery实现遍历复选框的方法示例
Mar 06 #Javascript
jQuery.cookie.js使用方法及相关参数解释
Mar 06 #Javascript
jQuery Validate 相关参数及常用的自定义验证规则
Mar 06 #Javascript
jQuery简单实现遍历单选框的方法
Mar 06 #Javascript
jQuery在header中设置请求信息的方法
Mar 06 #Javascript
jQuery插件FusionCharts实现的2D柱状图效果示例【附demo源码下载】
Mar 06 #Javascript
You might like
php的一个登录的类 [推荐]
2007/03/16 PHP
PHP面向接口编程 耦合设计模式 简单范例
2011/03/23 PHP
php 表单提交大量数据发生丢失的解决方法
2014/03/03 PHP
ThinkPHP连接数据库的方式汇总
2014/12/05 PHP
PHP创建文件,并向文件中写入数据,覆盖,追加的实现代码
2016/03/25 PHP
PHP实现二维数组中的查找算法小结
2018/06/09 PHP
php实现的支付宝网页支付功能示例【基于TP5框架】
2019/09/16 PHP
重定向实现代码
2006/11/20 Javascript
使用javascript访问XML数据的实例
2006/12/27 Javascript
js修改table中Td的值(定义td的单击事件)
2013/01/10 Javascript
jquery实现动态菜单的实例代码
2013/11/28 Javascript
禁止iframe页面的所有js脚本如alert及弹出窗口等
2014/09/03 Javascript
node.js中的fs.writeFile方法使用说明
2014/12/14 Javascript
基于node实现websocket协议
2016/04/25 Javascript
JavaScript中判断数据类型的方法总结
2016/05/24 Javascript
js实现String.Fomat的实例代码
2016/09/02 Javascript
JS中使用gulp实现压缩文件及浏览器热加载功能
2017/07/12 Javascript
node.js爬取中关村的在线电瓶车信息
2018/11/13 Javascript
微信小程序如何修改本地缓存key中单个数据的详解
2019/04/26 Javascript
浅析微信小程序modal弹窗关闭默认会执行cancel问题
2019/10/14 Javascript
详解vue中v-bind:style效果的自定义指令
2020/01/21 Javascript
JS JQuery获取data-*属性值方法解析
2020/09/01 jQuery
Python入门篇之文件
2014/10/20 Python
Pytorch使用PIL和Numpy将单张图片转为Pytorch张量方式
2020/05/25 Python
有关HTML5中背景音乐的自动播放功能
2017/10/16 HTML / CSS
Otticanet美国:最顶尖的世界名牌眼镜, 能得到打折季的价格
2019/03/10 全球购物
GUESS Factory加拿大:牛仔裤、服装及配饰
2019/09/20 全球购物
String是最基本的数据类型吗?
2013/06/13 面试题
自我介绍演讲稿范文
2014/08/21 职场文书
机关干部四风问题自查报告及整改措施
2014/10/26 职场文书
晚会闭幕词
2015/01/28 职场文书
党员转正大会主持词
2015/07/02 职场文书
遇事可以测出您的见识与格局
2019/09/16 职场文书
python之基数排序的实现
2021/07/26 Python
SQL Server使用T-SQL语句批处理
2022/05/20 SQL Server
使用Postman测试需要授权的接口问题
2022/06/21 Java/Android