はじめに
let x = x => x + 1; は ES6 の起手式のようで、return arr.map(fx).filter(isValid).reduce(accumulator) が ES5 の亮黑色であるのと同じく、ES6 を少し知っていて var で起手すると人から嫌われるでしょう
しかし、ES6 の中で最も痛くも痒くもない特性といえば let と const でしょう。もしすでに var の小脾气に慣れているのであれば
一.なぜ let と const が必要なのか?
var にはいくつかの小脾气があるからです。彼らは関数スコープが引き起こす「バグ」だと考えています。例えばこの小さな奇妙な問題:
var x = 4;
(function() {
console.log(x); // undefined
console.log(x + 1); // NaN
var x = 1;
// 因为 var 的提升特性,以上代码等价于
// var x;
// console.log(x);
// console.log(x + 1);
// x = 1;
})();
これは Hosting(巻き上げ)と呼ばれ、無理やり黒鍋をかぶせられました:
誰が巻き上げろと言ったんだ、一串の奇妙な undefined、NaN を作り出して、全部お前のせいだ
実際、特意に変数名を節約しない限り、この問題に遭遇するのは難しいです
もう一つの問題はすべての JS プレイヤーが出会ったことがあります。以下の通り:
(function() {
var arr = [1, 2, 3];
for (var i = 0; i < arr.length; i++) {
setTimeout(function() {
// 因为 50ms 后外部循环结束了,i === arr.length = 3
console.log(arr[i]); // undefined x 3
}, 50);
// 修复
// (function(i) {
// setTimeout(function() {
// console.log(arr[i]); // 1 2 3
// });
// })(i);
}
})();
クロージャが保持するのは外部スコープへのアクセス権であり、変数の値ではありません。50ms 後に i にアクセスすると、もちろん 3 が取得できます。これがクロージャの特性で、他の関数型言語のクロージャも同様です。彼らはまた言います:
誰が常理に合わないように言ったんだ、ループ本体実行時の状態をなぜ保存してくれないのか、一堆の undefined を取らされて困る
JS は無力に弁解し、自分は確かにいくつかの場所で正しくないと考えています:
-
大域スコープで
var宣言された変数は global オブジェクトのプロパティになる -
ブロックレベルスコープがない、20 年間 IIFE を苦労して使ってきた(明らかに一組の中括弧で処理すべきこと)
そこで let と const が生まれました
二.let の特徴
1.let 宣言の変数にはブロックレベルスコープがある
はい、20 年後、JS にもブロックレベルスコープができました
for (let i = 0; i < 3; i++) {
//...
}
console.log(i); // Uncaught ReferenceError: i is not defined
すると問題が生じます。「ブロック」を作成する最も簡単な方法は何でしょうか?まだ IIFE なら、何が違うのでしょうか?答えは後述します。JS 構文の細かい詳細に関わるため、ここでは展開しません
2.let にも巻き上げ特性がある
最初のサンプルコードの重要な var を let に置き換えてみてください:
var y = 4;
(function() {
console.log(y); // Uncaught ReferenceError: y is not defined
console.log(y + 1);
let y = 1;
// let 也有提升特性,以上代码不完全等价于
// let y;
// console.log(y); // undefined
// console.log(y + 1); // NaN
// y = 1;
})();
今回は直接エラーになりました。外側の y = 4 が屏蔽されました。let y が確かにブロックレベル変数 y を巻き上げたことを示しています。エラーになるのはlet 行を実行して初めて変数定義がロードされるからです(6 番目の特徴)
TDZ(後述)の存在により、コメントされていない部分はコメントされたコードと完全には等価ではありません(上はエラーになりますが、下はエラーになりません)
3.例外は現在の行でスローされる
let はエラーの位置特定に役立ちます。NaN 以外の場合、例外は現在の行でスローされます。例えば undefined。NaN を除外するのは:
let a;
console.log(a + 1); // NaN === undefined + 1
JS の弱タイプメカニズムは NaN を例外とは見なしません
他の例外は現在の行でスローされます。対比して確認できます:
// let 当前行报错
(() => {
x++; // Uncaught ReferenceError: x is not defined
[1, 2, 3][x][0];
let x = 1;
})();
// var 当前行不报错
(() => {
x++;
[1, 2, 3][x][0]; // Uncaught TypeError: Cannot read property '0' of undefined
var x = 1;
})();
明らかに x++ の時点で間違っていますが、他のエラーを引き起こすまでエラーになりません。let はこの状況をうまく回避しました
P.S. 上の (() => {/* 新版 IIFE */})(); は影響を隔離し、テストを容易にするためだけです。let + class + ES6 モジュールはIIFE を剔除するためのものです。合理的な ES6 コードには、スコープを隔離するためだけの IIFE は現れるべきではありません
4.let 宣言の大域変数は大域オブジェクトのプロパティではない
let b = 2;
console.log(window.b); // undefined
これは変数命名空間が不要だと言っているわけではありません。script タグにはスコープを隔離する効果はありません。window 上のカスタムプロパティは減りましたが、大域変数の問題は依然として存在します。ES6 モジュールスコープとの連携については、これは非常に遠いことのようです
P.S. webpack などのビルドツールは ES6 モジュールをサポートしていますが、ブラウザがこのモジュールスコープをサポートして初めて大域変数の問題を解決できます。その時はおそらく本当に命名空間は不要になるでしょう
P.S. V8 は数ヶ月前に 100% ES6 をサポートしたと主張していましたが、ES6 モジュールはずっとサポートされていません。おそらくサポートする予定もないでしょう。ES6 モジュールメカニズムはブラウザ環境に適さないためです。理由は後で詳しく説明します
5.let 宣言のループ変数は毎回イテレーションで再バインドされる
つまりループ本体中のクロージャはループ変数の値のコピーを保持するということです。以下の通り:
(function() {
var arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
// 闭包保留了循环变量的值的副本
setTimeout(function() {
console.log(arr[i]); // 1 2 3
}, 50);
}
})();
みんながループ本体実行時の状態を保存したいと望むなら、みんなの意向に従います。JS は妥協しましたが、一歩だけ後退しました。ループ変数に対して少し hack をしただけで、クロージャの大原則は乱せません(外部スコープへのアクセス権を保持する)
注意:ループ変数とは、既存の 3 つのループ方式 for-of、for-in および従来のセミコロンで区切る C 風ループに適用されます
6.let 行を実行して初めて変数定義がロードされる
それ之前にこの変数を使用すると ReferenceError がエラーになります。この間変数はスコープ内に存在しますが、まだロードされていません。TDZ(Temporal Dead Zone)に位置しています
let は故意です。こうすることで Hosting と互換性がありながら、同時にエラーも報告できます
7.let 変数のスコープはブロック全体で有効であり、宣言箇所からブロック末尾まで有効というわけではない
C 言語とは異なり、ブロックレベル Hosting と算是できます。非常に適切な説明があります:
JavaScript における var 宣言のスコープは Photoshop の塗りつぶしツールのようだ。宣言箇所から前後 2 つの方向に拡散し、関数境界に触れるまで停止しない
let の Hosting 方式は var とあまり違いはありません(境界がブロック境界になっただけで)、どちらもこの双方向拡散です
8.let 変数を再定義すると SyntaxError がエラーになる
詞法解析段階でエラーになり、実行時エラーではありません。しかも SyntaxError は try-catch でキャッチできません
var の「許容性」は非常に強く、以下の通り:
var x = 2;
var x;
var x = x++;
はい、x は依然として 2 です。2 文目は無視され、3 文目の var は無視されて代入が実行されました。どのように書いてもエラーになりません
let x = 2;
let x; // Uncaught SyntaxError: Identifier 'x' has already been declared
こうなると、以後の面接試験の問題は簡単になります:)
9.class 宣言も let と同様で、同名クラスは SyntaxError がエラーになる
class は出厂時から let と合作条約を結び、let 式宣言規則に従います:
class A {}
class A {} // Uncaught SyntaxError: Identifier 'A' has already been declared
三.const の特徴
const は let と似ていますが、const 変数は読み取り専用です
特徴:
-
const 変数を変更すると SyntaxError がエラーになるはずですが、Chrome47 では操作が無効でもエラーになりません
-
const 宣言は同時に赋值する必要があります。否则エラーになりますが、Chrome47 はエラーにならず、値は undefined です
注意:この 2 つの制約は Chrome51 ですでに実装されています。現在エラーになります(どの更新か分かりませんが、话说これらの ES6 ノートは 16 年 1 月のことで、不小心に規範の约束力を目撃しました)
サンプルは以下の通り:
// 尝试修改 const 变量
const PI = Math.PI;
PI = 3; // Uncaught TypeError: Assignment to constant variable.
PI++; // 同上
console.log(PI); // 注释掉上两句会输出 3.141592653589793
// 尝试声明时不赋值
const UNDEF; // 词法检查阶段报错
// Uncaught SyntaxError: Missing initializer in const declaration
console.log(UNDEF); // 前面报错了,到不了这
四.ブロックを作成する最も簡単な方法
ES6 にはブロックレベルスコープがあります。IIFE によるスコープ隔離は歴史になることを意味します。那么替代品は何でしょうか?
{
let tip = '这是我的领地';
}; //!!! 千万千万注意这个不起眼的分号
console.log(tip); // Uncaught ReferenceError: tip is not defined
{}; は IIFE よりさっぱりしています。待って、末尾のセミコロンは何でしょうか、役に立つのでしょうか?Java のコードブロックは明らかにセミコロンを必要としませんよね
注意:この不起眼なセミコロンは不可欠です。削除するとエラーになります。{} がオブジェクトリテラルとして解析され、構文エラーを引き起こすためです。Java には確かにこのセミコロンは必要ありません。オブジェクトリテラルに曖昧さがないため、詞法解析器が混乱することはありません
JS における中括弧
実際 JS には 4 種類の中括弧があります。それぞれ:
// 1.对象字面量
{
a: 1,
b: 2
}
// 2.复合语句(一组代码,单语句可以省略花括号)
if (true) {
console.log(1);
console.log(2);
}
// 3.作为语法结构(花括号是语法结构,不能省略)
try {}
catch (ex) {}
// 4.label(也是代码分组,用来支持 break、continue 的跨层跳转)
label: {}
ブロックとオブジェクトリテラルには曖昧さがあります。どちらも
{
//...
}
JS はこれを式として解析するため、オブジェクトリテラルの構文をチェックし、正しければエラーになります。JS にこれをブロックとして解析すべきだと伝えれば、曖昧さは自然になくなります。2 つの方法があります:
// 1.分号强制语句(和逗号强制表达式一样)
{/* 我是一个块语句 */};
// 2.复合语句
{{/* 我是一个复合语句 */}}
したがってもう少し面倒なブロック作成方法は:
// 一种可爱的方式
{{
let tip = '这是我的领地';
}}
console.log(tip); // Uncaught ReferenceError: tip is not defined
もちろん故意に label を使ってブロックを作成することもできます。反正 label は一般にあまり役に立たないので、以下の通り:
block: {
let tip = '这是我的领地';
}
console.log(tip); // Uncaught ReferenceError: tip is not defined
数えれば 3 通りの方法があります。もっと多くの発見があるかもしれません。JS 構文に関する詳細情報は『JavaScript 语言精髓与编程实践』をご覧ください
五.まとめ
let はより完璧な var です
let... を ES6 の起手式としても間違いではありませんが、let に関するものは var より少なくありません。let をうまく使うことも var をうまく使うのと同じくらい簡単ではありません
さらに、let は var の替代品ですが、古いコードに対して全文置換を一度行うことができるわけではありません。let の制限はより多く、「許容性」は当然 var に劣ります。しかし不管怎样、過ぎ去るべきものは過ぎ去ります。var は终将消失します。したがって、let を受け入れてみましょう
参考資料
-
『JavaScript 语言精髓与编程实践』:非常に素晴らしい本です。もし看完する耐心があれば
-
『ES6 in Depth』:InfoQ 中文站が提供する無料電子書籍
コメントはまだありません