引擎剖析:JS 中的字符串转数值(上)
in 技术 with 389 views and 5 comments

引擎剖析:JS 中的字符串转数值(上)

in 技术 with 5 comments

JS 中,字符串转数值的方式有以下 9 种:

  1. parseInt()
  2. parseFloat()
  3. Number()
  4. Double tilde (~~) Operator
  5. Unary Operator (+)
  6. Math.floor()
  7. Multiply with number
  8. The Signed Right Shift Operator(>>)
  9. The Unsigned Right Shift Operator(>>>)

这几种方式对运行结果的差异,如下表所示:

字符串转数值方案对比

对比表格的源码发布到了 https://airing.ursb.me/web/int.html,需要可自取。

除了运行结果上的存在差异之外,这些方法在性能上也存在着差异。在 NodeJS V8 环境下,这几个方法微基准测试的结果如下:

parseInt() x 19,140,190 ops/sec ±0.45% (92 runs sampled)
parseFloat() x 28,203,053 ops/sec ±0.25% (95 runs sampled)
Number() x 1,041,209,524 ops/sec ±0.20% (90 runs sampled)
Double tilde (~~) Operator x 1,035,220,963 ops/sec ±1.65% (97 runs sampled)
Math.floor() x 28,224,678 ops/sec ±0.23% (96 runs sampled)
Unary Operator (+) x 1,045,129,381 ops/sec ±0.17% (95 runs sampled)
Multiply with number x 1,044,176,084 ops/sec ±0.15% (93 runs sampled)
The Signed Right Shift Operator(>>) x 1,046,016,782 ops/sec ±0.11% (96 runs sampled)
The Unsigned Right Shift Operator(>>>) x 1,045,384,959 ops/sec ±0.08% (96 runs sampled)

可见,parseInt()parseFloat()Math.floor() 的效率最低,只有其他运算 2% 左右的效率,而其中又以parseInt()最慢,仅有 1%。

为什么这些方法存在着这些差异?这些运算在引擎层又是如何被解释执行的?接下来将从 V8、JavaScriptCore、QuickJS 等主流 JS 引擎的视角,探究这些方法的具体实现。

首先来看看 parsrInt()

1. parseInt()

ECMAScript (ECMA-262) parseInt

1.1 V8 中的 parseInt()

在 V8 [→ src/init/bootstrapper.cc] 中定义了 JS 语言内置的标准对象,我们可以找到其中关于 parseInt 的定义:

Handle<JSFunction> number_fun = InstallFunction(isolate_, global, "Number", JS_PRIMITIVE_WRAPPER_TYPE, JSPrimitiveWrapper::kHeaderSize, 0, isolate_->initial_object_prototype(), Builtin::kNumberConstructor);

// Install Number.parseInt and Global.parseInt.
Handle<JSFunction> parse_int_fun = SimpleInstallFunction(isolate_, number_fun, "parseInt", Builtin::kNumberParseInt, 2, true);

JSObject::AddProperty(isolate_, global_object, "parseInt", parse_int_fun,
 native_context()->set_global_parse_int_fun(*parse_int_fun);

可以见,Number.parseInt 和全局对象的 parseInt 都是基于 SimpleInstallFunction 注册的,它会将 API 安装到 isolate 中,并将该方法与 Builtin 做绑定。JS 侧调用 pasreInt 即为引擎侧调用 Builtin::kNumberParseInt

Builtin (Built-in Functions) 是 V8 中在 VM 运行时可执行的代码块,用于表达运行时对 VM 的更改。目前 V8 版本中 Builtin 有下述 5 种实现方式:

回到前文 Builtin::kNumberParseInt 这个函数,在 [→ src/builtins/builtins.h] 中可以看到其定义:

// Convenience macro to avoid generating named accessors for all builtins.
#define BUILTIN_CODE(isolate, name) \
  (isolate)->builtins()->code_handle(i::Builtin::k##name)

因此这个函数注册的原名是 NumberParseInt,实现在 [→ src/builtins/number.tq] 中,是个基于 Torque 的 Builtin 实现。

// ES6 #sec-number.parseint
transitioning javascript builtin NumberParseInt(
    js-implicit context: NativeContext)(value: JSAny, radix: JSAny): Number {
  return ParseInt(value, radix);
}

transitioning builtin ParseInt(implicit context: Context)(
    input: JSAny, radix: JSAny): Number {
  try {
    // Check if radix should be 10 (i.e. undefined, 0 or 10).
    if (radix != Undefined && !TaggedEqual(radix, SmiConstant(10)) &&
        !TaggedEqual(radix, SmiConstant(0))) {
      goto CallRuntime;
    }

    typeswitch (input) {
      case (s: Smi): {
        return s;
      }
      case (h: HeapNumber): {
        // Check if the input value is in Signed32 range.
        const asFloat64: float64 = Convert<float64>(h);
        const asInt32: int32 = Signed(TruncateFloat64ToWord32(asFloat64));
        // The sense of comparison is important for the NaN case.
        if (asFloat64 == ChangeInt32ToFloat64(asInt32)) goto Int32(asInt32);

        // Check if the absolute value of input is in the [1,1<<31[ range. Call
        // the runtime for the range [0,1[ because the result could be -0.
        const kMaxAbsValue: float64 = 2147483648.0;
        const absInput: float64 = math::Float64Abs(asFloat64);
        if (absInput < kMaxAbsValue && absInput >= 1.0) goto Int32(asInt32);
        goto CallRuntime;
      }
      case (s: String): {
        goto String(s);
      }
      case (HeapObject): {
        goto CallRuntime;
      }
    }
  } label Int32(i: int32) {
    return ChangeInt32ToTagged(i);
  } label String(s: String) {
    // Check if the string is a cached array index.
    const hash: NameHash = s.raw_hash_field;
    if (IsIntegerIndex(hash) &&
        hash.array_index_length < kMaxCachedArrayIndexLength) {
      const arrayIndex: uint32 = hash.array_index_value;
      return SmiFromUint32(arrayIndex);
    }
    // Fall back to the runtime.
    goto CallRuntime;
  } label CallRuntime {
    tail runtime::StringParseInt(input, radix);
  }
}

看这段代码前,先科普下 V8 中的几个数据结构:(V8 所有数据结构的定义可以见 [→ src/objects/objects.h])

我们知道 parseInt 接收两个形参, 即 parseInt(string, radix),此处亦如是。 实现流程如下:

那么焦点来到了 runtime::StringParseInt。[→ src/runtime/runtime-numbers.cc]

// ES6 18.2.5 parseInt(string, radix) slow path
RUNTIME_FUNCTION(Runtime_StringParseInt) {
  HandleScope handle_scope(isolate);
  DCHECK_EQ(2, args.length());
  Handle<Object> string = args.at(0);
  Handle<Object> radix = args.at(1);

  // Convert {string} to a String first, and flatten it.
  Handle<String> subject;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, subject,
                                     Object::ToString(isolate, string));
  subject = String::Flatten(isolate, subject);

  // Convert {radix} to Int32.
  if (!radix->IsNumber()) {
    ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, radix,
                                       Object::ToNumber(isolate, radix));
  }
  int radix32 = DoubleToInt32(radix->Number());
  if (radix32 != 0 && (radix32 < 2 || radix32 > 36)) {
    return ReadOnlyRoots(isolate).nan_value();
  }

  double result = StringToInt(isolate, subject, radix32);
  return *isolate->factory()->NewNumber(result);
}

这段逻辑比较简单,就不再一行行解读了。值得注意的是,根据标准,如果 radix 不在 2~36 的范围内,会返回 NaN。

1.2 JavaScriptCore 中的 parseInt()

接着我们来看看 JavaScriptCore 中的 parseInt()

JavaScriptCore 中关于 JS 语言内置对象的注册都在 [→ runtime/JSGlobalObjectFuntions.cpp] 文件中:

JSC_DEFINE_HOST_FUNCTION(globalFuncParseInt, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
    JSValue value = callFrame->argument(0);
    JSValue radixValue = callFrame->argument(1);

    // Optimized handling for numbers:
    // If the argument is 0 or a number in range 10^-6 <= n < INT_MAX+1, then parseInt
    // results in a truncation to integer. In the case of -0, this is converted to 0.
    //
    // This is also a truncation for values in the range INT_MAX+1 <= n < 10^21,
    // however these values cannot be trivially truncated to int since 10^21 exceeds
    // even the int64_t range. Negative numbers are a little trickier, the case for
    // values in the range -10^21 < n <= -1 are similar to those for integer, but
    // values in the range -1 < n <= -10^-6 need to truncate to -0, not 0.
    static const double tenToTheMinus6 = 0.000001;
    static const double intMaxPlusOne = 2147483648.0;
    if (value.isNumber()) {
        double n = value.asNumber();
        if (((n < intMaxPlusOne && n >= tenToTheMinus6) || !n) && radixValue.isUndefinedOrNull())
            return JSValue::encode(jsNumber(static_cast<int32_t>(n)));
    }

    // If ToString throws, we shouldn't call ToInt32.
    return toStringView(globalObject, value, [&] (StringView view) {
        return JSValue::encode(jsNumber(parseInt(view, radixValue.toInt32(globalObject))));
    });
}

WebKit 中的代码注释都很详尽易读,这里也不再解读了。最后,会调用 parseInt,JavaScriptCore 的 parseInt 的实现全放在了 [→ runtime/ParseInt.h] 中,核心代码如下:

ALWAYS_INLINE static bool isStrWhiteSpace(UChar c)
{
    // https://tc39.github.io/ecma262/#sec-tonumber-applied-to-the-string-type
    return Lexer<UChar>::isWhiteSpace(c) || Lexer<UChar>::isLineTerminator(c);
}

// ES5.1 15.1.2.2
template <typename CharType>
ALWAYS_INLINE
static double parseInt(StringView s, const CharType* data, int radix)
{
    // 1. Let inputString be ToString(string).
    // 2. Let S be a newly created substring of inputString consisting of the first character that is not a
    //    StrWhiteSpaceChar and all characters following that character. (In other words, remove leading white
    //    space.) If inputString does not contain any such characters, let S be the empty string.
    int length = s.length();
    int p = 0;
    while (p < length && isStrWhiteSpace(data[p]))
        ++p;

    // 3. Let sign be 1.
    // 4. If S is not empty and the first character of S is a minus sign -, let sign be -1.
    // 5. If S is not empty and the first character of S is a plus sign + or a minus sign -, then remove the first character from S.
    double sign = 1;
    if (p < length) {
        if (data[p] == '+')
            ++p;
        else if (data[p] == '-') {
            sign = -1;
            ++p;
        }
    }

    // 6. Let R = ToInt32(radix).
    // 7. Let stripPrefix be true.
    // 8. If R != 0,then
    //   b. If R != 16, let stripPrefix be false.
    // 9. Else, R == 0
    //   a. LetR = 10.
    // 10. If stripPrefix is true, then
    //   a. If the length of S is at least 2 and the first two characters of S are either ―0x or ―0X,
    //      then remove the first two characters from S and let R = 16.
    // 11. If S contains any character that is not a radix-R digit, then let Z be the substring of S
    //     consisting of all characters before the first such character; otherwise, let Z be S.
    if ((radix == 0 || radix == 16) && length - p >= 2 && data[p] == '0' && (data[p + 1] == 'x' || data[p + 1] == 'X')) {
        radix = 16;
        p += 2;
    } else if (radix == 0)
        radix = 10;

    // 8.a If R < 2 or R > 36, then return NaN.
    if (radix < 2 || radix > 36)
        return PNaN;

    // 13. Let mathInt be the mathematical integer value that is represented by Z in radix-R notation, using the letters
    //     A-Z and a-z for digits with values 10 through 35. (However, if R is 10 and Z contains more than 20 significant
    //     digits, every significant digit after the 20th may be replaced by a 0 digit, at the option of the implementation;
    //     and if R is not 2, 4, 8, 10, 16, or 32, then mathInt may be an implementation-dependent approximation to the
    //     mathematical integer value that is represented by Z in radix-R notation.)
    // 14. Let number be the Number value for mathInt.
    int firstDigitPosition = p;
    bool sawDigit = false;
    double number = 0;
    while (p < length) {
        int digit = parseDigit(data[p], radix);
        if (digit == -1)
            break;
        sawDigit = true;
        number *= radix;
        number += digit;
        ++p;
    }

    // 12. If Z is empty, return NaN.
    if (!sawDigit)
        return PNaN;

    // Alternate code path for certain large numbers.
    if (number >= mantissaOverflowLowerBound) {
        if (radix == 10) {
            size_t parsedLength;
            number = parseDouble(s.substring(firstDigitPosition, p - firstDigitPosition), parsedLength);
        } else if (radix == 2 || radix == 4 || radix == 8 || radix == 16 || radix == 32)
            number = parseIntOverflow(s.substring(firstDigitPosition, p - firstDigitPosition), radix);
    }

    // 15. Return sign x number.
    return sign * number;
}

ALWAYS_INLINE static double parseInt(StringView s, int radix)
{
    if (s.is8Bit())
        return parseInt(s, s.characters8(), radix);
    return parseInt(s, s.characters16(), radix);
}

template<typename CallbackWhenNoException>
static ALWAYS_INLINE typename std::invoke_result<CallbackWhenNoException, StringView>::type toStringView(JSGlobalObject* globalObject, JSValue value, CallbackWhenNoException callback)
{
    VM& vm = getVM(globalObject);
    auto scope = DECLARE_THROW_SCOPE(vm);
    JSString* string = value.toStringOrNull(globalObject);
    EXCEPTION_ASSERT(!!scope.exception() == !string);
    if (UNLIKELY(!string))
        return { };
    auto viewWithString = string->viewWithUnderlyingString(globalObject);
    RETURN_IF_EXCEPTION(scope, { });
    RELEASE_AND_RETURN(scope, callback(viewWithString.view));
}

// Mapping from integers 0..35 to digit identifying this value, for radix 2..36.
const char radixDigits[] = "0123456789abcdefghijklmnopqrstuvwxyz";

直接贴出了代码,因为 JavaScriptCore 中的 API 都是严格按照 ECMAScript (ECMA-262) parseInt 标准一步一步按流程实现,可读性和注释也很好,强烈建议读者自己阅读一下,此处不再解读。

1.3 QuickJS 中的 parseInt()

QuickJS 的核心代码都在 [→ quickjs.c] 中,首先是 parseInt 的注册代码:

/* global object */
static const JSCFunctionListEntry js_global_funcs[] = {
    JS_CFUNC_DEF("parseInt", 2, js_parseInt ),
    //...
}

js_parseInt 的实现逻辑如下:

static JSValue js_parseInt(JSContext *ctx, JSValueConst this_val,
                           int argc, JSValueConst *argv)
{
    const char *str, *p;
    int radix, flags;
    JSValue ret;

    str = JS_ToCString(ctx, argv[0]);
    if (!str)
        return JS_EXCEPTION;
    if (JS_ToInt32(ctx, &radix, argv[1])) {
        JS_FreeCString(ctx, str);
        return JS_EXCEPTION;
    }
    if (radix != 0 && (radix < 2 || radix > 36)) {
        ret = JS_NAN;
    } else {
        p = str;
        p += skip_spaces(p);
        flags = ATOD_INT_ONLY | ATOD_ACCEPT_PREFIX_AFTER_SIGN;
        ret = js_atof(ctx, p, NULL, radix, flags);
    }
    JS_FreeCString(ctx, str);
    return ret;
}

Bellard 大神的代码注释很少,但同时也非常精炼。

至此,本文介绍完了三个引擎下各自 parseInt 的实现,三者都是基于标准的实现,但由于代码风格不同,读起来也像是阅读三个风格不同散文大家的作品。

不过标准和实现,我们可以发现 parseInt 在真正执行字符串转数字这个操作做了非常多的前置操作,如入参合法判断、入参默认值、字符串格式判断与规整化、越界判断等等,最后再交由 runtime 处理。因此,我们不难推出其效率略低的原因。

接下来,我们再简单看看 parseFloat

2. parseFloat()

ECMAScript (ECMA-262) parseFloat

根据标准,parseFloat 与 parseInt 有两点明显的不同:

  1. 仅支持一个入参,不支持进制转换
  2. 返回值支持浮点型

2.1 V8 中的 parseFloat()

V8 中 parseFloat 的相关逻辑都紧挨着 parseInt,这里直接贴出关键实现:

[→ src/builtins/number.tq]

// ES6 #sec-number.parsefloat
transitioning javascript builtin NumberParseFloat(
    js-implicit context: NativeContext)(value: JSAny): Number {
  try {
    typeswitch (value) {
      case (s: Smi): {
        return s;
      }
      case (h: HeapNumber): {
        // The input is already a Number. Take care of -0.
        // The sense of comparison is important for the NaN case.
        return (Convert<float64>(h) == 0) ? SmiConstant(0) : h;
      }
      case (s: String): {
        goto String(s);
      }
      case (HeapObject): {
        goto String(string::ToString(context, value));
      }
    }
  } label String(s: String) {
    // Check if the string is a cached array index.
    const hash: NameHash = s.raw_hash_field;
    if (IsIntegerIndex(hash) &&
        hash.array_index_length < kMaxCachedArrayIndexLength) {
      const arrayIndex: uint32 = hash.array_index_value;
      return SmiFromUint32(arrayIndex);
    }
    // Fall back to the runtime to convert string to a number.
    return runtime::StringParseFloat(s);
  }
}

[→ src/runtime/runtime-numbers.cc]

// ES6 18.2.4 parseFloat(string)
RUNTIME_FUNCTION(Runtime_StringParseFloat) {
  HandleScope shs(isolate);
  DCHECK_EQ(1, args.length());
  Handle<String> subject = args.at<String>(0);

  double value = StringToDouble(isolate, subject, ALLOW_TRAILING_JUNK,
                                std::numeric_limits<double>::quiet_NaN());

  return *isolate->factory()->NewNumber(value);
}

因标准中的流程更为简易,因此较 parseInt 而言, parseFloat 更加简单易读。

2.2 JavaScriptCore 中的 parseFloat()

在 JavaScriptCore 中,parseFloat 的逻辑则更加简洁明了:

static double parseFloat(StringView s)
{
    unsigned size = s.length();

    if (size == 1) {
        UChar c = s[0];
        if (isASCIIDigit(c))
            return c - '0';
        return PNaN;
    }

    if (s.is8Bit()) {
        const LChar* data = s.characters8();
        const LChar* end = data + size;

        // Skip leading white space.
        for (; data < end; ++data) {
            if (!isStrWhiteSpace(*data))
                break;
        }

        // Empty string.
        if (data == end)
            return PNaN;

        return jsStrDecimalLiteral(data, end);
    }

    const UChar* data = s.characters16();
    const UChar* end = data + size;

    // Skip leading white space.
    for (; data < end; ++data) {
        if (!isStrWhiteSpace(*data))
            break;
    }

    // Empty string.
    if (data == end)
        return PNaN;

    return jsStrDecimalLiteral(data, end);
}

2.3 QuickJS 中的 parseFloat()

而对比 JavaScriptCore,QuickJS 则短短 12 行:

[→ quickjs.c]

static JSValue js_parseFloat(JSContext *ctx, JSValueConst this_val,
                             int argc, JSValueConst *argv)
{
    const char *str, *p;
    JSValue ret;

    str = JS_ToCString(ctx, argv[0]);
    if (!str)
        return JS_EXCEPTION;
    p = str;
    p += skip_spaces(p);
    ret = js_atof(ctx, p, NULL, 10, 0);
    JS_FreeCString(ctx, str);
    return ret;
}

不过对比之后可以知道,QuickJS 这里之所以短小,是没有做 ASCII 和 8Bit 的兼容。阅读 ECMAScript (ECMA-262) parseFloat 之后可以发现,QuickJS 这里的处理其实没有什么问题,最新的标准中并没有要求解释器要这样的兼容。

3. Number()

ECMAScript (ECMA-262) Number ( value )

3.1 V8 中的 Number()

Number 作为全局对象,定义还是在 [→ src/init/bootstrapper.cc] 中,在前文介绍 Number.parseInt 的注册时已然介绍过,我们回顾下:

Handle<JSFunction> number_fun = InstallFunction(
        isolate_, global, "Number", JS_PRIMITIVE_WRAPPER_TYPE,
        JSPrimitiveWrapper::kHeaderSize, 0,
        isolate_->initial_object_prototype(), Builtin::kNumberConstructor);
number_fun->shared().DontAdaptArguments();
number_fun->shared().set_length(1);
InstallWithIntrinsicDefaultProto(isolate_, number_fun,
                                     Context::NUMBER_FUNCTION_INDEX);

// Create the %NumberPrototype%
Handle<JSPrimitiveWrapper> prototype = Handle<JSPrimitiveWrapper>::cast(
        factory->NewJSObject(number_fun, AllocationType::kOld));
prototype->set_value(Smi::zero());
JSFunction::SetPrototype(number_fun, prototype);

// Install the "constructor" property on the {prototype}.
JSObject::AddProperty(isolate_, prototype, factory->constructor_string(),
                          number_fun, DONT_ENUM);

这段代码处理注册了 Number 这个对象之外,还初始化了它的原型链,并把构造函数添加到了它的原型链上。构造函数 Builtin::kNumberConstructor 是 Torque 实现的 Builtin,[→ src/builtins/constructor.tq] ,具体实现如下:

// ES #sec-number-constructor
transitioning javascript builtin
NumberConstructor(
    js-implicit context: NativeContext, receiver: JSAny, newTarget: JSAny,
    target: JSFunction)(...arguments): JSAny {
  // 1. If no arguments were passed to this function invocation, let n be +0.
  let n: Number = 0;
  if (arguments.length > 0) {
    // 2. Else,
    //    a. Let prim be ? ToNumeric(value).
    //    b. If Type(prim) is BigInt, let n be the Number value for prim.
    //    c. Otherwise, let n be prim.
    const value = arguments[0];
    n = ToNumber(value, BigIntHandling::kConvertToNumber);
  }

  // 3. If NewTarget is undefined, return n.
  if (newTarget == Undefined) return n;

  // 4. Let O be ? OrdinaryCreateFromConstructor(NewTarget,
  //    "%NumberPrototype%", « [[NumberData]] »).
  // 5. Set O.[[NumberData]] to n.
  // 6. Return O.

  // We ignore the normal target parameter and load the value from the
  // current frame here in order to reduce register pressure on the fast path.
  const target: JSFunction = LoadTargetFromFrame();
  const result = UnsafeCast<JSPrimitiveWrapper>(
      FastNewObject(context, target, UnsafeCast<JSReceiver>(newTarget)));
  result.value = n;
  return result;
}

注释中的 1-6 一一对应着[ECMAScript (ECMA-262) Number ( value )]标准中的流程 1-6,因此本文不再花篇章赘述其实现。需要注意的是,标准中明确说明了 Number 是支持 BigInt 的,各引擎的实现也着重注意了这点,这也证明了我们之前运算对照表中的结果。

3.2 JavaScriptCore 中的 Number()

JavaScriptCore 中的这段代码则缺少注释,但逻辑上与 V8 一模一样,遵循标准:

[→ runtime/NumberConstructor.cpp]

// ECMA 15.7.1
JSC_DEFINE_HOST_FUNCTION(constructNumberConstructor, (JSGlobalObject* globalObject, CallFrame* callFrame))
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);
    double n = 0;
    if (callFrame->argumentCount()) {
        JSValue numeric = callFrame->uncheckedArgument(0).toNumeric(globalObject);
        RETURN_IF_EXCEPTION(scope, { });
        if (numeric.isNumber())
            n = numeric.asNumber();
        else {
            ASSERT(numeric.isBigInt());
            numeric = JSBigInt::toNumber(numeric);
            ASSERT(numeric.isNumber());
            n = numeric.asNumber();
        }
    }

    JSObject* newTarget = asObject(callFrame->newTarget());
    Structure* structure = JSC_GET_DERIVED_STRUCTURE(vm, numberObjectStructure, newTarget, callFrame->jsCallee());
    RETURN_IF_EXCEPTION(scope, { });

    NumberObject* object = NumberObject::create(vm, structure);
    object->setInternalValue(vm, jsNumber(n));
    return JSValue::encode(object);
}

3.3 QuickJS 中的 Number()

Number 对象及其原型链的注册代码如下所示:

[→ quickjs.c]

void JS_AddIntrinsicBaseObjects(JSContext *ctx)
{
    //...

    /* Number */
    ctx->class_proto[JS_CLASS_NUMBER] = JS_NewObjectProtoClass(ctx, ctx->class_proto[JS_CLASS_OBJECT], JS_CLASS_NUMBER);

    JS_SetObjectData(ctx, ctx->class_proto[JS_CLASS_NUMBER], JS_NewInt32(ctx, 0));
    JS_SetPropertyFunctionList(ctx, ctx->class_proto[JS_CLASS_NUMBER], js_number_proto_funcs, countof(js_number_proto_funcs));

    number_obj = JS_NewGlobalCConstructor(ctx, "Number", js_number_constructor, 1, ctx->class_proto[JS_CLASS_NUMBER]);

    JS_SetPropertyFunctionList(ctx, number_obj, js_number_funcs, countof(js_number_funcs));
}

同样的时候,在原型链注册的时候绑上了构造函数 js_number_constructor

static JSValue js_number_constructor(JSContext *ctx, JSValueConst new_target,
                                     int argc, JSValueConst *argv)
{
    JSValue val, obj;
    if (argc == 0) {
        val = JS_NewInt32(ctx, 0);
    } else {
        val = JS_ToNumeric(ctx, argv[0]);
        if (JS_IsException(val))
            return val;
        switch(JS_VALUE_GET_TAG(val)) {
#ifdef CONFIG_BIGNUM
        case JS_TAG_BIG_INT:
        case JS_TAG_BIG_FLOAT:
            {
                JSBigFloat *p = JS_VALUE_GET_PTR(val);
                double d;
                bf_get_float64(&p->num, &d, BF_RNDN);
                JS_FreeValue(ctx, val);
                val = __JS_NewFloat64(ctx, d);
            }
            break;
        case JS_TAG_BIG_DECIMAL:
            val = JS_ToStringFree(ctx, val);
            if (JS_IsException(val))
                return val;
            val = JS_ToNumberFree(ctx, val);
            if (JS_IsException(val))
                return val;
            break;
#endif
        default:
            break;
        }
    }
    if (!JS_IsUndefined(new_target)) {
        obj = js_create_from_ctor(ctx, new_target, JS_CLASS_NUMBER);
        if (!JS_IsException(obj))
            JS_SetObjectData(ctx, obj, val);
        return obj;
    } else {
        return val;
    }
}

值得关注的是 QuickJS 追求精简小巧,因此可以自行配置是否支持 BigInt,其余逻辑依然遵循标准。

由于博客篇幅所限,不得不拆文否则会引发博客框架的渲染 bug,剩余部分在下篇:《引擎剖析:JS 中的字符串转数值(下)》https://me.ursb.me/archives/js-string-to-number-2.html

Responses
  1. 君君

    讲的真的超级详细,大赞!

    Reply
  2. 山山山山

    哇好强!!!

    Reply
  3. 好牛

    Reply
    1. @4Ark

      我看了你的博客,很喜欢你周刊的想法和形式,已添加进「友人帐」里了

      Reply
  4. Walle

    跟着dalao一起学习

    Reply