变量声明
变量声明
let和const是JavaScript里相对较新的变量声明方式。 像我们之前提到过的, let在很多方面与var是相似的,但是可以帮助大家避免在JavaScript里常见一些问题。 const是对let的一个增强,
它能阻止对一个变量再次赋值。因为TypeScript是JavaScript的超集,所以它本身就支持let和const。
var 声明
一直以来我们都是通过var关键字定义JavaScript变量。
var a=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。
function f() { var a = 1; a = 2; var b = g(); a = 3; return b; function g() { return a; } } f(); // returns 2
作用域规则
对于熟悉其它语言的人来说,var声明有些奇怪的作用域规则。 看下面的例子:
function f(shouldInitialize: boolean) { if (shouldInitialize) { var x = 10; } return x; } f(true); // returns '10' f(false); // returns 'undefined'
变量 x是定义在*if语句里面*,但是我们却可以在语句的外面访问它。 这是因为 var声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问(我们后面会详细介绍),
包含它的代码块对此没有什么影响。 有些人称此为* var作用域或函数作用域*。 函数参数也使用函数作用域。
这些作用域规则可能会引发一些错误。 其中之一就是,多次声明同一个变量并不会报错:
function sumMatrix(matrix: number[][]) { var sum = 0; for (var i = 0; i < matrix.length; i++) { var currentRow = matrix[i]; for (var i = 0; i < currentRow.length; i++) { sum += currentRow[i]; } } return sum; }
里层的for循环会覆盖变量i,因为所有i都引用相同的函数作用域内的变量.
函数内不声明直接使用的变量为全局变量,函数内不管在哪里声明变量,默认变量处于函数里的最顶端。这一句话基本概括了函数作用域。
捕获变量怪异之处
快速的猜一下下面的代码会返回什么:
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 100 * i); }
介绍一下,setTimeout会在若干毫秒的延时后执行一个函数(等待其它代码执行完毕,这涉及到js宏任务和微任务。。网上很多但本人没来的及总结这里就先不讲了)。
10 10 10 10 10 10 10 10 10 10
很多JavaScript程序员对这种行为已经很熟悉了,但如果你很不解,你并不是一个人。 大多数人期望输出结果是这样:
0 1 2 3 4 5 6 7 8 9
> 我们传给setTimeout的每一个函数表达式实际上都引用了相同作用域里的同一个i。
setTimeout在若干毫秒后执行一个函数,并且是在for循环结束后。 for循环结束后,i的值为10。 所以当函数被调用的时候,它会打印出 10!
一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时i的值:
1 for (var i = 0; i < 10; i++) { 2 // capture the current state of 'i' 3 // by invoking a function with its current value 4 (function(i) { 5 setTimeout(function() { console.log(i); }, 100 * i); 6 })(i); 7 }
这种奇怪的形式我们已经司空见惯了。 参数 i会覆盖for循环里的i。
let 声明
现在你已经知道了var存在一些问题,这恰好说明了为什么用let语句来声明变量。 除了名字不同外, let与var的写法一致。
let hello = "Hello!";
块作用域
当用let声明一个变量,它使用的是词法作用域或块作用域。 不同于使用 var声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或for循环之外是不能访问的。
1 function f(input: boolean) { 2 let a = 100; 3 4 if (input) { 5 // Still okay to reference 'a' 6 let b = a + 1; 7 return b; 8 } 9 10 // Error: 'b' doesn't exist here 11 return b; 12 }
在catch语句里声明的变量也具有同样的作用域规则:
try { throw "oh no!"; } catch (e) { console.log("Oh well."); } // Error: 'e' doesn't exist here console.log(e);
拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 暂时性死区。
它只是用来说明我们不能在 let语句之前访问它们,也就是let没有变量提升。
a++; // illegal to use 'a' before it's declared; let a;
注意一点,我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为ES2015,运行时会抛出一个错误;
function foo() { // okay to capture 'a' return a; } // 不能在'a'被声明前调用'foo' // 运行时应该抛出错误 foo(); let a;
重定义及屏蔽
我们提过使用var声明时,它不在乎你声明多少次;你只会得到1个。
1 function f(x) { 2 var x; 3 var x; 4 5 if (true) { 6 var x; 7 } 8 }
在上面的例子里,所有x的声明实际上都引用一个相同的x,并且这是完全有效的代码。 这经常会成为bug的来源。 好的是, let声明就不会这么宽松了。
1 let x = 10; 2 let x = 20; // 错误,不能在1个作用域里多次声明`x`
并不是要求两个均是块级作用域的声明TypeScript才会给出一个错误的警告。
1 function f(x) { 2 let x = 100; // error: interferes with parameter declaration 3 } 4 5 function g() { 6 let x = 100; 7 var x = 100; // error: can't have both declarations of 'x' 8 }
在一个嵌套作用域里引入一个新名字的行为称做屏蔽。 它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。
例如,假设我们现在用 let重写之前的sumMatrix函数。
1 function sumMatrix(matrix: number[][]) { 2 let sum = 0; 3 for (let i = 0; i < matrix.length; i++) { 4 var currentRow = matrix[i]; 5 for (let i = 0; i < currentRow.length; i++) { 6 sum += currentRow[i]; 7 } 8 } 9 10 return sum; 11 }
块级作用域变量的获取
在我们最初谈及获取用var声明的变量时,我们简略地探究了一下在获取到了变量之后它的行为是怎样的。 直观地讲,每次进入一个作用域时,它创建了一个变量的 环境。
就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。
1 function theCityThatAlwaysSleeps() { 2 let getCity; 3 4 if (true) { 5 let city = "Seattle"; 6 getCity = function() { 7 return city; 8 } 9 } 10 11 return getCity(); 12 }
因为我们已经在city的环境里获取到了city,所以就算if语句执行结束后我们仍然可以访问它。
回想一下前面setTimeout的例子,我们最后需要使用立即执行的函数表达式来获取每次for循环迭代里的状态。 实际上,我们做的是为获取到的变量创建了一个新的变量环境。
当let声明出现在循环体里时拥有完全不同的行为。 不仅是在循环里引入了一个新的变量环境,而是针对每次迭代都会创建这样一个新作用域。
这就是我们在使用立即执行的函数表达式时做的事,所以在 setTimeout例子里我们仅使用let声明就可以了。
1 for (let i = 0; i < 10 ; i++) { 2 setTimeout(function() {console.log(i); }, 100 * i); 3 }
const 声明
const 声明是声明变量的另一种方式。
const numLivesForCat = 9;
它们与let声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与 let相同的作用域规则,但是不能对它们重新赋值。
这很好理解,它们引用的值是不可变的。
1 const numLivesForCat = 9; 2 const kitty = { 3 name: "Aurora", 4 numLives: numLivesForCat, 5 } 6 7 // Error 8 kitty = { 9 name: "Danielle", 10 numLives: numLivesForCat 11 }; 12 13 // all "okay" 14 kitty.name = "Rory"; 15 kitty.name = "Kitty"; 16 kitty.name = "Cat"; 17 kitty.numLives--;
除非你使用特殊的方法去避免,实际上const变量的内部状态是可修改的。 幸运的是,TypeScript允许你将对象的成员设置成只读的。 接口一章有详细说明。
let vs. const
现在我们有两种作用域相似的声明方式,我们自然会问到底应该使用哪个。 与大多数泛泛的问题一样,答案是:依情况而定。
使用最小特权原则,所有变量除了你计划去修改的都应该使用const。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用 const也可以让我们更容易的推测数据的流动。
解构
解构数组
1 let input = [1, 2]; 2 let [first, second] = input; 3 console.log(first); // outputs 1 4 console.log(second); // outputs 2
1 first = input[0]; 2 second = input[1];
1 // swap variables 2 [first, second] = [second, first];
1 function f([first, second]: [number, number]) { 2 console.log(first); 3 console.log(second); 4 } 5 f(input);
1 let [first, ...rest] = [1, 2, 3, 4]; 2 console.log(first); // outputs 1 3 console.log(rest); // outputs [ 2, 3, 4 ]
1 let [first] = [1, 2, 3, 4]; 2 console.log(first); // outputs 1
1 let [, second, , fourth] = [1, 2, 3, 4];
对象解构
1 let o = { 2 a: "foo", 3 b: 12, 4 c: "bar" 5 }; 6 let { a, b } = o;
这通过 o.a and o.b 创建了 a 和 b 。 注意,如果你不需要 c 你可以忽略它。
就像数组解构,你可以用没有声明的赋值:
({ a, b } = { a: "baz", b: 101 });
注意,我们需要用括号将它括起来,因为Javascript通常会将以 { 起始的语句解析为一个块。
你可以在对象里使用...语法创建剩余变量:
1 let { a, ...passthrough } = o; 2 let total = passthrough.b + passthrough.c.length;
属性重命名
你也可以给属性以不同的名字:
1 let { a: newName1, b: newName2 } = o;
这里的语法开始变得混乱。 你可以将 a: newName1 读做 "a 作为 newName1"。 方向是从左到右,好像你写成了以下样子:
1 let newName1 = o.a; 2 let newName2 = o.b;
令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。
let {a, b}: {a: string, b: number} = o;
默认值
默认值可以让你在属性为 undefined 时使用缺省值:
1 function keepWholeObject(wholeObject: { a: string, b?: number }) { 2 let { a, b = 1001 } = wholeObject; 3 }
现在,即使 b 为 undefined , keepWholeObject 函数的变量 wholeObject 的属性 a 和 b 都会有值。
函数声明
解构也能用于函数声明。 看以下简单的情况:
1 type C = { a: string, b?: number } 2 function f({ a, b }: C): void { 3 // ... 4 }
但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要在默认值之前设置其格式。
1 function f({ a="", b=0 } = {}): void { 2 // ... 3 } 4 f();
其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道 C 的定义有一个 b 可选属性:
1 function f({ a, b = 0 } = { a: "" }): void { 2 // ... 3 } 4 f({ a: "yes" }); // ok, default b = 0 5 f(); // ok, default to {a: ""}, which then defaults b = 0 6 f({}); // error, 'a' is required if you supply an argument
展开
展开操作符正与解构相反。 它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。 例如:
1 let first = [1, 2]; 2 let second = [3, 4]; 3 let bothPlus = [0, ...first, ...second, 5];
你还可以展开对象:
1 let defaults = { food: "spicy", price: "$$", ambiance: "noisy" }; 2 let search = { ...defaults, food: "rich" };
谁在后面谁覆盖谁。
1 class C { 2 p = 12; 3 m() { 4 } 5 } 6 let c = new C(); 7 let clone = { ...c }; 8 clone.p; // ok 9 clone.m(); // error!
其次,TypeScript编译器不允许展开泛型函数上的类型参数。 这个特性会在TypeScript的未来版本中考虑实现。
end.17:21:43
浙公网安备 33010602011771号