[Effective JavaScript 笔记]第28条:不要信赖函数对象的toString方法

js函数有一个非凡的特性,即将其源代码重现为字符串的能力。

(function(x){
   return x+1
}).toString();//"function (x){   return x+1}"

反射获取函数源代码的功能很强大,使用函数对象的toString方法有严重的局限性。
toString方法的局限性
ECMAScript标准对函数对象的toString方法的返回结果(即该字符串)并没有任何要求。这意味着不同的js引擎将产生不同的字符串,甚至产生的字符串与该函数并不相关。

如果函数是使用纯js实现的,那么js引擎会试图提供该函数的源代码的真实表示。

一个失败的例子

(function(x){
     return x+1
}).bind(16).toString();//"function () { [native code] }"

失败原因:

使用了由宿主环境的内置库提供的函数。

  • 由于许多宿主环境中,bind函数是由其他编程语言实现的(通常是c++)。宿主环境提供的是一个编译后的函数,在此环境下该函数没有js的源代码供显示。
  • 由于标准允许浏览器引擎改变toString方法的输出,很容易使编写的程序一个js系统中正确运行,在其他js系统中却无法正确运行。程序对函数的源代码字符串的具体细节很敏感,即使js的实现有一点细微的变化都可能破坏程序。
  • 由toString方法生成的源代码并不展示闭包中保存的与内部变量引用相关的值
(function(x){
   return function(y){
      return x+y;
   }
})(42).toString();//"function (y){      return x+y;   }"

注意:尽管函数实际上是一个绑定x为42的闭包,但结果字符串仍然包含一个引用x的变量。

从某种意义上说,js的toString方法的这些局限使其用来提取函数源代码并不是特别有用和值得信赖。通常应该避免使用它。对提取函数源代码相当复杂的使用应当采用精心制作的js解释器和处理库。将js函数看作是一个不该违背的抽象是最稳妥的。

提示

  • 当调用函数的toString方法时,并没有要求js引擎能够精确地获取到函数的源代码
  • 由于在不同的引擎下调用toString方法的结果可能不同,所以绝不要信赖函数源代码的详细细节
  • toString方法的执行结果并不会暴露存储在闭包中的局部变量
  • 通常情况下,应该避免使用函数对象的toString方法

附录一:toString方法

不同数据类型调用toString方法的结果。
toString方法是Object原型对象中的一个方法,所以继承自这个类的对象都会继承这个方法,并可以对toString方法进行覆盖。
js标准库中的5种简单数据类型:Undefined,Null,Boolean,Number和String。还有一种复杂的数据类型Object,Object的本质是一组无序的名值对组成。

简单数据类型

//数字
(Undefined).toString();//"error"
(Null).toString();//error
(true).toString();//"true"
(1).toString();//"1"
(‘111‘).toString();//"111"

可以看出其中除了Undefined和Null类型外,为什么其它几个基本类型可以运行呢。
这在我们之前的文章《[Effective JavaScript 笔记] 第4条:原始类型优于封闭对象》中讲到,当简单数据类型调用toString方法会首先把原始类型转换成包装对象。
此时对应的包装对象为

  • 数字为Number对象
  • 布尔值为Boolean对象
  • 字符串为String对象

这些对象也都是继承自Object对象的,并重写了各自的toString方法。
但Undefined类型和Null类型都只有一个值undefined,null,并没有对应的封装对象。
虽然typeof null的值是"object",但并没用。

引用类型

//Object对象
({a:10,b:20}).toString();//"[object Object]"//Date对象
(new Date).toString();//"Tue Jun 07 2016 15:37:15 GMT+0800 (中国标准时间)"//RegExp对象
(/^sss$/g).toString();//"/^sss$/g"//Function对象function aa(){return "bb"}
aa.toString();//"function aa(){return "bb"}"//window对象window.toString();//"[object Window]"//Math对象Math.toString();//"[object Math]"

看到上面的toString方法,Object,window,Math是使用Object原型方法。其它对象都使用了自身覆盖的toString方法。

typeof操作符

对以上所有类型使用typeof操作符时会得到以下的结果

typeof 1;//"number"typeof ‘1‘;//"string"typeof true;//"boolean"typeof undefined;//"undefined"typeof (function a(){});//"function"typeof null;//"object"typeof {};//"object"typeof (new Date);//"object"typeof [];//"object"typeof window;//"object"typeof Math;//"object"typeof (/sdfsf/g);//"object"

可以看出,想使用单单的typeof操作符来对类型进行判断几乎是不可能的。
有人可能会说对于返回object字符串,可以使用构造函数来判断类型即instanceOf方法。

({}) instanceof Object;//true
(new Date) instanceof Date;//true
([]) instanceof Array;//true
(/sdfsf/g) instanceof RegExp;//true

然后null类型只要

var a=null;
a===null;//true;

好像可以实现下面这样的类型判断代码了

function getType(obj){
    if(typeof obj !== ‘object‘){
        return typeof obj;
    }else{
        if(obj===null){
            return ‘null‘;
        }
        if(obj===window){
            return ‘window‘;
        }
        if(obj===Math){
            return ‘Math‘
        }
        if((obj) instanceof Date){
            return ‘date‘;
        }
        if((obj) instanceof Array){
            return ‘array‘;
        }
        if((obj) instanceof RegExp){
            return ‘regexp‘;
        }
        if((obj) instanceof Object){
            return ‘object‘;
        }

    }
}

上面代码是否可以运行测试一下,并没有问题

getType(1);//"number"
getType(true);//"boolean"
getType(‘1‘);//"string"
getType(undefined);//"undefined"
getType(function(){});//"function"
getType(/sf/);//"regexp"
getType(null);//"null"
getType(window);//"window"
getType({});//"object"
getType([]);//"array"

但这里要注意的一个问题就是,这个代码里的对于object类型的检测一定要放到最后面。
如下所示,所有对象都是继承自Object,所以instanceof检测所有对象是否为Object类型的实例返回都是true

([]) instanceof Array;//true
([]) instanceof Object;//true

看到以上代码是不是觉得太复杂麻烦了,有没有一种更简单的方法来对类型进行判断呢?答案当然是有,下面来看toString方法的运用。

toString应用

如上面所说,继承自Object的对象都有toString方法,但每个对象实现了各自的toString方法,导致无法用toString方法进行类型判断。这里可以利用之前讲到过的call或apply方法来调用Object.prototype.toString方法。

function getType(obj){
   var toString=Object.prototype.toString;
   return toString.call(obj);
}

测试一下各类型会得到如下结果

getType(1);//"[object Number]"
getType(true);//"[object Boolean]"
getType(‘1‘);//"[object String]"
getType(undefined);//"[object Undefined]"
getType(function(){});//"[object Function]"
getType(/sf/);//"[object RegExp]"
getType(null);//"[object Null]"
getType(window);//"[object Window]"
getType({});//"[object Object]"
getType([]);//"[object Array]"
getType(Math);//"[object Math]"

所有类型都可以区分出来,是不是很简单呀?这个是不是就可以万事大吉了呢,错,还有个特殊的值没有处理NaN.

getType(NaN);//"[object Number]"

NaN并不是一个Number类型的数,它是表达不是一个数字的值,这里对这个值也要进行处理。可以关注之前文章《》里关于NaN的内容。处理代码如下

function isReallyNaN(x){
   return x!==x;
}

完整的版本

function getType(obj){
   if(obj!==obj)return "NaN";
   var toString=Object.prototype.toString;
   return toString.call(obj);
}
getType(NaN);//"NaN"

备忘:

这里需要去了解一下,js解释器的知识。
相关的链接有:
javascript设计模式之解释器模式详解
javascript设计模式 - 解释器模式(interpreter)
Chrome V8

时间: 2024-12-25 04:59:17

[Effective JavaScript 笔记]第28条:不要信赖函数对象的toString方法的相关文章

[Effective JavaScript 笔记]第58条:区分数组对象和类数组对象

示例 设想有两个不同类的API.第一个是位向量:有序的位集合 var bits=new BitVector(); bits.enable(4); bits.enable([1,3,8,17]); bits.bitAt(4);//1 bits.bitAt(8);//1 bits.bitAt(9);//0 enable方法被重载了,可以传入一个索引或索引的数组.第二个类的API是字符串集合:无序的字符串集合 var set=new StringSet(); set.add('Hamlet'); se

[Effective JavaScript 笔记]第27条:使用闭包而不是字符串来封装代码

函数是一种将代码作为数据结构存储的便利方式,代码之后可以被执行.这使得富有表现力的高阶函数抽象如map和forEach成为可能.它也是js异步I/O方法的核心.与此同时,也可以将代码表示为字符串的形式传递给eval函数以达到同样的功能.程序员面临一个选择:应该将代码表示为函数还是字符串?毫无疑问,应该将代码表示为函数.字符串表示代码不够灵活的一个重要原因是:它们不是闭包. 闭包回顾 看下面这个图 js的函数值包含了比调用它们时执行所需要的代码还要多的信息.而且js函数值还在内部存储它们可能会引用

[Effective JavaScript 笔记] 第4条:原始类型优于封闭对象

js有5种原始值类型:布尔值.数字.字符串.null和undefined. 用typeof检测一下: typeof true; //"boolean" typeof 2; //"number" typeof "s";//"string" typeof null;//"object":ECMAScript把null描述为独特的类型,但返回值却是对象类型,有点困惑. 可以使用Object.prototype.t

[Effective JavaScript 笔记]第45条:使用hasOwnProperty方法以避免原型污染

之前的43条,44条讨论了属性的枚举,但都没有彻底地解决属性查找中原型污染的问题.看下面关于字典的一些操作 'zhangsan' in dict; dict.zhangsan; dict.zhangsan=22; js的对象操作总是经继承的方式工作的.即使是一个空的对象字面量也是继承了Object.protoype属性. var dict={}; 'zhangsan' in dict;//false 'lisi' in dict;//false 'wangwu' in dict;//false'

[Effective JavaScript 笔记]第51条:在类数组对象上复用通用的数组方法

前面有几条都讲过关于Array.prototype的标准方法.这些标准方法被设计成其他对象可复用的方法,即使这些对象并没有继承Array. arguments对象 在22条中提到的函数arguments对象.它是一个类数组对象,并不是一个标准的数组,所以无法使用数组原型中的方法,因此无法使用arguments.forEach这样的形式来遍历每一个参数.这里我们必须使用call方法来对使用forEach方法. function highlight(){ [].forEach.call(argume

[Effective JavaScript 笔记]第47条:绝不要在Object.prototype中增加可枚举的属性

之前的几条都不断地重复着for...in循环,它便利好用,但又容易被原型污染.for...in循环最常见的用法是枚举字典中的元素.这里就是从侧面提出不要在共享的Object.prototype中增加可枚举的属性.这就导致,我们在开发的时候,不能在Object.prototype中添加有用的方法.如,我们想增加一个产生对象属性名数组的allKeys方法将会怎么样? Object.prototype.allKeys=function(){ var res=[]; for(var key in thi

[Effective JavaScript 笔记]第68条:使用promise模式清洁异步逻辑

构建异步API的一种流行的替代方式是使用promise(有时也被称为deferred或future)模式.已经在本章讨论过的异步API使用回调函数作为参数. downloadAsync('file.txt',function(file){ console.log('file:'+file); }); 基于promise的API不接收回调函数作为参数.相反,它返回一个promise对象,该对象通过其自身的then方法接收回调函数. var p=downloadP('file.txt'); p.th

[Effective JavaScript 笔记]第46条:使用数组而不要使用字典来存储有序集合

对象属性无序性 js对象是一个无序属性集合. var obj={}; obj.a=10; obj.b=30; 属性a和属性b并没有谁前谁后之说.for...in循环,先输出哪个属性都有可能.获取和设置不同的属性与顺序无关,都会以大致相同的效率产生相同的结果.也就是说访问属性a和访问属性b,没有哪个访问更快之说.ES标准并未规定属性存储的任何特定顺序,甚至于枚举对象也未涉及.for...in循环会挑选一定的顺序来枚举对象的属性,标准允许js引擎自由选择一个顺序,它们的选择会微妙地改变程序行为.如要

[Effective JavaScript 笔记]第15条:当心局部块函数声明笨拙的作用域

嵌套函数声明.没有标准的方法在局部块里声明函数,但可以在另一个函数的顶部嵌套函数声明. function f(){return "global"} function test(x){ var result=[]; function f(){return "local";}//block-local if(x){ result.push(f()); } result.push(f()); return result; } test(true);//["loc