变量声明
var声明
一直以来我们都是通过var关键字定义JavaScript变量,但var声明并不是我们熟悉的块级作用域,而是函数作用域。比如:
for (var i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
// 结果是:
//10
//10
//10
//10
//10
//10
//10
//10
//10
//10
并且使用var关键字会有变量提升,即声明的变量会自动提升到该作用域的最顶部,因此会出现先使用后声明的尴尬情况
let声明 - 块作用域
当用let声明一个变量,它使用的是块作用域。不同于使用var声明的变量那样可以在包含他们的函数外访问,块作用域变量包含在他们的块或for循环之外是不能被访问的。在catch
语句里声明的变量也具有同样的作用域规则
try{
throw "oh no!";
}
catch (e) {
console.log("Oh well");
}
console.log(e); // error
拥有块级作用域的变量的另一个特点是,他们不能在被声明之前读或写。虽然这些变量始终"存在"于它们的作用域里,但在直到声明它的代码之前的区域都属于暂时性死区。它只是用来说明我们不能在let语句之前访问它们,幸运的是typescript可以告诉我们这些信息。
注意一点,我们仍然可以在一个拥有块作用域变量被声明之前捕获它。只是我们不能在变量声明之前去调用那个函数。如果生成代码目标为ES2015,现代的运行时会抛出一个错误;然而,现今TypeScript是不会报错的。
function foo () {
return a;
}
// 不能在a被声明前调用'foo'函数
let a;
foo();
重定义及屏蔽
在使用var声明时,它不在乎你声明多少次,你只会得到一个,但是let只能声明一次。并不是要求两个均是块级作用域的声明typescript才会给出错误警告
function f(x) {
let x = 100; // error: interferes with parameter declaration
}
function g() {
let x = 100;
var x = 100; // error: can't have both declarations of 'x'
}
并不是说块级作用域变量不能用函数作用域变量来声明/而是块级作用域变量需要在明显不同的块里声明。
function f (condition,x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(true,10); // 100
f(false,10); // 10
块级作用域变量的获取
在我们最初谈及获取用var声明的变量时,我们简略地探究了一下在获取到了变量之后它的行为。直观的说,每次进入一个作用域时,它创建了一个变量的环境。就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function() {
return city;
}
}
return getCity();
}
因为我们已经在city的环境里获取到了city,所以就算if语句结束后我们仍然可以访问它。
当let声明出现在循环内体里时拥有完全不同的行为。不仅是在循环里引入了一个新的变量环境,而且针对每次迭代都会创建这样一个新作用域。所以在setTimeout里捕获到的i在循环结束后仍然能访问
for (let i = 0; i < 10 ; i++) {
setTimeout(function() {console.log(i); }, 100 * i);
}
// 输出 0-9
const声明
const声明与let声明类似,拥有相同的作用域规则,但是它们不能被重新赋值。
解构
解构数组
最简单的解构莫过于数组的解构赋值了
let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2
解构作用于已声明的变量会更好:
// swap variables
[first, second] = [second, first];
作用于函数参数:
function f ([first,second]: [number,number]){
console.log(first);
console.log(second);
}
f(input)
可以在数组里使用…语法创建剩余变量:
let [first,...rest] = [1,2,3,4];
console.log(first); // 1
console.log(rest); // [2,3,4]
可以忽略你不关心的尾随元素或其它元素
let [first] = [1,2,3,4];
console.log(first); // 1
let [,second,,fourth] = [1,2,3,4];
对象解构
可以解构对象
let o = {
a: 'foo',
b:12,
c: "bar"
};
let {a,b} = o;
就像数组解构,你可以用没有声明的赋值:
( { a , b } = { a:"baz", b: 101 } );
// 注意,我们需要用括号将它括起来,因为Javascript通常会将以 { 起始的语句解析为一个块。
属性重命名
你也可以给属性以不同的名字:
let { a: newName1, b: newnname2} = o;
这里语法开始变得混乱。 你可以将 a: newName1
读做 “a
作为 newName1
”。 方向是从左到右,好像你写成了以下样子:
let newName1 = o.a;
let newName2 = o.b;
令人困惑的是,这里的冒号不是指示类型的。如果你想指定它的类型,仍然需要在其后写上完整的模式
let { a: newName1, b: newnname2}: {a:string,b:number} = o;
默认值
默认值可以让你在属性为undefined时使用缺省值:
function keepWholeObject (wholeObject: {a:string, b?: number}) {
let { a , b = 1001 } = wholeObject;
}
现在,即使b为undefined,keepWholeObject函数的变量 wholeObject 的属性a 和 b都会有值
函数声明
解构也能用于函数声明,比如:
type C = { a:stting, b?: number}
function f ( {a , b } : C): void {
//...
}
但是,通常情况下更多的是指定默认值,解构默认值有些棘手。首先,你需要在默认值之前设置其格式.
function f ({ a = "", b = 0 } = {a: ""}): void {
//...
}
f();
其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。要知道c的定义有一个b可选属性:
function f ( {a , b = 0 } = { a: "" } ):void {
// ...
}
f ( { a: "yes" }); // ok, default b = 0;
f(); // ok, default to {a: ''}, whitch then defaults b = 0;
f({}); // error, a is required if you supply an argument
要小心使用解构。从前面的例子可看出,就算是最简单的解构表达式也是难以理解的。尤其当存在深层嵌套结构的时候,就算这时没有堆叠在一起的重命名,默认值和类型注解,也是令人难以理解的。解构表达式要尽量保持小而简单。你自己也可以直接使用解构将会生成的赋值表达式。
展开
展开操作符正与解构相反。它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。例如:
let defaults = { food: "spicy", price: "$$", ambiance: "noisy"};
let search = {...default, food: "rich"};
// search`的值为`{ food: "rich", price: "$$", ambiance: "noisy" }
对象的展开比数组的展开要复杂的多。像数组展开一样,它是从左至右进行处理,但结果仍为对象。这就意味着出现在展开对象后面的属性会覆盖前面的属性。
对象的展开还有其它一些意想不到的限制。首先,它仅包含对象自身的可枚举属性。大体上是说当你展开一个对象实例时,你会丢失其方法:
class C {
p = 12;
m () {
}
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m; // error
其次,typescript编译器不允许展开泛型函数上的类型参数。