1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-25 19:22:21 +00:00
es6tutorial/docs/let.md
2015-09-30 22:20:18 +08:00

451 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# let和const命令
## let命令
### 基本用法
ES6新增了`let`命令,用来声明变量。它的用法类似于`var`,但是所声明的变量,只在`let`命令所在的代码块内有效。
```javascript
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
```
上面代码在代码块之中,分别用`let``var`声明了两个变量。然后在代码块之外调用这两个变量,结果`let`声明的变量报错,`var`声明的变量返回了正确的值。这表明,`let`声明的变量只在它所在的代码块有效。
`for`循环的计数器就很合适使用let命令。
```javascript
for(let i = 0; i < arr.length; i++){}
console.log(i)
//ReferenceError: i is not defined
```
上面代码的计数器`i`,只在`for`循环体内有效。
下面的代码如果使用`var`最后输出的是10。
```javascript
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
```
如果使用`let`声明的变量仅在块级作用域内有效最后输出的是6。
```javascript
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
```
### 不存在变量提升
`let`不像`var`那样,会发生“变量提升”现象。
```javascript
console.log(foo); // ReferenceError
let foo = 2;
```
上面代码在声明`foo`之前,就使用这个变量,结果会抛出一个错误。
这也意味着`typeof`不再是一个百分之百安全的操作。
```javascript
typeof x; // ReferenceError
let x;
```
上面代码中,由于块级作用域内`typeof`运行时,`x`还没有值,所以会抛出一个`ReferenceError`
### 暂时性死区
只要块级作用域内存在`let`命令它所声明的变量就“绑定”binding这个区域不再受外部的影响。
```javascript
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
```
上面代码中,存在全局变量`tmp`,但是块级作用域内`let`又声明了一个局部变量`tmp`,导致后者绑定这个块级作用域,所以在`let`声明变量前,对`tmp`赋值会报错。
ES6明确规定如果区块中存在`let``const`命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些命令,就会报错。
总之在代码块内使用let命令声明变量之前该变量都是不可用的。这在语法上称为“暂时性死区”temporal dead zone简称TDZ
```javascript
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
```
上面代码中,在`let`命令声明变量`tmp`之前,都属于变量`tmp`的“死区”。
有些“死区”比较隐蔽,不太容易发现。
```javascript
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
```
上面代码中,调用`bar`函数之所以报错,是因为参数`x`默认值等于另一个参数`y`,而此时`y`还没有声明,属于”死区“。
需要注意的是函数的作用域是其声明时所在的作用域。如果函数A的参数是函数B那么函数B的作用域不是函数A。
```javascript
let foo = 'outer';
function bar(func = x => foo) {
let foo = 'inner';
console.log(func()); // outer
}
bar();
```
上面代码中,函数`bar`的参数`func`,默认是一个匿名函数,返回值为变量`foo`。这个匿名函数的作用域就不是`bar`。这个匿名函数声明时,是处在外层作用域,所以内部的`foo`指向函数体外的声明,输出`outer`。它实际上等同于下面的代码。
```javascript
let foo = 'outer';
let f = x => foo;
function bar(func = f) {
let foo = 'inner';
console.log(func()); // outer
}
bar();
```
ES6规定暂时性死区和不存在变量提升主要是为了减少运行时错误防止在变量声明前就使用这个变量从而导致意料之外的行为。这样的错误在ES5是很常见的现在有了这种规定避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
### 不允许重复声明
let不允许在相同作用域内重复声明同一个变量。
```javascript
// 报错
function () {
let a = 10;
var a = 1;
}
// 报错
function () {
let a = 10;
let a = 1;
}
```
因此,不能在函数内部重新声明参数。
```javascript
function func(arg) {
let arg; // 报错
}
function func(arg) {
{
let arg; // 不报错
}
}
```
## 块级作用域
### 为什么需要块级作用域?
ES5只有全局作用域和函数作用域没有块级作用域这带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量。
```javascript
var tmp = new Date();
function f(){
console.log(tmp);
if (false){
var tmp = "hello world";
}
}
f() // undefined
```
上面代码中函数f执行后输出结果为`undefined`原因在于变量提升导致内层的tmp变量覆盖了外层的tmp变量。
第二种场景,用来计数的循环变量泄露为全局变量。
```javascript
var s = 'hello';
for (var i = 0; i < s.length; i++){
console.log(s[i]);
}
console.log(i); // 5
```
上面代码中变量i只用来控制循环但是循环结束后它并没有消失泄露成了全局变量。
### ES6的块级作用域
let实际上为JavaScript新增了块级作用域。
```javascript
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
```
上面的函数有两个代码块都声明了变量n运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用var定义变量n最后输出的值就是10。
ES6允许块级作用域的任意嵌套。
```javascript
{{{{{let insane = 'Hello World'}}}}};
```
上面代码使用了一个五层的块级作用域。外层作用域无法读取内层作用域的变量。
```javascript
{{{{
{let insane = 'Hello World'}
console.log(insane); // 报错
}}}};
```
内层作用域可以定义外层作用域的同名变量。
```javascript
{{{{
let insane = 'Hello World';
{let insane = 'Hello World';}
}}}};
```
块级作用域的出现实际上使得获得广泛应用的立即执行匿名函数IIFE不再必要了。
```javascript
// IIFE写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
```
另外ES6也规定函数本身的作用域在其所在的块级作用域之内。
```javascript
function f() { console.log('I am outside!'); }
(function () {
if(false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
```
上面代码在ES5中运行会得到“I am inside!”但是在ES6中运行会得到“I am outside!”。这是因为ES5存在函数提升不管会不会进入if代码块函数声明都会提升到当前作用域的顶部得到执行而ES6支持块级作用域不管会不会进入if代码块其内部声明的函数皆不会影响到作用域的外部。
```javascript
{
let a = 'secret';
function f() {
return a;
}
}
f() // 报错
```
上面代码中,块级作用域外部,无法调用块级作用域内部定义的函数。如果确实需要调用,就要像下面这样处理。
```javascript
let f;
{
let a = 'secret';
f = function () {
return a;
}
}
f() // "secret"
```
需要注意的是如果在严格模式下函数只能在顶层作用域和函数内声明其他情况比如if代码块、循环代码块的声明都会报错。
## const命令
const也用来声明变量但是声明的是常量。一旦声明常量的值就不能改变。
```javascript
const PI = 3.1415;
PI // 3.1415
PI = 3;
PI // 3.1415
```
上面代码表明改变常量的值是不起作用的。需要注意的是,对常量重新赋值不会报错,只会默默地失败。
const的作用域与let命令相同只在声明所在的块级作用域内有效。
```javascript
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
```
const命令声明的常量也是不提升同样存在暂时性死区只能在声明的位置后面使用。
```javascript
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
```
上面代码在常量MAX声明之前就调用结果报错。
const声明的常量也与let一样不可重复声明。
```javascript
var message = "Hello!";
let age = 25;
// 以下两行都会报错
const message = "Goodbye!";
const age = 30;
```
由于const命令只是指向变量所在的地址所以将一个对象声明为常量必须非常小心。
```javascript
const foo = {};
foo.prop = 123;
foo.prop
// 123
foo = {} // 不起作用
```
上面代码中常量foo储存的是一个地址这个地址指向一个对象。不可变的只是这个地址即不能把foo指向另一个地址但对象本身是可变的所以依然可以为其添加新属性。
下面是另一个例子。
```js
const a = [];
a.push("Hello"); // 可执行
a.length = 0; // 可执行
a = ["Dave"]; // 报错
```
上面代码中常量a是一个数组这个数组本身是可写的但是如果将另一个数组赋值给a就会报错。
如果真的想将对象冻结应该使用Object.freeze方法。
```javascript
const foo = Object.freeze({});
foo.prop = 123; // 不起作用
```
上面代码中常量foo指向一个冻结的对象所以添加新属性不起作用。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
```javascript
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, value) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
```
## 跨模块常量
上面说过const声明的常量只在当前代码块有效。如果想设置跨模块的常量可以采用下面的写法。
```javascript
// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
```
## 全局对象的属性
全局对象是最顶层的对象在浏览器环境指的是window对象在Node.js指的是global对象。在JavaScript语言中所有全局变量都是全局对象的属性。Node的情况比较特殊这一条只对REPL环境适用模块环境必须显式声明成`global`的属性。)
ES6规定var命令和function命令声明的全局变量属于全局对象的属性let命令、const命令、class命令声明的全局变量不属于全局对象的属性。
```javascript
var a = 1;
// 如果在Node的REPL环境可以写成global.a
// 或者采用通用方法写成this.a
window.a // 1
let b = 1;
window.b // undefined
```
上面代码中,全局变量`a``var`命令声明,所以它是全局对象的属性;全局变量`b``let`命令声明,所以它不是全局对象的属性,返回`undefined`