JS 中,字符串轉數值的方式有以下 9 種:
- parseInt()
- parseFloat()
- Number()
- Double tilde (~~) Operator
- Unary Operator (+)
- Math.floor()
- Multiply with number
- The Signed Right Shift Operator(>>)
- 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 種實現方式:
- Platform-dependent assembly language:很高效,但需要手動適配到所有平台,並且難以維護。
- C++:風格與 runtime functions 非常相似,可以訪問 V8 強大的運行時功能,但通常不適合性能敏感區域。
- JavaScript:緩慢的運行時調用,受類型污染導致的不可預測的性能影響,以及複雜的 JS 語義問題。現在 V8 不再使用 JavaScript 內置函數。
- CodeStubAssembler:提供高效的低級功能,非常接近匯編語言,同時保持平台依賴無關性和可讀性。
- Torque:是 CodeStubAssembler 的改進版,其語法結合了 TypeScript 的一些特徵,非常簡單易讀。強調在不損失性能的前提下盡量降低使用難度,讓 Builtin 的開發更加容易一些。目前不少內置函數都是由 Torque 實現的。
回到前文 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])
- Smi:繼承自 Object,immediate small integer,只有 31 位
- HeapObject:繼承自 Object,superclass for everything allocated in the heap
- PrimitiveHeapObject:繼承自 HeapObject
- HeapNumber:繼承自 PrimitiveHeapObject,存儲了數字的堆對象,用於保存大整形的對象。
我們知道 parseInt
接收兩個形參, 即 parseInt(string, radix)
,此處亦如是。 實現流程如下:
- 首先判斷
radix
是否沒傳或者傳了 0 或 10,如果不是,那麼則不是十進制的轉換,就走 runtime 中提供的StringParseInt
函數runtime::StringParseInt
; - 如果是十進制轉換就繼續走,判斷第一個參數的數據類型。
- 如果是 Smi 或者是沒有越界(超 31 位)的 HeapNumber,那麼就直接 return 入參,相當於沒有轉化;否則同樣走
runtime::StringParseInt
。注意如果這裡越界了就會走ChangeInt32ToTagged
,其為 CodeStubAssembler 實現的一个函數,會強轉 Int32,如果當前執行環境不允許溢出 32 位,那麼轉換之後的數字就會不合預期。 - 如果是 String,則判斷是否是 hash,如果是的就找到對應整型 value 返回;否則依然走
runtime::StringParseInt
。
- 如果是 Smi 或者是沒有越界(超 31 位)的 HeapNumber,那麼就直接 return 入參,相當於沒有轉化;否則同樣走
那麼焦點來到了 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 有兩點明顯的不同:
- 僅支持一個入參,不支持進制轉換
- 返回值支持浮點型
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,其餘邏輯依然遵循標準。
4. Double tilde (~~) Operator#
ECMAScript (ECMA-262) Bitwise NOT Operator
使用~運算符利用到了標準中的第 2 步,對被計算的值做類型轉換,從而將字符串轉成數值。這裡我們關注這個環節具體是在引擎中的哪個步驟完成的。
4.1 V8 中的 BitwiseNot#
首先看看 V8 中對一元運算符的判斷:
[→ src/parsing/token.h]
static bool IsUnaryOp(Value op) { return base::IsInRange(op, ADD, VOID); }
定義在 ADD 和 VOID 範圍內的 op,都是一元運算符,具體包括 (可見 [→ src/parsing/token.h]),其中 SUB 和 ADD 定義在二元運算符列表的末端,在 IsUnaryOp 中它們也會命中一元符的判斷:
E(T, ADD, "+", 12)
E(T, SUB, "-", 12)
T(NOT, "!", 0)
T(BIT_NOT, "~", 0)
K(DELETE, "delete", 0)
K(TYPEOF, "typeof", 0)
K(VOID, "void", 0)
之後進入語法分析階段,解析 AST 樹的過程中,遇到一元運算符會做相應的處理,先調用 ParseUnaryOrPrefixExpression
之後構建一元運算符表達式 BuildUnaryExpression
:
[→ src/parsing/parser-base.h]
template <typename Impl>
typename ParserBase<Impl>::ExpressionT
ParserBase<Impl>::ParseUnaryExpression() {
// UnaryExpression ::
// PostfixExpression
// 'delete' UnaryExpression
// 'void' UnaryExpression
// 'typeof' UnaryExpression
// '++' UnaryExpression
// '--' UnaryExpression
// '+' UnaryExpression
// '-' UnaryExpression
// '~' UnaryExpression
// '!' UnaryExpression
// [+Await] AwaitExpression[?Yield]
Token::Value op = peek();
// 一元運算符處理
if (Token::IsUnaryOrCountOp(op)) return ParseUnaryOrPrefixExpression();
if (is_await_allowed() && op == Token::AWAIT) {
// await 處理
return ParseAwaitExpression();
}
return ParsePostfixExpression();
}
template <typename Impl>
typename ParserBase<Impl>::ExpressionT
ParserBase<Impl>::ParseUnaryOrPrefixExpression() {
//...
//...
// Allow the parser's implementation to rewrite the expression.
return impl()->BuildUnaryExpression(expression, op, pos);
}
[→ src/parsing/parser.cc]
Expression* Parser::BuildUnaryExpression(Expression* expression,
Token::Value op, int pos) {
DCHECK_NOT_NULL(expression);
const Literal* literal = expression->AsLiteral();
if (literal != nullptr) {
// !
if (op == Token::NOT) {
// Convert the literal to a boolean condition and negate it.
return factory()->NewBooleanLiteral(literal->ToBooleanIsFalse(), pos);
} else if (literal->IsNumberLiteral()) {
// Compute some expressions involving only number literals.
double value = literal->AsNumber();
switch (op) {
// +
case Token::ADD:
return expression;
// -
case Token::SUB:
return factory()->NewNumberLiteral(-value, pos);
// ~
case Token::BIT_NOT:
return factory()->NewNumberLiteral(~DoubleToInt32(value), pos);
default:
break;
}
}
}
return factory()->NewUnaryOperation(op, expression, pos);
}
如果字面量是數值型且一元運算符此刻不是 NOT(!),那麼會把 Value 會轉成 Number,如果是 BIT_NOT 再轉成 INT32 進行取反運算。
4.2 JavaScriptCore 中的 BitwiseNot#
同樣在語法分析生成 AST 階段,處理到 TILDE(~) 這個 token 後,創建表達式時會做類型轉換的工作:
[→ Parser/Parser.cpp]
template <typename LexerType>
template <class TreeBuilder> TreeExpression Parser<LexerType>::parseUnaryExpression(TreeBuilder& context)
{
//... 省略無關代碼
while (tokenStackDepth) {
switch (tokenType) {
//... 省略無關代碼
// ~
case TILDE:
expr = context.makeBitwiseNotNode(location, expr);
break;
// +
case PLUS:
expr = context.createUnaryPlus(location, expr);
break;
//... 省略無關代碼
}
}
}
[→ parser/ASTBuilder.h]
ExpressionNode* ASTBuilder::makeBitwiseNotNode(const JSTokenLocation& location, ExpressionNode* expr)
{
if (expr->isNumber())
return createIntegerLikeNumber(location, ~toInt32(static_cast<NumberNode*>(expr)->value()));
return new (m_parserArena) BitwiseNotNode(location, expr);
}
[→ parser/NodeConstructors.h]
inline BitwiseNotNode::BitwiseNotNode(const JSTokenLocation& location, ExpressionNode* expr)
: UnaryOpNode(location, ResultType::forBitOp(), expr, op_bitnot)
{
}
4.3 QuickJS 中的 BitwiseNot#
QuickJS 在語法分析階段,遇到~這個 token 會調用 emit_op(s, OP_not)
:
[→ quickjs.c]
/* allowed parse_flags: PF_ARROW_FUNC, PF_POW_ALLOWED, PF_POW_FORBIDDEN */
static __exception int js_parse_unary(JSParseState *s, int parse_flags)
{
int op;
switch(s->token.val) {
case '+':
case '-':
case '!':
case '~':
case TOK_VOID:
op = s->token.val;
if (next_token(s))
return -1;
if (js_parse_unary(s, PF_POW_FORBIDDEN))
return -1;
switch(op) {
case '-':
emit_op(s, OP_neg);
break;
case '+':
emit_op(s, OP_plus);
break;
case '!':
emit_op(s, OP_lnot);
break;
case '~':
emit_op(s, OP_not);
break;
case TOK_VOID:
emit_op(s, OP_drop);
emit_op(s, OP_undefined);
break;
default:
abort();
}
parse_flags = 0;
break;
//...
}
//...
}
emit_op
會生成 OP_not 字節碼操作符,並將源碼保存在 fd->byte_code 里。
static void emit_op(JSParseState *s, uint8_t val)
{
JSFunctionDef *fd = s->cur_func;
DynBuf *bc = &fd->byte_code;
/* Use the line number of the last token used, not the next token,
nor the current offset in the source file.
*/
if (unlikely(fd->last_opcode_line_num != s->last_line_num)) {
dbuf_putc(bc, OP_line_num);
dbuf_put_u32(bc, s->last_line_num);
fd->last_opcode_line_num = s->last_line_num;
}
fd->last_opcode_pos = bc->size;
dbuf_putc(bc, val);
}
int dbuf_putc(DynBuf *s, uint8_t c)
{
return dbuf_put(s, &c, 1);
}
int dbuf_put(DynBuf *s, const uint8_t *data, size_t len)
{
if (unlikely((s->size + len) > s->allocated_size)) {
if (dbuf_realloc(s, s->size + len))
return -1;
}
memcpy(s->buf + s->size, data, len);
s->size += len;
return 0;
}
QuickJS 解釋執行的函數是 JS_EvalFunctionInternal
,其會調用 JS_CallFree
進行字節碼的解釋執行,其核心邏輯是調用的 JS_CallInternal
函數。
/* argv[] is modified if (flags & JS_CALL_FLAG_COPY_ARGV) = 0. */
static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
JSValueConst this_obj, JSValueConst new_target,
int argc, JSValue *argv, int flags)
{
JSRuntime *rt = caller_ctx->rt;
JSContext *ctx;
JSObject *p;
JSFunctionBytecode *b;
JSStackFrame sf_s, *sf = &sf_s;
const uint8_t *pc;
// ...省略無關代碼
for(;;) {
int call_argc;
JSValue *call_argv;
SWITCH(pc) {
// ...
CASE(OP_not):
{
JSValue op1;
op1 = sp[-1];
// 如果是整型
if (JS_VALUE_GET_TAG(op1) == JS_TAG_INT) {
sp[-1] = JS_NewInt32(ctx, ~JS_VALUE_GET_INT(op1));
// 如果不是整型
} else {
if (js_not_slow(ctx, sp))
goto exception;
}
}
BREAK;
// ...
}
// ...
}
可見,解析到 OP_not 時, 如果是整型就直接取反,否則就調用 js_not_slow
:
static no_inline int js_not_slow(JSContext *ctx, JSValue *sp)
{
int32_t v1;
if (unlikely(JS_ToInt32Free(ctx, &v1, sp[-1]))) {
sp[-1] = JS_UNDEFINED;
return -1;
}
sp[-1] = JS_NewInt32(ctx, ~v1);
return 0;
}
js_not_slow
會嘗試轉整型,轉不了就轉 -1,轉的了就轉整型後取反。JS_ToInt32Free
轉換邏輯如下:
/* return (<0, 0) in case of exception */
static int JS_ToInt32Free(JSContext *ctx, int32_t *pres, JSValue val)
{
redo:
tag = JS_VALUE_GET_NORM_TAG(val);
switch(tag) {
case JS_TAG_INT:
case JS_TAG_BOOL:
case JS_TAG_NULL:
case JS_TAG_UNDEFINED:
ret = JS_VALUE_GET_INT(val);
break;
// ...
default:
val = JS_ToNumberFree(ctx, val);
if (JS_IsException(val)) {
*pres = 0;
return -1;
}
goto redo;
}
*pres = ret;
return 0;
}
對於字符串,會走到 JS_ToNumberFree
,之後調用 JS_ToNumberHintFree
,涉及到字符串處理的核心邏輯如下:
static JSValue JS_ToNumberHintFree(JSContext *ctx, JSValue val,
JSToNumberHintEnum flag)
{
uint32_t tag;
JSValue ret;
redo:
tag = JS_VALUE_GET_NORM_TAG(val);
switch(tag) {
// ...省略無關邏輯
case JS_TAG_STRING:
{
const char *str;
const char *p;
size_t len;
str = JS_ToCStringLen(ctx, &len, val);
JS_FreeValue(ctx, val);
if (!str)
return JS_EXCEPTION;
p = str;
p += skip_spaces(p);
if ((p - str) == len) {
ret = JS_NewInt32(ctx, 0);
} else {
int flags = ATOD_ACCEPT_BIN_OCT;
ret = js_atof(ctx, p, &p, 0, flags);
if (!JS_IsException(ret)) {
p += skip_spaces(p);
if ((p - str) != len) {
JS_FreeValue(ctx, ret);
ret = JS_NAN;
}
}
}
JS_FreeCString(ctx, str);
}
break;
// ...省略無關邏輯
}
// ...省略無關邏輯
}
可以轉化的用 JS_NewInt32
去處理,否則返回 NaN。
5. Unary Operator (+)#
ECMAScript (ECMA-262) Unary Plus Operator
一元運算符加號是筆者最喜歡用的一種字符串轉數值的方式,標準中它沒有什麼花里胡哨的、非常簡介明了,就是用來做數值類型轉換的。
5.1 V8 中的 UnaryPlus#
語法分析階段同 Double tilde (~~) Operator,此處不再贅述。
5.2 JavaScriptCore 中的 UnaryPlus#
語法分析階段同 Double tilde (~~) Operator,此處不再贅述。
5.3 QuickJS 中的 UnaryPlus#
語法分析階段同 Double tilde (~~) Operator,此處不再贅述。最後依然走到 JS_CallInternal
。
[→ quickjs.c]
/* argv[] is modified if (flags & JS_CALL_FLAG_COPY_ARGV) = 0. */
static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
JSValueConst this_obj, JSValueConst new_target,
int argc, JSValue *argv, int flags)
{
JSRuntime *rt = caller_ctx->rt;
JSContext *ctx;
JSObject *p;
JSFunctionBytecode *b;
JSStackFrame sf_s, *sf = &sf_s;
const uint8_t *pc;
// ...省略無關代碼
for(;;) {
int call_argc;
JSValue *call_argv;
SWITCH(pc) {
// ...
CASE(OP_plus):
{
JSValue op1;
uint32_t tag;
op1 = sp[-1];
tag = JS_VALUE_GET_TAG(op1);
if (tag == JS_TAG_INT || JS_TAG_IS_FLOAT64(tag)) {
} else {
if (js_unary_arith_slow(ctx, sp, opcode))
goto exception;
}
BREAK;
}
// ...省略無關代碼
}
}
// ...省略無關代碼
}
可以發現當操作數是 Int 或 Float 時,就直接不處理,和標準中規範的一致。而其他情況就調用 js_unary_arith_slow
,若調用過程中遇到異常就走異常邏輯:
static no_inline __exception int js_unary_arith_slow(JSContext *ctx, JSValue *sp, OPCodeEnum op)
{
JSValue op1;
double d;
op1 = sp[-1];
if (unlikely(JS_ToFloat64Free(ctx, &d, op1))) {
sp[-1] = JS_UNDEFINED;
return -1;
}
switch(op) {
case OP_inc:
d++;
break;
case OP_dec:
d--;
break;
case OP_plus:
break;
case OP_neg:
d = -d;
break;
default:
abort();
}
sp[-1] = JS_NewFloat64(ctx, d);
return 0;
}
這裡的 JS_ToFloat64Free
的內部處理邏輯和和 4.3 時的 JS_ToFloat64Free
一樣,不再贅述。js_unary_arith_slow
處理完數值轉換之後,若運算符是一元運算加號,則直接返回;否則還會根據運算符再做相應的運算處理,如自增符還需要 + 1 等。
至此,我們講解了以下 5 個方法在解釋器中的具體實現:
- parseInt()
- parseFloat()
- Number()
- Double tilde (~~) Operator
- Unary Operator (+)
除卻以上 5 個數值轉換方法之外,還有以下 4 個方法,因篇幅問題本文暫且不再詳述:
- Math.floor()
- Multiply with number
- The Signed Right Shift Operator(>>)
- The Unsigned Right Shift Operator(>>>)
字符串轉數值各有優劣,使用者可根據自己的需要進行選用,以下是我個人總結的一些經驗:
如果返回值只要求整形:
- 追求代碼簡潔和執行效率,對輸入值有一定的把握(無需防禦),優先選用 Unary Operator (+)
- 對輸入值沒有把握,需要做防禦式編程,使用 parseInt ()
- 需要支持 BigInt, 優先考慮使用 Number () ;如果用 Double tilde (~~) Operator,需要注意 31 位問題。
如果返回值要求浮點型:
- 追求代碼簡潔和執行效率,對輸入值有一定的把握(無需防禦),優先選用 Unary Operator (+)
- 對輸入值沒有把握,需要做防禦式編程,使用 parseFloat ()
- 需要支持 BigInt,使用 parseFloat ()