作用域与闭包基础
作用域与闭包基础
JavaScript的作用域规则决定了变量的可见性和生命周期。闭包是JavaScript中一个强大的特性,它允许函数访问其词法作用域外的变量。本文将介绍JavaScript的作用域类型和闭包的基本概念。
作用域基础
作用域是指程序中定义变量的区域,它决定了变量的可访问性和生命周期。理解作用域对于编写可维护的JavaScript代码至关重要。
什么是作用域?
作用域是一套规则,用于确定在何处以及如何查找变量。在JavaScript中,作用域主要有以下几种类型:
- 全局作用域
- 函数作用域
- 块级作用域(ES6引入)
全局作用域
在任何函数外部声明的变量都属于全局作用域,可以在整个程序中访问。
// 全局作用域中的变量
const globalVariable = '我是全局变量';
function testScope() {
console.log(globalVariable); // 可以访问全局变量
}
testScope(); // 输出: 我是全局变量
console.log(globalVariable); // 输出: 我是全局变量
注意事项:
- 全局变量会成为全局对象(浏览器中是
window
,Node.js中是global
)的属性 - 过多的全局变量可能导致命名冲突和内存泄漏
- 应尽量减少全局变量的使用
函数作用域
在函数内部声明的变量只在该函数内部可见,外部无法访问。
function functionScope() {
const localVariable = '我是局部变量';
console.log(localVariable); // 可以访问局部变量
}
functionScope(); // 输出: 我是局部变量
// console.log(localVariable); // 错误: localVariable is not defined
函数作用域遵循"内部可以访问外部,外部不能访问内部"的原则:
const outer = '外部变量';
function outerFunction() {
const inner = '内部变量';
console.log(outer); // 可以访问外部变量
console.log(inner); // 可以访问内部变量
function innerFunction() {
const innermost = '最内部变量';
console.log(outer); // 可以访问外部变量
console.log(inner); // 可以访问内部变量
console.log(innermost); // 可以访问最内部变量
}
innerFunction();
// console.log(innermost); // 错误: innermost is not defined
}
outerFunction();
// console.log(inner); // 错误: inner is not defined
块级作用域
ES6引入了let
和const
关键字,它们声明的变量具有块级作用域,即变量只在最近的一对花括号{}
内可见。
{
var varVariable = '使用var声明';
let letVariable = '使用let声明';
const constVariable = '使用const声明';
}
console.log(varVariable); // 输出: 使用var声明(var没有块级作用域)
// console.log(letVariable); // 错误: letVariable is not defined
// console.log(constVariable); // 错误: constVariable is not defined
块级作用域在循环中特别有用:
// 使用var(没有块级作用域)
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 输出三次: 3
}, 100);
}
console.log(i); // 输出: 3(i泄漏到循环外部)
// 使用let(有块级作用域)
for (let j = 0; j < 3; j++) {
setTimeout(function() {
console.log(j); // 输出: 0, 1, 2
}, 100);
}
// console.log(j); // 错误: j is not defined
作用域链
当JavaScript引擎查找变量时,它会从当前作用域开始,如果找不到,就会向外层作用域查找,直到全局作用域。这种查找过程形成了作用域链。
const global = '全局变量';
function outer() {
const outerVar = '外部函数变量';
function inner() {
const innerVar = '内部函数变量';
console.log(innerVar); // 首先查找当前作用域
console.log(outerVar); // 然后查找外部函数作用域
console.log(global); // 最后查找全局作用域
}
inner();
}
outer();
作用域链的查找规则是"由内向外",一旦找到变量就停止查找:
const value = '全局值';
function outer() {
function inner() {
console.log(value); // 输出: 全局值
}
inner();
}
function shadowOuter() {
const value = '局部值';
function inner() {
console.log(value); // 输出: 局部值(变量遮蔽)
}
inner();
}
outer();
shadowOuter();
变量提升
JavaScript在执行代码之前,会将变量和函数声明"提升"到其所在作用域的顶部。
变量提升
使用var
声明的变量会被提升,但初始化不会:
console.log(hoistedVar); // 输出: undefined(变量已提升但未初始化)
var hoistedVar = '我被提升了';
console.log(hoistedVar); // 输出: 我被提升了
// 等同于:
var hoistedVar;
console.log(hoistedVar);
hoistedVar = '我被提升了';
console.log(hoistedVar);
let
和const
声明的变量也会提升,但在初始化之前不能访问(暂时性死区):
// console.log(letVar); // 错误: Cannot access 'letVar' before initialization
let letVar = '使用let声明';
console.log(letVar); // 输出: 使用let声明
函数提升
函数声明会被完整提升,包括函数体:
hoistedFunction(); // 输出: 我是被提升的函数
function hoistedFunction() {
console.log('我是被提升的函数');
}
函数表达式不会被提升:
// notHoistedFunction(); // 错误: notHoistedFunction is not a function
var notHoistedFunction = function() {
console.log('我不会被提升');
};
notHoistedFunction(); // 输出: 我不会被提升
闭包基础
闭包是JavaScript中一个强大而常用的特性,它允许函数访问并操作其词法作用域外的变量。
什么是闭包?
闭包是指函数与其词法环境的组合,这个环境包含了函数创建时可以访问的所有变量。简单来说,闭包使函数可以"记住"并访问其所在的词法作用域,即使函数在其他地方被调用。
闭包的基本示例
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
函数创建了一个局部变量count
- 返回的内部函数形成了一个闭包,它可以访问
count
变量 - 即使
createCounter
函数已经执行完毕,返回的函数仍然可以访问和修改count
变量 count
变量对外部代码是"私有"的,只能通过闭包来访问
多个闭包共享相同的词法环境
function createFunctions() {
let count = 0;
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
},
getCount: function() {
return count;
}
};
}
const counter = createFunctions();
console.log(counter.increment()); // 输出: 1
console.log(counter.increment()); // 输出: 2
console.log(counter.decrement()); // 输出: 1
console.log(counter.getCount()); // 输出: 1
每个闭包都有自己的词法环境
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter1 = createCounter();
const counter2 = createCounter();
console.log(counter1()); // 输出: 1
console.log(counter1()); // 输出: 2
console.log(counter2()); // 输出: 1(counter2有自己的count变量)
console.log(counter1()); // 输出: 3
console.log(counter2()); // 输出: 2
闭包的实际应用
1. 数据封装和私有变量
闭包可以用来创建私有变量和方法,实现数据封装:
function createPerson(name, age) {
// 私有变量
let _name = name;
let _age = age;
// 返回公共API
return {
getName: function() {
return _name;
},
getAge: function() {
return _age;
},
setName: function(newName) {
if (typeof newName === 'string' && newName.length > 0) {
_name = newName;
}
},
setAge: function(newAge) {
if (typeof newAge === 'number' && newAge > 0) {
_age = newAge;
}
}
};
}
const person = createPerson('张三', 30);
console.log(person.getName()); // 输出: 张三
person.setName('李四');
console.log(person.getName()); // 输出: 李四
// console.log(person._name); // 无法直接访问私有变量
2. 函数工厂
闭包可以用来创建特定功能的函数:
function multiply(factor) {
return function(number) {
return number * factor;
};
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 输出: 10
console.log(triple(5)); // 输出: 15
3. 回调函数中保存状态
闭包在异步编程中特别有用,可以在回调函数中保存状态:
function loadData(url) {
const data = {}; // 闭包中的状态
return function(callback) {
if (data.result) {
// 已缓存的数据
callback(data.result);
} else {
// 模拟异步请求
setTimeout(function() {
data.result = `来自${url}的数据`;
callback(data.result);
}, 1000);
}
};
}
const getUser = loadData('https://api.example.com/user');
// 第一次调用会"加载"数据
getUser(function(result) {
console.log(result); // 1秒后输出: 来自https://api.example.com/user的数据
// 第二次调用使用缓存的数据
getUser(function(cachedResult) {
console.log(cachedResult); // 立即输出: 来自https://api.example.com/user的数据
});
});
4. 实现模块模式
闭包可以用来创建模块,隐藏内部实现细节:
const calculator = (function() {
// 私有变量和函数
let result = 0;
function validateNumber(num) {
return typeof num === 'number' && !isNaN(num);
}
// 公共API
return {
add: function(num) {
if (validateNumber(num)) {
result += num;
}
return this; // 支持链式调用
},
subtract: function(num) {
if (validateNumber(num)) {
result -= num;
}
return this;
},
multiply: function(num) {
if (validateNumber(num)) {
result *= num;
}
return this;
},
divide: function(num) {
if (validateNumber(num) && num !== 0) {
result /= num;
}
return this;
},
getResult: function() {
return result;
},
reset: function() {
result = 0;
return this;
}
};
})();
console.log(calculator.add(5).multiply(2).subtract(3).getResult()); // 输出: 7
calculator.reset();
console.log(calculator.getResult()); // 输出: 0
闭包的注意事项
1. 内存管理
闭包会保持对外部变量的引用,可能导致内存泄漏:
function createLargeArray() {
const largeArray = new Array(1000000).fill('大数组');
return function() {
// 即使只使用了数组的长度,整个数组仍然被保留在内存中
return largeArray.length;
};
}
const getArrayLength = createLargeArray(); // largeArray会一直存在于内存中
console.log(getArrayLength()); // 输出: 1000000
解决方法是在不需要闭包时解除引用:
let getArrayLength = createLargeArray();
console.log(getArrayLength()); // 输出: 1000000
// 使用完毕后解除引用
getArrayLength = null; // 允许垃圾回收器回收内存
2. 循环中创建闭包
在循环中创建闭包时,需要注意变量捕获问题:
// 错误示例:所有闭包共享同一个变量i
function createFunctions() {
var functions = [];
for (var i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}
return functions;
}
const functions = createFunctions();
functions[0](); // 输出: 3
functions[1](); // 输出: 3
functions[2](); // 输出: 3
解决方法:
- 使用立即执行函数表达式(IIFE)创建新的作用域:
function createFunctions() {
var functions = [];
for (var i = 0; i < 3; i++) {
functions.push(
(function(value) {
return function() {
console.log(value);
};
})(i)
);
}
return functions;
}
const functions = createFunctions();
functions[0](); // 输出: 0
functions[1](); // 输出: 1
functions[2](); // 输出: 2
- 使用ES6的
let
关键字(推荐):
function createFunctions() {
const functions = [];
for (let i = 0; i < 3; i++) {
functions.push(function() {
console.log(i);
});
}
return functions;
}
const functions = createFunctions();
functions[0](); // 输出: 0
functions[1](); // 输出: 1
functions[2](); // 输出: 2
3. this绑定问题
闭包不会自动绑定this
值,这可能导致意外行为:
const person = {
name: '张三',
greet: function() {
setTimeout(function() {
console.log(`你好,我是${this.name}`); // this不指向person
}, 1000);
}
};
person.greet(); // 输出: 你好,我是undefined
解决方法:
- 使用变量保存
this
引用:
const person = {
name: '张三',
greet: function() {
const self = this; // 保存this引用
setTimeout(function() {
console.log(`你好,我是${self.name}`);
}, 1000);
}
};
person.greet(); // 输出: 你好,我是张三
- 使用
bind
方法:
const person = {
name: '张三',
greet: function() {
setTimeout(function() {
console.log(`你好,我是${this.name}`);
}.bind(this), 1000);
}
};
person.greet(); // 输出: 你好,我是张三
- 使用箭头函数(推荐):
const person = {
name: '张三',
greet: function() {
setTimeout(() => {
console.log(`你好,我是${this.name}`);
}, 1000);
}
};
person.greet(); // 输出: 你好,我是张三
高级闭包模式
1. 柯里化(Currying)
柯里化是将一个接受多个参数的函数转换为一系列接受单个参数的函数的技术:
// 普通函数
function add(a, b, c) {
return a + b + c;
}
// 柯里化版本
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
console.log(add(1, 2, 3)); // 输出: 6
console.log(curriedAdd(1)(2)(3)); // 输出: 6
柯里化的实际应用:
// 创建一个通用的柯里化函数
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
function formatMessage(template, name, date) {
return template.replace('{name}', name).replace('{date}', date);
}
const curriedFormat = curry(formatMessage);
// 创建特定模板的格式化函数
const welcomeTemplate = '欢迎{name}!今天是{date}。';
const formatWelcome = curriedFormat(welcomeTemplate);
// 创建特定用户的欢迎函数
const welcomeUser = formatWelcome('张三');
// 使用最终函数
console.log(welcomeUser('2023年5月20日')); // 输出: 欢迎张三!今天是2023年5月20日。
2. 记忆化(Memoization)
记忆化是一种优化技术,通过缓存函数调用结果来避免重复计算:
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
console.log('从缓存中获取结果');
return cache[key];
}
console.log('计算新结果');
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
// 斐波那契数列函数(未优化)
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 记忆化版本
const memoizedFibonacci = memoize(function(n) {
if (n <= 1) return n;
return memoizedFibonacci(n - 1) + memoizedFibonacci(n - 2);
});
console.time('未优化');
console.log(fibonacci(30)); // 非常慢
console.timeEnd('未优化');
console.time('记忆化');
console.log(memoizedFibonacci(30)); // 非常快
console.timeEnd('记忆化');
3. 函数组合(Function Composition)
函数组合是将多个函数组合成一个函数的过程:
function compose(...fns) {
return function(x) {
return fns.reduceRight((value, fn) => fn(value), x);
};
}
const double = x => x * 2;
const increment = x => x + 1;
const square = x => x * x;
// (3 * 2 + 1)² = 49
const composed = compose(square, increment, double);
console.log(composed(3)); // 输出: 49
4. 部分应用(Partial Application)
部分应用是固定一个函数的一些参数,然后产生另一个更小元的函数:
function partial(fn, ...args) {
return function(...moreArgs) {
return fn(...args, ...moreArgs);
};
}
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = partial(greet, '你好');
const sayGoodbye = partial(greet, '再见');
console.log(sayHello('张三')); // 输出: 你好, 张三!
console.log(sayGoodbye('李四')); // 输出: 再见, 李四!
作用域和闭包的最佳实践
1. 避免全局变量
尽量避免使用全局变量,以减少命名冲突和意外修改:
// 不好的做法
var count = 0;
function increment() {
count++;
}
// 更好的做法
const counter = (function() {
let count = 0;
return {
increment: function() {
count++;
return count;
}
};
})();
2. 使用立即执行函数表达式(IIFE)创建私有作用域
// 创建私有作用域
(function() {
// 这里的变量对外部不可见
const privateVar = '私有变量';
function privateFunction() {
console.log(privateVar);
}
// 可以访问全局对象
window.publicFunction = function() {
privateFunction();
};
})();
// publicFunction(); // 输出: 私有变量
// console.log(privateVar); // 错误: privateVar is not defined
3. 使用块级作用域和const/let代替var
// 不好的做法
function example() {
var x = 1;
if (true) {
var x = 2; // 覆盖外部的x
console.log(x); // 2
}
console.log(x); // 2
}
// 更好的做法
function example() {
let x = 1;
if (true) {
let x = 2; // 不会覆盖外部的x
console.log(x); // 2
}
console.log(x); // 1
}
4. 小心闭包中的循环引用
function setupEvents() {
const element = document.getElementById('button');
element.addEventListener('click', function() {
console.log('按钮被点击了');
});
// 不再需要时,移除事件监听器以避免内存泄漏
function cleanup() {
element.removeEventListener('click', handler);
element = null; // 解除引用
}
}
5. 使用模块模式组织代码
const myModule = (function() {
// 私有变量和函数
let privateVar = '私有';
function privateFunction() {
return privateVar;
}
// 公共API
return {
publicVar: '公共',
publicFunction: function() {
return privateFunction() + ' 和 ' + this.publicVar;
}
};
})();
console.log(myModule.publicFunction()); // 输出: 私有 和 公共
// console.log(myModule.privateVar); // undefined
总结
JavaScript的作用域和闭包是该语言中最强大也最容易误解的特性之一。理解这些概念对于编写高效、可维护的JavaScript代码至关重要。
作用域决定了变量的可见性和生命周期:
- 全局作用域中的变量在整个程序中可见
- 函数作用域中的变量只在函数内部可见
- 块级作用域(使用
let
和const
)中的变量只在块内部可见
闭包是函数与其词法环境的组合,它允许函数访问其定义时的作用域,即使在其他地方调用。闭包的主要应用包括:
- 数据封装和私有变量
- 函数工厂和高阶函数
- 回调函数中保存状态
- 实现模块模式
通过掌握作用域和闭包,你可以更好地理解JavaScript的工作原理,编写更加简洁、高效和可维护的代码。
练习
- 创建一个计数器函数,每次调用时返回递增的数字,并提供重置功能
- 实现一个函数,可以限制另一个函数在指定时间内只能调用一次(防抖函数)
- 编写一个缓存函数,可以记住之前计算的结果(记忆化)
- 创建一个私有变量系统,只能通过特定方法访问和修改
- 实现一个模块,包含私有方法和公共API