06.JavaScript 作用域与变量详解
本文最后更新于333 天前,其中的信息可能已经过时,如有错误请发送邮件到963470751@qq.com

06.JavaScript 作用域与变量详解

一、什么是作用域

1. 作用域的定义

作用域(Scope)是指程序中定义变量的区域,它决定了变量的可见性和生命周期。

2. 作用域的主要功能

  • 变量可见性:确定在代码的哪些部分可以访问变量
  • 变量生命周期:确定变量何时被创建和销毁
  • 命名空间管理:防止变量命名冲突

二、JavaScript 作用域类型

1. 全局作用域(Global Scope)

// 全局作用域中的变量
var globalVar = "我是全局变量";
let globalLet = "我也是全局变量";
const globalConst = "我还是全局变量";

function showGlobal() {
    console.log(globalVar);  // 可以访问
    console.log(globalLet);  // 可以访问
    console.log(globalConst); // 可以访问
}

showGlobal();
console.log(globalVar);  // 在函数外部也可以访问

特点:

  • 在代码最外层声明的变量
  • 在任何地方都可以访问
  • 生命周期:页面加载时创建,页面关闭时销毁
  • 浏览器中全局变量会成为 window 对象的属性(使用 var 声明时)

2. 函数作用域(Function Scope)

function myFunction() {
    // 函数作用域中的变量
    var functionVar = "我是函数作用域变量";
    let functionLet = "我也是函数作用域变量";

    console.log(functionVar);  // 可以访问
    console.log(functionLet);  // 可以访问

    function innerFunction() {
        console.log(functionVar);  // 可以访问外层函数的变量
        let innerLet = "内层函数变量";
        console.log(innerLet);  // 可以访问
    }

    innerFunction();
    // console.log(innerLet);  // 错误!无法访问内层函数的变量
}

myFunction();
// console.log(functionVar);  // 错误!无法访问函数内部的变量

特点:

  • 在函数内部声明的变量
  • 只能在函数内部访问
  • 生命周期:函数调用时创建,函数执行完毕后销毁
  • var 声明的变量具有函数作用域

3. 块级作用域(Block Scope,ES6+)

// 块级作用域示例
if (true) {
    // 块级作用域
    let blockLet = "我是块级作用域变量";
    const blockConst = "我也是块级作用域变量";
    var blockVar = "我使用var声明,不是块级作用域";

    console.log(blockLet);    // 可以访问
    console.log(blockConst);  // 可以访问
    console.log(blockVar);    // 可以访问
}

console.log(blockVar);    // 可以访问(var没有块级作用域)
// console.log(blockLet);  // 错误!无法访问块级作用域变量
// console.log(blockConst); // 错误!

// 循环中的块级作用域
for (let i = 0; i < 3; i++) {
    // 每个循环迭代都有独立的 i
    setTimeout(function() {
        console.log(i);  // 0, 1, 2
    }, 100);
}

// 对比:使用var
for (var j = 0; j < 3; j++) {
    setTimeout(function() {
        console.log(j);  // 3, 3, 3(所有回调共享同一个j)
    }, 100);
}

特点:

  • 由一对花括号 {} 定义的区域
  • 使用 let 和 const 声明的变量具有块级作用域
  • 生命周期:进入块时创建,离开块时销毁
  • 常见于:if、for、while、switch 等语句

三、全局变量 vs 局部变量

对比表格

特性 全局变量 局部变量
声明位置 函数外部 函数内部或代码块内部
作用域 全局作用域 函数作用域或块级作用域
访问范围 任何地方都可以访问 只能在声明的作用域内访问
生命周期 页面加载时创建,页面关闭时销毁 函数/块执行时创建,执行完毕后销毁
内存占用 长期占用内存 临时占用,执行完释放
命名冲突 容易冲突 相对安全
推荐程度 尽量避免使用 推荐使用

代码示例对比

// 全局变量 - 不推荐
var globalCounter = 0;

function incrementGlobal() {
    globalCounter++;
    console.log("全局计数器:", globalCounter);
}

function resetGlobalCounter() {
    globalCounter = 0;  // 可能在其他地方被意外修改
    console.log("重置全局计数器");
}

incrementGlobal();  // 全局计数器: 1
incrementGlobal();  // 全局计数器: 2
resetGlobalCounter();  // 重置全局计数器
console.log(globalCounter);  // 0

// 局部变量 - 推荐
function createCounter() {
    let localCounter = 0;  // 局部变量,外部无法访问

    return {
        increment: function() {
            localCounter++;
            console.log("局部计数器:", localCounter);
            return localCounter;
        },
        reset: function() {
            localCounter = 0;
            console.log("计数器已重置");
        },
        getValue: function() {
            return localCounter;
        }
    };
}

const myCounter = createCounter();
myCounter.increment();  // 局部计数器: 1
myCounter.increment();  // 局部计数器: 2
myCounter.reset();      // 计数器已重置
console.log(myCounter.getValue());  // 0
// console.log(localCounter);  // 错误!无法直接访问局部变量

四、作用域链(Scope Chain)

1. 作用域链的概念

当访问一个变量时,JavaScript 引擎会按照以下顺序查找:

  1. 当前作用域
  2. 外层作用域
  3. 再外层作用域
  4. 直到全局作用域

2. 作用域链示例

// 全局作用域
let globalValue = "全局";

function outerFunction() {
    // 外层函数作用域
    let outerValue = "外层";

    function middleFunction() {
        // 中间函数作用域
        let middleValue = "中间";

        function innerFunction() {
            // 内层函数作用域
            let innerValue = "内层";

            console.log(innerValue);   // "内层" - 当前作用域
            console.log(middleValue);  // "中间" - 向外一层
            console.log(outerValue);   // "外层" - 向外两层
            console.log(globalValue);  // "全局" - 全局作用域
        }

        innerFunction();
    }

    middleFunction();
}

outerFunction();

3. 作用域链可视化

全局作用域 [globalValue]
    ↑
外层函数作用域 [outerValue]
    ↑
中间函数作用域 [middleValue]
    ↑
内层函数作用域 [innerValue]

五、变量提升(Hoisting)

1. var 的变量提升

console.log(myVar);      // undefined(不会报错)
var myVar = "Hello";
console.log(myVar);      // "Hello"

// 实际执行顺序相当于:
// var myVar;            // 声明提升到顶部
// console.log(myVar);   // undefined
// myVar = "Hello";      // 赋值保持原位
// console.log(myVar);   // "Hello"

2. let 和 const 的暂时性死区(TDZ)

// console.log(myLet);   // 错误!Cannot access 'myLet' before initialization
let myLet = "Hello";

// console.log(myConst); // 错误!
const myConst = "World";

3. 函数提升

sayHello();  // "Hello!"(可以正常调用)

function sayHello() {
    console.log("Hello!");
}

// 函数表达式不会提升
// sayGoodbye();  // 错误!Cannot access 'sayGoodbye' before initialization
const sayGoodbye = function() {
    console.log("Goodbye!");
};

六、闭包(Closure)与作用域

1. 闭包的基本概念

闭包是指函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。

2. 闭包示例

function createCounter() {
    let count = 0;  // 局部变量,外部无法直接访问

    // 返回一个函数,形成闭包
    return function() {
        count++;    // 可以访问外层函数的count变量
        return count;
    };
}

const counter = createCounter();
console.log(counter());  // 1
console.log(counter());  // 2
console.log(counter());  // 3
// count变量被闭包"记住"了,不会被垃圾回收

3. 闭包的实用场景

// 1. 数据封装
function createPerson(name) {
    let age = 0;  // 私有变量

    return {
        getName: () => name,
        getAge: () => age,
        setAge: (newAge) => {
            if (newAge >= 0) age = newAge;
        },
        birthday: () => age++
    };
}

const person = createPerson("Alice");
person.setAge(25);
person.birthday();
console.log(person.getName(), person.getAge());  // "Alice", 26

// 2. 函数工厂
function createMultiplier(factor) {
    return function(number) {
        return number * factor;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

// 3. 事件处理中的闭包
function setupButtons() {
    for (let i = 1; i <= 3; i++) {
        // 使用let创建块级作用域,每个循环都有独立的i
        const button = document.createElement("button");
        button.textContent = `按钮 ${i}`;
        button.onclick = function() {
            alert(`你点击了按钮 ${i}`);
        };
        document.body.appendChild(button);
    }
}

七、严格模式(Strict Mode)

1. 启用严格模式

"use strict";  // 在整个脚本或函数开头添加

// 或者只在函数内启用
function strictFunction() {
    "use strict";
    // 严格模式代码
}

2. 严格模式对作用域的影响

"use strict";

// 1. 不允许未声明就赋值(防止创建意外全局变量)
// undeclaredVar = 10;  // 错误!ReferenceError

// 2. 不允许删除变量、函数、函数参数
var x = 10;
// delete x;  // 错误!SyntaxError

// 3. 函数参数不能重名
// function duplicateParams(a, a, b) { }  // 错误!SyntaxError

// 4. 禁止使用 with 语句
// with (Math) { console.log(PI); }  // 错误!SyntaxError

// 5. 保留字不能用作变量名
// let let = 10;  // 错误!SyntaxError

八、最佳实践

1. 变量声明最佳实践

// 1. 优先使用 const,其次 let,避免 var
const PI = 3.14159;               // 不会改变的常量
let username = "John";            // 可能改变的值
// var oldWay = "avoid this";     // 避免使用

// 2. 声明时初始化
let count = 0;                    // 好的做法
let total;                        // 避免(除非必要)
// total = calculateTotal();      // 稍后赋值

// 3. 使用块级作用域限制变量可见性
{
    const temp = calculateValue();
    console.log(temp);
}
// temp在这里不可访问,减少命名冲突

// 4. 避免全局变量污染
// 不好的做法:很多全局变量
// var user, config, cache, logger, ...;

// 好的做法:使用命名空间或模块
const App = {
    user: {},
    config: {},
    cache: {},
    init: function() { /* ... */ }
};

2. 作用域管理技巧

// 1. IIFE(立即调用函数表达式)创建私有作用域
(function() {
    // 私有变量,不会污染全局作用域
    const privateData = "secret";

    function privateHelper() {
        console.log(privateData);
    }

    // 暴露公共接口
    window.MyModule = {
        publicMethod: function() {
            privateHelper();
        }
    };
})();

// 2. 模块模式
const CounterModule = (function() {
    let privateCount = 0;

    function changeBy(val) {
        privateCount += val;
    }

    return {
        increment: function() {
            changeBy(1);
        },
        decrement: function() {
            changeBy(-1);
        },
        value: function() {
            return privateCount;
        }
    };
})();

CounterModule.increment();
console.log(CounterModule.value());  // 1

// 3. 使用闭包时注意内存泄漏
function createHeavyClosure() {
    const largeData = new Array(1000000).fill("data");

    return function() {
        // 闭包引用了largeData,即使不再需要也不会被回收
        console.log(largeData.length);
    };
}

// 解决方案:在不再需要时清除引用
function createSafeClosure() {
    let largeData = new Array(1000000).fill("data");

    const closure = function() {
        if (largeData) {
            console.log(largeData.length);
        }
    };

    // 提供清理方法
    closure.cleanup = function() {
        largeData = null;
    };

    return closure;
}

3. 现代JavaScript模块作用域

// ES6模块(module.js)
// 每个模块有自己的作用域
const privateVariable = "模块私有";
export const publicVariable = "模块公开";

export function publicFunction() {
    console.log(privateVariable);  // 可以访问模块私有变量
}

// 在另一个文件中
import { publicVariable, publicFunction } from './module.js';
console.log(publicVariable);      // 可以访问
publicFunction();                 // 可以调用
// console.log(privateVariable);  // 错误!无法访问模块私有变量

九、常见问题与解决方案

1. 循环中的闭包问题

// 问题:使用var,所有闭包共享同一个i
function createFunctionsProblem() {
    var functions = [];
    for (var i = 0; i < 3; i++) {
        functions.push(function() {
            console.log(i);  // 所有函数都输出3
        });
    }
    return functions;
}

// 解决方案1:使用let(块级作用域)
function createFunctionsSolution1() {
    var functions = [];
    for (let i = 0; i < 3; i++) {
        functions.push(function() {
            console.log(i);  // 0, 1, 2
        });
    }
    return functions;
}

// 解决方案2:IIFE创建独立作用域
function createFunctionsSolution2() {
    var functions = [];
    for (var i = 0; i < 3; i++) {
        (function(index) {
            functions.push(function() {
                console.log(index);  // 0, 1, 2
            });
        })(i);
    }
    return functions;
}

// 解决方案3:使用forEach(每次迭代都有独立作用域)
function createFunctionsSolution3() {
    var functions = [];
    [0, 1, 2].forEach(function(i) {
        functions.push(function() {
            console.log(i);  // 0, 1, 2
        });
    });
    return functions;
}

2. 全局变量污染问题

// 问题:多个脚本可能使用相同的全局变量名
// script1.js
var config = { /* ... */ };

// script2.js(可能由其他人编写)
var config = { /* ... */ };  // 覆盖了第一个config!

// 解决方案1:使用命名空间
var MyApp = MyApp || {};
MyApp.config = { /* ... */ };

// 解决方案2:IIFE封装
(function() {
    var config = { /* ... */ };
    // 你的代码...
})();

// 解决方案3:使用ES6模块(最佳方案)

3. 变量遮蔽(Variable Shadowing)

let x = 10;

function test() {
    let x = 20;  // 遮蔽了外层的x
    console.log(x);  // 20
}

test();
console.log(x);  // 10(外层的x没有改变)

// 避免过度遮蔽
function calculate(radius) {
    // 不要这样:let radius = parseFloat(radius);  // 遮蔽参数
    // 应该这样:
    let r = parseFloat(radius);
    return Math.PI * r * r;
}

十、总结

1. 作用域核心要点

  • 全局作用域:最外层,随处可访问,生命周期最长
  • 函数作用域:函数内部,使用 var 声明
  • 块级作用域:{} 内部,使用 let/const 声明

2. 变量声明建议

关键字 作用域 是否提升 可重新赋值 推荐程度
var 函数作用域 是 是 避免使用
let 块级作用域 否(有TDZ) 是 推荐(需要重新赋值时)
const 块级作用域 否(有TDZ) 否 推荐(优先使用)

3. 最佳实践总结

  1. 优先使用 const,需要重新赋值时用 let
  2. 避免使用 var 和全局变量
  3. 使用严格模式("use strict")
  4. 合理使用闭包,注意内存管理
  5. 利用模块化(ES6模块)组织代码
  6. 保持作用域清晰,避免过度嵌套
  7. 使用IIFE或块级作用域隔离代码

4. 记忆口诀

  • "全局变量处处跑,局部变量家里蹲"
  • "var 提升爱乱跑,let/const 守规矩"
  • "闭包记住老家,作用域链寻亲"
  • "严格模式把关,避免意外全局"
// 最终示例:良好的作用域实践
(function() {
    "use strict";

    // 模块私有变量
    const MODULE_NAME = "MyModule";
    let instanceCount = 0;

    // 公共接口
    window.MyModule = {
        createInstance: function(config) {
            instanceCount++;

            // 实例私有变量
            const instanceId = instanceCount;
            let settings = Object.assign({}, config);

            return {
                getId: () => instanceId,
                getSettings: () => ({ ...settings }),
                updateSettings: (newSettings) => {
                    settings = Object.assign(settings, newSettings);
                }
            };
        },

        getInstanceCount: () => instanceCount
    };
})();

// 使用
const instance1 = MyModule.createInstance({ color: "red" });
const instance2 = MyModule.createInstance({ color: "blue" });

console.log(instance1.getId());           // 1
console.log(instance2.getId());           // 2
console.log(MyModule.getInstanceCount()); // 2
shi著
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇