学好JS,离全栈又近了一步,欢迎使用我的小程序👇👇👇👇
一、作用域基础
1.1 什么是作用域?
作用域是变量和函数的可访问范围,它决定了代码中不同部分的变量可见性。
// 全局作用域
var globalVar = "我是全局变量";
function testScope() {
// 函数作用域
var functionVar = "我是函数内变量";
console.log(globalVar); // 可以访问全局变量
console.log(functionVar); // 可以访问函数内变量
}
testScope();
console.log(globalVar); // ✅ 可以访问
console.log(functionVar); // ❌ ReferenceError: functionVar is not defined
1.2 JavaScript 的作用域类型
1)全局作用域
- 在函数外部声明的变量
- 在整个程序中都可见
var global = "全局变量";
let globalLet = "全局let";
const globalConst = "全局const";
// 没有使用 var/let/const 声明的变量也会成为全局变量
function createGlobal() {
accidentalGlobal = "意外创建的全局变量";
}
createGlobal();
console.log(accidentalGlobal); // ✅ 可以访问
2)函数作用域
- 使用
var声明的变量具有函数作用域 - 在函数内部声明的变量只能在函数内部访问
function functionScope() {
var a = 10;
if (true) {
var b = 20; // 使用var,仍然是函数作用域
console.log(a); // ✅ 10
}
console.log(b); // ✅ 20(var没有块级作用域)
}
functionScope();
console.log(a); // ❌ ReferenceError
console.log(b); // ❌ ReferenceError
3)块级作用域(ES6+)
- 使用
let和const声明的变量具有块级作用域 - 块是
{}包围的代码区域
function blockScope() {
if (true) {
let blockLet = "块级let";
const blockConst = "块级const";
var functionVar = "函数var";
console.log(blockLet); // ✅
console.log(blockConst); // ✅
console.log(functionVar); // ✅
}
console.log(functionVar); // ✅ var没有块级作用域
console.log(blockLet); // ❌ ReferenceError
console.log(blockConst); // ❌ ReferenceError
}
blockScope();
二、词法作用域(静态作用域)
2.1 什么是词法作用域?
词法作用域(Lexical Scope)意味着作用域在代码书写时就已经确定,而不是在运行时确定。
var name = "全局";
function outer() {
var name = "outer";
function inner() {
console.log(name); // 输出什么?
}
return inner;
}
var innerFunc = outer();
innerFunc(); // 输出:"outer"
2.2 作用域链
当访问一个变量时,JavaScript 引擎会从当前作用域开始查找,然后逐级向上,直到全局作用域。
var global = "全局";
function level1() {
var level1Var = "第一层";
function level2() {
var level2Var = "第二层";
function level3() {
var level3Var = "第三层";
console.log(level3Var); // ✅ 当前作用域
console.log(level2Var); // ✅ 父作用域
console.log(level1Var); // ✅ 祖父作用域
console.log(global); // ✅ 曾祖父作用域
console.log(notExist); // ❌ ReferenceError
}
level3();
}
level2();
}
level1();
2.3 词法作用域 vs 动态作用域
// JavaScript 是词法作用域(静态作用域)
var value = "全局value";
function foo() {
console.log(value);
}
function bar() {
var value = "bar的value";
foo(); // foo在定义时已经确定了作用域链
}
bar(); // 输出:"全局value"
// 如果是动态作用域,会输出:"bar的value"
三、闭包
3.1 什么是闭包?
闭包是一个函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。
function createCounter() {
let count = 0; // 闭包捕获的变量
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// 每次调用createCounter都会创建新的闭包
const counter2 = createCounter();
console.log(counter2()); // 1(独立的环境)
3.2 闭包的工作原理
function outer() {
const secret = "我是秘密";
return function inner() {
console.log(secret); // 可以访问outer的变量
};
}
const innerFunc = outer();
innerFunc(); // "我是秘密"
// 即使outer执行完毕,inner仍然可以访问secret
// 因为inner函数的作用域链保留了对外部环境的引用
3.3 闭包的常见应用场景
1)数据封装和私有变量
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有变量
return {
deposit: function(amount) {
balance += amount;
return balance;
},
withdraw: function(amount) {
if (amount <= balance) {
balance -= amount;
return balance;
}
return "余额不足";
},
getBalance: function() {
return balance;
}
};
}
const account = createBankAccount(1000);
console.log(account.getBalance()); // 1000
console.log(account.deposit(500)); // 1500
console.log(account.withdraw(200)); // 1300
// console.log(balance); // ❌ ReferenceError: balance is not defined
2)函数工厂
function createMultiplier(factor) {
return function(x) {
return x * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
3)事件处理和回调
function setupButtons() {
for (var i = 1; i <= 3; i++) {
// 使用闭包保存每个按钮的索引
(function(index) {
document.getElementById(`btn-${index}`).addEventListener('click', function() {
console.log(`按钮 ${index} 被点击`);
});
})(i);
}
}
// ES6+ 更简洁的写法(使用let的块级作用域)
function setupButtonsES6() {
for (let i = 1; i <= 3; i++) {
document.getElementById(`btn-${i}`).addEventListener('click', function() {
console.log(`按钮 ${i} 被点击`); // let为每个迭代创建新的块级作用域
});
}
}
4)模块模式
const calculator = (function() {
let memory = 0;
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function store(value) {
memory = value;
}
function recall() {
return memory;
}
// 只暴露公共接口
return {
add,
subtract,
store,
recall
};
})();
console.log(calculator.add(5, 3)); // 8
calculator.store(100);
console.log(calculator.recall()); // 100
// console.log(memory); // ❌ 无法直接访问私有变量
四、闭包的注意事项
4.1 内存泄漏
// 不当使用闭包可能导致内存泄漏
function createHeavyObject() {
const heavyData = new Array(1000000).fill('大数据');
return function() {
// 即使只需要一小部分数据,整个heavyData都被保留
return heavyData[0];
};
}
// 解决方法:使用后释放引用
function createOptimizedObject() {
const heavyData = new Array(1000000).fill('大数据');
const neededData = heavyData[0]; // 只提取需要的数据
// 允许垃圾回收heavyData
return function() {
return neededData;
};
}
4.2 循环中的闭包陷阱
// 常见问题
function problem() {
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 全部输出5
}, 100);
}
}
// 解决方案1:使用IIFE创建新作用域
function solution1() {
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0,1,2,3,4
}, 100);
})(i);
}
}
// 解决方案2:使用let(推荐)
function solution2() {
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 0,1,2,3,4
}, 100);
}
}
4.3 性能考虑
// 大量使用闭包可能影响性能
function createManyClosures() {
const closures = [];
for (let i = 0; i < 10000; i++) {
closures.push((function(value) {
return function() {
return value;
};
})(i));
}
return closures;
}
// 在性能敏感的场景中,考虑替代方案
五、实际应用示例
5.1 防抖(Debounce)
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 使用
const searchInput = document.getElementById('search');
const debouncedSearch = debounce(function(query) {
console.log(`搜索: ${query}`);
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});
5.2 节流(Throttle)
function throttle(func, limit) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// 使用
window.addEventListener('scroll', throttle(function() {
console.log('滚动事件处理');
}, 100));
5.3 缓存函数(Memoization)
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('从缓存获取');
return cache.get(key);
}
console.log('计算新值');
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// 使用
const fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(10)); // 计算
console.log(fibonacci(10)); // 从缓存获取
总结
- 词法作用域是JavaScript的基础特性,作用域在代码书写时确定
- 闭包是函数和其周围状态(词法环境)的组合,允许函数访问创建时的作用域
- 闭包的实际应用广泛,包括:
- 数据封装和私有变量
- 函数工厂和柯里化
- 事件处理和回调
- 模块化编程
- 注意闭包的内存管理和性能影响
- 合理使用闭包可以写出更安全、更模块化的代码
理解作用域和闭包是掌握JavaScript高级编程的关键,它们是许多设计模式和最佳实践的基础。