深入理解 JavaScript
请支持本书:购买捐赠
(广告,请勿屏蔽。)

2 JavaScript 中的类型强制



在本章中,我们将研究*类型强制*在 JavaScript 中的作用。我们将深入探讨这个主题,例如,研究 ECMAScript 规范如何处理强制转换。

2.1 什么是类型强制?

每个操作(函数、运算符等)都期望其参数具有特定类型。如果某个值对于某个参数没有正确的类型,例如,函数的三个常见选项是

  1. 该函数可以抛出异常

    function multiply(x, y) {
      if (typeof x !== 'number' || typeof y !== 'number') {
        throw new TypeError();
      }
      // ···
    }
  2. 该函数可以返回错误值

    function multiply(x, y) {
      if (typeof x !== 'number' || typeof y !== 'number') {
        return NaN;
      }
      // ···
    }
  3. 该函数可以将其参数转换为有用的值

    function multiply(x, y) {
      if (typeof x !== 'number') {
        x = Number(x);
      }
      if (typeof y !== 'number') {
        y = Number(y);
      }
      // ···
    }

在 (3) 中,操作执行隐式类型转换。这称为*类型强制*。

JavaScript 最初没有异常,这就是为什么它对大多数操作使用强制转换和错误值

// Coercion
assert.equal(3 * true, 3);

// Error values
assert.equal(1 / 0, Infinity);
assert.equal(Number('xyz'), NaN);

但是,在某些情况下(尤其是在涉及较新功能时),如果参数没有正确的类型,它也会抛出异常

2.1.1 处理类型强制

处理强制转换的两种常见方法是

我通常更喜欢前者,因为它阐明了我的意图:我希望 xy 不是数字,而是想将两个数字相乘。

2.2 ECMAScript 规范中帮助实现强制转换的操作

以下部分描述了 ECMAScript 规范使用的最重要的内部函数,用于将实际参数转换为预期类型。

例如,在 TypeScript 中,我们会这样写

function isNaN(number: number) {
  // ···
}

在规范中,它看起来如下(翻译成 JavaScript,以便于理解)

function isNaN(number) {
  let num = ToNumber(number);
  // ···
}

2.2.1 转换为原始类型和对象

每当需要原始类型或对象时,都会使用以下转换函数

这些内部函数在 JavaScript 中有非常相似的对应函数

> Boolean(0)
false
> Boolean(1)
true

> Number('123')
123

在引入了与数字并存的 bigint 之后,规范通常使用 ToNumeric() 来代替以前使用的 ToNumber()。继续阅读以了解更多信息。

2.2.2 转换为数字类型

目前,JavaScript 有两种内置数字类型:数字和 bigint。

表 1:按位数字运算符的操作数的强制转换(BigInt 运算符不限制位数)。
运算符 左操作数 右操作数 结果类型
<< ToInt32() ToUint32() Int32
有符号 >> ToInt32() ToUint32() Int32
无符号 >>> ToInt32() ToUint32() Uint32
&, ^, | ToInt32() ToUint32() Int32
~ ToInt32() Int32

2.2.3 转换为属性键

ToPropertyKey() 返回一个字符串或符号,并由以下各项使用

2.2.4 转换为数组索引

2.2.5 转换为类型化数组元素

当我们设置类型化数组元素的值时,将使用以下转换函数之一

2.3 插曲:用 JavaScript 表达规范算法

在本章的其余部分,我们将遇到几种规范算法,但“实现”为 JavaScript。以下列表显示了如何将一些常用模式从规范转换为 JavaScript

使用 let(而不是 const)来匹配规范的语言。

省略了一些内容——例如,ReturnIfAbrupt 简写 ?!

/**
 * An improved version of typeof
 */
function TypeOf(value) {
  const result = typeof value;
  switch (result) {
    case 'function':
      return 'object';
    case 'object':
      if (value === null) {
        return 'null';
      } else {
        return 'object';
      }
    default:
      return result;
  }
}

function IsCallable(x) {
  return typeof x === 'function';
}

2.4 强制转换算法示例

2.4.1 ToPrimitive()

操作 ToPrimitive() 是许多强制转换算法的中间步骤(我们将在本章后面看到其中的一些)。它将任意值转换为原始值。

ToPrimitive() 在规范中经常使用,因为大多数运算符只能处理原始值。例如,我们可以使用加法运算符 (+) 来添加数字和连接字符串,但不能使用它来连接数组。

这就是 ToPrimitive() 的 JavaScript 版本的样子

/**
 * @param hint Which type is preferred for the result:
 *             string, number, or don’t care?
 */
function ToPrimitive(input: any,
  hint: 'string'|'number'|'default' = 'default') {
    if (TypeOf(input) === 'object') {
      let exoticToPrim = input[Symbol.toPrimitive]; // (A)
      if (exoticToPrim !== undefined) {
        let result = exoticToPrim.call(input, hint);
        if (TypeOf(result) !== 'object') {
          return result;
        }
        throw new TypeError();
      }
      if (hint === 'default') {
        hint = 'number';
      }
      return OrdinaryToPrimitive(input, hint);
    } else {
      // input is already primitive
      return input;
    }
  }

ToPrimitive() 允许对象通过 Symbol.toPrimitive 覆盖到原始值的转换(第 A 行)。如果对象不这样做,它将被传递给 OrdinaryToPrimitive()

function OrdinaryToPrimitive(O: object, hint: 'string' | 'number') {
  let methodNames;
  if (hint === 'string') {
    methodNames = ['toString', 'valueOf'];
  } else {
    methodNames = ['valueOf', 'toString'];
  }
  for (let name of methodNames) {
    let method = O[name];
    if (IsCallable(method)) {
      let result = method.call(O);
      if (TypeOf(result) !== 'object') {
        return result;
      }
    }
  }
  throw new TypeError();
}
2.4.1.1 ToPrimitive() 的调用者使用哪些提示?

参数 hint 可以具有以下三个值之一

以下是一些关于各种操作如何使用 ToPrimitive() 的示例

正如我们所见,默认行为是将 'default' 处理为 'number'。只有 SymbolDate 的实例会覆盖此行为(稍后显示)。

2.4.1.2 调用哪些方法将对象转换为原始值?

如果未通过 Symbol.toPrimitive 覆盖到原始值的转换,则 OrdinaryToPrimitive() 将调用以下两种方法之一或两者

以下代码演示了它是如何工作的

const obj = {
  toString() { return 'a' },
  valueOf() { return 1 },
};

// String() prefers strings:
assert.equal(String(obj), 'a');

// Number() prefers numbers:
assert.equal(Number(obj), 1);

具有属性键 Symbol.toPrimitive 的方法会覆盖到原始值的正常转换。这在标准库中只做了两次

2.4.1.3 Date.prototype[Symbol.toPrimitive]()

这就是日期处理转换为原始值的方式

Date.prototype[Symbol.toPrimitive] = function (
  hint: 'default' | 'string' | 'number') {
    let O = this;
    if (TypeOf(O) !== 'object') {
      throw new TypeError();
    }
    let tryFirst;
    if (hint === 'string' || hint === 'default') {
      tryFirst = 'string';
    } else if (hint === 'number') {
      tryFirst = 'number';
    } else {
      throw new TypeError();
    }
    return OrdinaryToPrimitive(O, tryFirst);
  };

与默认算法的唯一区别是 'default' 变为 'string'(而不是 'number')。如果我们使用将 hint 设置为 'default' 的操作,则可以观察到这一点

这是 JavaScript 版本的 ToString()

function ToString(argument) {
  if (argument === undefined) {
    return 'undefined';
  } else if (argument === null) {
    return 'null';
  } else if (argument === true) {
    return 'true';
  } else if (argument === false) {
    return 'false';
  } else if (TypeOf(argument) === 'number') {
    return Number.toString(argument);
  } else if (TypeOf(argument) === 'string') {
    return argument;
  } else if (TypeOf(argument) === 'symbol') {
    throw new TypeError();
  } else if (TypeOf(argument) === 'bigint') {
    return BigInt.toString(argument);
  } else {
    // argument is an object
    let primValue = ToPrimitive(argument, 'string'); // (A)
    return ToString(primValue);
  }
}

请注意,此函数如何使用 ToPrimitive() 作为对象的中间步骤,然后再将原始结果转换为字符串(A 行)。

ToString()String() 的工作方式有一个有趣的区别:如果 argument 是一个符号,前者会抛出一个 TypeError,而后者不会。为什么呢?符号的默认行为是将它们转换为字符串会抛出异常。

> const sym = Symbol('sym');

> ''+sym
TypeError: Cannot convert a Symbol value to a string
> `${sym}`
TypeError: Cannot convert a Symbol value to a string

该默认行为在 String()Symbol.prototype.toString() 中被覆盖(两者将在接下来的小节中描述)。

> String(sym)
'Symbol(sym)'
> sym.toString()
'Symbol(sym)'
2.4.2.1 String()
function String(value) {
  let s;
  if (value === undefined) {
    s = '';
  } else {
    if (new.target === undefined && TypeOf(value) === 'symbol') {
      // This function was function-called and value is a symbol
      return SymbolDescriptiveString(value);
    }
    s = ToString(value);
  }
  if (new.target === undefined) {
    // This function was function-called
    return s;
  }
  // This function was new-called
  return StringCreate(s, new.target.prototype); // simplified!
}

String() 的工作方式不同,具体取决于它是通过函数调用还是通过 new 调用。它使用 new.target 来区分这两种情况。

以下是辅助函数 StringCreate()SymbolDescriptiveString()

/** 
 * Creates a String instance that wraps `value`
 * and has the given protoype.
 */
function StringCreate(value, prototype) {
  // ···
}

function SymbolDescriptiveString(sym) {
  assert.equal(TypeOf(sym), 'symbol');
  let desc = sym.description;
  if (desc === undefined) {
    desc = '';
  }
  assert.equal(TypeOf(desc), 'string');
  return 'Symbol('+desc+')';
}
2.4.2.2 Symbol.prototype.toString()

除了 String() 之外,我们还可以使用方法 .toString() 将符号转换为字符串。其规范如下所示。

Symbol.prototype.toString = function () {
  let sym = thisSymbolValue(this);
  return SymbolDescriptiveString(sym);
};
function thisSymbolValue(value) {
  if (TypeOf(value) === 'symbol') {
    return value;
  }
  if (TypeOf(value) === 'object' && '__SymbolData__' in value) {
    let s = value.__SymbolData__;
    assert.equal(TypeOf(s), 'symbol');
    return s;
  }
}
2.4.2.3 Object.prototype.toString

.toString() 的默认规范如下所示。

Object.prototype.toString = function () {
  if (this === undefined) {
    return '[object Undefined]';
  }
  if (this === null) {
    return '[object Null]';
  }
  let O = ToObject(this);
  let isArray = Array.isArray(O);
  let builtinTag;
  if (isArray) {
    builtinTag = 'Array';
  } else if ('__ParameterMap__' in O) {
    builtinTag = 'Arguments';
  } else if ('__Call__' in O) {
    builtinTag = 'Function';
  } else if ('__ErrorData__' in O) {
    builtinTag = 'Error';
  } else if ('__BooleanData__' in O) {
    builtinTag = 'Boolean';
  } else if ('__NumberData__' in O) {
    builtinTag = 'Number';
  } else if ('__StringData__' in O) {
    builtinTag = 'String';
  } else if ('__DateValue__' in O) {
    builtinTag = 'Date';
  } else if ('__RegExpMatcher__' in O) {
    builtinTag = 'RegExp';
  } else {
    builtinTag = 'Object';
  }
  let tag = O[Symbol.toStringTag];
  if (TypeOf(tag) !== 'string') {
    tag = builtinTag;
  }
  return '[object ' + tag + ']';
};

如果我们将普通对象转换为字符串,则使用此操作。

> String({})
'[object Object]'

默认情况下,如果我们将类的实例转换为字符串,也会使用此操作。

class MyClass {}
assert.equal(
  String(new MyClass()), '[object Object]');

通常,我们会覆盖 .toString() 以配置 MyClass 的字符串表示形式,但我们也可以更改方括号内“object”之后的字符串。

class MyClass {}
MyClass.prototype[Symbol.toStringTag] = 'Custom!';
assert.equal(
  String(new MyClass()), '[object Custom!]');

.toString() 的覆盖版本与 Object.prototype 中的原始版本进行比较是很有趣的。

> ['a', 'b'].toString()
'a,b'
> Object.prototype.toString.call(['a', 'b'])
'[object Array]'

> /^abc$/.toString()
'/^abc$/'
> Object.prototype.toString.call(/^abc$/)
'[object RegExp]'

2.4.3 ToPropertyKey()

ToPropertyKey() 被括号运算符等使用。以下是它的工作原理。

function ToPropertyKey(argument) {
  let key = ToPrimitive(argument, 'string'); // (A)
  if (TypeOf(key) === 'symbol') {
    return key;
  }
  return ToString(key);
}

同样,在处理原始值之前,对象会被转换为原始值。

ToNumeric() 被乘法运算符 (*) 等使用。以下是它的工作原理。

function ToNumeric(value) {
  let primValue = ToPrimitive(value, 'number');
  if (TypeOf(primValue) === 'bigint') {
    return primValue;
  }
  return ToNumber(primValue);
}
2.4.4.1 ToNumber()

ToNumber() 的工作原理如下。

function ToNumber(argument) {
  if (argument === undefined) {
    return NaN;
  } else if (argument === null) {
    return +0;
  } else if (argument === true) {
    return 1;
  } else if (argument === false) {
    return +0;
  } else if (TypeOf(argument) === 'number') {
    return argument;
  } else if (TypeOf(argument) === 'string') {
    return parseTheString(argument); // not shown here
  } else if (TypeOf(argument) === 'symbol') {
    throw new TypeError();
  } else if (TypeOf(argument) === 'bigint') {
    throw new TypeError();
  } else {
    // argument is an object
    let primValue = ToPrimitive(argument, 'number');
    return ToNumber(primValue);
  }
}

ToNumber() 的结构类似于 ToString() 的结构。

2.5 强制转换的操作

2.5.1 加法运算符 (+)

以下是 JavaScript 的加法运算符的规范。

function Addition(leftHandSide, rightHandSide) {
  let lprim = ToPrimitive(leftHandSide);
  let rprim = ToPrimitive(rightHandSide);
  if (TypeOf(lprim) === 'string' || TypeOf(rprim) === 'string') { // (A)
    return ToString(lprim) + ToString(rprim);
  }
  let lnum = ToNumeric(lprim);
  let rnum = ToNumeric(rprim);
  if (TypeOf(lnum) !== TypeOf(rnum)) {
    throw new TypeError();
  }
  let T = Type(lnum);
  return T.add(lnum, rnum); // (B)
}

此算法的步骤:

2.5.2 抽象相等比较 (==)

/** Loose equality (==) */
function abstractEqualityComparison(x, y) {
  if (TypeOf(x) === TypeOf(y)) {
    // Use strict equality (===)
    return strictEqualityComparison(x, y);
  }

  // Comparing null with undefined
  if (x === null && y === undefined) {
    return true;
  }
  if (x === undefined && y === null) {
    return true;
  }

  // Comparing a number and a string
  if (TypeOf(x) === 'number' && TypeOf(y) === 'string') {
    return abstractEqualityComparison(x, Number(y));
  }
  if (TypeOf(x) === 'string' && TypeOf(y) === 'number') {
    return abstractEqualityComparison(Number(x), y);
  }

  // Comparing a bigint and a string
  if (TypeOf(x) === 'bigint' && TypeOf(y) === 'string') {
    let n = StringToBigInt(y);
    if (Number.isNaN(n)) {
      return false;
    }
    return abstractEqualityComparison(x, n);
  }
  if (TypeOf(x) === 'string' && TypeOf(y) === 'bigint') {
    return abstractEqualityComparison(y, x);
  }

  // Comparing a boolean with a non-boolean
  if (TypeOf(x) === 'boolean') {
    return abstractEqualityComparison(Number(x), y);
  }
  if (TypeOf(y) === 'boolean') {
    return abstractEqualityComparison(x, Number(y));
  }

  // Comparing an object with a primitive
  // (other than undefined, null, a boolean)
  if (['string', 'number', 'bigint', 'symbol'].includes(TypeOf(x))
    && TypeOf(y) === 'object') {
      return abstractEqualityComparison(x, ToPrimitive(y));
    }
  if (TypeOf(x) === 'object'
    && ['string', 'number', 'bigint', 'symbol'].includes(TypeOf(y))) {
      return abstractEqualityComparison(ToPrimitive(x), y);
    }
  
  // Comparing a bigint with a number
  if ((TypeOf(x) === 'bigint' && TypeOf(y) === 'number')
    || (TypeOf(x) === 'number' && TypeOf(y) === 'bigint')) {
      if ([NaN, +Infinity, -Infinity].includes(x)
        || [NaN, +Infinity, -Infinity].includes(y)) {
          return false;
        }
      if (isSameMathematicalValue(x, y)) {
        return true;
      } else {
        return false;
      }
    }
  
  return false;
}

以下操作未在此处显示:

现在我们已经仔细研究了 JavaScript 的类型强制转换的工作原理,让我们以一个与类型转换相关的术语简要词汇表来结束。

[来源:维基百科]