Web前端基础系列(一): 作用域与闭包

本文介绍了JavaScript中的作用域,包括词法作用域、块作用域和函数作用域,以及作用域链的概念。接着详细讲解了闭包,它是如何形成的,并通过例子展示了闭包在模块化、防抖和节流、缓存以及库的实现等场景中的应用。最后提到了闭包与垃圾回收机制的关系,为后续文章埋下伏笔。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

几个概念:

  • 作用域:用于确定在何处以及如何查找变量(标识符)的一套规则。
  • 词法作用域:词法作用域是定义在词法阶段的作用域。词法作用域是由写代码时将代码和块作用域写在哪里来决定的,因此当词法作用域处理代码是会保持作用域不变(大部分情况)。
  • 块作用域:指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常用{}包裹)。常见的块级作用域有 with,try/catch,let,const 等。
  • 函数作用域:属于这个函数的全部变量都可以在整个函数范围内使用及复用(包括嵌套作用域)。
  • 作用域链:查找变量时,先从当前作用域开始查找,如果没有找到,就会到父级(词法层面上的父级)作用域中查找,一直找到全局作用域。作用域链正是包含这些作用域的列表。

什么是闭包?

  • 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。 —— MDN
function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

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

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


add5add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。

闭包的应用:

1、用来定义模块,限制对代码的访问:下面的privateCounter只能通过Counter.value()去访问

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

var Counter2 = makeCounter();
console.log(Counter2.value()); /* logs 0 */


2、防抖和节流

function throttle(func, duration) {
  let shouldWait = false
  return function (...args) {
    if (!shouldWait) {
      func.apply(this, args)
      shouldWait = true
      setTimeout(function () {
        shouldWait = false
      }, duration)
    }
  }
}

const button = document.getElementById('button')

button.addEventListener(
  'click',
  throttle(function () {
    throwBall()
  }, 500)
)

function throwBall() {
	console.log('throwBall throttle')
}


节流使用场景:

  • window resize 导致UI持续更新
  • server端或者client端一些Performance-heavy的用户操作
function debounce(func, duration) {
  let timeout
  return function (...args) {
    const effect = () => {
      timeout = null
      return func.apply(this, args)
    }
    clearTimeout(timeout)
    timeout = setTimeout(effect, duration)
  }
}

const button = document.getElementById('button')

button.addEventListener(
  'click',
  debounce(function () {
    throwBall()
  }, 500)
)

function throwBall() {
	console.log('throwBall debounce')
}


放抖使用场景:

  • 根据输入异步获取搜索建议
  • server端批处理(例如多个update重叠)

3、用来做缓存

function keys() {
  const cache = {}
  return function getRandom() {
    let m = Math.floor(Math.random() * 1000)
    while (cache[m]) {
      m = Math.floor(Math.random() * 1000)
    }
    cache[m] = m
    return m
  }
}
// 随机产生不重复的key
generateKeys = keys()

generateKeys()
generateKeys()


4、各种JS库中使用

  • Vue watch

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    ...
    const watcher = new Watcher(vm, expOrFn, cb, options)
    ...
    return function unwatchFn () {
      watcher.teardown()
    }
}
let unWatch = this.$watch("obj.name",function(newVal, oldVal){
	console.log(`${newVal};${oldVal};`)
})
// 取消监听,取消后,obj.name的改变不再会通知
unWatch()

watch执行的时候,会返会一个unwatchFn函数,在这个函数里面,调用了watcher下的unWatch方法.这样就不用关心如何去取消监听,直接调用这个方法就可以了.

为什么闭包能起到这些作用呢? 就要从JS的垃圾回收机制去考虑了,下一篇文章一起看一下.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值