块级作用域

为什么需要块级作用域

es5中只有块级作用域和函数作用域,这会产生某些不合理的场景。

1
2
3
4
5
6
7
8
9
10
var a = 'test'

function f(){
console.log(a);
if(true){
var a = 'test1';
}
}

f() // undefined

本来调用f()希望得到的结果是test,但是由于函数f内的a变量提升到该函数的顶部,导致输出的a为undefined。

1
2
3
4
function f(){
for(var i = 0; i < 10; i++){}
i //10
}

循环中计数的循环变量由于没有消失一直存在f()函数作用域里,但是该变量本应该只存在于循环中,所以可以理解为该变量的作用域被泄露了。

es6中的块级作用域

es6中的let的出现实际为JavaScript增加了块级作用域

1
2
3
4
5
6
7
function f1(){
let n = 5;
if(true){
let n = 10;
}
console.log(n) // 5
}

这表明外层块级作用域不受内层块级作用域的影响。

1
2
3
4
5
6
7
(function(){
...
}())

{
...
}

块级作用域的出现实际上使得自执行函数不再必要了。

let命令

let命令:let命令用来声明的变量,其声明的变量只在其所在的代码块中有效。

用法

es5中的作用域只有函数作用域和全局作用域,在函数体中使用var定义的变量,会被提到函数开始处进行定义,作用域为整个函数。

1
2
3
4
5
6
7
8
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
i; // 10

因为for循环并不是一个函数体,所以for循环中定义的变量的作用域是其所在的函数体,所以这里的i的输出是10,而a6的输出为10是因为i一直保存的是值引用,所以显示10.

1
2
3
4
5
6
7
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6

因为let声明的变量只在块级作用域中生效,每一个循环都会产生一个新的作用域,每次循环就声明一个新的变量i,那么当前循环中i的值是如何确定的?原因是有一个中间变量__status = {i}用来存储当前的循环变量。

1
2
3
4
5
6
7
for(let i = 0; i < 3; i++){
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc

实际上在这里,整个for循环有两个作用域,设置循环变量的部分为父作用域,循环体那部分为子作用域。因为这个循环执行了三次,父作用域没有受到子作用域的影响。

不存在变量提升

变量提升:指的是变量可以在变量声明前使用该变量,但是会输出undefined。

为了纠正这种情况,使用let声明的变量不可以在变量声明前使用,否则会报错。

1
2
3
4
5
6
7
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

暂时性死区

在代码块内,使用let命令声明变量之前,该变量都是不可用的,该现象称为“暂时性死区”(temporal dead zone)

1
2
3
4
5
6
7
8
9
10
11
x = 'a';
let x = 'b';
会报错:该变量未定义

typeof x; //报错
let x = 'a';
typeof undeclear_var // undefined
会使得typeof操作符的使用不再安全

var x = x //undefined
let x = x //报错

不允许重复声明

在同一块级作用域中不允许重复声明变量。

1
2
3
4
5
6
7
8
9
function f(args){
let args;
} //报错

function f1(args){
{
let args;
}
} //不报错

const命令

const声明的变量指向的地址所保存的数据不可变,一旦声明后就不可以修改,一旦声明就要初始化。

const与let一样,只在声明的块级作用域中有效,并且声明的常量也不可以提升,同样存在暂时性死区。

1
2
3
4
5
6
7
8
9
10
11
12
13
const a = {};
a.property = 1; //不报错
a = {}; //报错
只要保证a变量指向地址保存的数据不改变就不会报错

function f(obj){
Object.freeze(obj);
Object.keys(obj).forEach((key) => {
if(typeof obj[key] === 'object'){
f(obj[key]);
}
})
}

变量的解构赋值

实际上可以看作一种模式匹配。

数组的解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let [foo] = [];
let [bar, foo] = [1];
foo // undefined
解构不成功就会返回undefined。

let [x, y] = [1, 2, 3];
不完全解构。

let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
会报错,因为等号右边是不可便利的结构。

let [x, y = 'b'] = ['a', undefined];
// x='a', y='b' 只有对应的位置严格等于(===)undefined,默认值才会生效。

对象的解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let {foo, bar} = {foo: "aaa", bar: "bbb"};
let {foo: foo, bar: bar} = {foo: "aaa", bar: "bbb"};
let {foo: baz} = {foo: "aaa", bar: "bbb"} // baz->"aaa" foo->报错
let {foo} = {bar: "bbb'} //解构失败 foo->undefined
解构赋值的键其实是匹配的模式,而值才是指向真正的值。

var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
默认值的生效条件是对应匹配的值为undefined。

let arr = [1, 2, 3];
let {0: first, [arr.length - 1]: last} = arr;
first // 1
last // 3
数组可以进行对象属性的结构,**数组本质是特殊的对象**

字符串的解构赋值

1
2
3
let {length: len} = 'hello';
len //5
String类中有个length属性与之匹配。

数值和布尔值的解构赋值

1
2
3
4
5
6
7
8
9
10
let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true
如果等号右边是数值或者布尔值,则会转为包装对象

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
undefined和null无法转为对象。

函数参数的解构赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function f({x = 0, y = 0} = {}) {
return [x, y];
}
f({x: 3, y: 3}); // [3, 8]
f({x: 3}); // [3, 0]
f({}); // [0, 0]
f(); // [0, 0]

function f({x, y} = {x: 0, y: 0}) {
return [x, y];
}
f({x: 3, y: 3}); // [3, 8]
f({x: 3}); // [3, undefined]
f({}); // [undefined, undefined]
f(); // [0, 0]
注意上面两种情况的比较。

[1, undefined, 3].map((x = 'yes') => x);
// [1, 'yes', 3]

函数的扩展

Promise

####  点击托盘图标停止录制定义

#### 特点

#### 优点

#### 缺点