서문에
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 을 했을 뿐이며, 클로저의대원칙은 어지럽힐 수 없습니다(외부 스코프의 접근권을 유지)
주의: 루프 변수란, 기존의 세 루프 방식 for-of、for-in 및 전통적인 세미콜론으로 구분하는 C 스타일 루프에 적용됩니다
6.let 행을 실행해야만 변수 정의가 로드된다
그 전에 이 변수를 사용하면 ReferenceError 가 에러 발생합니다. 이 시간 동안 변수는 스코프 내에 있지만 아직 로드되지 않았습니다. TDZ(Temporal Dead Zone) 에 위치합니다
let 은 고의입니다. 이렇게 하면 Hosting 과 호환되면서 동시에 에러도 보고할 수 있습니다
7.let 변수 스코프는 블록 전체에서 유효하며, 선언 곳에서부터 블록 끝까지 유효한 것이 아니다
C 언어와 다르며, 블록 레벨 호이스팅이라고 할 수 있습니다. 매우 적절한 설명이 있습니다:
JavaScript 에서 var 선언의 스코프는 Photoshop 의 페인트 통 도구와 같다. 선언 곳에서부터 전후 두 방향으로 확산되며, 함수 경계에 닿을 때까지 멈추지 않는다
let 의 호이스팅 방식은 var 와 크게 다르지 않습니다 (경계가 블록 경계가 되었을 뿐이며), 모두 이런 양방향 확산입니다
8.let 변수를 재정의하면 SyntaxError 가 에러 발생한다
어법 해석 단계에서 에러가 발생하며, 런타임 에러가 아닙니다. 게다가 SyntaxError 는 try-catch 로 캡처할 수 없습니다
var 의 "내성" 은 매우 강합니다. 다음과 같습니다:
var x = 2;
var x;
var x = x++;
네, x 는 여전히 2 입니다. 두 번째 문장은 무시되었고, 세 번째 문장의 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 입니다
주의: 이 두 제약은 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 에게 이것이 블록으로 해석되어야 한다고 알려주면, 모호함은 자연스럽게 사라집니다. 두 가지 방법이 있습니다:
// 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 中文站에서 제공하는 무료 전자책
아직 댓글이 없습니다