深入了解 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

在與數字並存的大整數引入後,規格經常在以前使用 ToNumber() 的地方使用 ToNumeric()。請繼續閱讀以了解更多資訊。

2.2.2 轉換為數值類型

目前,JavaScript 有 兩種內建數值類型:數字和大整數。

表 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 轉換為 Typed Array 元素

當我們設定 Typed Array 元素的值時,會使用下列轉換函式之一

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 的類型強制轉換如何運作,讓我們用一個與類型轉換相關的簡短詞彙表來作結

[來源:維基百科]