関数

関数は、繰り返し呼び出すことができるコードのブロックです。関数は入力パラメーターを受け入れることもでき、さまざまなパラメーターには対応する一意の戻り値があります。

概要

関数宣言

JavaScript には関数を宣言する 3 つの方法があります。

(1) 関数コマンド

function コマンドで宣言されたコード ブロックは関数です。 function コマンドの後には関数名が続き、その後に関数に渡されるパラメータを含む一対のかっこが続きます。関数本体は中括弧内に配置されます。

関数 print(s) {
  コンソールのログ;
}

上記のコードは、print 関数に名前を付けています。将来、print() 形式を使用して、対応するコードを呼び出すことができます。これを関数宣言と呼びます。

(2) 関数式

function コマンドを使用して関数を宣言するだけでなく、変数の代入も使用できます。

var print = 関数 {
  コンソールのログ;
};

この書き方では、変数に無名関数を代入します。このとき、代入文中の等号の右側には式しか置けないため、この無名関数を関数式(Function Expression)とも呼びます。

関数式を使用して関数を宣言する場合、「function」コマンドの後に関数名は続きません。関数名を付加した場合、その関数名は関数本体内でのみ有効となり、関数本体外では無効となります。

var print = 関数 x(){
  console.log(x のタイプ);
};

×
// 参照エラー: x が定義されていません

プリント()
// 関数

上記のコードは、関数名 x を関数式に追加します。この「x」は、関数式自体を参照する関数本体内でのみ使用でき、他の場所では使用できません。この記述方法には 2 つの用途があります。1 つは関数本体内で自分自身を呼び出すことができること、もう 1 つはデバッグを容易にすることです (デバッグ ツールが関数呼び出しスタックを表示するときに、関数の呼び出しスタックを表示する代わりに関数名が表示されます)。無名関数)。したがって、次の形式で関数を宣言することも非常に一般的です。

var f = 関数 f() {};

関数式では、ステートメントの終わりを示すためにステートメントの最後にセミコロンが必要であることに注意してください。関数宣言では、右中括弧の後にセミコロンは必要ありません。一般に、関数を宣言するこれら 2 つの方法の違いは非常に微妙であり、ほぼ同等であると考えることができます。

(3) 関数コンストラクタ

関数を宣言する 3 番目の方法は、Function コンストラクターです。

var add = 新しい関数(
  「×」、
  「はい」、
  'x + y を返す'
);

// と同等
関数 add(x, y) {
  x + y を返します。
}

上記のコードでは、Function コンストラクターは 3 つのパラメーターを受け取りますが、add 関数の「関数本体」である最後のパラメーターを除き、他のパラメーターはすべて add 関数のパラメーターです。

Function コンストラクターには任意の数のパラメーターを渡すことができます。パラメーターが 1 つしかない場合、そのパラメーターが関数本体として扱われます。

var foo = 新しい関数(
  '「Hello World」を返します;'
);

// と同等
関数 foo() {
  「Hello World」を返します。
}

Function コンストラクターは new コマンドを使用する必要はなく、返される結果はまったく同じです。

全体として、関数を宣言するこの方法は非常に直感的ではなく、ほとんど誰も使用しません。

関数の繰り返し宣言

同じ関数が複数回宣言された場合、後続の宣言によって以前の宣言が上書きされます。

関数 f() {
  コンソール.ログ(1);
}
f() // 2

関数 f() {
  コンソール.ログ(2);
}
f() // 2

上記のコードでは、後の関数宣言が前の関数宣言を上書きします。また、関数名の巻き上げにより、前の宣言はいつでも無効になることに注意することが重要です (以下を参照)。

括弧演算子、return ステートメント、および再帰

関数を呼び出すときは、括弧演算子を使用します。括弧内に関数パラメータを追加できます。

関数 add(x, y) {
  x + y を返します。
}

add(1, 1) // 2

上記のコードでは、関数名の後にかっこのペアが続く場合、その関数が呼び出されます。

関数本体内の return ステートメントはリターンを示します。 JavaScript エンジンは、return ステートメントに遭遇すると、return に続く式の値を直接返します。その後に他のステートメントがあっても、それは実行されません。つまり、「return」文に含まれる式が関数の戻り値となります。 return ステートメントは必須ではありません。そうでない場合、関数は値を返さないか、unknown を返します。

関数はそれ自体を呼び出すことができます。これが再帰です。以下は、再帰によってフィボナッチ数列を計算するコードです。

関数 fib(数値) {
  if (num === 0) は 0 を返します。
  if (num === 1) は 1 を返します。
  fib(数値 - 2) + fib(数値 - 1) を返します。
}

fib(6) // 8

上記のコードでは、「fib」関数内で「fib」が呼び出され、フィボナッチ数列の 6 番目の要素が 8 と計算されます。

第一級市民

JavaScript 言語は、関数を他の値 (数値、文字列、ブール値など) と同じレベルの値として扱います。値を使用できる場所であればどこでも、関数を使用できます。たとえば、関数を変数やオブジェクトのプロパティに割り当てたり、パラメータとして他の関数に渡したり、関数の結果として返したりすることができます。関数は実行できる単なる値であり、特別なことは何もありません。

関数は他のデータ型と同等の地位を持っているため、JavaScript 言語では第一級市民とも呼ばれます。

関数 add(x, y) {
  x + y を返します。
}

// 関数を変数に代入する
var 演算子 = 追加;

// 関数をパラメータとして使用し、値を返します
関数 a(op){
  戻り演算子;
}
a(加算)(1, 1)
// 2

関数名のプロモーション

JavaScript エンジンは関数名を変数名として扱うため、「function」コマンドを使用して関数を宣言すると、関数全体が変数宣言と同じようにコードの先頭に昇格されます。したがって、次のコードはエラーを報告しません。

f();

関数 f() {}

表面的には、上記のコードは関数 f を宣言する前に呼び出しているように見えます。しかし実際には、「変数昇格」により、関数 f はコードの先頭に昇格されます。つまり、関数 f は呼び出される前に宣言されています。ただし、代入ステートメントを使用して関数を定義すると、JavaScript はエラーを報告します。

f();
var f = 関数 (){};
// TypeError: 未定義は関数ではありません

上記のコードは、次の形式と同等です。

変数 f;
f();
f = 関数 () {};

上記のコードの 2 行目で、f を呼び出すと、f は宣言されただけで値が割り当てられていないため、unknown と同等となり、エラーが報告されます。

以下の例のように、同じ関数を function コマンドと var 代入文で宣言した場合、関数の昇格により、最終的には var 代入文の定義が使用されることに注意してください。

var f = 関数 () {
  console.log('1');
}

関数 f() {
  console.log('2');
}

f() // 1

上記の例では、表面的には、後で宣言された関数 f が前の var 代入ステートメントをオーバーライドするはずですが、関数プロモーションの存在により、実際にはその逆になります。

関数のプロパティとメソッド

名前属性

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

関数 f1() {}
f1.name // "f1"

関数が変数割り当てを通じて定義されている場合、name 属性は変数名を返します。

var f2 = 関数 () {};
f2.name // "f2"

ただし、上記の状況は、変数の値が無名関数の場合にのみ当てはまります。変数の値が名前付き関数の場合、name 属性は、function キーワードに続いて関数の名前を返します。

var f3 = 関数 myName() {};
f3.name // '私の名前'

上記のコードでは、f3.name は関数式の名前を返します。実際の関数名は依然として f3 であり、名前 myName は関数本体内でのみ使用できることに注意してください。

name 属性の使用法の 1 つは、パラメーター関数の名前を取得することです。

var myFunc = function () {};

関数テスト(f) {
  console.log(f.name);
}

test(myFunc) // myFunc

上記のコードでは、関数 test 内の name 属性を通じて、パラメータに渡された関数が何であるかを知ることができます。

長さ属性

関数の length プロパティは、関数に渡されると予想されるパラメーターの数、つまり関数定義内のパラメーターの数を返します。

関数 f(a, b) {}
f.length // 2

上記のコードは空の関数 f を定義しており、その length 属性は定義時のパラメータの数です。呼び出し時に入力したパラメーターの数に関係なく、「length」プロパティは常に 2 に等しくなります。

「length」属性は、オブジェクト指向プログラミングの「メソッドのオーバーロード」を実装するために、パラメータの定義時と呼び出し時の違いを判断するメカニズムを提供します。

toString()

関数の toString() メソッドは、その内容が関数のソース コードである文字列を返します。

関数 f() {
  a();
  b();
  c();
}

f.toString()
// 関数 f() {
// a();
// b();
// c();
// }

上記の例では、関数 ftoString() メソッドは、改行文字を含む f のソース コードを返します。

これらのネイティブ関数の場合、toString() メソッドは function (){[ネイティブ コード]} を返します。

Math.sqrt.toString()
// "関数 sqrt() { [ネイティブ コード] }"

上記のコードでは、Math.sqrt() は JavaScript エンジンによって提供されるネイティブ関数であり、toString() メソッドはネイティブ コード プロンプトを返します。

関数内のコメントも返すことができます。

関数 f() {/*
  これは
  複数行のコメント
*/}

f.toString()
// "関数 f(){/*
// これは
// 複数行のコメント
// */}"

これを使用すると、複数行の文字列を偽装して実装できます。

var multiline = 関数 (fn) {
  var arr = fn.toString().split('\n');
  return arr.slice(1, arr.length - 1).join('\n');
};

関数 f() {/*
  これは
  複数行のコメント
*/}

複数行(f);
// 「これは
// 複数行のコメント"

上の例では、関数 f 内に複数行のコメントがあります。toString() メソッドが f のソース コードを取得した後、最初と最後の 2 行を削除して複数行を取得します。弦。

関数のスコープ

意味

スコープとは、変数が存在するスコープを指します。 ES5 仕様では、JavaScript のスコープは 2 種類のみです。1 つはグローバル スコープで、変数はプログラム全体に存在し、どこでも読み取ることができます。もう 1 つは関数スコープで、変数は関数内にのみ存在します。 ES6 ではブロックレベルのスコープが追加されていますが、このチュートリアルでは説明しません。

トップレベル関数の場合、関数の外で宣言された変数はグローバル変数であり、関数内で読み取ることができます。

var v = 1;

関数 f() {
  コンソール.ログ(v);
}

f()
// 1

上記のコードは、グローバル変数 v が関数 f 内で読み取れることを示しています。

関数の内部で定義され、外部から読み取ることができない変数を「ローカル変数」といいます。

関数 f(){
  var v = 1;
}

v // ReferenceError: v が定義されていません

上記のコードでは、変数 v は関数内で定義されているため、ローカル変数となり、関数の外から読み取ることはできません。

関数内で定義された変数は、このスコープ内の同じ名前のグローバル変数を上書きします。

var v = 1;

関数 f(){
  var v = 2;
  コンソール.ログ(v);
}

f() // 2
v // 1

上記のコードでは、変数 v が関数の外側と内側の両方で定義されています。その結果、関数内で定義されると、ローカル変数 v がグローバル変数 v をオーバーライドします。

var コマンドの場合、ローカル変数は関数内でのみ宣言できることに注意してください。他のブロックで宣言されている場合、それらはすべてグローバル変数になります。

if (true) {
  var x = 5;
}
コンソール.log(x); // 5

上記のコードでは、条件判定ブロック内で変数 x を宣言しており、その結果はブロック外から読み出せるグローバル変数となっています。

関数内の変数のプロモーション

グローバル スコープと同様に、関数スコープ内でも「変数昇格」現象が発生します。 var コマンドで宣言された変数は、どこにあっても関数本体の先頭に昇格されます。

関数 foo(x) {
  if (x > 100) {
    var tmp = x - 100;
  }
}

// と同等
関数 foo(x) {
  var tmp;
  if (x > 100) {
    tmp = x - 100;
  };
}

関数自体のスコープ

関数自体も値であり、独自のスコープを持ちます。そのスコープは、実行されるスコープに関係なく、変数のスコープと同じであり、変数が宣言されたスコープです。

変数 a = 1;
var x = 関数 () {
  コンソール.ログ(a);
};

関数 f() {
  変数 a = 2;
  x();
}

f() // 1

上記のコードでは、関数 x は関数 f の外側で宣言されているため、そのスコープは関数 f の本体の値を内部変数 a に取りません。したがって、 2 の代わりに 1 を出力します。

つまり、関数が実行されるスコープは、関数が呼び出されるスコープではなく、関数が定義されるスコープです。

関数「B」が関数「A」の内部変数を参照していないことを考慮せずに、関数「A」が関数「B」を呼び出すと間違いを犯しやすくなります。

var x = 関数 () {
  コンソール.ログ(a);
};

関数 y(f) {
  変数 a = 2;
  f();
}

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

上記のコードは、関数 x をパラメータとして受け取り、関数 y に渡します。しかし、関数 x が関数 y の外で宣言されており、スコープが外層にバインドされているため、関数 y の内部変数 a が見つからず、エラーが発生します。

同様に、関数本体内で宣言された関数は、関数本体内にスコープがバインドされます。

関数 foo() {
  var x = 1;
  関数 bar() {
    コンソール.ログ(x);
  }
  リターンバー。
}

var x = 2;
var f = foo();
f() // 1

上記のコードでは、関数 bar が関数 foo 内で宣言されており、bar のスコープは foo にバインドされています。 foo の外側の bar を取り出して実行すると、変数 x は foo の外側の x ではなく、foo の内側の x を指します。このメカニズムが、以下で説明する「閉鎖」現象を構成します。

パラメータ

概要

関数の実行時には、外部データを提供する必要がある場合があります。この外部データはパラメータと呼ばれます。

関数 square(x) {
  x * x を返します。
}

square(2) // 4
square(3) // 9

上の式の「x」は「square」関数のパラメータです。この値は実行するたびに指定する必要があります。そうしないと結果が得られません。

パラメータの省略

関数の引数は必須ではなく、JavaScript では省略できます。

関数 f(a, b) {
  を返します。
}

f(1, 2, 3) // 1
f(1) // 1
f() // 未定義

f.length // 2

上記のコードの関数「f」は 2 つのパラメータを定義していますが、実行時にパラメータがいくつ指定されても (またはパラメータが指定されなくても)、JavaScript はエラーを報告しません。省略されたパラメータの値は「未定義」になります。関数の「length」属性は、実際に渡されるパラメータの数とは関係がないことに注意してください。これは、関数によって渡されると予想されるパラメータの数を反映するだけです。

ただし、最初のパラメータだけを省略し、後のパラメータを保持する方法はありません。最初のパラメータを省略する必要がある場合は、明示的に「unknown」を渡すことしかできません。

関数 f(a, b) {
  を返します。
}

f( , 1) // 構文エラー: 予期しないトークン ,(…)
f(未定義, 1) // 未定義

上記のコードで、最初のパラメータを省略すると、エラーが報告されます。

配送方法

関数のパラメータがプリミティブ型の値(数値、文字列、ブール値)の場合、受け渡し方法は値渡しとなります。これは、関数本体内のパラメーター値を変更しても、関数の外部には影響しないことを意味します。

var p = 2;

関数 f(p) {
  p = 3;
}
f(p);

p // 2

上記のコードでは、変数 p はプリミティブ型の値であり、関数 f に渡す方法は値渡しです。したがって、関数内では p の値は元の値のコピーとなり、どのように変更しても元の値には影響しません。

ただし、関数パラメータが複合型値 (配列、オブジェクト、その他の関数) の場合、受け渡し方法は参照渡しになります。つまり、関数の元の値のアドレスが渡されるため、関数内のパラメーターを変更すると元の値に影響します。

var obj = { p: 1 };

関数 f(o) {
  o.p = 2;
}
f(オブジェクト);

obj.p // 2

上記のコードでは、関数 f に渡されるのはパラメータ オブジェクト obj のアドレスです。したがって、関数内で obj の属性 p を変更すると、元の値に影響します。

関数内で変更されるのがパラメータ オブジェクトの特定の属性ではなく、パラメータ全体を置き換える場合、元の値は影響を受けないことに注意してください。

var obj = [1, 2, 3];

関数 f(o) {
  o = [2, 3, 4];
}
f(オブジェクト);

obj // [1、2、3]

上記のコードでは、関数 f() 内で、パラメータ オブジェクト obj が別の値に完全に置き換えられます。現時点では、元の値は影響を受けません。これは、仮パラメータ (o) の値が実際にはパラメータ obj のアドレスであるため、値を o に再代入すると、元のアドレスに保存された値が o になるからです。もちろん影響を受けません。

同じ名前のパラメータ

同じ名前のパラメータがある場合は、最後に表示される値が使用されます。

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

f(1, 2) // 2

上記のコードでは、関数 f() には 2 つのパラメータがあり、パラメータ名は両方とも a です。値を取得する場合は、後続の a が値を持たない場合や省略された場合でも優先されます。

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

f(1) // 未定義

関数 f() を呼び出す場合、2 番目のパラメータは指定されず、a の値は undefine になります。このとき、最初の a の値を取得したい場合は、arguments オブジェクトを使用できます。

関数 f(a, a) {
  console.log(引数[0]);
}

f(1) // 1

引数オブジェクト

(1)定義

JavaScript では関数に無制限の数のパラメーターを含めることができるため、関数本体内のすべてのパラメーターを読み取るメカニズムが必要です。これが「arguments」オブジェクトの由来です。

arguments オブジェクトには、関数の実行時のすべてのパラメータが含まれます。arguments[0] が最初のパラメータ、arguments[1] が 2 番目のパラメータになります。このオブジェクトは関数本体内でのみ使用できます。

var f = 関数 (1) {
  console.log(引数[0]);
  console.log(引数[1]);
  console.log(引数[2]);
}

f(1, 2, 3)
// 1
// 2
// 3

通常モードでは、「arguments」オブジェクトは実行時に変更できます。

var f = function(a, b) {
  引数[0] = 3;
  引数[1] = 2;
  a + b を返します。
}

f(1, 1) // 5

上記のコードでは、関数 f() の呼び出し時に渡されるパラメータが関数内で 32 に変更されます。

厳密モードでは、「arguments」オブジェクトは関数パラメータとリンク関係を持ちません。言い換えれば、「arguments」オブジェクトを変更しても、実際の関数パラメータには影響しません。

var f = function(a, b) {
  'use strict' // strict モードをオンにする
  引数[0] = 3;
  引数[1] = 2;
  a + b を返します。
}

f(1, 1) // 2

上記のコードでは、関数本体はこの時点で arguments オブジェクトを変更しても、実際のパラメータ ab には影響しません。

「arguments」オブジェクトの「length」属性を通じて、関数の呼び出し時に取得されるパラメータの数を決定できます。

関数 f() {
  引数を返します。長さ;
}

f(1, 2, 3) // 3
f(1) // 1
f() // 0

(2) 配列との関係

「arguments」は配列に似ていますが、オブジェクトであることに注意してください。配列固有のメソッド (sliceforEach など) を arguments オブジェクトで直接使用することはできません。

「arguments」オブジェクトで配列メソッドを使用したい場合、実際の解決策は「arguments」を実数の配列に変換することです。一般的に使用される 2 つの変換方法は次のとおりです。「スライス」方法と新しい配列を 1 つずつ埋める方法です。

var args = Array.prototype.slice.call(arguments);

// または
var args = [];
for (var i = 0; i < argument.length; i++) {
  args.push(引数[i]);
}

(3)呼び出し先属性

arguments オブジェクトには callee 属性があり、対応する元の関数を返します。

var f = 関数 () {
  console.log(arguments.callee === f);
}

f() // true

arguments.callee を使用すると、関数自体を呼び出す目的を達成できます。この属性は厳密モードでは無効になるため、使用はお勧めできません。

関数に関するその他の知識ポイント

閉鎖

クロージャは JavaScript 言語の難点であり、特徴でもあります。多くの高度なアプリケーションはクロージャに依存しています。

クロージャを理解するには、まず変数のスコープを理解する必要があります。前述したように、JavaScript にはグローバル スコープと関数スコープの 2 種類のスコープがあります。グローバル変数は関数内で直接読み取ることができます。

var n = 999;

関数 f1() {
  コンソール.log(n);
}
f1() // 999

上記のコードでは、関数 f1 はグローバル変数 n を読み取ることができます。

ただし、通常の状況では、関数内で宣言された変数を関数の外で読み取ることはできません。

関数 f1() {
  var n = 999;
}

コンソール.ログ(n)
// キャッチされない ReferenceError: n が定義されていません(

上記のコードでは、関数 f1 内で宣言された変数 n を関数の外部から読み取ることはできません。

さまざまな理由により、関数内でローカル変数を取得する必要がある場合。通常の状況では、これは不可能であり、回避策によってのみ達成できます。つまり、関数内に別の関数を定義します。

関数 f1() {
  var n = 999;
  関数 f2() {
コンソール.log(n); // 999
  }
}

上記のコードでは、関数 f2 は関数 f1 内にあります。この時点で、f1 内のすべてのローカル変数が f2 から見えます。しかし、その逆は機能しません。 f2 内のローカル変数は f1 には見えません。これは、JavaScript 言語の独自の「チェーン スコープ」構造で、子オブジェクトはすべての親オブジェクトの変数をレベルごとに検索します。したがって、親オブジェクトのすべての変数は子オブジェクトから見えますが、その逆はありません。

f2f1 のローカル変数を読み取ることができるので、f2 を戻り値として使用する限り、f1 の外部でその内部変数を読み取ることはできません!

関数 f1() {
  var n = 999;
  関数 f2() {
    コンソール.log(n);
  }
  f2 を返します。
}

var 結果 = f1();
結果(); // 999

上記のコードでは、関数 f1 の戻り値が関数 f2 になります。f2 は f1 の内部変数を読み込むことができるので、f1 の内部変数を外部から取得することができます。

クロージャは関数 f2 で、他の関数の内部変数を読み取ることができる関数です。 JavaScript 言語では、関数内のサブ関数のみが内部変数を読み取ることができるため、クロージャは単純に「関数内で定義された関数」として理解できます。クロージャの最大の特徴は、自分が生まれた環境を「記憶」できることです。例えば、f2は自分が生まれた環境f1を記憶しているので、f1の内部変数を取得することができます。 「f2」。本質的に、クロージャは関数の内部と関数の外部を接続するブリッジです。

クロージャの最大の用途は 2 つあり、1 つは外部関数内の変数を読み取ること、もう 1 つはこれらの変数をメモリ内に保持することです。つまり、クロージャは作成された環境を常に存在させることができます。以下の例を参照してください。クロージャにより、内部変数が最後の呼び出しの結果を記憶します。

関数 createIncrementor(start) {
  戻り関数 () {
    start++ を返します。
  };
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

上記のコードでは、start は関数 createIncrementor の内部変数です。クロージャを通じて「start」の状態が保持され、各呼び出しは前の呼び出しに基づいて計算されます。ご覧のとおり、クロージャ inc により、関数 createIncrementor の内部環境が常に存在します。したがって、クロージャは関数の内部スコープへのインターフェイスとして見ることができます。

なぜクロージャは外部関数の内部変数を返すことができるのでしょうか?その理由は、クロージャ (上記の例では inc) が外部変数 (start) を使用しているため、外部関数 (createIncrementor) がメモリから解放されないためです。クロージャがガベージ コレクション メカニズムによってクリアされない限り、外部関数によって提供される実行環境はクリアされず、その内部変数はクロージャが読み取るための現在の値を常に保持します。

クロージャのもう 1 つの用途は、オブジェクトのプライベート プロパティとメソッドをカプセル化することです。

関数 人(名前) {
  var_age;
  関数 setAge(n) {
    _年齢 = n;
  }
  関数 getAge() {
    _年齢を返す;
  }

  戻る {
    名前: 名前、
    getAge: getAge、
    setAge: setAge
  };
}

var p1 = 人物('張三');
p1.setAge(25);
p1.getAge() // 25

上記のコードでは、関数 person の内部変数 _ageが、クロージャgetAgesetAgeを通じて返されるオブジェクトp1` のプライベート変数になります。

外部関数が実行されるたびに新しいクロージャが生成され、このクロージャは外部関数の内部変数を保持するため、メモリ消費量が非常に多くなることに注意してください。したがって、クロージャを悪用することはできません。悪用すると、Web ページでパフォーマンスの問題が発生します。

即時に呼び出される関数式 (IIFE)

JavaScript 構文によれば、関数名の後に括弧「()」が続き、関数の呼び出しを示します。たとえば、「print()」は「print」関数を呼び出すことを意味します。

場合によっては、関数を定義した直後に関数を呼び出す必要があります。この場合、関数定義の後に括弧を追加することはできないため、構文エラーが発生します。

function(){ /* コード */ }();
// SyntaxError: 予期しないトークン (

このエラーの理由は、「function」キーワードがステートメントと式の両方として使用できるためです。

// 声明
関数 f() {}

//表現
var f = 関数 f() {}

式として使用すると、定義後に括弧を使用して関数を直接呼び出すことができます。

var f = function f(){ return 1}();
f//1

上記のコードでは、関数は括弧で囲まれた定義の直後に呼び出され、エラーは報告されません。その理由は、「関数」が式として使用され、エンジンが関数定義を値として扱うためです。この場合、エラーは報告されません。

解析のあいまいさを避けるために、JavaScript では、「function」キーワードが行の先頭にある場合、常にステートメントとして解釈されると規定しています。したがって、エンジンは行の先頭にある function キーワードを見つけると、この段落は関数の定義であり、括弧で終わるべきではないと判断し、エラーを報告します。

関数を定義した直後に関数を呼び出すための解決策は、「関数」を行の先頭に出現させず、エンジンにそれを式として理解させることです。最も簡単な解決策は、括弧で囲むことです。

(function(){ /* コード */ }());
// または
(function(){ /* コード */ })();

上記の 2 つの記述方法は両方とも括弧で始まり、エンジンはその後に続くものが関数定義ステートメントではなく式であると判断するため、エラーが回避されます。これは、「即時呼び出し関数式」 (略して IIFE) と呼ばれます。

上記 2 つの記述方法の末尾のセミコロンは必須であることに注意してください。セミコロンを省略して IIFE を 2 つ接続すると、エラーが報告される場合があります。

// エラーを報告する
(function(){ /* コード */ }())
(function(){ /* コード */ }())

上記のコードの 2 行の間にセミコロンがない場合、JavaScript はそれらをまとめて解釈し、2 行目を 1 行目のパラメータとして解釈します。

拡張すると、次の 3 つの書き方のように、インタプリタが式を使用して関数定義を処理できる方法であれば、同様の効果を得ることができます。

var i = function(){ return 10;
true && function(){ /* コード */ }();
0, function(){ /* コード */ }();

以下のように書くことも可能です。

!function () { /* コード */ }();
~function () { /* コード */ }();
-function () { /* コード */ }();
+function () { /* コード */ }();

通常、この「即時関数式」は匿名関数でのみ使用してください。これには 2 つの目的があります。1 つは、関数に名前を付ける必要がないため、グローバル変数の汚染が回避されます。2 つ目は、IIFE が内部で別のスコープを形成し、外部から読み取れないプライベート変数をカプセル化できることです。

//書き方その1
var tmp = 新しいデータ;
プロセスデータ(tmp);
ストアデータ(tmp);

//書き方2
(関数 () {
  var tmp = 新しいデータ;
  プロセスデータ(tmp);
  ストアデータ(tmp);
}());

上記のコードでは、グローバル変数の汚染を完全に回避できるため、メソッド 2 を記述する方がメソッド 1 を記述するよりも優れています。

評価コマンド

基本的な使い方

eval コマンドは文字列を引数として受け取り、この文字列をステートメントとして実行します。

eval('var a = 1;');
// 1

上記のコードは文字列をステートメントとして実行し、変数 a を生成します。

パラメータ文字列をステートメントとして実行できない場合は、エラーが報告されます。

eval('3x') // キャッチされない SyntaxError: 無効または予期しないトークン

eval に配置される文字列は独自の意味を持つ必要があり、eval 以外のコマンドと組み合わせて使用​​することはできません。たとえば、次のコードはエラーを報告します。

eval('return;'); // キャッチされない構文エラー: 不正な return ステートメント

return は単独で使用できず、関数内で使用する必要があるため、上記のコードはエラーを報告します。

eval への引数が文字列でない場合、それは変更されずに返されます。

eval(123) // 123

eval は独自のスコープを持たず、現在のスコープ内で実行されるため、現在のスコープ内の変数の値が変更され、セキュリティ上の問題が発生する可能性があります。

変数 a = 1;
eval('a = 2');

// 2

上記のコードでは、「eval」コマンドは外部変数「a」の値を変更します。このため、「eval」はセキュリティ上のリスクとなります。

このリスクを防ぐために、JavaScript では、厳密モードが使用される場合、「eval」内で宣言された変数は外側のスコープに影響を与えないことを規定しています。

(関数 f() {
  '厳密を使用';
  eval('var foo = 123');
  console.log(foo); // 参照エラー: foo が定義されていません
})()

上記のコードでは、関数 f は内部的には strict モードになっており、eval 内で宣言された foo 変数は外部には影響しません。

ただし、厳密モードでも、「eval」は現在のスコープ内の変数を読み書きできます。

(関数 f() {
  '厳密を使用';
  var foo = 1;
  eval('foo = 2');
  コンソール.log(foo); // 2
})()

上記のコードでは、厳密モードでも、「eval」が内部で外部変数を書き換えています。これは、セキュリティ リスクがまだ存在していることを示しています。

つまり、「eval」の本質は、現在のスコープにコードを挿入することです。セキュリティ上のリスクと JavaScript エンジン最適化の実行速度への悪影響のため、通常、この使用は推奨されません。通常、eval の最も一般的な使用例は JSON データの文字列を解析することですが、正しいアプローチはネイティブの JSON.parse メソッドを使用することです。

evalのエイリアスコール

前に述べたように、「eval」はエンジン最適化の実行速度に貢献しません。さらに問題なのは、次の状況では、エンジンは静的コード分析段階で「eval」が実行されていることを認識できないことです。

var m = 評価;
m('var x = 1');
× // 1

上記のコードでは、変数 meval のエイリアスです。静的コード分析フェーズでは、エンジンは m('var x = 1')eval コマンドを実行していることを認識できません。

「eval」のエイリアスがコードの最適化に影響しないようにするために、JavaScript 標準では、「eval」がエイリアスを使用して実行されるときは常に「eval」の内部スコープが常にグローバルであると規定しています。

変数 a = 1;

関数 f() {
  変数 a = 2;
  変数 e = 評価;
  e('console.log(a)');
}

f() // 1

上記のコードでは、eval はエイリアス呼び出しであるため、関数内であってもスコープはグローバル スコープであるため、出力 a はグローバル変数です。この場合、エンジンは「e()」が現在の関数スコープに影響を与えないことを確認でき、この行は最適化中に除外できます。

eval へのエイリアス呼び出しにはさまざまな形式がありますが、それが直接呼び出しでない限り、それはエイリアス呼び出しです。エンジンは eval() の形式を直接呼び出しとしてのみ区別できるからです。

eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')

上記の形式はすべて「eval」のエイリアス呼び出しであり、スコープはすべてグローバル スコープです。

参考リンク


作者: wangdoc

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

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