返回面试题列表

JavaScript闭包详解

JavaScript闭包详解

什么是闭包?

闭包(Closure)是JavaScript中的一个重要概念。简单来说,闭包是指有权访问另一个函数作用域中变量的函数。更具体地说,闭包是由函数以及创建该函数的词法环境组合而成,这个环境包含了这个闭包创建时所能访问的所有局部变量。

闭包的形成

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行的。

function createCounter() {
  let count = 0; // 私有变量
  
  return function() {
    count++; // 访问外部函数的变量
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2

在上面的例子中,counter是一个闭包,它"记住了"它被创建时的环境(包含count变量)。

闭包的特性

  1. 访问外部函数作用域:闭包可以访问外部函数的变量,即使外部函数已经返回。

  2. 保持状态:闭包可以"记住"它被创建时的环境。

  3. 封装私有变量:闭包可以用来创建私有变量,外部无法直接访问。

闭包的实际应用

1. 数据私有化

function createPerson(name) {
  let age = 0;
  
  return {
    getName: function() {
      return name;
    },
    getAge: function() {
      return age;
    },
    setAge: function(newAge) {
      if (newAge > 0) {
        age = newAge;
      }
    }
  };
}

const person = createPerson('张三');
console.log(person.getName()); // 张三
person.setAge(25);
console.log(person.getAge()); // 25
console.log(person.age); // undefined,无法直接访问

2. 函数工厂

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

const add5 = makeAdder(5);
const add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

3. 模块模式

const calculator = (function() {
  let result = 0;
  
  return {
    add: function(x) {
      result += x;
      return this;
    },
    subtract: function(x) {
      result -= x;
      return this;
    },
    multiply: function(x) {
      result *= x;
      return this;
    },
    getResult: function() {
      return result;
    }
  };
})();

calculator.add(5).multiply(2).subtract(3);
console.log(calculator.getResult()); // 7

闭包的注意事项

1. 内存泄漏

闭包会保持对外部变量的引用,如果不小心,可能导致内存泄漏。

function createLargeArray() {
  let largeArray = new Array(1000000).fill('大数组');
  
  return function() {
    console.log(largeArray[0]); // 持有对大数组的引用
  };
}

const printArray = createLargeArray(); // largeArray 无法被垃圾回收
// ...使用后
printArray = null; // 手动释放引用

2. 循环中创建闭包的问题

// 常见错误
function createFunctions() {
  var result = [];
  for (var i = 0; i < 3; i++) {
    result.push(function() {
      console.log(i); // 所有函数都会打印 3
    });
  }
  return result;
}

// 正确做法
function createFunctions() {
  var result = [];
  for (var i = 0; i < 3; i++) {
    result.push(
      (function(num) {
        return function() {
          console.log(num); // 0, 1, 2
        };
      })(i)
    );
  }
  return result;
}

// 使用 let (ES6)
function createFunctions() {
  var result = [];
  for (let i = 0; i < 3; i++) {
    result.push(function() {
      console.log(i); // 0, 1, 2 (let 创建块级作用域)
    });
  }
  return result;
}

常见面试题

题目1:下面的代码输出什么?

for (var i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

答案:连续输出五次 6。

原因:setTimeout 中的函数形成了闭包,但是它们都引用同一个变量 i。当 setTimeout 中的函数执行时,循环已经结束,i 的值已经变成了 6。

修复方案

// 使用IIFE
for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);
    }, 1000);
  })(i);
}

// 使用let
for (let i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

题目2:实现一个缓存函数

function memoize(fn) {
  const cache = {};
  
  return function(...args) {
    const key = JSON.stringify(args);
    if (!cache[key]) {
      cache[key] = fn.apply(this, args);
    }
    return cache[key];
  };
}

// 使用
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

const memoizedFib = memoize(fibonacci);
console.time('未缓存');
console.log(fibonacci(35));
console.timeEnd('未缓存');

console.time('已缓存');
console.log(memoizedFib(35));
console.timeEnd('已缓存');

总结

闭包是JavaScript中非常强大的特性,理解闭包对于深入学习JavaScript非常重要。闭包主要用于数据隐藏和封装,创建私有变量,以及在特定场景下保持状态。但使用不当也可能导致内存泄漏等问题,需要谨慎使用。在面试中,闭包相关的问题是检验候选人JavaScript基础是否扎实的常见题目。