JavaScriptのMath.expm1():小さな指数変化を精度よく計算する
Math.expm1()で指数関数から1を引く計算の精度低下を抑える方法を解説。引数・戻り値、ブラウザーとNode.jsの実行手順、期待出力、入力検証と値の平滑化への応用を学べます。
目的:小さな変化を計算から消さない
Math.expm1()は、自然対数の底である約2.718の数をeとしたとき、eのx乗から1を引いた数を返す関数です。指数関数による増加量や、目標値へ徐々に近づける計算で利用できます。
数学的にはMath.exp(x) - 1と同じ式ですが、コンピューター上では計算の途中で丸めが入ります。xが非常に小さいと、指数関数の結果は1に近くなり、その時点で小さな差が失われることがあります。あとから1を引いても、失われた情報は戻りません。
そこで、最終的に必要な「1との差」を求めるためのMath.expm1()を使います。ゼロに近い入力を扱うコードで、式をそのまま書くより数値的に安定した計算を選べることが、この関数を学ぶ目的です。MDNのMath.expm1()でも、この精度の違いが説明されています。
動作環境と対応バージョン
2026年10月11日時点で、MDNはMath.expm1()を広く利用可能な機能として扱い、2015年7月からブラウザー間で利用できるとしています。現在のChrome、Edge、Firefox、Safariで利用できる標準組み込み関数です。古いブラウザーや組み込みWebViewでは、使用前に存在を確認してください。
Node.jsでも利用できます。本記事の再現用環境はNode.js 22系以降とします。これは記事で使用する環境の指定であり、機能が初めて導入されたバージョンを意味しません。Node.js 22がV8 12.4を採用していることは公式リリース案内で確認できます。
この関数はDOMに依存しません。以下のサンプルはブラウザーとNode.jsの両方で同じコードを使え、追加パッケージや通信も不要です。
console.log(typeof Math.expm1);対応環境ではfunctionと表示されます。undefinedなら、その環境で後続のコードを実行せず、対応する環境に切り替えてください。単にMath.exp(x) - 1へ置き換えると、この記事で扱う精度上の利点が失われます。
引数と戻り値
呼び出し方はMath.expm1(x)です。Mathは生成するクラスではなく、関数を直接呼び出すための組み込みオブジェクトです。
- 引数
xには、指数として使う数値を渡します。 - 戻り値は
eのx乗から1を引いたnumberです。 xが0なら0、正なら正の値、負なら負の値を返します。InfinityにはInfinity、-Infinityには-1を返します。NaNにはNaNを返し、通常の数値計算の失敗を例外として通知する関数ではありません。
数値以外の入力に注意する
JavaScriptの数値を期待する組み込み処理では、入力の数値変換が起こります。数値文字列が計算できても、それを入力検証として利用すると、空文字列などの意図しない値まで受け入れかねません。BigIntやSymbolの暗黙の数値変換はTypeErrorになります。変換規則はMDNのNumberを参照してください。
アプリケーションの関数では、最初に有限のnumberだけを受け付けると、入力の問題と計算結果を区別しやすくなります。
コピーして動かす基本例
次のコードはそのまま実行できます。出力を小数6桁にそろえ、環境ごとの末尾の丸め差を読み取りやすくしています。
(() => {
"use strict";
if (typeof Math.expm1 !== "function") {
console.log("この環境はMath.expm1に対応していません");
return;
}
function calculateChange(x) {
if (typeof x !== "number" || !Number.isFinite(x)) {
throw new TypeError("xは有限の数値にしてください");
}
const result = Math.expm1(x);
if (!Number.isFinite(result)) {
throw new RangeError("計算結果が有限の範囲を超えました");
}
return result;
}
for (const x of [0, 1, -1]) {
console.log(`${x}: ${calculateChange(x).toFixed(6)}`);
}
try {
calculateChange("1");
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
})();対応環境での期待出力は次のとおりです。ここで示す結果は公式例と計算の確認によるもので、実機での実行検証は行っていません。
0: 0.000000
1: 1.718282
-1: -0.632121
TypeError: xは有限の数値にしてくださいブラウザーでの実行方法
ブラウザーで空のタブを開き、開発者ツールのConsoleを選びます。コード全体を貼り付けて実行すると、Consoleに結果が出ます。貼り付け制限が表示される場合は、ブラウザーの案内を確認するか、コードを手入力してください。
コード全体を即時実行関数で囲んでいるため、繰り返し実行してもConsoleのトップレベルで同じ変数名を再宣言する問題を避けられます。ページ内の要素を用意する必要はありません。
Node.jsでの実行方法
Node.jsをインストールした環境で、コードをUTF-8のexpm1-demo.jsとして保存します。ターミナルでそのファイルのあるディレクトリーへ移動し、次のコマンドを実行してください。
node --version
node expm1-demo.js最初のコマンドで22系以降の環境であることを確認し、次のコマンドで標準出力へ結果を表示します。ファイル名をnodeへ渡す実行方法はNode.js公式の実行ガイドに沿っています。
失敗例:指数を計算してから1を引く
次のコードは、小さな入力で計算方法を比較します。前のサンプルと独立して、同じ手順で実行できます。
(() => {
const x = 1.1111111111e-15;
const direct = Math.exp(x) - 1;
const stable = Math.expm1(x);
console.log(`直接計算: ${direct.toExponential(10)}`);
console.log(`expm1: ${stable.toExponential(10)}`);
})();MDNに掲載された計算値に基づく期待出力は次のとおりです。指数関数の近似計算では、実装によって末尾に差が出る場合があります。
直接計算: 1.1102230246e-15
expm1: 1.1111111111e-15ゼロに非常に近いxでは、求める結果もおおむねxに近くなります。比較すると、直接計算した値は入力から相対的に大きくずれています。処理を何度も繰り返す用途では、こうした誤差が計算全体に影響する可能性があります。MDNのMath.exp()も、ゼロに近い引数で1との差を求める場合にMath.expm1()を案内しています。
活用例:目標値へ滑らかに近づける
表示値を急に切り替えず、目標との差を少しずつ縮めたい場合を考えます。経過時間をdt、追従の強さをrateとすると、更新割合を1 - eの-rate * dt乗として計算できます。これを-Math.expm1(-rate * dt)と書けば、経過時間が小さい場合にも1との差を直接求められます。
次は、現在値0を目標値100へ近づける自己完結した計算例です。実際のアニメーションではなく、更新1回分の割合と結果を表示します。
(() => {
function smoothingWeight(rate, dt) {
if (!Number.isFinite(rate) || !Number.isFinite(dt)) {
throw new TypeError("rateとdtは有限の数値にしてください");
}
if (rate < 0 || dt < 0) {
throw new RangeError("rateとdtは0以上にしてください");
}
const product = rate * dt;
if (!Number.isFinite(product)) {
throw new RangeError("rateとdtの積が大きすぎます");
}
return -Math.expm1(-product);
}
const current = 0;
const target = 100;
const rate = 2;
const dt = 0.5;
const weight = smoothingWeight(rate, dt);
const next = current + (target - current) * weight;
console.log(`更新割合: ${weight.toFixed(6)}`);
console.log(`更新後: ${next.toFixed(6)}`);
console.log(`経過時間0: ${smoothingWeight(rate, 0).toFixed(6)}`);
})();期待出力は次のとおりです。
更新割合: 0.632121
更新後: 63.212056
経過時間0: 0.000000この例では、0.5秒で差の約63.2%を埋めます。dtを秒で渡すなら、rateも秒を基準に設定します。ミリ秒を混ぜると追従速度が大きく変わるため、呼び出し側で単位を統一してください。
応用先にはメーターの表示値や、計測値の急な変化を和らげる処理があります。追従が遅れる性質もあるため、瞬間的なピークを把握したい値では、元の値も残す設計が適しています。
制約と実装時の確認点
Math.expm1()を使っても任意精度計算にはなりません。JavaScriptのnumberは64ビットの浮動小数点数で、表現できる範囲と精度に限界があります。入力の乗算ですでに情報が失われた場合、その後にこの関数を呼んでも復元できません。
大きな正の入力では結果がInfinityになる可能性があります。基本例では戻り値にも有限性の検査を入れ、こうした状況を独自のRangeErrorへ変換しています。一方、平滑化の例では入力の積を検査しています。どの値を異常として扱うかは、用途ごとに決める必要があります。
また、toFixed()は表示用の文字列を作ります。後続の計算には整形前の数値を渡してください。毎回表示用に丸めた値を計算へ戻すと、この関数で抑えた誤差とは別の丸め誤差が加わります。
このAPIは同期処理で、Promiseを返しません。awaitは不要です。入力検証で投げた例外は呼び出し側で扱い、NaNやInfinityの結果を例外が自動的に拾うと考えないことが大切です。
公式参考資料
Googleの優先するニュース提供元にSEの部屋を追加
Googleで、いつも読みたい情報源を選べます。登録可否はGoogleの画面で確認できます。
候補にSEの部屋が表示されない場合は、まだ追加できません。