引擎剖析:JS 中的字符串转数值(下)
in 技术 with 1,078 views and 0 comment

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

in 技术 with 0 comment

书接上文《引擎剖析: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 个方法在解释器中的具体实现:

  1. parseInt()
  2. parseFloat()
  3. Number()
  4. Double tilde (~~) Operator
  5. Unary Operator (+)

除却以上 5 个数值转换方法之外,还有以下 4 个方法,因篇幅问题本文暂且不再详述:

字符串转数值各有优劣,使用者可根据自己的需要进行选用,以下是我个人总结的一些经验:

如果返回值只要求整形:

如果返回值要求浮点型:

Responses