TypeScript学习-变量声明

146 阅读4分钟

变量声明

var声明

ES6以前声明变量的关键字是var

var foo = 10

上面代码定义了一个变量foo,值为10的这样一个变量。

function foo() {
    var a = 10
    return function bar() {
        var b = a + 1
        return b
    }
}
var a = foo()
console.log(a()) // 11

上面代码中,定义了一个函数foo(),通过外部调用函数,从而获取到函数内部的值,外部声明的变量a的返回值是一个Function bar()函数,调用该函数返回的是11

function foo() {
    var a = 1
    a = 2
    var b = g()
    a = 3
    return b

    function g() {
        return a
    }
}
console.log(foo()) // 2

上面代码中,定义了一个foo()函数,通过外部直接调用该函数,foo()函数内部声明了两个变量ab,并且还声明了一个g()方法,外部调用foo()函数,在内部运行到var b = g()时,因为g()方法是一个带返回值的,它返回a的值,当前a的值为2,所以调用foo()函数就会得到结果2

作用域规则

使用var声明的变量,通过外部调用可以访问到内部的值,形成作用域规则。

function foo(bool: boolean) {
    if (bool) {
        var a = 10
    }
    return a
}
console.log(foo(true)) // 10
console.log(foo(false)) // undefined

上面代码中,创建一个foo()函数,并且该函数携带一个布尔值的参数,外部调用该函数,当函数参数为true是外部调用函数输出10,当函数参数为false,变为undefined,首先函数内部存在一个变量a赋初值为10

上面代码中的foo(bool: boolean)参数就是一个作用域。

这些作用域规则有时候会引发一些错误。

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
}

上面代码中,在函数sumMatrix()中存在两个for循环,都使用var声明了变量i,这在后期代码维护以及上线部署中往往会出现问题。

在日常开发中,对于for循环参数,一直提倡使用let关键字声明变量,不使用var声明,因为var存在变量提升。

for (var i = 0; i < 5; i++) {
    setTimeout(function () {
        console.log(i)
    }, 1000)
}
// 5
// 5
// 5
// 5
// 5

上面代码是不是和你预期输出的不一样,运行上面的代码是输出55,那为什么会这样呢,首先就是传递给setTimeout的每一个i其实都是在同一个作用域,然后在JavaScript运行时,由于使用var上面循环体内部参数,其实i就是一个全局的,变量会提升,然后每次是循环结束之后才会有结果。运行结果如下所示。

输出结果

那么有什么办法让他变成我们期待的结果是如下呢,只需要将var改为let什么就行。

for (let i = 0; i < 5; i++) {
    setTimeout(function () {
        console.log(i)
    }, 1000)
}
// 0
// 1
// 2
// 3
// 4

输出结果

let

ES6标准中,采用let声明变量,这是一种新的声明变量的方式,在ES5以前,声明变量使用的是var,但是var声明变量是一个全局作用域的,所以避免变量污染,导致变量名重复,引入了let来声明变量,let是存在作用域,并且不存在变量提升,只存在块级作用域。

let hello = "hello"

块级作用域

当使用let声明一个变量之后,它就存在了块级作用域,不同于使用var声明的变量,可以在包含它们的函数外访问,块作用域变量在包含它们的代码块或for循环之外是不能访问的。

function foo(i: boolean) {
    let a = 10
    if (i) {
        let b = a + 1
        return b
    }
    return b
}
console.log(foo(true)) // Cannot find name 'b'

上面代码中,先创建一个foo()函数,并且该函数返回值是一个具体数值,由于变量b是由let声明,并且存在if的作用域中,在外部调用返回b就会报错。这就是作用域的作用。

拥有块级作用域的变量的另外一个特点是,不能在声明之前进行读写,虽然这些变量始终存在于它们的作用域中,都是直到声明它的代码之前都存在一个暂时性死区,它告诉我们不能在声明变量之前先使用它。

console.log(a) // Variable 'a' is used before being assigned
let a = 10

上面代码中,就很好的说明了这个报错信息。不能在未定义之前使用。

重定义和屏蔽

使用var可以多次声明同一个变量,不管你声明多少次,最终结果也只是一个。

function foo(x: any) {
    var x
    var x
    if (true) {
        return x
    }
}
console.log(foo(1)) // 1

上面代码中,使用let声明多个变量x,所有声明的x都来自一个x,并且这个代码是不会报错的,在日常开发中这个往往是bug的来源。

当使用let声明变量的时候,就不能在同一作用域声明该变量了。

function foo(x: any) {
    let x
    let x
    if (true) {
        return x
    }
}
console.log(foo(1)) // Duplicate identifier 'x'

上面代码提示重复声明了一个变量类型x,所以这就比较严格,也是开发中需要注意到的。

屏蔽是指在一个嵌套作用域里面引入一个新名字的行为叫屏蔽。

function sumMatrix(matrix: number[][]) {
    let sum = 0
    for (let i = 0; i < matrix.length; i++) {
        let currentRow = matrix[i]
        for (let i = 0; i < currentRow.length; i++) {
            sum += currentRow[i]
        }
    }
    return sum
}

上面代码就可以输出正确的结果,因为内层的循环可以屏蔽掉外部的循环体变量,如果我们不采用let声明,结果就会报错。

块级作用域变量的获取

在我们最初谈及获取用var声明的变量时,我们简略地探究了一下在获取到了变量之后它的行为是怎样的。直观地讲,每次进入一个作用域时,它创建了一个变量的环境。就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。

function thenCityThatAlwaysSleeps() {
    let getCity
    if (true) {
        let city = "china"
        getCity = function () {
            return city
        }
    }
    return getCity()
}
console.log(thenCityThatAlwaysSleeps()) // china

上面代码编写了一个获取城市的函数thenCityThatAlwaysSleeps(),在函数内部的if代码块中,我们声明了一个city的变量,并且该变量被getCity方法作为函数返回,函数内部也能到他外部的变量city,最后thenCityThatAlwaysSleeps()函数返回的是getCity(),所以最后输出结果为china

当let声明出现在循环体里时拥有完全不同的行为。不仅是在循环里引入了一个新的变量环境,而是针对每次迭代都会创建这样一个新作用域。这就是我们在使用立即执行的函数表达式时做的事,所以在setTimeout例子里我们仅使用let声明就可以了。

for (let i = 0; i < 5; i++) {
    setTimeout(() => {
        console.log(i)
    }, 100 * i)
}
// 0
// 1
// 2
// 3
// 4

const

const声明是声明变量的另一种方式。

它们与let声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与let相同的作用域规则,但是不能对它们重新赋值,也就是说它的引入的值是不可以改变的。

const numLivesForCat = 10
const kitty = {
    name: "typescript",
    numLives: numLivesForCat
}
// Cannot assign to 'kitty' because it is a constant
kitty = {
    name: "javascript",
    numLives: numLivesForCat
}

console.log(kitty.name = "hello") // hello
console.log(kitty.numLives = 1) // 1

那么到底什么时候使用let,什么时候使用const,这里其实就是看情况而定,首先,假如你所声明的变量不会在下面进行重新赋值,那么久推荐使用const,这样你就很清楚的指知道数据的来源以及去向,然后再推荐使用let

解构

解构就是依据一定的规则键一些数据类型进行顺序输出,这样的方式叫解构。

字符串解构

首先字符串解构可以采用拓展运算符...进行,可以将字符串按照字母依次解构,也可以解构成数组。

let str: string = "typescript"
console.log(...str) // t y p e s c r i p t
console.log([...str])
/* 
[
  't', 'y', 'p', 'e',
  's', 'c', 'r', 'i',
  'p', 't'
]
*/

数组解构

解构在日常开发中,是一个很普遍应用场景,依次按照数组的下标进行元素的读取。

let array: any[] = [1, 2, "3"]
let [a, b, c] = array
console.log(a) // 1
console.log(b) // 2
console.log(c) // "3"

用于函数的结构。

let array: any[] = [1, 2, "3"]
function foo([a, b, c]: any[]) {
    console.log(a)
    console.log(b)
    console.log(c)
}
foo([...array])
// 1
// 2
// "3"

上面代码通过拓展运算符进行数组解构,构建foo()函数,函数参数是一个数组,利用结构对数组array解构。然后给foo()参数赋值。

数组解构

对象解构

对象也可以进行解构,并且也可以进行拓展运算解构。

let object = {
    a: "1",
    b: "2",
    c: "3"
}
let { a, b, c } = object
console.log(a)
console.log(b)
console.log(c)

对象解构

可以采用拓展运算符...进行对象的解构,使用拓展运算法首先是在最后才可以使用解构。

let object = {
    a: "1",
    b: "2",
    c: "3"
}
let { a, ...c } = object
console.log(c.b) // 2
console.log(c.c) // 3

上面代码中,对象object的解构是在{}中使用,在最后的位置进行使用。

函数声明

解构也可以用于函数的声明。

type C = { a: string, b?: number }
function foo({ a, b }: C): void {
    // 
}

通常使用解构赋值一般都是指定默认值,在设置默认值之前要设置其格式。

function foo({ a = "", b = 0 } = {}): void {
    // 
}
foo()