变量声明
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();//return 11
function f() {
var a = 1;
a = 2;
var b = g();//这里调用了函数,传入了a=2
a = 3;//这里的赋值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);// 10;
f(false);// undefined
变量虽然定义在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);
}//10个10
在这个for
循环中,setTimeout
在若干秒后执行一个函数,并且是在for
循环结束后.for
循环结束后,i
的值为10.所以函数被调用的时候,它会打印出10.
一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时i
的值
for (var i = 0; i <10; i++){
(function(i) {
setTimeout(function(){
console.log(i);
}, 100 * i)
})(i);
}
参数i
会覆盖for
循环里的i
,但是因为我们起了同样的名字,所以我们不用怎么改for
循环里的代码
let
声明
除了名字不同外,let
与var
的写法一致
let hello = "Hello";
主要的区别不在语法上,而是语义.
块作用域
当用let
声明一个变量,它使用的是词法作用域或块作用域.不同于使用var
声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或for循环之外是不能访问的.
function f(input: boolean) {
let a = 100;
if (input){
let b = a + 1;
return b;
}
return b;//报错
}
a 的作用域在f
函数体内,而b
的作用域是if
语句块里.
在catch
语句里声明的变量也具有同样的作用域规则.
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.")
}
console.log(e);//报错,该变量未定义
拥有块级作用域的变量的另一个特点是,它们不能在声明之前读或写.虽然这些变量始终"存在"与它们的作用域里,但在直到声明它的代码之前的区域都属于暂时性死区.它只能说明我们不能在let
语句之前访问它们,而TypeScript可以告诉我们这些信息
a++;//illegal to user 'a' before it's declared;
let a;
注意:我们仍然可以在一个拥有块作用域变量被声明前获取它.只是我们不能在变量声明前去调用那个函数,如果生成代码为ES2015运行时会抛出一个错误,然而TypeScript是不会报错的.
function foo(){
return a;//这里可以使用a变量
}
foo();//这里调用在变量声明前应该报错(但是TypeScript不报错)
let a;
重定义及屏蔽
使用var
声明时它不在乎你声明多少次:你只会得到一个.
function (){
var x;
var x;
if (true) {
var x;
}
}
在上面的代码里所有x
的声明实际上都引用一个相同的'x',并且这是完全有效的代码,这经常会导致一些bug的出现.而现在,let
声明就不会那么宽松了.
let x = 10;
let x = 20;//错误,不能在1个作用域里多次声明'x'
并不是要求两个均是块级作用域的声明TypeScript才会给出一个错误的警告
fun f(x){
let x = 100;//因为其实在函数的传参过程中,x已经被声明,所以会报错;
}
function g(){
let x = 100;
var x = 100;//这里的意思就是说同一个块级作用域下不能声明两个变量(只要有一个使用了let)
}
并不是说块级作用域变量不能用函数作用域变量来声明.而是块级作用域变量需要在明显不同的块里声明
function f(condition, x){
if (condition){
let x = 100;
return x;//这里面的x是if块级作用域里声明的
}
return x;//这个x是函数传参时声明的其实省略了else
}
f(false, 0);//0
f(true, 0);//100
在一个嵌套作用域里引入一个新名字的行为称作屏蔽.它是一把双刃剑,它可能不小心引入新问题,同时也可能解决一些错误,例如:
function sumMatrix(matrix: number[][]) {
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
这个版本的循环能够得到正确的结果,因为内层循环的i
可以屏蔽外层循环的i
.通常来说我们应该避免使用屏蔽,但是有些场景又需要利用它,看情况而定
块级作用域变量的获取
获取用var
声明的变量时,每次进入一个作用域时创建了一个变量的环境,就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在.
function theCityThatAlwaysSleeps (){
let getCity;
if (true) {
let city = "Seattle";
getCity = function(){
return city;
}
}
return getCity();
} // Seattle
因为我们已经在city
的环境里获取到了city
,所以就算if
语句执行结束后我们仍然可以访问它.
当let
声明出现在循环体里拥有完全不同于var
的行为,不仅是在循环里引入了一个新的变量环境,而且针对每次迭代都会创建一个新的作用域,这就是我们在使用立即执行的函数表达式时做的事
for (let i = 0; i < 10; i++){
setTimeout(function(){
console.log(i);
}, 100 * i);
}
这样就能得到我们想要的结果.
const
声明
const
声明是声明变量的另一种方式.
const numberLivesForCat = 9;
与let
声明类似,但是const
被赋值后不能再改变.也就是说,const
拥有与let
相同的作用域规则,但是不能对它们重新赋值.
const num= 1;
const kitty = {
name: 'xiaoji',
numLives: num;
}
kitty = {
name: "jiji",
numLives: num
}//报错,因为const声明的变量的值是不可变的
//但是可以像下面一样进行变量内部的改变
kitty.name = 'xiaogang';//ok
kitty.numLives--;//ok
就是说,除非使用特使的方法去避免,实际上const
变量的内部状态是可修改的.
使用最小特权原则,所有变量除了你计划去修改的都应该使用const。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用 const也可以让我们更容易的推测数据的流动。
解构
解构数组
最简单的解构:数组的解构赋值
let input = [1, 2];
let [first, second] = input;
console.log(first);//1
console.log(second);//2
上面的代码相当于:
first = input[0];
second = input[1];
解构作用于已声明的变量:
[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];
console.log(second);//2
consol.log(fourth);//4
对象解构
let o = {
a: 'foo',
b: 12,
c: 'bar'
};
let {a, b} = o;
console.log(a);//'foo'
console.log(b);//12
就像数组解构,可以使用没有声明的赋值:
({a, b} = {a: 'baz', b: 101});
注意我们需要用括号将它包起来,因为JavaScript通常会将以{
起始的语句解析为一个块,上面这行代码经测试直接运行会报错,还是需要在前面加上let a, b;
可以在对象里使用...
语法创建剩余变量
let {a, ..passthrough} = o;
let total = passthrough.b + passthrough.c.length;
属性重命名
可以给属性以不同的名字:
let { a: newName1, b: newName2} = o;
这里可以读作"a
作为newName1
,意思是:
let newName1 = o.a;
let newName2 = o.b;
这里的冒号不是指定类型的,如果想指定它的类型,仍然需要在其后写上完整的模式
let {a, b} : {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: string, b?: number }
function f({a, b}: C):void {
//...
}
通常更多情况下是指定默认值:
function f({a, b} = {a: "", b: 0}): void {
//...
}
f();//ok default to {a: "", b: 0}
function f({ a, b = 0 } = { a: "" }): void {
// ...
}
f({ a: "yes" }); // ok, default b = 0
f(); // ok, default to {a: ""}, which then defaults b = 0
f({}); // error, 'a' is required if you supply an argument
展开
展开操作符与解构相反,它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象:
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];//[0, 1, 2, 3, 4, 5]
展开对象
let defaults = {food: "spicy", price: "$$", amibiance: "noisy"};
let search = {...defaults, food: "rich"};//{food: "rich", price: "$$", amibiance: "noisy"}
如果将defaults
放在了search
后面,则展开为:
{ food: 'spicy', price: '$$', ambiance: 'no
isy' }
所以这点要注意,默认值的覆盖问题
对象展开仅包含对象自身的可枚举属性,意思是说当你展开一个对象实例时,你会丢失其方法:
class C {
p = 12;
m() {
}
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!