作用域与闭包
一、作用域 (Scope)
作用域定义了变量的可访问范围。JavaScript 采用词法作用域 (Lexical Scope),即函数的作用域在函数定义时就决定了。
作用域类型
| 类型 | 创建方式 | 特点 |
|---|---|---|
| 全局 | 顶层代码 | 整个程序可访问 |
| 函数 | function | 函数内部可访问 |
| 块级 | let/const + {} | ES6 引入 |
javascript
var a = 1; // 全局
function foo() {
var b = 2; // 函数作用域
if (true) {
let c = 3; // 块级作用域
const d = 4; // 块级作用域
}
console.log(c); // ReferenceError
}作用域链
javascript
const global = 'global';
function outer() {
const outer = 'outer';
function inner() {
const inner = 'inner';
console.log(global, outer, inner); // 沿作用域链查找
}
inner();
}二、执行上下文
代码执行前创建执行上下文,包含:
- 变量环境: var 声明、函数声明
- 词法环境: let/const 声明、块级作用域
- this 绑定
变量提升
javascript
console.log(a); // undefined (var 提升)
console.log(b); // ReferenceError (暂时性死区)
var a = 1;
let b = 2;
// 函数声明整体提升
foo(); // works
function foo() {}
// 函数表达式只提升变量
bar(); // TypeError: bar is not a function
var bar = function() {};三、闭包 (Closure)
定义
闭包是指有权访问另一个函数作用域中变量的函数。即使外部函数已返回,内部函数引用的外部变量依然保存在内存中。
javascript
function createCounter() {
let count = 0; // 被闭包引用
return {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; }
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getCount(); // 2
// count 变量被保留在内存中经典面试题
javascript
// 问题:输出什么?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3, 3, 3
// 解决方案 1:使用 let
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:0, 1, 2
// 解决方案 2:IIFE 创建闭包
for (var i = 0; i < 3; i++) {
((j) => {
setTimeout(() => console.log(j), 100);
})(i);
}
// 解决方案 3:setTimeout 第三个参数
for (var i = 0; i < 3; i++) {
setTimeout((j) => console.log(j), 100, i);
}应用场景
javascript
// 1. 数据隐私 (模块模式)
const module = (function() {
let privateVar = 0;
return {
getPrivate: () => privateVar,
setPrivate: (val) => { privateVar = val; }
};
})();
// 2. 柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...more) => curried(...args, ...more);
};
}
const add = curry((a, b, c) => a + b + c);
add(1)(2)(3); // 6
add(1, 2)(3); // 6
// 3. 防抖
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 4. 节流
function throttle(fn, delay) {
let last = 0;
return function(...args) {
const now = Date.now();
if (now - last >= delay) {
last = now;
fn.apply(this, args);
}
};
}内存泄漏
javascript
// 闭包导致的内存泄漏
function leaky() {
const largeData = new Array(1000000).fill('x');
return function() {
// 即使不使用 largeData,它也不会被回收
console.log('closure');
};
}
// 解决方案:手动解除引用
function notLeaky() {
let largeData = new Array(1000000).fill('x');
const result = processData(largeData);
largeData = null; // 解除引用
return function() {
console.log(result);
};
}四、this 指向
| 调用方式 | this 指向 |
|---|---|
| 默认绑定 | 全局对象 (严格模式 undefined) |
| 隐式绑定 | 调用对象 (obj.method()) |
| 显式绑定 | call/apply/bind 指定 |
| new 绑定 | 新创建的实例 |
| 箭头函数 | 继承外层 this |
javascript
const obj = {
name: 'obj',
regular: function() {
console.log(this.name);
},
arrow: () => {
console.log(this.name);
}
};
obj.regular(); // 'obj'
obj.arrow(); // undefined (继承外层)
const fn = obj.regular;
fn(); // undefined (默认绑定)
// call/apply/bind
fn.call({ name: 'custom' }); // 'custom'
const bound = fn.bind({ name: 'bound' });
bound(); // 'bound'面试高频题
Q1: var/let/const 区别?
| 特性 | var | let | const |
|---|---|---|---|
| 作用域 | 函数 | 块级 | 块级 |
| 提升 | 是 | TDZ | TDZ |
| 重复声明 | 可以 | 不可以 | 不可以 |
| 修改 | 可以 | 可以 | 不可以 |
Q2: 什么是闭包?有什么用?
闭包是引用了外部函数变量的内部函数。用于数据隐私、柯里化、防抖节流。
Q3: 箭头函数和普通函数的区别?
- 没有自己的 this,继承外层
- 没有 arguments
- 不能作为构造函数
- 没有 prototype