1. JavaScript 基础

  2. Document

  3. 运算符

  4. 深入数据和类型

  5. 函数进阶

  6. 原型、继承

  7. 浏览器存储

  8. Web API

  9. 事件

  10. 错误处理

  11. 异步编程

  12. 网络请求

  13. 模块

  14. 练习

  15. 实例

  16. 工具与规范

  17. 软件架构模式

  18. 设计模式

let、const、var 和作用域

声明一个变量时有三个可供选择的关键字:

  • let:声明一个变量。
  • const:声明一个不可改变的变量,又称为常量。
  • var:声明一个变量,已经过时的关键字不推荐使用。

$$warning

var 是一个过时的关键字,已经不推荐使用。

$$

let

以下是 letconst 共有的特性。

访问必须在声明之后

$$jsdemo$$
$$edit$$
alert(name) // Cannot access 'name' before initialization
let name = "鸣人"

不允许重新声明

$$jsdemo$$
$$edit$$
let name = "鸣人"
let name = "宁次" // Identifier 'name' has already been declared

const

const 在行为上与 let 是一致的,唯一不同是声明赋值之后不可以被改变:

$$jsdemo$$
$$edit$$
const name = "鸣人"
name = "宁次" // Assignment to constant variable.

但如果 const 是一个对象,那它里面的值则是可以改变的:

const person = {
    name: "鸣人",
}

person.name = "宁次"
alert(person.name) // 宁次

作用域

在作用域上 letconst 的行为是完全一致的,而 var 则略有不同,这点在稍后的章节中会介绍。

$$tip

很多教程会区分以下名词,但我觉得已经无需区分作用域类型了,反而会增加学习复杂度。

  • 全局使用域:最外层的作用域,定义的变量能在任何地方被访问。
  • 块级作用域:非函数的大括号内的作用域,例如 if、while、for 等。
  • 函数作用域:函数内的作用域。

$$

作用域指的是变量所能被访问的区域,每个大括号(代码块)都能产生一个新的作用域,内层作用域可以访问外层作用域内的变量,而反之则不行。

$$jsdemo$$
$$edit$$
let name = "鸣人"

{
    alert(name) // ok

    let age = 20
}

alert(age) // age is not defined

因为此时以上的代码内有两个作用域如下图所示,内层作用域能访问外层作用域定义的 name ,而外层作用域无法访问内层作用域定义的 age

image

$$tip

现在的编辑器都有对代码块的缩进,方便我们区分。

$$

同理,以下代码中 for 循环也属于代码块,因此 i 也不能被外层作用域访问。

$$jsdemo$$
$$edit$$
for (let i = 0; i < 5; i++) {
    console.log(i)
}

console.log(i) // i is not defined

访问变量的顺序

访问变量时,会优先在当前作用域查找,如果找不到则查找父级作用域,以此类推。如果达到最外层作用域时还是找不到,则产生 is not defined 的错误。

$$jsdemo$$
$$edit$$
// 第一层
let x = 1
let y = 1
console.log(`第一层的 x:${x}`) // 1
{
    // 第二层
    console.log(`第二层的 x:${x}`) // 1
    {
        // 第三层
        let x = 3
        console.log(`第三层的 x:${x}`) // 3
        console.log(`第三层的 y:${y}`) // 1
        console.log(`第三层的 z:${z}`) // z is not defined
    }
}

作用域的好处

作用域可以避免出现变量访问混乱的情况,可以在各作用域间定义变量而不会出现冲突,也不用担心有没有不小心覆盖其他作用域内的变量。

$$jsdemo$$
$$edit$$
let name = "鸣人"

function sayHello() {
    let name = "路飞"
    alert(`${name},你好~`)
}

sayHello() // 路飞,你好~

alert(name) // 鸣人

作用域产生于函数定义而不是调用

$$jsdemo$$
$$edit$$
function getFunction() {
    let name = "鸣人"
    return function () {
        alert(name)
    }
}

let func = getFunction()

let name = "柯南"
func() // 鸣人

$$tip 以上的 func 函数也被称为闭包。

当存在一个函数,能长期地访问其他函数作用域内的变量时就被称为闭包。 比如 func 函数能长期访问 getFunction 函数作用域内的变量。 $$

闭包内的变量是相互独立的,这意味着我们可以独立操作这些变量。比如以下代码,createConter 函数每次返回一个独立的计数器。

$$jsdemo$$
$$edit$$
function createCounter() {
    let count = 0
    return function () {
        return count++
    }
}

let counter1 = createCounter()
let counter2 = createCounter()

console.log(counter1()) // 0
console.log(counter1()) // 1

console.log(counter2()) // 0
console.log(counter2()) // 1

console.log(counter1()) // 2
console.log(counter2()) // 2

以下代码则是使用闭包的原理,在函数中独立保存 nameage 变量。

$$jsdemo$$
$$edit$$
function createPerson() {
    let name
    let age
    return {
        setName(n) {
            // 设置名称
            name = n
        },
        setAge(a) {
            // 设置年龄
            age = a
        },
        getInfo() {
            return `名字:${name},年龄:${age}。`
        },
    }
}

let mingren = createPerson()
mingren.setName("鸣人")
mingren.setAge(20)

let lufei = createPerson()
lufei.setName("路飞")
lufei.setAge(18)

console.log(mingren.getInfo()) // 名字:鸣人,年龄:20。
console.log(lufei.getInfo()) // 名字:路飞,年龄:18。

var

尽管 var 已不被推荐使用,但在阅读早期的代码文件时仍能看见它的身影。因此以下内容可以根据需求选学,

varlet 有以下不同点。

没有块级作用域

var 无法在块内(if、while、for、纯大括号)起作用域的作用,只能在函数内起作用。

$$jsdemo$$
$$edit$$
if (true) {
    // 没有块级作用域
    var title = "三眼鸭的编程教室"
}
alert(title) // 三眼鸭的编程教室,仍然可以访问

允许重新声明

let 在同个作用域内只允许声明一次,而 var 则可以多次声明,后声明的则会覆盖之前声明的。

$$jsdemo$$
$$edit$$
var name = "鸣人"
var name = "路飞"

alert(name) // 路飞

let age = 18
let age = 20 // Identifier 'age' has already been declared

变量提升

var 声明的变量,可以在声明前被使用。

$$jsdemo$$
$$edit$$
function sayHello() {
    // var word,被隐性提升到开头
    word = "你好~"
    alert(word)

    var word
}

sayHello() // 你好~

未使用关键字声明的变量

如果没有声明一个变量而是直接赋值,那么变量会被绑定到 window 对象中。 window 对象中的属性可以在任何地方访问。

$$jsdemo$$
$$edit$$
{
    x = "x"
    // 等同于 window.x = "x"
}

function printX() {
    console.log(window.x) // x, 成为window 对象的属性
    console.log(x) // x, 访问 window 对象的属性可以不用加 window
}

printX()

$$warning 千万不要这样做,全局的变量会造成代码的混乱,并且你很可能会覆盖掉 window 对象中一些关键的属性或方法,比如 alert 方法其实就是 window 对象中的一个方法。

$$jsdemo$$
$$edit$$
alert("你好")
window.alert("你好")

// 到控制台中查看 window 所拥有的属性或方法吧
console.log(window)

{
    alert = "覆盖"
}

alert("你好") // alert is not a function

$$

$$tip 有一个最小作用域原则,指的是在代码能正常运行的前提下,变量的作用域应该越小越好。 $$


练习

  1. 以下代码的输出结果是什么,为什么?
let n
let x

for (let i = 0; i < 10; i++) {
    let x

    n = i
    x = i
}

alert(n)
alert(x)

$$answer

$$jsdemo$$
$$edit$$
let n
let x

for (let i = 0; i < 10; i++) {
    let x

    n = i
    x = i
}

alert(n) // 9,循环内赋值的 n 是外层定义的 n
alert(x) // undefined,循环内赋值的 x 是循环内定义的 x

$$

  1. 以下代码的输出结果是什么,为什么?
function getFunction() {
    let name = "鸣人"
    return function () {
        name = "路飞"
        return function () {
            return function () {
                alert(name) // 输出什么?
            }
            name = "柯南"
        }
    }
}

let name = "艾伦"
getFunction()()()()

$$answer

$$jsdemo$$
$$edit$$
function getFunction() {
    let name = "鸣人"
    return function () {
        name = "路飞" // 赋值上层作用的 name
        return function () {
            return function () {
                alert(name) // 路飞,使用的是上层作用域的 name
            }
            name = "柯南" // return 之后的代码永远不会执行
        }
    }
}

let name = "艾伦"
getFunction()()()()

$$