06.JavaScript 作用域与变量详解

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);
}

特点

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

三、全局变量 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
小恐龙
花!
上一篇
下一篇