函数、作用域与闭包
函数声明、箭头函数、作用域链与闭包原理。
1. 函数定义 (Definitions)
1.1 函数声明
语法:
function functionName(parameters) {
// 函数体
return value; // 可选
}
特点:
- 存在函数提升(hoisting),可以在声明前调用
- 函数名是必需的 示例:
// 可以在声明前调用
console.log(add(2, 3)); // 输出: 5
function add(a, b) {
return a + b;
}
1.2 函数表达式
语法:
const functionName = function (parameters) {
// 函数体
return value; // 可选
};
特点:
- 不存在函数提升,不能在声明前调用
- 可以是匿名函数
- 可以作为变量赋值 示例:
// 不能在声明前调用,会报错
// console.log(subtract(5, 2)); // 错误: subtract is not defined
const subtract = function (a, b) {
return a - b;
};
console.log(subtract(5, 2)); // 输出: 3
1.3 箭头函数 (ES6+)
语法:
// 基本语法
const functionName = (parameters) => {
// 函数体
return value;
};
// 单个参数可以省略括号
const double = (num) => {
return num * 2;
};
// 单个表达式可以省略花括号和 return
const triple = (num) => num * 3;
// 无参数
const greet = () => console.log('Hello!');
// 多个参数
const multiply = (a, b) => a * b;
特点:
- 不绑定
this,继承父级作用域的this - 不能作为构造函数使用
- 没有
arguments对象 - 语法简洁,适合作为回调函数 示例:
// 箭头函数作为回调
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map((num) => num * 2);
console.log(doubled); // 输出: [2, 4, 6, 8, 10]
// 箭头函数与 this
const obj = {
name: '张三',
regularFunction: function () {
console.log(this.name); // 输出: 张三
},
arrowFunction: () => {
console.log(this.name); // 输出: undefined (继承全局作用域的 this)
},
};
obj.regularFunction();
obj.arrowFunction();
1.4 函数参数
1.4.1 默认参数 (ES6+)
function greet(name = '世界') {
return `Hello, ${name}!`;
}
console.log(greet()); // 输出: Hello, 世界!
console.log(greet('张三')); // 输出: Hello, 张三!
1.4.2 剩余参数 (ES6+)
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 输出: 15
1.4.3 解构参数 (ES6+)
function printUser({ name, age }) {
console.log(`Name: ${name}, Age: ${age}`);
}
const user = { name: '张三', age: 30 };
printUser(user); // 输出: Name: 张三, Age: 30
2. 作用域 (Scope)
2.1 全局作用域
定义:在所有函数外部定义的变量,在整个脚本中都可以访问。 示例:
const globalVar = '全局变量';
function test() {
console.log(globalVar); // 可以访问全局变量
}
test(); // 输出: 全局变量
console.log(globalVar); // 可以访问全局变量
2.2 函数作用域
定义:在函数内部定义的变量,只能在函数内部访问。 示例:
function test() {
const localVar = '局部变量';
console.log(localVar); // 可以访问局部变量
}
test(); // 输出: 局部变量
// console.log(localVar); // 错误: localVar is not defined
2.3 块级作用域 (ES6+)
定义:使用 let 或 const 在 {} 内部定义的变量,只能在块内部访问。
示例:
if (true) {
let blockVar = '块级变量';
const constBlockVar = '常量块级变量';
console.log(blockVar); // 可以访问
console.log(constBlockVar); // 可以访问
}
// console.log(blockVar); // 错误: blockVar is not defined
// console.log(constBlockVar); // 错误: constBlockVar is not defined
2.4 作用域链
定义:当访问一个变量时,JavaScript 会从当前作用域开始查找,如果找不到,就会向上一级作用域查找,直到找到或到达全局作用域。 示例:
const globalVar = '全局变量';
function outer() {
const outerVar = '外部函数变量';
function inner() {
const innerVar = '内部函数变量';
console.log(innerVar); // 内部函数变量
console.log(outerVar); // 外部函数变量
console.log(globalVar); // 全局变量
}
inner();
// console.log(innerVar); // 错误: innerVar is not defined
}
outer();
// console.log(outerVar); // 错误: outerVar is not defined
3. 闭包 (Closures)
3.1 闭包的概念
定义:闭包是函数与其绑定的词法作用域的组合。简单来说,闭包是一个函数,它可以访问其创建时所在的作用域,即使该函数在其他作用域中被调用。 示例:
function createCounter() {
let count = 0; // 私有变量
return function () {
return ++count;
};
}
const counter = createCounter();
console.log(counter()); // 输出: 1
console.log(counter()); // 输出: 2
console.log(counter()); // 输出: 3
3.2 闭包的作用
- 私有化变量:通过闭包可以创建私有变量,外部无法直接访问。
- 数据持久化:闭包可以保存函数创建时的环境,使变量的值在多次调用之间保持。
- 模拟模块:使用闭包可以创建模块化的代码,避免全局变量污染。
3.3 闭包的应用场景
3.3.1 计数器
function createCounter(initialValue = 0) {
let count = initialValue;
return {
increment: function () {
return ++count;
},
decrement: function () {
return --count;
},
reset: function () {
count = initialValue;
return count;
},
getCount: function () {
return count;
},
};
}
const counter = createCounter(10);
console.log(counter.increment()); // 输出: 11
console.log(counter.increment()); // 输出: 12
console.log(counter.decrement()); // 输出: 11
console.log(counter.reset()); // 输出: 10
console.log(counter.getCount()); // 输出: 10
3.3.2 模块模式
const mathModule = (function () {
// 私有变量
const PI = 3.14159;
// 私有函数
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
// 暴露公共接口
return {
add: add,
subtract: subtract,
getPI: function () {
return PI;
},
};
})();
console.log(mathModule.add(5, 3)); // 输出: 8
console.log(mathModule.subtract(5, 3)); // 输出: 2
console.log(mathModule.getPI()); // 输出: 3.14159
// console.log(mathModule.PI); // 输出: undefined (私有变量)
3.3.3 事件处理
function createButtonClickHandler() {
let clickCount = 0;
return function () {
clickCount++;
console.log(`按钮被点击了 ${clickCount} 次`);
};
}
const button = document.createElement('button');
button.textContent = '点击我';
document.body.appendChild(button);
button.addEventListener('click', createButtonClickHandler());
3.4 闭包的注意事项
- 内存泄漏:闭包会持有对外部变量的引用,导致这些变量无法被垃圾回收。如果闭包被长时间保存,可能会导致内存泄漏。
- 性能影响:闭包会增加内存使用,并且在访问外部变量时需要通过作用域链查找,可能会影响性能。
- 变量共享:在循环中创建闭包时,要注意变量共享的问题。 示例:
// 问题代码
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // 输出: 5, 5, 5, 5, 5
}, 1000);
}
// 解决方案 1: 使用 let
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i); // 输出: 0, 1, 2, 3, 4
}, 1000);
}
// 解决方案 2: 使用立即执行函数表达式 (IIFE)
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j); // 输出: 0, 1, 2, 3, 4
}, 1000);
})(i);
}
4. this 指向机制
4.1 普通函数中的 this
规则:普通函数中的 this 指向调用者,如果没有调用者,则指向全局对象(在严格模式下指向 undefined)。
示例:
function test() {
console.log(this);
}
// 直接调用,this 指向全局对象 (window)
test();
// 作为对象方法调用,this 指向对象
const obj = {
name: '张三',
test: test,
};
obj.test(); // this 指向 obj
// 使用 call 或 apply 显式绑定 this
const anotherObj = { name: '李四' };
test.call(anotherObj); // this 指向 anotherObj
4.2 箭头函数中的 this
规则:箭头函数不绑定自己的 this,而是继承父级作用域的 this。
示例:
const obj = {
name: '张三',
regularFunction: function () {
console.log(this.name); // 输出: 张三
// 普通函数,this 指向全局对象
setTimeout(function () {
console.log(this.name); // 输出: undefined
}, 1000);
// 箭头函数,继承父级作用域的 this
setTimeout(() => {
console.log(this.name); // 输出: 张三
}, 1000);
},
};
obj.regularFunction();
4.3 构造函数中的 this
规则:构造函数中的 this 指向新创建的实例。
示例:
function Person(name, age) {
this.name = name;
this.age = age;
this.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
}
const person = new Person('张三', 30);
console.log(person.name); // 输出: 张三
person.greet(); // 输出: Hello, my name is 张三
4.4 显式绑定 this
4.4.1 call() 方法
语法:function.call(thisArg, arg1, arg2, ...)
示例:
function greet(greeting) {
console.log(`${greeting}, ${this.name}!`);
}
const person = { name: '张三' };
greet.call(person, 'Hello'); // 输出: Hello, 张三!
4.4.2 apply() 方法
语法:function.apply(thisArg, [argsArray])
示例:
function sum(a, b, c) {
return a + b + c;
}
const numbers = [1, 2, 3];
const result = sum.apply(null, numbers);
console.log(result); // 输出: 6
4.4.3 bind() 方法
语法:function.bind(thisArg, arg1, arg2, ...)
示例:
function greet() {
console.log(`Hello, ${this.name}!`);
}
const person = { name: '张三' };
const boundGreet = greet.bind(person);
boundGreet(); // 输出: Hello, 张三!
5. 最佳实践
5.1 函数使用最佳实践
- 优先使用箭头函数:对于简短的函数,尤其是回调函数,优先使用箭头函数。
- 使用默认参数:对于有默认值的参数,使用 ES6 的默认参数语法。
- 使用剩余参数:对于不确定数量的参数,使用剩余参数语法。
- 使用解构参数:对于对象或数组参数,使用解构语法可以使代码更清晰。
5.2 作用域最佳实践
- 使用 let 和 const:优先使用
let和const而不是var,以避免变量提升和全局变量污染。 - 最小化作用域:变量的作用域应该尽可能小,只在需要的地方定义。
- 避免全局变量:尽量避免使用全局变量,使用模块化的方式组织代码。
5.3 闭包最佳实践
- 合理使用闭包:只在需要时使用闭包,避免不必要的闭包。
- 注意内存泄漏:如果闭包被长时间保存,要确保其中引用的变量不会导致内存泄漏。
- 避免循环中的闭包问题:在循环中创建闭包时,要注意变量共享的问题,使用
let或 IIFE 来解决。
5.4 this 指向最佳实践
- 理解 this 的指向:要清楚不同情况下
this的指向规则。 - 使用箭头函数:在需要继承父级作用域
this的场景中,使用箭头函数。 - 使用 bind():在需要固定
this指向的场景中,使用bind()方法。 - 避免混合使用普通函数和箭头函数:在对象方法中,要注意普通函数和箭头函数的
this指向差异。
6. 实际应用示例
6.1 示例 1:防抖函数
function debounce(func, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 使用示例
const debouncedSearch = debounce(function (query) {
console.log('搜索:', query);
// 实际的搜索逻辑
}, 300);
// 输入时会防抖,只有停止输入 300ms 后才会执行搜索
debouncedSearch('JavaScript');
debouncedSearch('JavaScript 闭包');
debouncedSearch('JavaScript 闭包 this');
// 最终只会执行一次: 搜索: JavaScript 闭包 this
6.2 示例 2:节流函数
function throttle(func, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// 使用示例
const throttledScroll = throttle(function () {
console.log('滚动事件触发');
// 实际的滚动处理逻辑
}, 1000);
// 滚动时会节流,每 1000ms 最多执行一次
window.addEventListener('scroll', throttledScroll);
6.3 示例 3:模块化代码
// 模块定义
const calculator = (function () {
// 私有方法
function validateNumber(num) {
return typeof num === 'number' && !isNaN(num);
}
// 公共接口
return {
add: function (a, b) {
if (validateNumber(a) && validateNumber(b)) {
return a + b;
}
return NaN;
},
subtract: function (a, b) {
if (validateNumber(a) && validateNumber(b)) {
return a - b;
}
return NaN;
},
multiply: function (a, b) {
if (validateNumber(a) && validateNumber(b)) {
return a * b;
}
return NaN;
},
divide: function (a, b) {
if (validateNumber(a) && validateNumber(b) && b !== 0) {
return a / b;
}
return NaN;
},
};
})();
// 使用示例
console.log(calculator.add(5, 3)); // 输出: 8
console.log(calculator.subtract(5, 3)); // 输出: 2
console.log(calculator.multiply(5, 3)); // 输出: 15
console.log(calculator.divide(5, 3)); // 输出: 1.6666666666666667
console.log(calculator.divide(5, 0)); // 输出: NaN
更新日志 (Changelog)
- 2026-04-05: 深入细化闭包与 This 指向原理。
- 2026-04-05: 扩写内容,增加详细的函数定义、作用域、闭包和 this 指向的概念、示例和最佳实践。