Skip to content

JavaScript 中的函数、作用域、this 与闭包

JavaScript 里最容易“越学越乱”的地方,通常不是对象,而是函数。

因为函数在这门语言里不只是“可执行代码块”,它还会牵扯出:

  1. 作用域
  2. 执行上下文
  3. this 绑定
  4. 闭包

如果把这几个词混成一团,很多问题都会解释不清。

1. 函数在 JavaScript 里意味着什么

函数可以看成 一段可以被调用、可以传递、可以返回、还能携带上下文信息的行为单元

这也是为什么 JavaScript 里的函数非常灵活:

  1. 可以像普通值一样赋值给变量
  2. 可以作为参数传给别的函数
  3. 可以作为返回值返回出去

2. 作用域到底在控制什么

作用域可以看成 一个变量在什么范围内可以被访问

JavaScript 主要有这些常见作用域:

  1. 全局作用域
  2. 函数作用域
  3. 块级作用域

2.1 词法作用域是什么意思

词法作用域可以看成 变量的可访问范围,在代码写出来时就已经由嵌套关系决定了

这也是为什么内部函数可以访问外层变量,而外层通常不能直接访问内部变量。

js
const name = 'outer'

function demo() {
  const age = 18

  function inner() {
    console.log(name)
    console.log(age)
  }

  inner()
}

2.2 函数作用域和块级作用域为什么经常导致结果不同

这两个词看起来只是“范围大小不同”,但工程里很多 bug 都是从这里来的。

js
if (true) {
  var a = 1
  let b = 2
  const c = 3
}

console.log(a) // 1
console.log(b) // ReferenceError
console.log(c) // ReferenceError

这里最核心的区别是:

  1. var 不认普通代码块,它主要认函数边界
  2. letconst 会把当前 {} 代码块也视作独立作用域

所以:

  1. 条件判断、循环、独立代码块里,var 更容易把变量泄漏到外层
  2. letconst 更适合写局部状态,边界更清楚

3. varletconst 的差异本质是什么

工程里经常会把它们当成“声明变量的三种写法”,但更贴近实际的是,它们代表的是不同的作用域与可变性选择。

3.1 var

更老的声明方式。

它的关键特点是:

  1. 函数作用域
  2. 存在变量提升
  3. 在循环和条件块里更容易制造意外共享问题

3.2 let

块级作用域。

更适合:

  1. 需要重新赋值的局部变量
  2. 循环中的索引变量

3.3 const

也是块级作用域。

它强调的是:变量绑定关系不再重新指向别处。

要特别注意:

const 不能重新赋值,不等于对象内容完全不可变。

3.4 严格模式和非严格模式下,作用域与执行行为有什么差别

这是 JavaScript 里非常容易被一句“加个 'use strict' 就行”带过去,但实际上很值得讲透的地方。

可以直接把它理解成:严格模式不是换了一门语言,而是把那些容易制造歧义、静默出错或污染作用域的行为收紧了。

常见开启方式:

js
'use strict'

function demo() {
  return 1
}

另外,ES Module 天然就是严格模式。

3.5 严格模式下为什么更不容易产生“意外全局变量”

看下面这个例子:

js
function setName() {
  name = 'tom'
}

setName()

在非严格模式下,如果当前作用域链里找不到 name,这类赋值有机会直接落到全局对象上,制造出一个意外全局变量。

但在严格模式下,这会直接报错。

这件事很重要,因为它意味着:

  1. 非严格模式更容易让变量污染到全局
  2. 严格模式会强制你把变量声明边界写清楚

3.6 严格模式和非严格模式下,普通函数直接调用时的 this 为什么不同

js
function show() {
  console.log(this)
}

show()

这段代码在非严格模式下,普通函数直接调用时的 this 通常会落到全局对象。

但在严格模式下,this 会是 undefined

它背后的意义是:

  1. 非严格模式会帮你做一层“兜底绑定”
  2. 严格模式则拒绝这种模糊行为,让你显式面对“当前并没有调用者上下文”

这也是为什么很多工程代码里,一旦切到严格模式,某些老写法会突然暴露问题。

3.7 严格模式下,eval 为什么不再轻易污染外层作用域

这是一个很少被提到,但和作用域边界很相关的点。

js
function demo() {
  'use strict'
  eval('var inner = 1')
  console.log(inner)
}

在非严格模式下,直接 eval 里的声明更容易和当前作用域纠缠在一起。

但在严格模式下,eval 会拥有更收紧的作用域边界,不再适合作为“偷偷往外层塞变量”的手段。

工程上真正要记住的是:严格模式在尽量减少那些会让作用域边界变模糊的动态行为。

3.8 严格模式还额外收紧了哪些和作用域相关的行为

常见还有这些:

  1. 禁止使用 with
  2. 不允许重复参数名
  3. 对某些静默失败的赋值或删除操作,会直接抛错

这些限制看起来像“更严格”,但本质上是在帮你做两件事:

  1. 让变量解析路径更清楚
  2. 让错误更早暴露,而不是悄悄吞掉

4. this 到底在说什么

this 最容易被误解成“函数定义时自带的上下文”。

实际写项目时,更多是这样:大多数情况下,this 取决于函数是怎么被调用的,而不是怎么定义的。

4.1 普通函数直接调用

js
function show() {
  console.log(this)
}

show()

这种情况下:

  1. 非严格模式下,通常指向全局对象
  2. 严格模式下,通常是 undefined

也就是说,this 的这条规则和上面的严格模式并不是两套知识,而是同一件事在函数调用场景下的体现。

4.2 作为对象方法调用

js
const user = {
  name: 'tom',
  say() {
    console.log(this.name)
  }
}

user.say()

这里的关键不是“定义在对象里”,而是:它是通过 user.say() 这种方式被调用的。

4.3 用 callapplybind 显式绑定

js
function greet(city) {
  console.log(this.name, city)
}

const user = { name: 'tom' }

greet.call(user, 'shanghai')
greet.apply(user, ['beijing'])
const bound = greet.bind(user, 'hangzhou')
bound()

它们的区别可以压缩成:

  1. call:立即调用,参数逐个传
  2. apply:立即调用,参数以数组形式传
  3. bind:不立即调用,而是返回绑定后的新函数

4.4 用 new 调用

js
function User(name) {
  this.name = name
}

const user = new User('tom')

这里的 this 会指向新创建的实例对象。

4.5 箭头函数为什么特殊

箭头函数没有自己的 this

更贴近箭头函数的说法是:继承外层作用域中的 this。

js
const user = {
  name: 'tom',
  say() {
    const inner = () => {
      console.log(this.name)
    }
    inner()
  }
}

这里箭头函数里的 this,通常会沿用 say 执行时的 this

5. 闭包到底是什么

闭包可以看成 函数在离开原来的定义位置后,仍然能够访问定义时词法作用域中的变量

js
function createCounter() {
  let count = 0

  return function () {
    count += 1
    return count
  }
}

const counter = createCounter()

console.log(counter()) // 1
console.log(counter()) // 2

这里内部函数之所以还能访问 count,不是因为变量“神秘保留”了,而是因为:这个函数和它定义时的词法环境被一起保留下来了。

6. 闭包解决什么问题

闭包最常见的价值包括:

  1. 封装私有状态
  2. 做函数工厂
  3. 延迟执行时仍能拿到外层变量
  4. 在框架和库内部维护上下文

6.1 一个典型场景

js
function createAdder(base) {
  return function (value) {
    return base + value
  }
}

const add10 = createAdder(10)
console.log(add10(5)) // 15

7. 为什么闭包也可能带来问题

闭包不是“高级语法奖励”,它也有代价。

要注意的是:

  1. 不必要地保留大量外层变量,会增加内存占用
  2. 在循环和异步回调里,如果变量绑定没处理好,容易拿到意料之外的值

例如老写法里的经典问题:

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

这里最终通常会输出三个 3,原因不是定时器“出错”,而是:

  1. var 只有函数作用域
  2. 三个回调共享了同一个 i
  3. 等异步回调真正执行时,循环早已结束

8. this 和闭包为什么容易混

因为它们都和“函数执行时能拿到什么”有关。

但两者并不是同一套规则:

  1. this 取决于调用方式
  2. 闭包取决于词法作用域

也就是说:

  1. this 说的是“当前函数是以谁为上下文被调用的”
  2. 闭包说的是“当前函数还能访问哪些定义时的外层变量”

9. 一张关系图

mermaid
flowchart TD
    A[函数执行] --> B[作用域查找变量]
    A --> C[this 绑定]
    B --> D[闭包保留外层变量]
    C --> E[由调用方式决定]

10. 工程上更稳的建议

  1. 默认优先用 const,需要重新赋值时再用 let
  2. 少用 var,除非你在维护历史代码
  3. 解释 this 时看调用方式,不要先猜定义位置
  4. 使用闭包时,要知道自己到底保留了哪些变量
  5. 异步回调和循环组合时,优先用块级作用域

11. 小结

这条主线压缩起来,可以记住:

  1. 作用域决定变量能在哪里被访问
  2. this 决定函数调用时指向哪个上下文
  3. 闭包决定函数是否还能访问定义时的外层变量

下一步最自然的是进入 对象、原型链、class 与继承,因为函数和对象在 JavaScript 里是紧密连在一起的。

基于 VitePress 构建的个人技术笔记。