Skip to content

闭包

一: 什么是闭包?

闭包(Closure)是指在 JavaScript 中,一个函数能够访问其词法作用域之外的变量。具体来说,当一个函数在定义时包含对其外部作用域变量的引用,并且该函数在这个外部作用域之外被调用时,就形成了闭包。

闭包的特点:

  1. 访问外部变量:

    • 闭包内的函数可以访问外部函数的局部变量,即使外部函数已经执行完毕。
  2. 保留词法作用域:

    • 闭包可以保留其被创建时所处的词法作用域,使得在之后的调用中仍能访问该作用域中的变量。

示例:

javascript
function outerFunction() {
  let outerVariable = 'I am from outer function';

  function innerFunction() {
    console.log(outerVariable);
  }

  return innerFunction;
}

// 创建闭包
const closure = outerFunction();

// 调用闭包
closure(); // 输出:I am from outer function

在这个例子中,innerFunction 是一个闭包,它可以访问外部函数 outerFunction 中的 outerVariable。即使 outerFunction 已经执行完毕,闭包 innerFunction 仍然能够访问并引用 outerVariable

闭包的用途:

  1. 数据封装:

    • 通过闭包可以创建私有变量,实现数据的封装,防止外部直接访问或修改。
    javascript
    function createCounter() {
      let count = 0;
    
      return function() {
        return ++count;
      };
    }
    
    const counter = createCounter();
    console.log(counter()); // 输出:1
    console.log(counter()); // 输出:2
  2. 模块化开发:

    • 闭包可以用于创建模块,将一组相关的功能封装在一个闭包中,防止全局命名冲突。
    javascript
    const myModule = (function() {
      let privateVariable = 'I am private';
    
      function privateFunction() {
        console.log('This is private');
      }
    
      return {
        publicVariable: 'I am public',
        publicFunction: function() {
          console.log('This is public');
        }
      };
    })();
    
    console.log(myModule.publicVariable);
    myModule.publicFunction();
  3. 实现函数柯里化:

    • 闭包可以用于实现函数柯里化,将多参数的函数转化为一系列接受单一参数的函数。
    javascript
    function curry(func) {
      return function curried(...args) {
        if (args.length >= func.length) {
          return func(...args);
        } else {
          return function(...nextArgs) {
            return curried(...args, ...nextArgs);
          };
        }
      };
    }
    
    function add(a, b, c) {
      return a + b + c;
    }
    
    const curriedAdd = curry(add);
    console.log(curriedAdd(1)(2)(3)); // 输出:6

闭包在 JavaScript 中有许多用途,上述例子仅仅是其中一些常见的应用场景。通过使用闭包,可以更灵活、有效地编写代码。

二: 在实际项目中,闭包可用于多种场景

其中一些常见的用法包括:

  1. 私有变量和方法的封装:

    • 使用闭包创建私有变量和方法,确保在外部无法直接访问或修改这些变量,从而实现数据的封装。这有助于防止全局污染和提高代码的安全性。
    javascript
    function createCounter() {
      let count = 0;
    
      return {
        increment: function() {
          return ++count;
        },
        decrement: function() {
          return --count;
        },
        getCount: function() {
          return count;
        }
      };
    }
    
    const counter = createCounter();
    console.log(counter.increment()); // 输出:1
    console.log(counter.getCount());   // 输出:1
  2. 模块化开发:

    • 使用闭包创建模块,将相关的功能封装在一个闭包中,可以减少全局变量的使用,防止命名冲突,提高代码的可维护性。
    javascript
    const myModule = (function() {
      let privateVariable = 'I am private';
    
      function privateFunction() {
        console.log('This is private');
      }
    
      return {
        publicVariable: 'I am public',
        publicFunction: function() {
          console.log('This is public');
          privateFunction(); // 调用私有方法
        }
      };
    })();
    
    console.log(myModule.publicVariable);
    myModule.publicFunction();
  3. 实现函数柯里化:

    • 使用闭包实现函数柯里化,将多参数的函数转化为接受单一参数的一系列函数,提高代码的复用性和灵活性。
    javascript
    function curry(func) {
      return function curried(...args) {
        if (args.length >= func.length) {
          return func(...args);
        } else {
          return function(...nextArgs) {
            return curried(...args, ...nextArgs);
          };
        }
      };
    }
    
    function add(a, b, c) {
      return a + b + c;
    }
    
    const curriedAdd = curry(add);
    console.log(curriedAdd(1)(2)(3)); // 输出:6
  4. 事件处理程序:

    • 使用闭包来处理事件,可以在事件处理程序中访问外部函数的变量,从而避免将变量暴露在全局作用域中。
    javascript
    function setupEventListeners() {
      const button = document.getElementById('myButton');
      let count = 0;
    
      button.addEventListener('click', function() {
        console.log(`Button clicked ${++count} times`);
      });
    }
    
    setupEventListeners();

这些例子展示了在实际项目中如何使用闭包,从而在代码中实现数据封装、模块化开发、函数柯里化和事件处理等方面获得好处。

三: 防抖和节流

防抖(Debounce)和节流(Throttle)是用于控制函数执行频率的两种常见技术。它们在处理一些高频触发的事件(比如滚动、输入等)时非常有用,可以提升性能和用户体验。

防抖(Debounce):

防抖的思想是,当连续触发事件时,只有当一定时间内没有新的事件触发时,才执行函数。如果在这段时间内有新的事件触发,就重新开始计时。

示例:

javascript
function debounce(func, delay) {
  let timeoutId;

  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// 使用防抖
const debouncedFunction = debounce(() => {
  console.log('Debounced function called');
}, 300);

// 在某个高频触发的事件中使用
window.addEventListener('scroll', debouncedFunction);

节流(Throttle):

节流的思想是,无论事件触发频率多高,只在固定的时间间隔内执行一次函数。比如每隔 200 毫秒执行一次。

示例:

javascript
function throttle(func, delay) {
  let lastExecTime = 0;

  return function(...args) {
    const now = Date.now();

    if (now - lastExecTime >= delay) {
      func.apply(this, args);
      lastExecTime = now;
    }
  };
}

// 使用节流
const throttledFunction = throttle(() => {
  console.log('Throttled function called');
}, 200);

// 在某个高频触发的事件中使用
window.addEventListener('scroll', throttledFunction);

注意事项:

  • 防抖和节流的实现可以根据具体需求进行调整,例如在防抖中,可以选择在起始或结束时执行函数。
  • 在实际项目中,可以根据具体场景选择使用防抖还是节流,以提升性能和用户体验。

这两者的选择取决于具体业务需求,防抖适用于一连串的事件中只执行最后一次,而节流适用于高频率触发但在一段时间内只执行一次的场景。

四: 防抖(Debounce)

Debounce是一种用于限制函数执行频率的技术,有多种实现形式。以下是几种常见的防抖形式:

  1. 基础版防抖:

    这是防抖的基本实现,利用 setTimeout 来延迟函数的执行。

    javascript
    function debounce(func, delay) {
      let timeoutId;
    
      return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
          func.apply(this, args);
        }, delay);
      };
    }
  2. 立即执行版防抖:

    在防抖函数中,可以选择在触发事件时立即执行一次函数,然后在延迟期间内不再执行,确保至少执行一次。

    javascript
    function debounceImmediate(func, delay) {
      let timeoutId;
    
      return function(...args) {
        if (!timeoutId) {
          func.apply(this, args);
        }
    
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
          timeoutId = null;
        }, delay);
      };
    }
  3. 返回值版本防抖:

    这个版本返回的是包装后函数的执行结果,而不是原函数的执行结果。

    javascript
    function debounceWithResult(func, delay) {
      let timeoutId;
    
      return function(...args) {
        clearTimeout(timeoutId);
        return new Promise((resolve) => {
          timeoutId = setTimeout(() => {
            resolve(func.apply(this, args));
          }, delay);
        });
      };
    }
  4. 使用 leading 或 trailing 选项的防抖:

    在某些实现中,防抖函数可能提供 leadingtrailing 选项,用于控制是在延迟开始时执行一次函数(leading),还是在延迟结束时执行一次函数(trailing)。

    javascript
    function debounceWithOptions(func, delay, options) {
      let timeoutId;
      let lastArgs;
      let lastThis;
      let leading = options.leading;
    
      return function(...args) {
        lastArgs = args;
        lastThis = this;
    
        clearTimeout(timeoutId);
    
        if (leading && !timeoutId) {
          func.apply(lastThis, lastArgs);
        }
    
        timeoutId = setTimeout(() => {
          if (!leading) {
            func.apply(lastThis, lastArgs);
          }
    
          timeoutId = null;
        }, delay);
      };
    }
  5. 使用 leading 和 trailing 选项的防抖:

以下是带有 leadingtrailing 选项的防抖函数的实现:

javascript
function debounceWithOptions(func, delay, options) {
  let timeoutId;
  let lastArgs;
  let lastThis;
  let leading = options.leading;
  let trailing = options.trailing;

  return function (...args) {
    lastArgs = args;
    lastThis = this;

    clearTimeout(timeoutId);

    if (leading && !timeoutId) {
      func.apply(lastThis, lastArgs);
    }

    timeoutId = setTimeout(() => {
      timeoutId = null;

      if (trailing) {
        func.apply(lastThis, lastArgs);
      }
    }, delay);
  };
}

// 使用带有选项的防抖
const debouncedFunction = debounceWithOptions(
  () => {
    console.log('Debounced function called');
  },
  300,
  {
    leading: true,
    trailing: false, // 或者设置为 true,视情况而定
  }
);

// 在某个高频触发的事件中使用
window.addEventListener('scroll', debouncedFunction);

在上述实现中,通过 leadingtrailing 选项,可以灵活控制防抖函数的行为:

  • 如果 leadingtrue,则表示在延迟开始时立即执行一次函数。
  • 如果 trailingtrue,则表示在延迟结束时再执行一次函数。

这样,可以根据业务需求选择是在延迟开始时执行函数、在延迟结束时执行函数,还是同时满足两者。

这些是防抖的一些常见实现形式,具体的选择取决于业务需求和使用场景。在实际项目中,可以根据具体情况选择适合的防抖方式。

五: 节流(Throttle)

Throttle是一种用于控制函数执行频率的技术,以确保函数在一定时间内不会被过于频繁地执行。以下是几种常见的节流形式:

  1. 基础版节流:

    基础版节流使用时间戳来判断是否超过了指定的间隔时间,如果超过了,则执行函数。

    javascript
    function throttle(func, delay) {
      let lastExecTime = 0;
    
      return function (...args) {
        const now = Date.now();
    
        if (now - lastExecTime >= delay) {
          func.apply(this, args);
          lastExecTime = now;
        }
      };
    }
  2. 定时器版节流:

    定时器版节流使用 setTimeout 来确保在指定的间隔时间内只执行一次函数。

    javascript
    function throttleTimer(func, delay) {
      let timerId;
    
      return function (...args) {
        if (!timerId) {
          func.apply(this, args);
          timerId = setTimeout(() => {
            timerId = null;
          }, delay);
        }
      };
    }

function throttle (fn, delay=400) { let timer = null return function () { if (timer) { return } timer = setTimeout(() => { fn.apply(this, arguments) timer = null }, delay) } }


1. **双重定时器版节流:**

双重定时器版节流是为了解决一些特定场景下的问题,使用两个定时器来确保在指定的间隔时间内执行函数。

```javascript
function throttleDoubleTimer(func, delay) {
  let timerId1;
  let timerId2;

  return function (...args) {
    if (!timerId1) {
      func.apply(this, args);
      timerId1 = setTimeout(() => {
        timerId1 = null;
      }, delay);
    } else if (!timerId2) {
      timerId2 = setTimeout(() => {
        timerId2 = null;
        func.apply(this, args);
      }, delay);
    }
  };
}

在实际实现中,节流函数可以分为两种类型:前置节流和后置节流,这两者的区别在于函数在时间间隔的开始时是否立即执行。

前置节流(Leading Throttle):

  • 在时间间隔的开始时立即执行一次函数,然后在规定的时间间隔内不再执行,直到下一个时间间隔开始。
javascript
function leadingThrottle(func, delay) {
  let lastExecTime = 0;

  return function (...args) {
    const now = Date.now();

    if (now - lastExecTime >= delay) {
      func.apply(this, args);
      lastExecTime = now;
    }
  };
}

使用示例:

javascript
const leadingThrottledFunction = leadingThrottle(() => {
  console.log('Leading throttled function called');
}, 300);

window.addEventListener('scroll', leadingThrottledFunction);

后置节流(Trailing Throttle):

  • 在时间间隔的结束时执行一次函数,然后在规定的时间间隔内不再执行,直到下一个时间间隔结束。
javascript
function trailingThrottle(func, delay) {
  let timerId;

  return function (...args) {
    if (!timerId) {
      timerId = setTimeout(() => {
        func.apply(this, args);
        timerId = null;
      }, delay);
    }
  };
}

使用示例:

javascript
const trailingThrottledFunction = trailingThrottle(() => {
  console.log('Trailing throttled function called');
}, 300);

window.addEventListener('scroll', trailingThrottledFunction);

在选择前置节流和后置节流时,根据业务需求和预期的效果来决定。如果希望函数在时间间隔的开始时立即执行一次,选择前置节流;如果希望函数在时间间隔的结束时执行一次,选择后置节流。 这些是一些常见的节流形式,具体的选择取决于业务需求和使用场景。在实际项目中,可以根据具体情况选择适合的节流方式。

六: 防抖(Debounce)和节流(Throttle)是两种用于优化前端性能和处理事件的常见技术。它们的应用场景各不相同,以下是它们在前端中常见的应用场景:

确定提交

防抖(Debounce):

防抖的目的是确保在事件触发后的一定时间内不再触发相同事件,只有过了指定的时间间隔才会执行。

1. 输入框搜索:

在用户输入搜索关键词时,使用防抖可以确保只有在用户停止输入一段时间后才触发实际的搜索请求,减轻服务器负担。

javascript
// 使用 Lodash 的防抖函数
const debounceSearch = _.debounce(searchFunction, 500);

inputElement.addEventListener('input', debounceSearch);

2. 窗口大小变化:

当窗口大小变化时,防抖可以确保只有在用户停止调整窗口大小一段时间后才执行相关的响应逻辑。

javascript
// 使用原生 JavaScript 的防抖实现
let resizeTimer;

window.addEventListener('resize', function() {
  clearTimeout(resizeTimer);
  resizeTimer = setTimeout(handleResize, 300);
});

节流(Throttle):

节流的目的是确保在一定时间内不会触发相同事件的处理函数,而是每隔一段时间执行一次。 滚动事件 拖拽事件

1. 滚动事件:

在处理滚动事件时,使用节流可以减少事件处理函数的触发频率,提高性能。

javascript
// 使用 Lodash 的节流函数
const throttleScroll = _.throttle(scrollFunction, 200);

window.addEventListener('scroll', throttleScroll);

2. 点击按钮:

在处理按钮点击事件时,使用节流可以确保用户点击按钮的频率不会导致过多的请求或处理。

javascript
// 使用原生 JavaScript 的节流实现
let clickTimer;

buttonElement.addEventListener('click', function() {
  if (!clickTimer) {
    processClick();
    clickTimer = setTimeout(function() {
      clickTimer = null;
    }, 1000);
  }
});

总结:

  • 防抖适用于在一定时间内只需执行一次事件处理函数的场景,例如输入框搜索、窗口大小变化等。

  • 节流适用于在一定时间内控制事件处理函数的触发频率,例如滚动事件、按钮点击等。

  • 输入框搜索中,假设想要输入完关键词之后再提示,用防抖。(注重结果)

  • 输入框搜索中,假设想要输入关键词过程中就及时提示,用节流。(注重过程)

七: 能分享一个你在项目中使用防抖的例子吗?它解决了什么问题?

当在用户输入框中实时搜索时,防抖是一个常见的应用场景。例如,当用户在搜索框中输入关键字时,可以使用防抖来确保只在用户停止输入一段时间后才触发实时搜索请求,而不是在每次键盘按键时都触发。

八: 在什么情况下,你觉得使用节流比使用防抖更为合适?

选择使用节流(throttle)还是防抖(debounce)通常取决于特定的应用场景和需求。以下是一些情况下,你可能更倾向于选择使用节流:

  1. 实时位置更新:

    • 当需要在用户滚动或拖拽等连续事件中更新元素的实时位置时,节流可能更适合。这确保了在一定时间间隔内执行位置更新,以避免过于频繁的计算和布局操作。
  2. 持续触发的动画效果:

    • 对于需要持续触发的动画效果,如滚动动画或拖拽操作中的动态效果,使用节流可以确保动画的平滑性,而不会因为过于频繁的事件而导致性能问题。
  3. 限制 API 请求的频率:

    • 当需要限制对某个 API 的请求频率时,例如在搜索框实时搜索时,节流可以确保在一定时间间隔内发送一次请求,而不是在每次用户输入时都发送请求。
  4. 防止按钮重复点击:

    • 如果有一个按钮在短时间内被多次点击可能会导致问题(例如多次提交表单),使用节流可以确保在一定时间间隔内只执行一次点击操作。
  5. 滚动加载更多:

    • 在实现滚动加载更多的功能时,使用节流可以确保在滚动事件中间隔一段时间触发加载更多的操作,而不是在每次滚动时都触发。

总体而言,节流适用于需要在一定时间间隔内执行操作的场景,而防抖适用于需要等待一段时间后执行唯一操作的场景。在实际项目中,具体选择哪种方式还需要根据具体需求和用户体验做出权衡。