一道面试题引发的对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 相关文章推荐
浏览器图片选择预览、旋转、批量上传的JS代码实现
Dec 04 Javascript
jQuery-1.9.1源码分析系列(十一)DOM操作续之克隆节点
Dec 01 Javascript
jquery使用on绑定a标签无效 只能用live解决
Jun 02 Javascript
JavaScript中有关一个数组中最大值和最小值及它们的下表的输出的解决办法
Jul 01 Javascript
jQuery实现复制到粘贴板功能
Feb 11 Javascript
bootstrap datepicker插件默认英文修改为中文
Jul 28 Javascript
详谈构造函数加括号与不加括号的区别
Oct 26 Javascript
JS实现520 表白简单代码
May 21 Javascript
JavaScript设计模式之工厂模式简单实例教程
Jul 03 Javascript
vue.extend与vue.component的区别和联系
Sep 19 Javascript
Vue源码之关于vm.$delete()/Vue.use()内部原理详解
May 01 Javascript
基于vue中的scoped坑点解说
Sep 04 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
《OVERLORD》手游英文版即将上线 手机上也能扮演骨王
2020/04/09 日漫
探讨PHP使用eAccelerator的API开发详解
2013/06/09 PHP
php实现无限级分类查询(递归、非递归)
2016/03/10 PHP
PHP与Web页面的交互示例详解二
2020/08/04 PHP
农历与西历对照
2006/09/06 Javascript
javascript中获取选中对象的类型
2007/04/02 Javascript
JAVASCRIPT下判断IE与FF的比较简单的方式
2008/10/17 Javascript
Mootools 1.2教程 定时器和哈希简介
2009/09/15 Javascript
判断js对象是否拥有某一个属性的js代码
2013/08/16 Javascript
原生js实现日期联动
2015/01/12 Javascript
JavaScript与HTML的结合方法详解
2015/11/23 Javascript
jquery实现无刷新验证码的简单实例
2016/05/19 Javascript
jQuery插件Validation快速完成表单验证的方式
2016/07/28 Javascript
js实现碰撞检测特效代码分享
2016/10/16 Javascript
Node.JS利用PhantomJs抓取网页入门教程
2017/05/19 Javascript
jquery.guide.js新版上线操作向导镂空提示jQuery插件(推荐)
2017/05/20 jQuery
angularjs中判断ng-repeat是否迭代完的实例
2018/09/12 Javascript
微信小程序获取用户绑定手机号方法示例
2019/07/21 Javascript
js动态获取时间的方法分析
2019/08/02 Javascript
[01:23]2014DOTA2国际邀请赛 球迷无处不在Ti现场世界杯受关注
2014/07/10 DOTA
[02:54]辉夜杯主赛事第二日败者组 iG.V赛后采访
2015/12/26 DOTA
Python 过滤字符串的技巧,map与itertools.imap
2008/09/06 Python
Python和Ruby中each循环引用变量问题(一个隐秘BUG?)
2014/06/04 Python
python中的多重继承实例讲解
2014/09/28 Python
Python编写生成验证码的脚本的教程
2015/05/04 Python
Django框架实现分页显示内容的方法详解
2019/05/10 Python
Python中Numpy mat的使用详解
2019/05/24 Python
Python 分发包中添加额外文件的方法
2019/08/16 Python
Python序列对象与String类型内置方法详解
2019/10/22 Python
python 使用shutil复制图片的例子
2019/12/13 Python
PyTorch 中的傅里叶卷积实现示例
2020/12/11 Python
美国运动鞋和服装网上商店:YCMC
2018/09/15 全球购物
澳大利亚第一旅行车和房车配件店:Caravan RV Camping
2020/12/26 全球购物
简述数据库的设计过程
2015/06/22 面试题
学生会竞选演讲稿
2014/04/24 职场文书
主持人开场白台词
2015/05/29 职场文书