书接上文《引擎剖析:JS 中的字符串转数值(上)》,本文继续讲解利用位运算做数值转换的两种方式。
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)
{
}
[→ parser/ResultType.h]
static constexpr ResultType forBitOp()
{
return bigIntOrInt32Type();
}
static constexpr ResultType bigIntOrInt32Type()
{
return ResultType(TypeMaybeBigInt | TypeInt32 | TypeMaybeNumber);
}
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()
本文由 Airing 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为: May 3, 2022 at 06:52 am