機能拡張

関数パラメータのデフォルト値

基本的な使い方

ES6 より前では、関数パラメータのデフォルト値を直接指定することはできず、回避策を使用することしかできませんでした。

関数 log(x, y) {
  y = y || '世界';
  コンソール.log(x, y);
}

log('Hello') // ハローワールド
log('Hello', 'China') // こんにちは中国
log('Hello', '') // ハローワールド

上記のコードは、関数 log() のパラメータ y に値が割り当てられているかどうかをチェックします。割り当てられていない場合、デフォルト値は World として指定されます。この書き方の欠点は、パラメータ y に値が割り当てられていても、対応するブール値が false である場合、その割り当ては効果がないことです。上記のコードの最後の行と同様に、パラメータ y は null 文字と等しく、結果はデフォルト値に変更されます。

この問題を回避するには、通常、パラメータ 'y' に値が割り当てられているかどうかを最初に確認する必要があります。割り当てられていない場合は、デフォルト値と同じになります。

if (typeof y === '未定義') {
  y = 'ワールド';
}

ES6 では、パラメータ定義の直後に、関数パラメータのデフォルト値を設定できます。

関数 log(x, y = 'ワールド') {
  コンソール.log(x, y);
}

log('Hello') // ハローワールド
log('Hello', 'China') // こんにちは中国
log('こんにちは', '') //こんにちは

ご覧のとおり、ES6 の書き方は ES5 よりもはるかにシンプルで自然です。別の例を示します。

関数ポイント(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}

const p = 新しいポイント();
p // { x: 0, y: 0 }

ES6 の記述方法には、単純さに加えて 2 つの利点があります。1 つ目は、コードを読む人が、関数本体やドキュメントを見なくても、どのパラメータを省略できるかをすぐに認識できることです。2 つ目は、将来のコードの最適化に役立ちます。外部インターフェイスでは、このパラメータを完全に削除しても、以前のコードの実行が失敗することはありません。

パラメータ変数はデフォルトで宣言されるため、letconst を使用して再度宣言することはできません。

関数 foo(x = 5) {
  let x = 1; // エラー
  const x = 2;
}

上記のコードでは、パラメータ変数 x は関数本体でデフォルトで宣言されています。let または const を使用して再度宣言することはできません。そうしないと、エラーが報告されます。

パラメータのデフォルト値を使用する場合、関数は同じ名前のパラメータを持つことができません。

// エラーを報告しません
関数 foo(x, x, y) {
  // ...
}

// エラーを報告する
関数 foo(x, x, y = 1) {
  // ...
}
// SyntaxError: このコンテキストでは重複したパラメータ名は許可されません

また、見落とされやすい点は、パラメータのデフォルト値が値渡しではなく、デフォルト値式の値が毎回再計算されることです。つまり、パラメーターのデフォルト値は遅延評価されます。

x = 99 とします。
関数 foo(p = x + 1) {
  コンソール.ログ(p);
}

foo() // 100

x = 100;
foo() // 101

上記のコードでは、パラメータ p のデフォルト値は x + 1 です。このとき、関数 foo() が呼び出されるたびに、デフォルトの p である 100 の代わりに x + 1 が再計算されます。

代入のデフォルト値の構造化と組み合わせて使用​​されます。

パラメーターのデフォルト値は、割り当てを分割するためのデフォルト値と組み合わせて使用​​できます。

関数 foo({x, y = 5}) {
  コンソール.log(x, y);
}

foo({}) // 未定義 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: 未定義のプロパティ 'x' を読み取れません

上記のコードは、オブジェクトの構造化代入のデフォルト値のみを使用し、関数パラメーターのデフォルト値を使用しません。関数 foo() の引数がオブジェクトの場合のみ、変数 xy が構造化代入によって生成されます。関数 foo() がパラメータを指定せずに呼び出された場合、変数 xy は生成されず、エラーが報告されます。この状況は、関数パラメータにデフォルト値を提供することで回避できます。

関数 foo({x, y = 5} = {}) {
  コンソール.log(x, y);
}

foo() // 未定義 5

上記のコードは、パラメータが指定されない場合、関数 foo のパラメータがデフォルトで空のオブジェクトになることを指定しています。

次に、割り当てられたデフォルト値を構造化する別の例を示します。

関数 fetch(url, { body = ''、メソッド = 'GET'、ヘッダー = {} }) {
  console.log(メソッド);
}

fetch('http://example.com', {})
// "得る"

fetch('http://example.com')
// エラーを報告する

上記のコードでは、関数 fetch() の 2 番目のパラメーターがオブジェクトの場合、その 3 つのプロパティにデフォルト値を設定できます。この書き方は第二引数を省略できません。関数の引数のデフォルト値と組み合わせると第二引数を省略できます。この時点で、二重デフォルトが表示されます。

関数 fetch(url, { body = ''、メソッド = 'GET'、ヘッダー = {} } = {}) {
  console.log(メソッド);
}

fetch('http://example.com')
// "得る"

上記のコードでは、関数 fetch に 2 番目のパラメータがない場合、関数パラメータのデフォルト値が有効になり、次に構造化代入のデフォルト値が有効になり、変数 method はデフォルト値は「GET」です。

関数パラメータのデフォルト値が有効になった後も、パラメータの構造化と代入が引き続き実行されることに注意してください。

関数 f({ a, b = 'ワールド' } = { a: 'こんにちは' }) {
  コンソール.ログ(b);
}

f() // 世界

上記の例では、関数 f() がパラメーターなしで呼び出されるため、パラメーターのデフォルト値 { a: 'hello' } が有効になり、このデフォルト値が構造化されて割り当てられ、それによって のデフォルト値がトリガーされます。パラメータ変数 b が有効になります。

練習として、次の 2 つの関数の書き方の違いについて考えてみましょう。

//書き方その1
関数 m1({x = 0, y = 0} = {}) {
  [x, y] を返します。
}

//書き方2
関数 m2({x, y} = { x: 0, y: 0 }) {
  [x, y] を返します。
}

// 関数にパラメータがない場合
m1() // [0, 0]
m2() // [0, 0]

// x と y の両方に値がある場合
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]

// x には値がありますが、y には値がありません
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, 未定義]

// x と y の両方に値がない場合
m1({}) // [0, 0];
m2({}) // [未定義、未定義]

m1({z:3}) // [0, 0]
m2({z: 3}) // [未定義, 未定義]

パラメータのデフォルト値の位置

通常、デフォルト値が定義されているパラメータは関数の末尾パラメータである必要があります。どのパラメータが省略されているかを確認しやすいためです。末尾以外のパラメータがデフォルト値に設定されている場合、このパラメータは実際には省略できません。

//例1
関数 f(x = 1, y) {
  [x, y] を返します。
}

f() // [1, 未定義]
f(2) // [2, 未定義]
f(, 1) // エラーを報告する
f(未定義, 1) // [1, 1]

//例2
関数 f(x, y = 5, z) {
  [x, y, z] を返します。
}

f() // [未定義, 5, 未定義]
f(1) // [1, 5, 未定義]
f(1, ,2) // エラーを報告する
f(1, 未定義, 2) // [1, 5, 2]

上記のコードでは、デフォルト値を持つパラメータは末尾パラメータではありません。このとき、明示的に「unknown」と入力しない限り、以降のパラメータを省略せずにこのパラメータだけを省略することはできません。

unknown が渡された場合、パラメータがデフォルト値と等しくなるようにトリガーされますが、null にはこの効果はありません。

関数 foo(x = 5, y = 6) {
  コンソール.log(x, y);
}

foo(未定義、null)
// 5 ヌル

上記のコードでは、「x」パラメータは「unknown」に対応し、デフォルト値がトリガーされます。「y」パラメータは「null」に等しいため、デフォルト値はトリガーされません。

関数の長さ属性

デフォルト値を指定すると、関数の length 属性はデフォルト値を指定せずにパラメータの数を返します。つまり、デフォルト値を指定すると、length プロパティが歪んでしまいます。

(関数(a) {}).length // 1
(関数 (a = 5) {}).length // 0
(関数 (a, b, c = 5) {}).length // 2

上記のコードでは、length 属性の戻り値は、関数のパラメーターの数から、指定されたデフォルト値を持つパラメーターの数を引いたものと等しくなります。たとえば、上記の最後の関数は 3 つのパラメータを定義しており、そのうち 1 つのパラメータ c はデフォルト値を指定しているため、length 属性は 3 から 1 を引いた値に等しく、最終的に 2 になります。

これは、「length」属性の意味が関数に渡されると予想されるパラメータの数であるためです。パラメーターにデフォルト値が割り当てられた後、渡されると予想されるパラメーターの数には、このパラメーターは含まれません。同様に、残りのパラメータは後で length 属性に含まれなくなります。

(function(...args) {}).length // 0

デフォルト値を持つパラメータが最後のパラメータではない場合、length 属性は以降のパラメータにカウントされなくなります。

(関数 (a = 0, b, c) {}).length // 0
(関数 (a, b = 1, c) {}).length // 1

範囲

パラメータのデフォルト値が設定されると、関数が宣言されて初期化されるときに、パラメータは別のスコープ (コンテキスト) を形成します。初期化が完了すると、このスコープは消えます。パラメーターのデフォルト値が設定されていない場合、この構文の動作は表示されません。

var x = 1;

関数 f(x, y = x) {
  コンソール.ログ(y);
}

f(2) // 2

上記のコードでは、パラメータ y のデフォルト値は変数 x と同じです。関数 f が呼び出されるとき、パラメーターは別のスコープを形成します。このスコープでは、デフォルト値変数 x はグローバル変数 x ではなく、最初のパラメーター x を指すため、出力は 2 になります。

次の例をもう一度見てください。

x = 1 とします。

関数 f(y = x) {
  x = 2 とします。
  コンソール.ログ(y);
}

f() // 1

上記のコードでは、関数 f が呼び出されるとき、パラメーター y = x が別のスコープを形成します。このスコープでは、変数 x 自体は定義されていないため、外部グローバル変数 x を指します。関数が呼び出されたとき、関数本体内のローカル変数 x はデフォルト値変数 x に影響を与えません。

この時点でグローバル変数 x が存在しない場合は、エラーが報告されます。

関数 f(y = x) {
  x = 2 とします。
  コンソール.ログ(y);
}

f() // ReferenceError: x が定義されていません

このように書くとエラーも報告されます。

var x = 1;

関数 foo(x = x) {
  // ...
}

foo() // ReferenceError: 初期化前に 'x' にアクセスできません

上記のコードでは、パラメーター x = x が別のスコープを形成します。実際に実行されるのは「let x = x」です。一時的なデッドゾーンのため、このコード行はエラーを報告します。

パラメータのデフォルト値が関数の場合、関数のスコープもこの規則に従います。以下の例を参照してください。

foo = 'アウター' とします。

関数 bar(func = () => foo) {
  foo = '内部' とします。
  console.log(func());
}

bar(); // 外側

上記のコードでは、関数 bar のパラメータ func のデフォルト値は無名関数であり、戻り値は変数 foo です。関数パラメータによって形成される別個のスコープでは、変数 foo が定義されていないため、foo は外部グローバル変数 foo を指し、したがって outer が出力されます。

このように書くとエラーが報告されます。

関数 bar(func = () => foo) {
  foo = '内部' とします。
  console.log(func());
}

bar() // ReferenceError: foo が定義されていません

上記のコードでは、匿名関数の foo は関数の外層を指していますが、関数の外層では変数 foo が宣言されていないため、エラーが報告されます。

より複雑な例を次に示します。

var x = 1;
関数 foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  コンソール.ログ(x);
}

foo() // 3
× // 1

上記のコードでは、関数 foo のパラメーターが別のスコープを形成します。このスコープでは、変数 x が最初に宣言され、次に変数 y が宣言されます。 y のデフォルト値は匿名関数です。この無名関数内の変数 x は、同じスコープ内の最初のパラメータ x を指します。内部変数 x は関数 foo 内で宣言されています。この変数と最初のパラメータ x は同じスコープにないため、内部変数 y を実行すると同じ変数ではなくなります。 x 外部グローバル変数 x の値は変更されていません。

var x = 3var が削除されると、関数 foo の内部変数 x は最初のパラメータ x を指します。これは無名関数内の x と一致します。最終出力は 2 ですが、外部グローバル変数 x はまだ影響を受けません。

var x = 1;
関数 foo(x, y = function() { x = 2; }) {
  x = 3;
  y();
  コンソール.ログ(x);
}

foo() // 2
× // 1

応用

パラメーターのデフォルト値を使用すると、特定のパラメーターを省略してはならないことを指定でき、省略するとエラーがスローされます。

関数 throwIfMissing() {
  throw new Error('パラメータがありません');
}

function foo(mustBeProvided = throwIfMissing()) {
  提供する必要があるものを返します。
}

foo()
// エラー: パラメータがありません

上記のコードの foo 関数がパラメーターなしで呼び出された場合、デフォルト値の throwIfMissing 関数が呼び出され、エラーがスローされます。

また、上記のコードから、パラメーター mustBeProvided のデフォルト値が throwIfMissing 関数の実行結果と等しいこともわかります (関数名 throwIfMissing の後に一対のかっこがあることに注意してください)。パラメータのデフォルト値は定義時に実行されず、実行時に実行されます。パラメータに値が割り当てられている場合、デフォルト値の関数は実行されません。

さらに、パラメータのデフォルト値を「未定義」に設定でき、このパラメータを省略できることを示します。

関数 foo(オプション = 未定義) { ··· }

残りのパラメータ

ES6 では、関数の冗長パラメータを取得するために使用される REST パラメータ (「...変数名」の形式) が導入されているため、「引数」オブジェクトを使用する必要はありません。残りのパラメータと一致する変数は配列であり、追加のパラメータがその配列に入れられます。

関数 add(...values) {
  合計 = 0 とします。

  for (値の var val) {
    合計 += 値;
  }

  合計を返します。
}

add(2, 5, 3) // 10

上記のコードの「add」関数は、残りのパラメータを使用して、任意の数のパラメータを関数に渡すことができます。

以下は、「arguments」変数を置換する残りのパラメータの例です。

// 引数変数の書き方
関数 sortNumbers() {
  Array.from(arguments).sort() を返します。
}

// レストパラメータの書き方
const sortNumbers = (...numbers) =>numbers.sort();

上記の 2 つのコードの記述方法を比較すると、rest パラメーターを記述する方法の方が自然で簡潔であることがわかります。

「arguments」オブジェクトは配列ではなく、配列のようなオブジェクトです。したがって、配列メソッドを使用するには、まず Array.from を使用して配列に変換する必要があります。残りのパラメータにはこの問題はありません。これは実数の配列であり、配列固有のメソッドはすべて使用できます。以下は、rest パラメータを使用して配列の push メソッドをオーバーライドする例です。

関数プッシュ(配列, ...アイテム) {
  items.forEach(関数(アイテム) {
    配列.プッシュ(項目);
    コンソール.ログ(項目);
  });
}

var a = [];
プッシュ(a、123)

残りのパラメータの後には他のパラメータを置くことはできません (つまり、最後のパラメータのみにすることができます)。そうでない場合は、エラーが報告されます。

// エラーを報告する
関数 f(a, ...b, c) {
  // ...
}

残りのパラメーターを除く、関数の length プロパティ。

(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1

厳密モード

ES5 からは、内部で関数を strict モードに設定できるようになりました。

関数 doSomething(a, b) {
  '厳密を使用';
  // コード
}

ES2016 ではいくつかの変更が加えられ、関数パラメーターがデフォルト値、構造化代入、または展開演算子を使用している限り、関数を内部で明示的に厳密モードに設定することはできず、そうでない場合はエラーが報告されることが規定されています。

// エラーを報告する
関数 doSomething(a, b = a) {
  '厳密を使用';
  // コード
}

// エラーを報告する
const doSomething = function ({a, b}) {
  '厳密を使用';
  // コード
};

// エラーを報告する
const doSomething = (...a) => {
  '厳密を使用';
  // コード
};

const obj = {
  // エラーを報告する
  doSomething({a, b}) {
    '厳密を使用';
    // コード
  }
};

この規定の理由は、関数内の厳密モードが関数本体と関数パラメーターの両方に適用されるためです。ただし、関数が実行されるときは、関数パラメータが最初に実行され、次に関数本体が実行されます。これには不合理な点があります。関数本体からのみパラメーターを厳密モードで実行する必要があるかどうかを知ることができますが、パラメーターは関数本体の前に実行される必要があります。

// エラーを報告する
関数 doSomething(値 = 070) {
  '厳密を使用';
  戻り値;
}

上記のコードでは、パラメータ value のデフォルト値は 8 進数 070 ですが、厳密モードではプレフィックス 0 を使用して 8 進数を表すことができないため、エラーが報告されます。しかし実際には、JavaScript エンジンは最初に「value = 070」を正常に実行し、次に関数本体に入り、厳密モードで実行する必要があることがわかり、そのとき初めてエラーが報告されます。

関数本体のコードを最初に解析してからパラメーター コードを実行することもできますが、これにより間違いなく複雑さが増します。したがって、標準では単にこの使用法が禁止されており、パラメータがデフォルト値、分割代入、またはスプレッド演算子を使用している限り、厳密モードを明示的に指定することはできません。

この制限を回避するには 2 つの方法があります。 1 つ目は、合法的なグローバル厳密モードを設定することです。

'厳密を使用';

関数 doSomething(a, b = a) {
  // コード
}

2 つ目は、パラメーターを指定せずにすぐに実行される関数で関数をラップすることです。

const doSomething = (function () {
  '厳密を使用';
  戻り関数(値 = 42) {
    戻り値;
  };
}());

名前属性

関数の name 属性は、関数の関数名を返します。

関数 foo() {}
foo.name // "フー"

この属性は長い間ブラウザーで広くサポートされてきましたが、ES6 まで標準に書き込まれませんでした。

ES6 では、このプロパティの動作にいくつかの変更が加えられていることに注意してください。匿名関数が変数に割り当てられている場合、ES5 の name 属性は空の文字列を返しますが、ES6 の name 属性は実際の関数名を返します。

var f = function () {};

//ES5
f.name // ""

//ES6
f.name // "f"

上記のコードでは、変数 f は匿名関数と等しく、ES5 と ES6 の name 属性によって返される値は異なります。

名前付き関数が変数に割り当てられている場合、ES5 と ES6 の両方の name 属性は名前付き関数の元の名前を返します。

const bar = function baz() {};

//ES5
bar.name // "バズ"

//ES6
bar.name // "バズ"

Function コンストラクターによって返される関数インスタンス。name 属性の値は anonymous です。

(新しい関数).name // "匿名"

bind によって返される関数の場合、name 属性値には bound という接頭辞が付けられます。

関数 foo() {};
foo.bind({}).name // "バインドされた foo"

(function(){}).bind({}).name // "バウンド"

アロー関数

基本的な使い方

ES6 では、「矢印」(=>) を使用して関数を定義できます。

var f = v => v;

// と同等
var f = 関数 (v) {
  v を返します。
};

アロー関数にパラメータが必要ない場合、または複数のパラメータが必要な場合は、括弧を使用してパラメータ部分を表します。

var f = () => 5;
// と同等
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// と同等
var sum = function(num1, num2) {
  num1 + num2 を返します。
};

アロー関数のコード ブロックが複数のステートメントである場合は、中かっこを使用してそれらを囲み、「return」ステートメントを使用して戻ります。

var sum = (num1, num2) => { return num1 + num2 }

中括弧はコード ブロックとして解釈されるため、アロー関数がオブジェクトを直接返す場合は、オブジェクトの外側に括弧を追加する必要があります。追加しないとエラーが報告されます。

// エラーを報告する
let getTempItem = id => { id: id, name: "Temp" };

// エラーを報告しません
let getTempItem = id => ({ id: id, name: "Temp" });

以下は、機能するものの、間違った結果が得られる特殊なケースです。

let foo = () => { a: 1 };
foo() // 未定義

上記のコードでは、本来の目的はオブジェクト { a: 1 } を返すことですが、エンジンは中括弧がコード ブロックであると考えるため、ステートメント a: 1 の行が実行されます。このとき、aは文のラベルとして解釈できるため、実際に実行される文は1;となり、戻り値を返さずに関数は終了します。

アロー関数のステートメントが 1 行のみで、値を返す必要がない場合は、中括弧を記述せずに次の記述方法を使用できます。

let fn = () => void DoesNotReturn();

アロー関数は変数の構造化と組み合わせて使用​​できます。

const full = ({ first, last }) => first + ' ' + last;

// と同等
関数フル(人) {
  person.first + ' ' + person.last を返します。
}

アロー関数を使用すると式がより簡潔になります。

const isEven = n => n % 2 === 0;
const square = n => n * n;

上記のコードは 2 行だけを使用して 2 つの単純なツール関数を定義しています。アロー関数が使用されていなかったら、複数行を占める可能性があり、今ほど目を引くものではなかったでしょう。

アロー関数の使用法の 1 つは、コールバック関数を簡素化することです。

// 通常の関数の書き方
[1,2,3].map(関数 (x) {
  x * x を返します。
});

//アロー関数の書き方
[1,2,3].map(x => x * x);

別の例は

// 通常の関数の書き方
var result = value.sort(function (a, b) {
  a - b を返します。
});

//アロー関数の書き方
var result = value.sort((a, b) => a - b);

以下は、残りのパラメータとアロー関数を組み合わせた例です。

const 数値 = (...nums) => 数値;

数字(12345)
//[1,2,3,4,5]

const headAndTail = (頭, ...尾) => [頭, 尾];

headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]

使用上の注意

アロー関数を使用する場合、いくつかの注意点があります。

(1) アロー関数には独自の this オブジェクトがありません (詳細は以下を参照)。

(2) コンストラクタとして使用することはできません。つまり、「new」コマンドをアロー関数で使用することはできません。そうでない場合は、エラーがスローされます。

(3) 関数本体に存在しない「arguments」オブジェクトは使用できません。これを使用したい場合は、代わりにrestパラメータを使用できます。

(4) yieldコマンドが使用できないため、アロー関数をジェネレータ関数として使用することはできません。

上記の 4 つのポイントのうち、最も重要なのは最初のポイントです。通常の関数の場合、内部の「this」は関数が実行されているオブジェクトを指しますが、これはアロー関数には当てはまりません。独自の this オブジェクトはなく、内部の this は定義されたときの上位スコープの this です。つまり、アロー関数内の this ポインタは固定ですが、通常の関数の this ポインタは可変です。

関数 foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

変数ID = 21;

foo.call({ id: 42 });
//ID:42

上記のコードでは、setTimeout() のパラメータはアロー関数です。このアロー関数の定義は、foo 関数が生成されたときに有効になり、実際の実行は 100 ミリ秒後まで待機しません。通常の関数であれば、実行時に「this」はグローバルオブジェクト「window」を指し、「21」が出力されるはずです。ただし、アロー関数を使用すると、this は常に関数定義が有効になるオブジェクト (この場合は {id: 42}) を指すため、42 が出力されます。

次の例は、コールバック関数がそれぞれアロー関数と通常の関数であることを示し、内部の「this」ポイントを比較します。

関数タイマー() {
  this.s1 = 0;
  this.s2 = 0;
  // アロー関数
  setInterval(() => this.s1++, 1000);
  // 通常の関数
  setInterval(関数() {
    これ.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1:3
// s2:0

上記のコードでは、「Timer」関数内にアロー関数と通常の関数をそれぞれ使用して 2 つのタイマーが設定されています。前者の this は、それが定義されているスコープ (つまり、Timer 関数) にバインドされており、後者の this は、ランタイムが配置されているスコープ (つまり、グローバル物体)。したがって、3100 ミリ秒後、timer.s1 は 3 回更新され、timer.s2 は 1 度も更新されていません。

アロー関数は実際に「this」を固定小数点にポイントさせ、「this」をバインドして可変ではなくなるようにすることができます。この機能はコールバック関数をカプセル化するのに非常に役立ちます。以下は、DOM イベントのコールバック関数がオブジェクトにカプセル化される例です。

var ハンドラー = {
  ID: '123456'init: function() {
    document.addEventListener('クリック',
      イベント => this.doSomething(event.type), false);
  }、

  doSomething: function(type) {
    console.log('処理 ' + type + ' for ' + this.id);
  }
};

上記のコードの init() メソッドではアロー関数が使用されており、これによりアロー関数の this が常に handler オブジェクトを指すようになります。コールバック関数が通常の関数の場合、「this」が「document」オブジェクトを指しているため、「this.doSomething()」行を実行するとエラーが報告されます。

つまり、アロー関数には独自の「this」がまったくないため、内部の「this」は外側のコード ブロックの「this」になります。 this がないため、コンストラクタとして使用できません。

以下は、Babel のアロー関数によって生成された ES5 コードであり、「this」の方向性を明確に示しています。

//ES6
関数 foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

//ES5
関数 foo() {
  var _this = これ;

  setTimeout(関数() {
    console.log('id:', _this.id);
  }, 100);
}

上記のコードでは、変換された ES5 バージョンは、アロー関数が独自の this をまったく持たず、外側の this を参照していることを明確に示しています。

次のコードでは、「this」は何点ありますか?

関数 foo() {
  return() => {
    return() => {
      return() => {
        console.log('id:', this.id);
      };
    };
  };
}

var f = foo.call({id: 1});

var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

答えは、「this」への点は 1 つだけであり、それは関数「foo」の「this」です。これは、すべての内部関数がアロー関数であり、独自の「this」を持たないためです。実際には、外側の foo 関数の最後の this です。したがって、どれほどネストされていても、t1t2t3 はすべて同じ結果を出力します。この例のすべての内部関数が通常の関数として記述されている場合、各関数の「this」はランタイムが配置されている別のオブジェクトを指します。

this に加えて、次の 3 つの変数はアロー関数には存在せず、外部関数の対応する変数を指します: argumentssuper、および new.target

関数 foo() {
  setTimeout(() => {
    console.log('args:', 引数);
  }, 100);
}

foo(2, 4, 6, 8)
// 引数: [2, 4, 6, 8]

上記のコードでは、アロー関数内の変数 arguments は、実際には関数 fooarguments 変数です。

また、アロー関数には独自の this がないので、当然のことながら、call()、apply()、bind() を使って this のポインタを変更することはできません。

(関数() {
  戻る [
    (() => this.x).bind({ x: 'inner' })()
  ];
}).call({ x: '外側' });
//['外側']

上記のコードでは、アロー関数には独自の this がないため、bind メソッドは無効であり、内部の this は外部の this を指します。

JavaScript 言語の「this」オブジェクトは、長い間悩みの種でした。オブジェクト メソッドで「this」を使用する場合は、細心の注意を払う必要があります。アロー関数は「this」を「バインド」し、この問題を大幅に解決します。

適用できない

アロー関数は this を「動的」から「静的」に変更するため、次の 2 つの状況ではアロー関数を使用しないでください。

最初の機会はオブジェクトのメソッドを定義することであり、そのメソッドには内部的に「this」が含まれています。

const cat = {
  生存: 9、
  ジャンプ: () => {
    これは生きています--;
  }
}

上記のコードでは、cat.jumps() メソッドはアロー関数ですが、これは間違っています。 cat.jumps() を呼び出す場合、通常の関数であればメソッド内の this は cat を指しますが、上記のようにアロー関数として記述した場合は this はグローバルオブジェクトを指します。期待した結果は得られません。これは、オブジェクトが個別のスコープを構成しないため、「jumps」アロー関数が定義されているときのスコープはグローバル スコープであるためです。

別の例を見てみましょう。

globalThis.s = 21;

const obj = {
  s:42m: () => console.log(this.s)
};

obj.m() // 21

上記の例では、「obj.m()」はアロー関数を使用して定義されています。 JavaScript エンジンの処理方法では、まずこのアロー関数をグローバル空間で生成し、それを obj.m に代入します。これにより、アロー関数内の this がグローバル オブジェクトを指すことになるので、 obj.m( ) は、オブジェクト内の 42 ではなく、空間内の 21 を出力します。上記のコードは実際には以下のコードと同等です。

globalThis.s = 21;
globalThis.m = () => console.log(this.s);

const obj = {
  s:42m: globalThis.m
};

obj.m() // 21

上記の理由により、オブジェクトのプロパティを定義するには、アロー関数を使用するのではなく、従来の記述方法を使用することをお勧めします。

2 番目のケースは、動的な「this」が必要な場合であり、アロー関数は使用すべきではありません。

var button = document.getElementById('press');
button.addEventListener('クリック', () => {
  this.classList.toggle('on');
});

上記のコードを実行すると、ボタンをクリックするとエラーが報告されます。これは、button の listen 関数がアロー関数であり、内部の this がグローバル オブジェクトになるためです。通常の関数に変更すると、this はクリックされたボタン オブジェクトを動的に指します。

さらに、関数本体が非常に複雑で行数が多い場合、または値の計算だけでなく関数内に多数の読み取りおよび書き込み操作がある場合は、アロー関数を使用すべきではなく、通常の関数を使用する必要があります。を使用すると、コードの可読性が向上します。

ネストされたアロー関数

アロー関数はアロー関数内で使用することもできます。以下は、ES5 構文の複数ネストされた関数です。

関数挿入(値) {
  return {into: 関数 (配列) {
    return {after: function (afterValue) {
      array.splice(array.indexOf(afterValue) + 1, 0, value);
      配列を返します。
    }};
  }};
}

insert(2).into([1, 3]).after(1);

上記の関数はアロー関数を使用して書き換えることができます。

let insert = () => ({into: (配列) => ({after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  配列を返します。
}})});

insert(2).into([1, 3]).after(1);

以下は、デプロイメント パイプライン メカニズム (パイプライン) の例です。つまり、前の関数の出力が次の関数の入力になります。

const パイプライン = (...funcs) =>
  val => funcs.reduce((a, b) => b(a), val);

const plus1 = a => a + 1;
const mult2 = a => a * 2;
const addThenMult = パイプライン(plus1, mult2);

addThenMult(5)
// 12

上記の書き方では読みにくいと感じる場合は、次のような書き方も可能です。

const plus1 = a => a + 1;
const mult2 = a => a * 2;

マルチ2(プラス1(5))
// 12

アロー関数のもう 1 つの機能は、ラムダ計算を簡単に書き換えることができることです。

// ラムダ計算の書き方
fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

// ES6の書き込み方法
var fix = f => (x => f(v => x(x)(v)))
               (x => f(v => x(x)(v)));

上記 2 つの書き方はほぼ 1 対 1 に対応します。ラムダ計算はコンピューター サイエンスにとって非常に重要であるため、コンピューター サイエンスを探索するための代替ツールとして ES6 を使用できるようになります。

テールコールの最適化

テールコールとは何ですか?

テールコールは関数型プログラミングにおける重要な概念であり、非常に簡単であり、関数の最後のステップで別の関数を呼び出すことを意味します。

関数 f(x){
  g(x) を返します。
}

上記のコードでは、関数 f の最後のステップは関数 g を呼び出すことであり、これは末尾呼び出しと呼ばれます。

次の 3 つの状況は末尾呼び出しではありません。

// 状況 1
関数 f(x){
  y = g(x) とします。
  y を返します。
}

// ケース 2
関数 f(x){
  g(x) + 1 を返します。
}

// 状況 3
関数 f(x){
  g(x);
}

上記のコードの最初の状況は、関数 g を呼び出した後に代入操作があるため、セマンティクスはまったく同じですが、末尾呼び出しではありません。ケース 2 では、1 行で記述されている場合でも、呼び出し後の操作も含まれます。ケース 3 は以下のコードと同等です。

関数 f(x){
  g(x);
  未定義を返します。
}

末尾呼び出しは、最後のステップである限り、必ずしも関数の最後に現れる必要はありません。

関数 f(x) {
  if (x > 0) {
    m(x)を返す
  }
  n(x) を返します。
}

上記のコードでは、関数 mn は両方とも末尾呼び出しです。これは、これらが関数 f の最後の操作であるためです。

末尾呼び出しの最適化

テールコールが他のコールと異なる理由は、その特殊な呼び出し位置にあります。

関数呼び出しは、呼び出し位置や内部変数などの情報を保存する「呼び出しフレーム」とも呼ばれる「呼び出しレコード」をメモリ内に形成することがわかっています。関数「A」内で関数「B」が呼び出された場合、「A」の呼び出しフレームの上に「B」の呼び出しフレームが形成されます。 B の呼び出しフレームが消える前に、B が実行を終了して結果を A に返すまで待ち​​ます。関数 B が関数 C も内部的に呼び出す場合、C の別の呼び出しフレームが存在します。すべての呼び出しフレームは「呼び出しスタック」を形成します。

末尾呼び出しは関数の最後のステップであるため、呼び出し位置、内部変数、その他の情報は再度使用されないため、外側の関数の呼び出しフレームを保持する必要はなく、内側の関数の呼び出しフレームを使用するだけです。関数を直接置き換えて外部関数を呼び出すだけで十分です。

関数 f() {
  m = 1 とします。
  n = 2 とします。
  g(m + n) を返します。
}
f();

// と同等
関数 f() {
  g(3) を返します。
}
f();

// と同等
g(3);

上記のコードでは、関数 g が末尾呼び出しでない場合、関数 f は内部変数 m と n の値、g の呼び出し位置などの情報を保存する必要があります。しかし、関数 fg を呼び出した後に終了するため、実行の最後のステップでは、f(x) の呼び出しフレームは削除され、g(3) の呼び出しフレームのみが保持されます。

これを「Tail call optimization」(テールコール最適化)といい、内部関数の呼び出しフレームのみを保持します。すべての関数が末尾呼び出しである場合、実行されるたびに呼び出しフレームを 1 つだけにすることができるため、メモリが大幅に節約されます。これが「テールコールの最適化」の意味です。

外側の関数の内部変数が使用されなくなった場合にのみ、内側の関数の呼び出しフレームが外側の関数の呼び出しフレームを置き換えることに注意してください。それ以外の場合、「末尾呼び出しの最適化」は実行できません。

関数 addOne(a){
  変数 1 = 1;
  関数 inner(b){
    b + 1 を返します。
  }
  inner(a) を返します。
}

内部関数 inner は外部関数 addOne の内部変数 one を使用するため、上記の関数は末尾呼び出しの最適化を実行しません。

現在、末尾呼び出しの最適化をサポートしているのは Safari ブラウザのみであり、Chrome と Firefox はサポートしていないことに注意してください。

末尾再帰

関数自体を呼び出すことを再帰といいます。末尾がそれ自体を呼び出す場合、それは末尾再帰と呼ばれます。

再帰は、数千または数百の呼び出しフレームを同時に保存する必要があり、「スタック オーバーフロー」エラーが簡単に発生する可能性があるため、非常にメモリを消費します。ただし、末尾再帰の場合、呼び出しフレームが 1 つだけであるため、「スタック オーバーフロー」エラーは発生しません。

関数階乗(n) {
  if (n === 1) は 1 を返します。
  n * 階乗 (n - 1) を返します。
}

階乗(5) // 120

上記のコードは階乗関数です。「n」の階乗を計算するには、最大で「n」件の通話記録を保存する必要があり、計算量は O(n) です。

末尾再帰として書き換えると、通話記録は 1 つだけ保持され、計算量は O(1) になります。

関数階乗(n, total) {
  if (n === 1) 合計を返します。
  階乗を返します(n - 1, n * 合計);
}

階乗(5, 1) // 120

もう 1 つの有名な例は、フィボナッチ数列の計算です。これは、末尾再帰的最適化の重要性を十分に説明できます。

非末尾再帰フィボナッチ数列は次のように実装されます。

関数フィボナッチ (n) {
  if ( n <= 1 ) {return 1};

  フィボナッチ(n - 1) + フィボナッチ(n - 2) を返します。
}

フィボナッチ(10) // 89
フィボナッチ(100) // タイムアウト
フィボナッチ(500) // タイムアウト

末尾再帰的に最適化されたフィボナッチ数列は次のように実装されます。

関数 Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};

  フィボナッチ 2 (n - 1, ac2, ac1 + ac2) を返します。
}

フィボナッチ2(100) // 573147844013817200000
フィボナッチ2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // 無限大

「末尾呼び出しの最適化」は再帰的操作にとって非常に重要であることがわかり、一部の関数型プログラミング言語ではそれを言語仕様に記述しています。 ES6 についても同様であり、すべての ECMAScript 実装で「末尾呼び出しの最適化」を導入する必要があることが初めて明確に規定されました。これは、末尾再帰が ES6 で使用されている限り、スタック オーバーフロー (または再帰の層によって引き起こされるタイムアウト) が発生せず、比較的メモリを節約できることを意味します。

再帰関数の書き換え

末尾再帰の実装では、多くの場合、最後のステップがそれ自体のみを呼び出すように再帰関数を書き直す必要があります。これを行う方法は、使用されているすべての内部変数を関数パラメータとして書き換えることです。たとえば、上記の例では、階乗関数 fastial は中間変数 total を使用し、この中間変数を関数のパラメーターとして書き換える必要があります。この欠点は、あまり直観的ではないことです。なぜ「5」の階乗を計算するのに 2 つのパラメータ「5」と「1」を渡す必要があるのか​​を理解するのが難しいのです。

この問題を解決するには 2 つの方法があります。方法 1 は、末尾再帰関数に加えて正規形関数を提供することです。

関数 tailFactory(n, total) {
  if (n === 1) 合計を返します。
  return tailFactory(n - 1, n * total);
}

関数階乗(n) {
  return tailFactory(n, 1);
}

階乗(5) // 120

上記のコードは、階乗関数 factorial の正規形式を介して末尾再帰関数 tailFactory を呼び出していますが、これはより正規に見えます。

関数型プログラミングにはカリー化と呼ばれる概念があり、これは複数パラメーターの関数を単一パラメーターの形式に変換することを意味します。ここでカレーを作ることもできます。

関数カリーリング(fn, n) {
  戻り関数 (m) {
    return fn.call(this, m, n);
  };
}

関数 tailFactory(n, total) {
  if (n === 1) 合計を返します。
  return tailFactory(n - 1, n * total);
}

const 階乗 = カリーリング(tailFactory, 1);

階乗(5) // 120

上記のコードは、末尾再帰関数 tailFactorial を、カリー化を通じて 1 つのパラメーターのみを受け入れる factorial に変更します。

2 番目の方法は非常に簡単で、ES6 関数のデフォルト値を使用します。

関数階乗(n, 合計 = 1) {
  if (n === 1) 合計を返します。
  階乗を返します(n - 1, n * 合計);
}

階乗(5) // 120

上記のコードでは、パラメーター total のデフォルト値は 1 であるため、呼び出し時にこの値を指定する必要はありません。

要約すると、再帰は本質的にループ操作です。純粋な関数型プログラミング言語にはループ命令がありません。すべてのループは再帰的に実装されるため、これらの言語では末尾再帰が非常に重要です。 「末尾呼び出しの最適化」をサポートする他の言語 (Lua、ES6 など) の場合は、ループを再帰で置き換えることができることを知っておいてください。再帰を使用した後は、末尾再帰を使用することをお勧めします。

ストリクトモード

ES6 の末尾呼び出しの最適化は、厳密モードでのみ有効になり、通常モードでは無効になります。

これは、通常モードでは、関数の呼び出しスタックを追跡できる関数内に 2 つの変数があるためです。

  • func.arguments: 呼び出されたときに関数のパラメータを返します。
  • func.caller: 現在の関数を呼び出す関数を返します。

末尾呼び出しの最適化が発生すると、関数の呼び出しスタックが書き換えられるため、上記の 2 つの変数が歪められます。 Strict モードではこれら 2 つの変数が無効になるため、末尾呼び出しモードは strict モードでのみ有効になります。

関数制限付き() {
  '厳密を使用';
  Limited.caller; // エラーレポート
  制限付き.arguments; // エラーレポート
}
制限付き();

末尾再帰最適化の実装

末尾再帰的最適化は厳密モードでのみ有効ですが、通常モード、またはこの機能をサポートしていない環境で末尾再帰的最適化を使用する方法はありますか?答えは「はい」です。末尾再帰最適化を自分で実装する必要があるだけです。

その原理は非常にシンプルです。末尾再帰を最適化する必要がある理由は、呼び出しスタックが多すぎるとオーバーフローが発生するため、呼び出しスタックが削減される限り、オーバーフローは発生しません。コールスタックを減らすにはどうすればよいでしょうか? 「再帰」の代わりに「ループ」を使用してください。

以下は通常の再帰関数です。

関数 sum(x, y) {
  if (y > 0) {
    合計(x + 1, y - 1)を返します。
  } それ以外 {
    x を返します。
  }
}

合計(1, 100000)
// キャッチされない RangeError: 最大呼び出しスタック サイズを超えました(…)

上記のコードでは、「sum」は再帰関数、パラメータ「x」は累算される値、パラメータ「y」は再帰の回数を制御します。 100,000 回再帰するように「sum」を指定すると、呼び出しスタック回数の最大値を超えたことを示すエラーが報告されます。

トランポリン機能は、再帰的実行を周期的実行に変換できます。

関数トランポリン(f) {
  while (f && f 関数のインスタンス) {
    f = f();
  }
  fを返します;
}

上記は、関数 f をパラメータとして受け取るトランポリン関数の実装です。 f が実行後に関数を返す限り、実行は続行されます。ここでは、関数内で関数を呼び出すのではなく、関数を返してからその関数を実行していることに注意してください。これにより、再帰的な実行が回避され、コール スタックが大きすぎる問題が解消されます。

次に、各ステップで別の関数を返すように元の再帰関数を書き直すだけです。

関数 sum(x, y) {
  if (y > 0) {
    sum.bind(null, x + 1, y - 1) を返します。
  } それ以外 {
    x を返します。
  }
}

上記のコードでは、「sum」関数を実行するたびに、それ自体の別のバージョンが返されます。

ここで、トランポリン関数を使用して「sum」を実行すると、コールスタックのオーバーフローは発生しません。

トランポリン(sum(1, 100000))
// 100001

トランポリン関数は真の末尾再帰最適化ではありません。以下の実装はそうです。

関数 tco(f) {
  var 値。
  var アクティブ = false;
  var 累積 = [];

  戻り関数アキュムレータ() {
    累積.push(引数);
    if (!active) {
      アクティブ = true;
      while (累積長さ) {
        値 = f.apply(this,Accumulated.shift());
      }
      アクティブ = false;
      戻り値;
    }
  };
}

var sum = tco(function(x, y) {
  if (y > 0) {
    合計(x + 1, y - 1)を返します。
  }
  それ以外 {
    ×を返す
  }
});

合計(1, 100000)
// 100001

上記のコードでは、「tco」関数は末尾再帰的最適化の実装であり、その秘密は状態変数「active」にあります。デフォルトでは、この変数は非アクティブです。末尾再帰的最適化プロセスに入ると、この変数がアクティブになります。次に、再帰的 sum の各ラウンドは unknown を返すため、再帰的実行は回避され、accumulated 配列には sum 実行の各ラウンドのパラメータが格納されます。これは常に値を持ち、これにより while が確実に実行されます。 accumulator 関数内のループは常に実行されます。このようにして、「再帰」が「ループ」に巧みに変更され、次のラウンドのパラメーターが前のラウンドのパラメーターを置き換えて、呼び出しスタックの層が 1 つだけになるようにします。

関数パラメータの末尾のカンマ

ES2017 許可 関数の最後の引数には末尾にカンマが付いています。

以前は、関数を定義または呼び出すときに、最後のパラメーターの後にカンマを使用することはできませんでした。

どこでもピエロ関数(
  パラメータ1、
  パラメータ2
) { /* ... */ }

どこでもピエロ(
  「ふー」、
  'バー'
);

上記のコードで、「param2」または「bar」の後にカンマが追加されている場合、エラーが報告されます。

上記のように複数行にパラメータを記述した場合(つまり、各パラメータが 1 行を占める)、将来コードを修正するときに、関数 clownsEverywhere に 3 番目のパラメータを追加したり、順序を調整したりする必要がある場合に、パラメータを指定するには、最後のパラメータを元のパラメータに追加する必要があります。その後にカンマを追加します。バージョン管理システムの場合、カンマが追加された行も変更されたことがわかります。これは少し冗長に見えるため、新しい構文では、末尾にコンマを付けて定義と呼び出しを行うことができます。

どこでもピエロ関数(
  パラメータ1、
  パラメータ2、
) { /* ... */ }

どこでもピエロ(
  「ふー」、
  'バー'、
);

この規定により、関数パラメータが配列およびオブジェクトの末尾のカンマ規則と一致するようになります。

Function.prototype.toString()

ES2019 関数インスタンスの toString() メソッドを変更しました。

toString() メソッドは関数コード自体を返します。コメントとスペースは以前は省略されていました。

function /* foo コメント */ foo () {}

foo.toString()
// 関数 foo() {}

上記のコードでは、関数 foo の元のコードにはコメントが含まれており、関数名 foo と括弧の間にはスペースがありますが、 toString() メソッドはコメントを省略しています。

変更された toString() メソッドでは、まったく同じ元のコードが返されることが明示的に要求されます。

function /* foo コメント */ foo () {}

foo.toString()
// "関数 /* foo コメント */ foo () {}"

catchコマンドのパラメータは省略されています

JavaScript 言語の try...catch 構造では、以前は、catch コマンドの後にパラメータを指定し、try コード ブロックによってスローされたエラー オブジェクトを受け入れる必要があることが明示的に要求されていました。

試す {
  // ...
} キャッチ (エラー) {
  // エラーを処理します
}

上記のコードでは、「catch」コマンドの後にパラメータ「err」が続きます。

多くの場合、このパラメータは「catch」コード ブロックでは使用されません。ただし、文法が正しいことを確認するには、やはり書く必要があります。 ES2019 「catch」ステートメントでパラメータを省略できるように変更が加えられました。

試す {
  // ...
} キャッチ {
  // ...
}

作者: wangdoc

アドレス: https://wangdoc.com/

ライセンス: クリエイティブ・コモンズ 3.0