JavaScript作用域

在本章节中,我们将学习JavaScript作用域的相关知识,包括作用域的定义、类型、作用域链和变量提升等。

1. 什么是作用域?

作用域是指变量和函数的可访问范围,它决定了代码中的变量和函数在何处可以被访问。作用域可以控制变量的可见性和生命周期,防止变量被意外修改或访问。

简单来说,作用域就是变量和函数的有效范围。在JavaScript中,作用域可以分为以下几种类型:

  • 全局作用域
  • 函数作用域
  • 块级作用域

2. 全局作用域

全局作用域是指在所有函数和代码块之外定义的变量和函数,它们可以在代码的任何地方被访问。

2.1 全局变量

在全局作用域中定义的变量称为全局变量。

// 全局变量
var globalVar = "我是全局变量";
let globalLet = "我也是全局变量";
const globalConst = "我是全局常量";

function test() {
    // 可以访问全局变量
    console.log(globalVar); // 输出:我是全局变量
    console.log(globalLet); // 输出:我也是全局变量
    console.log(globalConst); // 输出:我是全局常量
}

test();

// 在函数外部也可以访问全局变量
console.log(globalVar); // 输出:我是全局变量

2.2 全局对象

在浏览器环境中,全局变量会成为全局对象window的属性。

var globalVar = "我是全局变量";

console.log(window.globalVar); // 输出:我是全局变量
console.log(globalVar === window.globalVar); // 输出:true

注意:使用letconst声明的全局变量不会成为全局对象的属性。

let globalLet = "我是全局变量";
const globalConst = "我是全局常量";

console.log(window.globalLet); // 输出:undefined
console.log(window.globalConst); // 输出:undefined

3. 函数作用域

函数作用域是指在函数内部定义的变量和函数,它们只能在函数内部被访问。

3.1 局部变量

在函数内部定义的变量称为局部变量。

function test() {
    // 局部变量
    var localVar = "我是局部变量";
    let localLet = "我也是局部变量";
    const localConst = "我是局部常量";
    
    console.log(localVar); // 输出:我是局部变量
    console.log(localLet); // 输出:我也是局部变量
    console.log(localConst); // 输出:我是局部常量
}

test();

// 在函数外部无法访问局部变量
console.log(localVar); // 报错:localVar is not defined
console.log(localLet); // 报错:localLet is not defined
console.log(localConst); // 报错:localConst is not defined

3.2 函数参数

函数参数也是函数作用域内的局部变量。

function test(param1, param2) {
    // param1和param2是函数作用域内的局部变量
    console.log(param1); // 输出:1
    console.log(param2); // 输出:2
}

test(1, 2);

// 在函数外部无法访问函数参数
console.log(param1); // 报错:param1 is not defined

4. 块级作用域

块级作用域是指在代码块(由{}包围的代码)内部定义的变量,它们只能在代码块内部被访问。

块级作用域是ES6(2015)引入的新特性,通过letconst关键字实现。

4.1 块级作用域示例

// if语句块
if (true) {
    var varInsideIf = "var声明的变量";
    let letInsideIf = "let声明的变量";
    const constInsideIf = "const声明的变量";
}

console.log(varInsideIf); // 输出:var声明的变量(var没有块级作用域)
console.log(letInsideIf); // 报错:letInsideIf is not defined(let有块级作用域)
console.log(constInsideIf); // 报错:constInsideIf is not defined(const有块级作用域)

// for循环块
for (var i = 0; i < 5; i++) {
    // 循环体
}
console.log(i); // 输出:5(var没有块级作用域)

for (let j = 0; j < 5; j++) {
    // 循环体
}
console.log(j); // 报错:j is not defined(let有块级作用域)

// 函数块
function test() {
    let insideFunction = "函数内部的变量";
}
console.log(insideFunction); // 报错:insideFunction is not defined

4.2 var、let和const的作用域对比

关键字 作用域 变量提升 重复声明 重新赋值
var 函数作用域 支持 允许 允许
let 块级作用域 不支持 不允许 允许
const 块级作用域 不支持 不允许 不允许

5. 作用域链

当访问一个变量时,JavaScript引擎会先在当前作用域中查找该变量,如果找不到,就会向上查找父级作用域,直到找到该变量或到达全局作用域。这种由多个作用域组成的链式结构称为作用域链。

5.1 作用域链示例

// 全局作用域
let globalVar = "全局变量";

function outer() {
    // 外部函数作用域
    let outerVar = "外部函数变量";
    
    function inner() {
        // 内部函数作用域
        let innerVar = "内部函数变量";
        
        // 可以访问内部函数变量
        console.log(innerVar); // 输出:内部函数变量
        // 可以访问外部函数变量
        console.log(outerVar); // 输出:外部函数变量
        // 可以访问全局变量
        console.log(globalVar); // 输出:全局变量
    }
    
    inner();
    
    // 可以访问外部函数变量
    console.log(outerVar); // 输出:外部函数变量
    // 可以访问全局变量
    console.log(globalVar); // 输出:全局变量
    // 无法访问内部函数变量
    console.log(innerVar); // 报错:innerVar is not defined
}

outer();

// 可以访问全局变量
console.log(globalVar); // 输出:全局变量
// 无法访问外部函数变量
console.log(outerVar); // 报错:outerVar is not defined
// 无法访问内部函数变量
console.log(innerVar); // 报错:innerVar is not defined

5.2 作用域链的查找顺序

作用域链的查找顺序是从内到外,即:

  1. 当前函数作用域
  2. 外部函数作用域
  3. 全局作用域

如果在全局作用域中仍然找不到该变量,就会抛出ReferenceError错误。

6. 变量提升

变量提升是指JavaScript引擎在执行代码之前,会将变量声明提升到当前作用域的顶部。这意味着可以在变量声明之前使用变量。

6.1 var的变量提升

使用var声明的变量会被提升到当前作用域的顶部。

// 变量提升示例
console.log(x); // 输出:undefined(变量被提升,但值未被初始化)
var x = 10;
console.log(x); // 输出:10

// 上面的代码等价于:
var x;
console.log(x); // undefined
x = 10;
console.log(x); // 10

6.2 let和const的变量提升

使用letconst声明的变量也会被提升,但不会被初始化,而是处于"暂时性死区"(Temporal Dead Zone,TDZ)中,在声明之前访问会抛出错误。

// let的暂时性死区
console.log(y); // 报错:Cannot access 'y' before initialization
let y = 20;

// const的暂时性死区
console.log(z); // 报错:Cannot access 'z' before initialization
const z = 30;

6.3 函数声明的提升

函数声明也会被提升到当前作用域的顶部,可以在函数声明之前调用函数。

// 函数声明提升示例
test(); // 输出:我是函数声明

function test() {
    console.log("我是函数声明");
}

// 上面的代码等价于:
function test() {
    console.log("我是函数声明");
}

test(); // 输出:我是函数声明

6.4 函数表达式的提升

函数表达式不会被提升,只能在函数表达式定义之后调用函数。

// 函数表达式不会被提升
test(); // 报错:test is not a function

var test = function() {
    console.log("我是函数表达式");
};

// 上面的代码等价于:
var test;
test(); // 此时test是undefined,不是函数

test = function() {
    console.log("我是函数表达式");
};

7. 闭包

闭包是指有权访问另一个函数作用域中变量的函数。闭包可以让函数访问其定义时所处的作用域,即使该函数在其定义的作用域之外被调用。

7.1 闭包示例

function outer() {
    let outerVar = "外部函数变量";
    
    function inner() {
        // inner函数可以访问outer函数的变量
        console.log(outerVar); // 输出:外部函数变量
    }
    
    return inner;
}

// 创建闭包
let closure = outer();

// 调用闭包,即使outer函数已经执行完毕,仍然可以访问outerVar
closure(); // 输出:外部函数变量

7.2 闭包的应用

闭包可以用于:

  • 封装私有变量
  • 实现函数柯里化
  • 实现模块模式
  • 保持变量的状态
// 使用闭包封装私有变量
function createCounter() {
    let count = 0;
    
    return {
        increment: function() {
            count++;
            return count;
        },
        decrement: function() {
            count--;
            return count;
        },
        getCount: function() {
            return count;
        }
    };
}

let counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined(无法直接访问私有变量)

8. 常见问题解答

Q: 什么是作用域?

A: 作用域是指变量和函数的可访问范围,它决定了代码中的变量和函数在何处可以被访问。

Q: 全局作用域和局部作用域有什么区别?

A: 全局作用域中的变量可以在代码的任何地方被访问,而局部作用域中的变量只能在定义它的函数或代码块内部被访问。

Q: var、let和const的作用域有什么区别?

A: var声明的变量具有函数作用域,let和const声明的变量具有块级作用域。

Q: 什么是作用域链?

A: 作用域链是由多个作用域组成的链式结构,当访问一个变量时,JavaScript引擎会先在当前作用域中查找,找不到就会向上查找父级作用域,直到找到该变量或到达全局作用域。

Q: 什么是变量提升?

A: 变量提升是指JavaScript引擎在执行代码之前,会将变量声明提升到当前作用域的顶部。使用var声明的变量会被提升并初始化为undefined,而使用let和const声明的变量会被提升但处于暂时性死区中。

Q: 什么是闭包?

A: 闭包是指有权访问另一个函数作用域中变量的函数。闭包可以让函数访问其定义时所处的作用域,即使该函数在其定义的作用域之外被调用。

Q: 什么是暂时性死区?

A: 暂时性死区是指使用let和const声明的变量,在声明之前访问会抛出错误的区域。

9. 练习项目

  1. 创建一个HTML文件,包含以下内容:

    • 一个按钮,用于增加计数
    • 一个按钮,用于减少计数
    • 一个显示计数的区域
  2. 使用JavaScript实现以下功能:

    • 使用闭包封装一个计数器
    • 点击增加按钮时,计数加1
    • 点击减少按钮时,计数减1
    • 在显示区域显示当前计数
    • 确保计数变量是私有的,无法直接访问
  3. 测试不同的操作,确保计数器功能正常

10. 小结

  • 作用域是指变量和函数的可访问范围
  • JavaScript作用域包括全局作用域、函数作用域和块级作用域
  • 全局作用域中的变量可以在代码的任何地方被访问
  • 函数作用域中的变量只能在函数内部被访问
  • 块级作用域中的变量只能在代码块内部被访问
  • 作用域链是由多个作用域组成的链式结构,用于查找变量
  • 变量提升是指JavaScript引擎在执行代码之前,会将变量声明提升到当前作用域的顶部
  • 闭包是指有权访问另一个函数作用域中变量的函数
  • 建议优先使用let和const,避免使用var,以减少作用域相关的问题

在下一章节中,我们将学习JavaScript变量提升的相关知识。

« 上一篇 JavaScript语句 下一篇 » JavaScript变量提升