iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 3
0
Modern Web

JavaScript 之旅系列 第 3

JavaScript 之旅 (3):Exponentiation Operator (指數運算子)

寫程式應該很常會用到指數運算,過去我們會用 Math.pow(),但在 ES2016 (ES7) 提供了 exponentiation operator (指數運算子) 的讓寫法更簡潔。那這兩個差在哪?讓我們從 ECMAScript spec 中一探究竟吧。

本文同步發表於 Titangene Blog:JavaScript 之旅 (3):Exponentiation Operator (指數運算子)

「JavaScript 之旅」系列文章發文於:

過去的 Math.pow()

過去要進行指數運算 (即計算 a 的 n 次方,表達式為 $a^n$,a 為底數,n 為指數,通常指數寫成上標,放在底數的右邊),常用的是 Math.pow() (應該不會想手動寫多個 * 運算子 (multiplicative operators) 吧?)。例如:

平方:

let x = 2;

console.log(Math.pow(x, 2));  // 4

// 等同於
console.log(x * x);           // 4

三次方:

let x = 2;

console.log(Math.pow(x, 3));  // 8

// 等同於
console.log(x * x * x);       // 8

寫成函數可能像是這樣:

let square = x => Math.pow(x, 2);
let cube = x => Math.pow(x, 3);

console.log(square(6));  // 36
console.log(cube(3));    // 27

現代的 Exponentiation Operator

在 ES2016 (ES7) 提供了 exponentiation operator (指數運算子),用於指數運算。

exponentiation 運算子是一種 infix notation (中綴表示法),比用函數表示法還要更簡潔。

let square = x => x ** 2;
let cube = x => x ** 3;

註:有些程式語言會用 ^ 運算子來進行指數計算,但 JavaScript 的 ^ 運算子是 bitwise XOR 運算子,用來進行位元運算,例如:

let a = 5;          // 00000000000000000000000000000101
let b = 3;          // 00000000000000000000000000000011

console.log(a ^ b); // 00000000000000000000000000000110

若與 NaN 計算,結果都會是 NaN

console.log(NaN ** 2);  // NaN
console.log(2 ** NaN);  // NaN

若與 undefined 計畫,結果也都會是 NaN

console.log(undefined ** 2);  // NaN
console.log(2 ** undefined);  // NaN

因為 exponentiation 運算子會將指數和底數值進行 ToNumeric() 強制轉型,很像使用 Number() 的行為,如果你嘗試用 Number(undefined) 會得到 NaN

後面會提到 spec 是如何定義的。

若與 null 計算,null 會被 ToNumeric() 強制轉型成 0

console.log(null ** 2);  // 0
// 大約等同於 Number(null) ** 2,所以 0 ** 2 的計算結果為 0

console.log(2 ** null);  // 1
// 大約等同於 2 ** Number(null),所以 2 ** 0 的計算結果為 1

Associativity

exponentiation 運算子的 associativity 是右到左 (即 right-associative),也就是說,下面兩個寫法是相同的:

console.log(2 ** 3 ** 2);    // 512
console.log(2 ** (3 ** 2));  // 512

// 計算過程:
// 1. 2 ** 3 ** 2
// 2. 2 ** 9
// 3. 512

所以會與下面的計算結果不同,因為你讓他先計算括號的結果:

console.log((2 ** 3) ** 2);  // 64

// 計算過程:
// 1. (2 ** 3) ** 2
// 2. 8 ** 2
// 3. 64

後面會說 spec 是如何定義的。

註:在程式語言中,operator associativity 是在沒有括號時,如何將有相同優先級的運算子進行分組,運算元要用哪種運算會取決於運算子的 associativity。

分為以下幾種:

  • associative:可任意分組運算子
  • left-associative:運算子從左側分組
  • right-associative:運算子從右側分組
  • non-associative:運算子不能 chained,通常是因為 output typ 和 input type 不相容

詳情可參閱 Operator associativity - Wikipedia

Unary Operation 的用法

不過,exponentiation 運算子不能讓你在底數前面使用 unary operation (一元運算子,包括 +-~!deletevoidtypeof ),否則會出現 SyntaxError 錯誤,所以以下都會是 SyntaxError

let a = 2;
let n = 3;

console.log(+a ** n);
console.log(-a ** n);
console.log(~a ** n);
console.log(!a ** n);
delete a ** n;
void a ** n;
typeof a ** n;

// SyntaxError: Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence

例如:要把負數當作底數就不能像下面這樣直接寫負號:

let a = 2;
let n = 3;

console.log(-2 ** 3);
console.log(-a ** n);
// SyntaxError: Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence

上面是 Chrome 的錯誤訊息,已經提醒你要加上括號。

而是要加上括號才能計算:

console.log((-2) ** 3);  // -8

另一個範例:想將指數計算結果變為負數,也需要用括號:

console.log(-(2 ** 10));  // -1024

Assignment Operator 的用法

exponentiation 運算子也有 assignment operator (賦值運算子),語法是 **=

例如:

let x = 2;

x **= 3;
console.log(x);  // 8

等同於:

let x = 2;

x = x ** 3;
console.log(x);  // 8

Exponentiation Operator vs. Math.pow()

共通點:都可以計算幾次方

但有一些不同:exponentiation 運算子可以計算 BigInt 型別的值,但 Math.pow() 不行

console.log(2n ** 3n);  // 8n

console.log(Math.pow(2n, 3n));  // TypeError: Cannot convert a BigInt value to a number

Sepc 定義

Exponentiation Operator 的 spec 定義

exponentiation 運算子是 ECMAScript 內建 Numeric 型別 (Number 和 BigInt 型別都是 Numeric 型別) 的 operation:

下面是 Number::exponentiate operation 的定義:

下面是 BigInt::exponentiate operation 的定義:

接著從 spec 定義的語法可以看出,exponentiation 運算子的 associativity 是右到左 (即 right-associative):

下面是 spec 定義 exponentiation 運算子的計算過程

從步驟 5 和 6 可以看到,exponentiation 運算子會把底數和指數進行 ToNumeric() 的強制轉型。

若我故意讓底數和指數是 String 型別的值時,exponentiation 運算子就會自動幫我強制轉型成 Number 型別:

console.log('2' ** '3');  // 8

下面是 ToNumeric() 的定義:

至於 ToNumeric() 在定義中提到的 ToPrimitive()ToNumber() 是 ECMAScript 強制轉型的定義了,這邊不會深入探討,有興趣的朋友可以看看,我有附上連結。

若以簡化的方式來說,我們常用的 Number() constructor 的其中一個步驟就會使用到 spec 中定義的 ToNumeric()

註:不知道看到這邊會不會亂掉 XD,有些是 spec 中定義的,我們是無法透過寫程式直接使用的,例如:ToNumeric()ToPrimitive()ToNumber() 都是。

Number() 才是我們真的可以使用的 constructor。

所以不要搞混囉!

Math.pow() 的 spec 定義

接著來看 Math.pow() 在 spec 的定義:

可以看到 Math.pow() 和 exponentiation 運算子對底數和指數值的處理不同:

  • Math.pow() 是進行 ToNumber() 的強制轉型
  • exponentiation 運算子則是進行 ToNumeric() 的強制轉型

資料來源


上一篇
JavaScript 之旅 (2):Array.prototype.includes()
下一篇
JavaScript 之旅 (4):Object.keys() & Object.values() & Object.entries()
系列文
JavaScript 之旅30

尚未有邦友留言

立即登入留言