小结 Number 对引用类型的处理

在《JavaScript 高级程序设计》的数据转换一节中, 作者写到将非数字型数据转换为数字型时, 可以使用 Number 转型函数. 对于数字, Number 会调用 parseInt 或 parseFloat 方法; 而对于字符串, undefined, null, 布尔值或引用类型, 则会按 Number 自己的一套转换原则进行处理.

最后, 作者提到(第 26 页): 在处理引用类型时, “如果是对象, 则调用对象的 valueOf() 方法, 然后依照前面的规则转换返回的值. 如果转换的结果是 NaN, 则调用对象的 toString() 方法, 然后再依照前面的规则转换返回的字符串”. 也就是说, 对于会返回 NaN 的情况, Number 都会再调用该对象的 toString 方法. 但事实似乎并不如此. 可以看看这个例子:

var obj = {
    valueOf: function() {alert('valueOf')},
    toString: function() {alert('toString')}
};

Number(obj) // 调用 valueOf(), 并且返回 NaN

运行这段代码, 会发现 Number 只调用了对象的 valueOf 方法, 并返回 NaN. toString 没有调用. 这是为什么呢? 开始我猜测会不会和 valueOf 的返回值有关系? 这次我让它明确返回 NaN 试试:

valueOf: function() {return NaN}

还是没有调用 toString.

但我在测试中发现如果不改写 valueOf, 会有一个有趣的现象:

var obj = {
    toString: function() {alert("toString")}
};

Number(obj) // 调用 toString()

这说明 Number 确实会调用对象的 toString 方法. 不过很明显, 根据上面的测试结果, Number 只会在某些情况下才会调有它.

有些头绪了, 再回到 valueOf. 默认情况下它会返回对象本身. 在第一次改写中, valueOf 的返回值是 undefined. 而在 Number 转换规则中, undefined 会返回为 NaN. 这是否是说, Number 会根据对象 valueOf 的返回值类型不同决定是否调用 toString 方法呢? 再来做一个测试:

var obj = {
    valueOf: function() {alert("valueOf"); return {}},
    toString: function() {alert("toString")}
};

Number(obj) // 成功

这下终于找到了症结所在: Number 在转换引用类型时, 首先会调用 valueOf 方法, 然后再根据其返回值的类型, 再决定下一步操作: 返回值为基本类型, 根据 Number 的转换原则进行处理; 如果返回值是引用类型, 则调用 toString 方法进行处理(toString 后返回的值就是字符串了).

看到我研究这个, 邱吉给我找到了 V8 的源码: 点击查看. 在这里帮我查到了 Number 的实现机制. 有了这个就更清楚到底是怎么一回事了:

function NonNumberToNumber(x) {
        if (IS_STRING(x)) {
            return %_HasCachedArrayIndex(x) ? %_GetCachedArrayIndex(x) : %StringToNumber(x);
        }
        if (IS_BOOLEAN(x)) return x ? 1 : 0;
        if (IS_UNDEFINED(x)) return $NaN;
        return (IS_NULL(x)) ? 0 : ToNumber(%DefaultNumber(x));
    }

我们具体来看下当 x 为引用类型时怎么处理. 根据代码, 最终会调用 DefaultNumber() 函数对引用类型进行处理. 再看看这个函数是怎么定义的:

function DefaultNumber(x) {
        var valueOf = x.valueOf;
        if (IS_SPEC_FUNCTION(valueOf)) {
            var v = %_CallFunction(x, valueOf);
            if (%IsPrimitive(v)) return v;
        }

      var toString = x.toString;
        if (IS_SPEC_FUNCTION(toString)) {
            var s = %_CallFunction(x, toString);
            if (%IsPrimitive(s)) return s;
        }

      throw %MakeTypeError('cannot_convert_to_primitive', []);
    }

可以看到, 首先 DefaultNumber() 会取到引用类型的 valueOf 方法, 并确认其是一个可执行的函数. 然后调用它, 再判断返回值. 这又涉及到另一个函数 IsPrimitive():

function IsPrimitive(x) {
        // Even though the type of null is "object", null is still
        // considered a primitive value. IS_SPEC_OBJECT handles this correctly
        // (i.e., it will return false if x is null).
        return !IS_SPEC_OBJECT(x);
    }

这个函数很简单, 会判断参数值是否为引用类型, 并返回布尔值. 结合之前的代码, 当 valueOf 的返回值不是引用类型时, 则直接给出这个返回值(接着再根据 Number 函数的转型规则给出最终答案); 如果是引用类型, 就再调用 toString 方法, 并返回结果. 当然, 如果 toString 的值还是一个引用类型, 就直接抛出错误鸟.

至此就豁然开朗了: DefaultNumber() 函数会根据 valueOf 的返回值类型, 决定是否调用 toString.
由此可见, Number 对引用类型的处理并非这么简单, 当 valueOf 返回使 Number 函数给出 NaN 的返回值时就调用对象的 toString 方法. 这是作者的疏忽, 还是翻译的漏洞, 就不得而知鸟…

2011-12-23 17:57185