揭秘面试官常见问题 —— JavaScript 闭包

1. 什么是闭包?

闭包是 JavaScript 中的一种特性,它允许一个函数在定义的环境之外仍然能够访问和操作定义时的作用域中的变量。换句话说,闭包是指函数可以“记住”并访问它被创建时所处的词法作用域。

简单来说:

- 当一个函数被嵌套在另一个函数内部时,内部函数可以访问外部函数的变量。

- 即使外部函数已经执行完毕,内部函数仍然可以通过闭包访问这些变量。

1.1 基本概念

1、作用域链:函数内部可以访问外部作用域的变量,原因在于作用域链的存在。作用域链决定了函数在何处寻找变量,通常由当前函数作用域和外层作用域构成。

2、函数嵌套:当一个函数在另一个函数内部定义时,内部函数可以访问外部函数的变量。

举个 🌰

function outerFunction() {
  let outerVariable = 'I am from the outer scope!';
  function innerFunction() {
    console.log(outerVariable);
  }
  return innerFunction;
}

const closureFunction = outerFunction();
closureFunction(); // 输出: "I am from the outer scope!"

例子中, innerFunction 是一个闭包,它获取 outerFunction 中的变量 outerVariable,即使 outerFunction 已经执行完毕,但 innerFunction 依然可以访问 outerVariable。

2. 常见使用场景

2.1 数据封装和私有化

闭包可以用来创建私有变量,这些变量只能通过特定的接口进行访问和修改,这在 JavaScript 中模拟了其他编程语言中的“私有”变量。

举个 🌰

function createCounter() {
  let count = 0;

  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    },
    getCount() {
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 输出: 1
console.log(counter.increment()); // 输出: 2
console.log(counter.getCount());  // 输出: 2
console.log(counter.decrement()); // 输出: 1
2.2 块级作用域

在 ES6 之前,JavaScript 没有块级作用域(let 和 const 引入之前),闭包可以用于模拟块级作用域。

举个 🌰

for (var i = 0; i < 3; i++) {
  (function(i) {
    setTimeout(() => {
      console.log(i);
    }, 1000);
  })(i);
}

说到这个例子,按道理而已,是每一秒输出一个数字,但仔细看看后发现,因为 setTimeout 的回调都是一秒后执行,所以三个 console.log 语句在同一时刻执行,导致它们几乎同时输出。

不符合我们的要求,可以更改一下代码:

for (var i = 0; i < 3; i++) {
  (function (i) {
    setTimeout(() => {
      console.log(i);
    }, i * 1000); // 每个迭代延迟时间增加 i * 1000 毫秒
  })(i);
}
2.3 函数柯里化

闭包是函数柯里化的基础,柯里化是一种将函数拆分为多个函数的技巧,每个函数只接受一部分参数。 

举个 🌰:非常典型

function multiply(a) {
  return function(b) {
    return a * b;
  };
}

const multiplyByTwo = multiply(2);
console.log(multiplyByTwo(5)); // 输出: 10

const multiplyByThree = multiply(3);
console.log(multiplyByThree(5)); // 输出: 15
2.4 延迟执行和回调

闭包常用于回调函数中,尤其是在处理异步操作时。 

举个 🌰

function fetchData(url) {
  setTimeout(() => {
    console.log('Fetching data from ' + url);
  }, 1000);
}

function loadUserData() {
  const url = 'https://api.example.com/user';
  fetchData(url);
}

loadUserData(); // 输出: "Fetching data from https://api.example.com/user"

在 fetchData 函数中,setTimeout 的回调函数在 1 秒 后执行。当这个回调函数被定义时,它捕获了 fetchData 函数的参数 url,因此 url 变量形成了闭包。

3. 潜在问题和解决方法

3.1 内存泄漏

闭包可能导致内存泄漏,因为闭包会使外部函数的变量一直保存在内存中,导致无法被垃圾回收。

解决方法:确保不再需要使用闭包时,手动将闭包变量置为 null,或通过适当的函数生命周期来避免。

举个 🌰

function createClosure() {
  let largeData = new Array(10000).fill('*');
  return function() {
    console.log('Using closure', largeData);
  };
}
let closure = createClosure();
// 使用完成后手动解除引用
closure = null; 
3.2 意外的变量共享

在循环中使用闭包时,所有的闭包都可能引用同一个变量,导致输出不符合预期。

解决方法:使用 let 代替 var,或者使用立即调用的函数表达式(IIFE)来创建每次迭代时的独立作用域。

举个 🌰

for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 1000);
}
// 输出:0, 1, 2

4. 隐藏的知识点

4.1 闭包和性能

闭包会保留整个作用域链中所有变量,而不仅仅是闭包中使用的变量。这可能导致占用更多的内存。

举个 🌰

function outerFunction() {
  let a = 1;
  let b = 2;
  let c = 3;

  return function innerFunction() {
    console.log(a); // 虽然只使用了 a,但 b 和 c 也会被保留
  };
}
4.2 闭包和垃圾回收

由于闭包引用的变量不会被立即回收,可能导致内存无法释放。

4.3 闭包和 this 关键字

闭包不会绑定 this,它只会捕获外部函数的作用域。如果需要访问当前对象的 this,可以使用 bind()、call()、apply() 或箭头函数。

举个 🌰

function Counter() {
  this.count = 0;

  setTimeout(() => {
    this.count++;
    console.log(this.count);
  }, 1000);
}

const counter = new Counter(); // 输出:1

既然引出了改变 this 指向的方法,那就简单介绍一下。

假设我们有个对象 Person,其中包含一个方法 greet,我们希望在内部函数中访问当前对象的 this。

1、使用 bind()

bind() 方法可以创建一个新函数,该函数在调用时会绑定特定的 this 值。

举个 🌰,如果不进行绑定 this 的话,获取的值为空。

function Person(name) {
  this.name = name;
  
  this.greet = function() {
    setTimeout(function() {
      console.log('Hello, ' + this.name); // this 在这里指向全局对象(在浏览器中是 window)
    }.bind(this), 1000); // 使用 bind() 绑定 this 到当前对象
  };
}

const person = new Person('Alice');
person.greet(); // 输出: "Hello, Alice"

在这个代码中,bind(this) 保证 setTimeout 的回调函数中的 this 仍然指向 Person 实例。

2、使用 call()

call() 方法调用一个函数,并且显式地设置 this 值。它的第一个参数是 this 的值,其余参数是传递给函数的参数。

举个 🌰

function greet() {
  console.log('Hello, ' + this.name);
}

const person = {
  name: 'Bob'
};

greet.call(person); // 输出: "Hello, Bob"

call() 方法直接调用 greet 函数,并将 this 绑定到 person 对象。

3、使用 apply()

apply() 方法与 call() 类似,但它接受参数数组。

举个 🌰

function greet(arg) {
  console.log(arg+ ', ' + this.name);
}

const person = {
  name: 'Charlie'
};

greet.apply(person, ['Hi']); // 输出: "Hi, Charlie"

这里 apply() 用于将 greet 函数的 this 绑定到 person 对象,并传递一个参数数组。

4、使用箭头函数

箭头函数不会创建自己的 this,而是从外部作用域继承 this,不需要显式绑定。

举个 🌰

function Person(name) {
  this.name = name;
  
  this.greet = function() {
    setTimeout(() => {
      console.log('Hello, ' + this.name); // 箭头函数的 this 继承自外部作用域,即 Person 实例
    }, 1000);
  };
}

const person = new Person('Diana');
person.greet(); // 输出: "Hello, Diana"

这里,箭头函数的 this 自动绑定到 Person 实例,因为箭头函数没有自己的 this。

5. 面试题回答

什么是闭包?

你怎么理解 JS 中的闭包?

闭包导致什么问题?

1、闭包是 JS 的一种特性,它允许一个函数访问和操作其词法作用域中的变量,即使这个函数在其原始作用域之外执行。闭包的关键点在于,它能够记住并访问外部函数作用域中的变量。

2、闭包的作用在于持久化函数作用域,即使这个变量的作用域已经结束。常用于回调函数(比如:setTimeout)、创建一组共享某些外部变量的函数(比如:计数器)、数据封装(私有化变量和方法)等。

3、当闭包长期持有外部作用域变量的引用时,会影响垃圾回收机制清理,导致内存泄露;当大量闭包在高频率函数调用中创建时,会导致性能下降;当使用时不注意变量的作用域和生命周期,可能导致多个闭包意外共享同一个变量,产生意料之外的结果。

其余内容可以根据上文进行概括,关键是理解含义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值