变量声明
1. var
一直以来我们都是通过var关键字定义JavaScript变量:
var num = 10;我们也可以在函数内部定义变量:
function f() {
var message = "Hello, world!";
return message;
}并且我们也可以在其它函数内部访问相同的变量。
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
}
}
var g = f();
g(); // returns 11;上面的例子里,g可以获取到f函数里定义的a变量。 每当 g被调用时,它都可以访问到f里的a变量。 即使当 g在f已经执行完后才被调用,它仍然可以访问及修改a。
作用域规则
对于熟悉其它语言的人来说,var声明有些奇怪的作用域规则。 看下面的例子:
变量 x是定义在if语句里面,但是我们却可以在语句的外面访问它。 这是因为 var声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问(我们后面会详细介绍),包含它的代码块对此没有什么影响。 有些人称此为 var作用域或函数作用域。 函数参数也使用函数作用域。
这些作用域规则可能会引发一些错误。 其中之一就是,多次声明同一个变量并不会报错:
这里很容易看出一些问题,里层的for循环会覆盖变量i,因为所有i都引用相同的函数作用域内的变量。 有经验的开发者们很清楚,这些问题可能在代码审查时漏掉,引发无穷的麻烦。
捕获变量怪异之处
快速的猜一下下面的代码会返回什么:
介绍一下,setTimeout会在若干毫秒的延时后执行一个函数(等待其它代码执行完毕)。
运行结果:
很多JavaScript程序员对这种行为已经很熟悉了,但如果你很不解,你并不是一个人。 大多数人期望输出结果是这样:
我们传给
setTimeout的每一个函数表达式实际上都引用了相同作用域里的同一个i。
setTimeout在若干毫秒后执行一个函数,并且是在for循环结束后。 for循环结束后,i的值为5。 所以当函数被调用的时候,它会打印出 5!
一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时i的值:
参数 i会覆盖for循环里的i,但是因为我们起了同样的名字,所以我们不用怎么改for循环体里的代码。
2. let
let与var的写法一致。
块作用域
当用let声明一个变量,它使用的是词法作用域或块作用域。
块作用域变量在包含它们的块或for循环之外是不能访问的。
这里我们定义了2个变量a和b。 a的作用域是f函数体内,而b的作用域是if语句块里。
在catch语句里声明的变量也具有同样的作用域规则。
拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 暂时性死区。它只是用来说明我们不能在 let语句之前访问它们,幸运的是TypeScript可以告诉我们这些信息。
重定义和屏蔽
我们提过使用var声明时,它不在乎你声明多少次;你只会得到1个。
let声明就不会这么宽松:
同样let声明和var声明,以及参数名重复时,也会给出错误
在一个嵌套作用域里引入一个新名字的行为称做屏蔽。 它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。 例如,假设我们现在用 let重写之前的sumMatrix函数。
这个版本的循环能得到正确的结果,因为内层循环的i可以屏蔽掉外层循环的i。
通常来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 同时也有些场景适合利用它。
块级作用域变量的获取
用var声明的变量时,就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。
因为我们已经在city的环境里获取到了city,所以就算if语句执行结束后我们仍然可以访问它。
当let声明出现在循环体里时拥有完全不同的行为。 不仅是在循环里引入了一个新的变量环境,而是针对 每次迭代都会创建这样一个新作用域。 这就是我们在使用立即执行的函数表达式时做的事,所以在 setTimeout例子里我们仅使用let声明就可以了。
会输出与预料一致的结果:
3. const
const 声明是声明变量的另一种方式。
它们与let声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与 let相同的作用域规则,但是不能对它们重新赋值。
这很好理解,它们引用的值是不可变的。
除非你使用特殊的方法去避免,实际上const变量的内部状态是可修改的。
4. let VS const
使用最小特权原则,所有变量除了你计划去修改的都应该使用const,使用 const也可以让我们更容易的推测数据的流动。
5. 解构
解构数组
最简单的解构莫过于数组的解构赋值了:
解构作用于已声明的变量会更好:
作用于函数参数:
你可以在数组里使用...语法创建剩余变量:
当然,由于是JavaScript, 你可以忽略你不关心的尾随元素:
或其它元素:
结构对象
这通过 o.a and o.b 创建了 a 和 b 。 注意,如果你不需要 c 你可以忽略它。
就像数组解构,你可以用没有声明的赋值:
注意,我们需要用括号将它括起来,因为Javascript通常会将以 { 起始的语句解析为一个块。
你可以在对象里使用...语法创建剩余变量:
属性重命名
你也可以给属性以不同的名字:
令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。
默认值
默认值可以让你在属性为 undefined 时使用缺省值:
现在,即使 b 为 undefined , keepWholeObject 函数的变量 wholeObject 的属性 a 和 b 都会有值。
函数声明
解构也能用于函数声明。 看以下简单的情况:
但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要在默认值之前设置其格式。
其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道 C 的定义有一个 b 可选属性:
展开
展开操作符正与解构相反。 它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。 例如:
展开操作创建了 first和second的一份浅拷贝。 它们不会被展开操作所改变。
还可以展开对象:
像数组展开一样,它是从左至右进行处理,但结果仍为对象。 这就意味着出现在展开对象后面的属性会覆盖前面的属性。 因此,如果我们修改上面的例子,在结尾处进行展开的话:
对象展开还有其它一些意想不到的限制。 首先,它仅包含对象自身的可枚举属性。 大体上是说当你展开一个对象实例时,你会丢失其方法:
TypeScript编译器不允许展开泛型函数上的类型参数。
Last updated