Appearance
JavaScript 中的函数、作用域、this 与闭包
JavaScript 里最容易“越学越乱”的地方,通常不是对象,而是函数。
因为函数在这门语言里不只是“可执行代码块”,它还会牵扯出:
- 作用域
- 执行上下文
this绑定- 闭包
如果把这几个词混成一团,很多问题都会解释不清。
1. 函数在 JavaScript 里意味着什么
函数可以看成 一段可以被调用、可以传递、可以返回、还能携带上下文信息的行为单元。
这也是为什么 JavaScript 里的函数非常灵活:
- 可以像普通值一样赋值给变量
- 可以作为参数传给别的函数
- 可以作为返回值返回出去
2. 作用域到底在控制什么
作用域可以看成 一个变量在什么范围内可以被访问。
JavaScript 主要有这些常见作用域:
- 全局作用域
- 函数作用域
- 块级作用域
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这里最核心的区别是:
var不认普通代码块,它主要认函数边界let和const会把当前{}代码块也视作独立作用域
所以:
- 条件判断、循环、独立代码块里,
var更容易把变量泄漏到外层 let、const更适合写局部状态,边界更清楚
3. var、let、const 的差异本质是什么
工程里经常会把它们当成“声明变量的三种写法”,但更贴近实际的是,它们代表的是不同的作用域与可变性选择。
3.1 var
更老的声明方式。
它的关键特点是:
- 函数作用域
- 存在变量提升
- 在循环和条件块里更容易制造意外共享问题
3.2 let
块级作用域。
更适合:
- 需要重新赋值的局部变量
- 循环中的索引变量
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,这类赋值有机会直接落到全局对象上,制造出一个意外全局变量。
但在严格模式下,这会直接报错。
这件事很重要,因为它意味着:
- 非严格模式更容易让变量污染到全局
- 严格模式会强制你把变量声明边界写清楚
3.6 严格模式和非严格模式下,普通函数直接调用时的 this 为什么不同
js
function show() {
console.log(this)
}
show()这段代码在非严格模式下,普通函数直接调用时的 this 通常会落到全局对象。
但在严格模式下,this 会是 undefined。
它背后的意义是:
- 非严格模式会帮你做一层“兜底绑定”
- 严格模式则拒绝这种模糊行为,让你显式面对“当前并没有调用者上下文”
这也是为什么很多工程代码里,一旦切到严格模式,某些老写法会突然暴露问题。
3.7 严格模式下,eval 为什么不再轻易污染外层作用域
这是一个很少被提到,但和作用域边界很相关的点。
js
function demo() {
'use strict'
eval('var inner = 1')
console.log(inner)
}在非严格模式下,直接 eval 里的声明更容易和当前作用域纠缠在一起。
但在严格模式下,eval 会拥有更收紧的作用域边界,不再适合作为“偷偷往外层塞变量”的手段。
工程上真正要记住的是:严格模式在尽量减少那些会让作用域边界变模糊的动态行为。
3.8 严格模式还额外收紧了哪些和作用域相关的行为
常见还有这些:
- 禁止使用
with - 不允许重复参数名
- 对某些静默失败的赋值或删除操作,会直接抛错
这些限制看起来像“更严格”,但本质上是在帮你做两件事:
- 让变量解析路径更清楚
- 让错误更早暴露,而不是悄悄吞掉
4. this 到底在说什么
this 最容易被误解成“函数定义时自带的上下文”。
实际写项目时,更多是这样:大多数情况下,this 取决于函数是怎么被调用的,而不是怎么定义的。
4.1 普通函数直接调用
js
function show() {
console.log(this)
}
show()这种情况下:
- 非严格模式下,通常指向全局对象
- 严格模式下,通常是
undefined
也就是说,this 的这条规则和上面的严格模式并不是两套知识,而是同一件事在函数调用场景下的体现。
4.2 作为对象方法调用
js
const user = {
name: 'tom',
say() {
console.log(this.name)
}
}
user.say()这里的关键不是“定义在对象里”,而是:它是通过 user.say() 这种方式被调用的。
4.3 用 call、apply、bind 显式绑定
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()它们的区别可以压缩成:
call:立即调用,参数逐个传apply:立即调用,参数以数组形式传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. 闭包解决什么问题
闭包最常见的价值包括:
- 封装私有状态
- 做函数工厂
- 延迟执行时仍能拿到外层变量
- 在框架和库内部维护上下文
6.1 一个典型场景
js
function createAdder(base) {
return function (value) {
return base + value
}
}
const add10 = createAdder(10)
console.log(add10(5)) // 157. 为什么闭包也可能带来问题
闭包不是“高级语法奖励”,它也有代价。
要注意的是:
- 不必要地保留大量外层变量,会增加内存占用
- 在循环和异步回调里,如果变量绑定没处理好,容易拿到意料之外的值
例如老写法里的经典问题:
js
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i)
}, 0)
}这里最终通常会输出三个 3,原因不是定时器“出错”,而是:
var只有函数作用域- 三个回调共享了同一个
i - 等异步回调真正执行时,循环早已结束
8. this 和闭包为什么容易混
因为它们都和“函数执行时能拿到什么”有关。
但两者并不是同一套规则:
this取决于调用方式- 闭包取决于词法作用域
也就是说:
this说的是“当前函数是以谁为上下文被调用的”- 闭包说的是“当前函数还能访问哪些定义时的外层变量”
9. 一张关系图
mermaid
flowchart TD
A[函数执行] --> B[作用域查找变量]
A --> C[this 绑定]
B --> D[闭包保留外层变量]
C --> E[由调用方式决定]10. 工程上更稳的建议
- 默认优先用
const,需要重新赋值时再用let - 少用
var,除非你在维护历史代码 - 解释
this时看调用方式,不要先猜定义位置 - 使用闭包时,要知道自己到底保留了哪些变量
- 异步回调和循环组合时,优先用块级作用域
11. 小结
这条主线压缩起来,可以记住:
- 作用域决定变量能在哪里被访问
this决定函数调用时指向哪个上下文- 闭包决定函数是否还能访问定义时的外层变量
下一步最自然的是进入 对象、原型链、class 与继承,因为函数和对象在 JavaScript 里是紧密连在一起的。