Skip to content

作用域与闭包

一、作用域 (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();
}

二、执行上下文

代码执行前创建执行上下文,包含:

  1. 变量环境: var 声明、函数声明
  2. 词法环境: let/const 声明、块级作用域
  3. 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 区别?

特性varletconst
作用域函数块级块级
提升TDZTDZ
重复声明可以不可以不可以
修改可以可以不可以

Q2: 什么是闭包?有什么用?

闭包是引用了外部函数变量的内部函数。用于数据隐私、柯里化、防抖节流。

Q3: 箭头函数和普通函数的区别?

  1. 没有自己的 this,继承外层
  2. 没有 arguments
  3. 不能作为构造函数
  4. 没有 prototype

前端面试知识库