从v8源码和react深入浅出理解 JavaScript 作用域链与闭包

前言

text 复制代码
为什么外层函数已经执行完了,里面的变量还能被访问?

先看一道题

js 复制代码
var name = 'global';

function outer() {
  var name = 'outer';

  return function inner() {
    console.log(name);
  };
}

var fn = outer();

fn();

输出是:

text 复制代码
outer

这个结果有两个容易误解的地方:

  1. inner 是在全局作用域里通过 fn() 调用的,为什么没有输出 global
  2. outer() 已经执行完了,为什么 outer 里的 name 还没有被回收?

JavaScript 是词法作用域,函数能访问哪些变量,在函数定义时就确定了;闭包让函数在离开原始执行位置后,仍然能继续访问当时的词法环境。

作用域:变量查找的规则

作用域解决的是两个问题:

  1. 变量声明放在哪里。
  2. 使用变量时,应该去哪里找。

比如:

js 复制代码
var a = 1;

function foo() {
  var b = 2;
  console.log(a);
}

foo();
console.log(b);

输出:

text 复制代码
1
ReferenceError: b is not defined

foo 能访问外层的 a,但外层不能访问 foo 内部的 b

还有一个经常混在一起的问题:

js 复制代码
var obj = {};

console.log(obj.name);
console.log(name);

前者是对象属性查找,找不到返回 undefined。后者是标识符查找,沿作用域链找不到就会抛出 ReferenceError

词法作用域:看定义位置,不看调用位置

JavaScript 采用的是词法作用域,也叫静态作用域。

看一个很经典的例子:

js 复制代码
var value = 1;

function foo() {
  console.log(value);
}

function bar() {
  var value = 2;
  foo();
}

bar();

输出:

text 复制代码
1

如果 JavaScript 是动态作用域,foo()bar() 里面调用,就可能拿到 bar 里的 value = 2

但 JavaScript 不是这样。foo 定义在全局作用域,所以它向外找变量时,会去全局环境找 value,而不是去调用它的 bar 里找。

也就是说:

text 复制代码
函数的作用域链,和函数在哪里调用没关系,和函数在哪里定义有关。

作用域链:变量是怎么一路往外找的

再看一个例子:

js 复制代码
const globalCount = 1;

function outer() {
  const outerCount = 2;

  function inner() {
    const innerCount = 3;
    console.log(innerCount, outerCount, globalCount);
  }

  inner();
}

outer();

inner 里执行 console.log(innerCount, outerCount, globalCount) 时,变量查找过程大致是:

  1. 先在 inner 自己的作用域里找,找到 innerCount
  2. 找不到 outerCount,就去外层 outer 的作用域里找。
  3. 还需要 globalCount,继续去全局作用域里找。
  4. 如果一直找到最外层还没有,就抛出 ReferenceError

这条"从内到外"的查找路径,就是作用域链。

可以画成这样:

flowchart LR A[&#34;inner 作用域<br/>innerCount&#34;] --> B[&#34;outer 作用域<br/>outerCount&#34;] B --> C[&#34;全局作用域<br/>globalCount&#34;] C --> D[&#34;null&#34;]

作用域链不是代码执行时临时猜出来的。函数创建时,它能访问的外层环境就已经确定了。

从规范角度看:词法环境

在 ECMAScript 规范里,作用域链对应的核心概念叫 Lexical EnvironmentEnvironment Record

text 复制代码
Lexical Environment = 当前这层作用域的变量记录 + 指向外层环境的引用

也就是说,每一层环境里大概有两样东西:

js 复制代码
{
  record: {
    // 当前作用域里的变量、函数、参数等绑定
  },
  outer: 上一层词法环境,
}

比如前面的例子,可以简化成:

text 复制代码
inner lexical environment
  record: innerCount
  outer -> outer lexical environment
    record: outerCount
    outer -> global lexical environment
      record: globalCount
      outer -> null

规范里还有一个很关键的操作:查找标识符时,如果当前 Environment Record 没有这个绑定,就继续找 [[OuterEnv]]

这就是作用域链在规范层面的表达。

需要注意的是,Environment Record 是规范用来描述语义的模型,不代表浏览器里一定有一个同名对象让我们直接访问。不同引擎会有自己的实现方式,但行为要符合这套语义。

从 V8 源码看:闭包变量为什么还能活

本文看的 Chromium/V8 版本固定为:

text 复制代码
Chromium: 148.0.7778.178
commit: d096af1c9e98c45c3596e59620622b1a049bfecb

先说结论:

text 复制代码
闭包不是把整个函数执行上下文都留在内存里。

更接近 V8 实现的说法是:

text 复制代码
需要跨函数继续访问的变量,会被分配到堆上的 Context 里;闭包函数持有这个 Context,所以变量还能活。

Scope:先分析哪些变量需要进 Context

V8 解析代码时,会建立 ScopeScope 里有两个很关键的字段:

cpp 复制代码
int num_stack_slots() const { return num_stack_slots_; }
int num_heap_slots() const { return num_heap_slots_; }

也就是说,变量大致有两类去处:

  1. 放在栈帧或寄存器里,用完就可以跟着调用结束。
  2. 放在堆上的 Context 里,给后续闭包继续访问。

Scope::NeedsContext() 的判断也很直白:

cpp 复制代码
bool NeedsContext() const {
  return num_heap_slots() > 0;
}

那什么变量必须进 Context

Scope::MustAllocateInContext 的注释和逻辑,核心原因是:

text 复制代码
如果变量会被内层作用域访问,或者可能通过 eval / runtime lookup 被访问,就必须分配到 context。

源码里对应的是:

cpp 复制代码
return var->has_forced_context_allocation() || inner_scope_calls_eval();

在变量分配时,V8 会走到类似下面的分支:

cpp 复制代码
if (MustAllocateInContext(var)) {
  AllocateHeapSlot(var);
} else {
  AllocateStackSlot(var);
}

所以,闭包的成本不是"外层函数所有局部变量都永远不释放",而是"被闭包需要的那部分变量会进入堆上的 context slot"。

回到开头的例子:

js 复制代码
function outer() {
  var name = 'outer';

  return function inner() {
    console.log(name);
  };
}

name 被内层函数 inner 访问,不能只放在 outer 的普通栈帧里,否则 outer() 返回后就没了。因此它需要进入 Context

Context:运行时真正保存闭包变量的对象

V8 里的 Context 是一个堆对象。

contexts.h 里可以看到两个很关键的槽位:

cpp 复制代码
enum Field {
  SCOPE_INFO_INDEX,
  PREVIOUS_INDEX,
  EXTENSION_INDEX,
  ...
};

可以简单理解成:

  1. SCOPE_INFO_INDEX:指向这层作用域的元信息。
  2. PREVIOUS_INDEX:指向外层 Context,这就是运行时的作用域链。
  3. 后面的 slot:保存真正的闭包变量。

这和前面规范里的 Lexical Environment 很像:

text 复制代码
Context
  scope_info -> 当前作用域有哪些变量
  previous   -> 外层 Context
  slots      -> 变量值

ScopeInfo 则保存作用域的静态元信息。比如 scope-info.tq 里就有:

cpp 复制代码
context_local_count
context_local_names
context_local_infos
outer_scope_info

也就是说:

  • ScopeInfo 更像"说明书":这层作用域有哪些 context local,名字是什么,模式是什么,外层 scope info 是谁。
  • Context 更像"运行时存储":这一轮函数调用里,context slot 里的真实值是什么。

CreateFunctionContext:函数执行时创建 Context

当一个函数作用域确实需要 Context 时,字节码生成阶段会生成创建 context 的逻辑。

bytecode-generator.cc 里,BuildNewLocalActivationContext() 会根据 scope 创建对应的 context:

cpp 复制代码
int slot_count = scope->num_heap_slots() - Context::MIN_CONTEXT_SLOTS;

builder()->CreateFunctionContext(scope, slot_count);

bytecodes.h 里也能看到相关字节码:

cpp 复制代码
CreateClosure
CreateFunctionContext
PushContext
PopContext
LdaContextSlot
StaContextSlot
  1. 创建函数执行需要的 FunctionContext
  2. 把当前 context 压到执行环境里。
  3. 访问闭包变量时,通过 LdaContextSlot / StaContextSlot 读写 context slot。
  4. 创建内层函数时,通过 CreateClosure 创建闭包函数。

CreateClosure:函数对象会带上当前 Context

继续看 VisitFunctionLiteral()

cpp 复制代码
builder()->CreateClosure(entry, GetNewClosureSlot(expr), flags);

真正运行时创建闭包函数的地方,可以看 runtime-scopes.cc 里的 Runtime_NewClosure

cpp 复制代码
DirectHandle<Context> context(isolate->context(), isolate);

return *Factory::JSFunctionBuilder{isolate, shared, context}
            .set_feedback_cell(feedback_cell)
            .Build();

这段就很关键了:创建 JSFunction 时,会把当前的 context 一起传进去。

也就是说,inner 被创建出来时,不只是一个函数代码入口,它还带着当时的外层 Context

所以开头例子的运行过程可以重新写成:

  1. 编译阶段发现 inner 访问了 outername
  2. V8 把 name 分配到 outerFunctionContext
  3. 执行 outer() 时创建这个 Context,并把 name = 'outer' 存进去。
  4. 执行到 return function inner() { ... } 时,CreateClosure 创建 inner,并让 inner 持有当前 Context
  5. outer() 返回后,普通栈帧可以结束,但那个 Context 还被 inner 引用着。
  6. 后面调用 fn() 时,读取 name 就是沿着 inner 持有的 context 链去找。

这就是"外层函数执行完了,变量还活着"的真正原因。

闭包到底是什么

回到开头的例子:

js 复制代码
function outer() {
  var name = 'outer';

  return function inner() {
    console.log(name);
  };
}

var fn = outer();

fn();

outer() 执行完后,按直觉它的执行上下文应该出栈,局部变量也应该没了。

inner 是在 outer 里面创建的,它创建时就记录了自己所在的词法环境。放到 V8 里看,就是 inner 这个 JSFunction 持有了创建时的 Context。只要 inner 这个函数还可以被外部访问,它引用到的那部分外层环境就不能被释放。

text 复制代码
闭包 = 函数 + 函数创建时能访问的外层词法环境

所以闭包不是"某种特殊写法",也不是"return 一个函数才叫闭包"。只要一个函数能访问外层作用域里的变量,它就和那个外层环境形成了闭包。只是返回函数、事件回调、定时器回调这些场景,更容易把闭包的效果表现出来。

闭包保留的是绑定,不是复制值

来看这个例子:

js 复制代码
function createCounter() {
  let count = 0;

  return {
    add() {
      count++;
    },
    get() {
      return count;
    },
  };
}

const counter = createCounter();

counter.add();
counter.add();

console.log(counter.get());

输出:

text 复制代码
2

addget 访问的是同一个 count 绑定。add 修改了 countget 再读取时就能看到最新值。

所以更准确的理解是:

text 复制代码
闭包让函数继续引用外层词法环境里的变量绑定,而不是复制当时的变量值。

这也是闭包能用来做私有状态的原因。

闭包的常见用法

私有状态

js 复制代码
function createWallet() {
  let balance = 0;

  return {
    deposit(amount) {
      balance += amount;
    },
    getBalance() {
      return balance;
    },
  };
}

const wallet = createWallet();

wallet.deposit(100);
console.log(wallet.getBalance());
console.log(wallet.balance);

输出:

text 复制代码
100
undefined

外部不能直接访问 balance,只能通过返回出来的方法间接操作它。

函数工厂

js 复制代码
function makeAdder(x) {
  return function add(y) {
    return x + y;
  };
}

const add10 = makeAdder(10);
const add20 = makeAdder(20);

console.log(add10(1));
console.log(add20(1));

输出:

text 复制代码
11
21

每次调用 makeAdder 都会创建一套新的词法环境,所以 add10add20 互不影响。

事件回调

js 复制代码
function bindButton(id, text) {
  const button = document.getElementById(id);

  button.addEventListener('click', function () {
    console.log(text);
  });
}

bindButton('save', '保存');

点击事件发生时,bindButton 早就执行完了。但回调函数仍然能访问 text,这也是闭包。

React 里为什么到处都是闭包

因为函数组件本身就是函数:

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    console.log(count);
    setCount(count + 1);
  }

  return <button onClick={handleClick}>{count}</button>;
}

每次渲染 Counter,本质上都是重新执行一次 Counter()

所以每次渲染都会产生一套新的词法环境:

text 复制代码
render 1:
  count = 0
  handleClick -> 捕获 render 1 的 count

render 2:
  count = 1
  handleClick -> 捕获 render 2 的 count

这就是 React 里很多闭包问题的根源:

text 复制代码
你拿到的函数,不一定来自最新一次 render。

useState:状态不在你的闭包里,dispatch 会进入 React 队列

先看 useState

在 React 19.2.4 的 react-dom-client.development.js 里,mountState 大致会做两件事:

js 复制代码
hook.memoizedState = hook.baseState = initialState;
hook.queue = {
  pending: null,
  dispatch: null,
  lastRenderedReducer: basicStateReducer,
  lastRenderedState: initialState,
};

然后创建 dispatch

js 复制代码
dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
queue.dispatch = dispatch;
return [hook.memoizedState, dispatch];

这说明一件事:

text 复制代码
count 这个值属于当前 render 的词法环境;真正的状态记录在 Fiber 对应的 hook 链表和 update queue 里。

所以 setCount 不是直接修改你这次闭包里的 count,而是把更新放进 React 的队列:

js 复制代码
setCount(count + 1);

count + 1 会先用当前闭包里的 count 算出一个值,再把这个值交给 React。

如果连续写:

jsx 复制代码
function add() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}

这三个 count + 1 用的是同一次 render 里的同一个 count,所以它们不是自然递增三次。

更稳的写法是函数式更新:

jsx 复制代码
function add() {
  setCount((c) => c + 1);
  setCount((c) => c + 1);
  setCount((c) => c + 1);
}

这不是为了"绕过闭包",而是告诉 React:下一次状态应该基于队列里的上一个状态计算。

useEffect:依赖数组其实是在管理闭包刷新

再看一个 stale closure 例子:

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log(count);
    }, 1000);

    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount(count + 1)}>{count}</button>;
}

页面上的 count 会变,但 setInterval 里一直打印 0

原因是:

  1. 首次 render 时,count = 0
  2. useEffect(..., []) 只在挂载时注册一次。
  3. setInterval 的回调捕获的是首次 render 的 count
  4. 后续 render 产生了新的 count,但这个 interval 回调没有被替换。

所以 React 文档才会强调:useEffect 的依赖项应该包含 effect 里用到的 reactive values,也就是 props、state,以及组件内部声明的变量和函数。

如果希望 interval 随 count 更新,可以写:

jsx 复制代码
useEffect(() => {
  const id = setInterval(() => {
    console.log(count);
  }, 1000);

  return () => clearInterval(id);
}, [count]);

这表示:count 变了,就清理旧闭包,注册新闭包。

注意 cleanup 也很有意思。React 会先用旧值执行 cleanup,再用新值执行 setup。这正好体现了闭包:

text 复制代码
旧 effect 的 cleanup,拿到的是旧 render 的变量;
新 effect 的 setup,拿到的是新 render 的变量。

useRef:用稳定对象读取最新值

如果我们不想反复重建 interval,但又想读取最新值,可以用 useRef

jsx 复制代码
function Counter() {
  const [count, setCount] = useState(0);
  const countRef = useRef(count);

  countRef.current = count;

  useEffect(() => {
    const id = setInterval(() => {
      console.log(countRef.current);
    }, 1000);

    return () => clearInterval(id);
  }, []);

  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}

useRef 的源码也很简单:

js 复制代码
function mountRef(initialValue) {
  var hook = mountWorkInProgressHook();
  initialValue = { current: initialValue };
  return (hook.memoizedState = initialValue);
}

ref 对象本身是稳定的,闭包捕获的是这个稳定对象。后续 render 改的是 ref.current,所以旧闭包也能读到最新值。

这也是很多 React 代码里用 ref 解决 stale closure 的原因。

useCallback:缓存函数身份,不是自动更新闭包

useCallback 也经常和闭包混在一起。

先看一个容易写错的例子:

jsx 复制代码
function Search({ keyword }) {
  const submit = useCallback(() => {
    console.log(keyword);
  }, []);

  return <button onClick={submit}>搜索</button>;
}

如果 keyword 后续变了,submit 里还是旧值。

原因不是 React 奇怪,而是你用 [] 告诉 React:这个 callback 永远不用换。

React 19.2.4 里的 updateCallback 逻辑也很直接:

js 复制代码
if (deps !== null && areHookInputsEqual(deps, prevState[1])) {
  return prevState[0];
}

hook.memoizedState = [callback, deps];
return callback;

也就是说:

text 复制代码
依赖没变,React 返回上一次的 callback;依赖变了,React 保存这一次 render 创建的新 callback。

所以 useCallback 缓存的是函数身份,不会让旧函数"自动看到新变量"。

正确写法应该是:

jsx 复制代码
const submit = useCallback(() => {
  console.log(keyword);
}, [keyword]);

或者如果它不应该响应 keyword,就不要在里面读 keyword

React 19.2 的 useEffectEvent:把"响应式"和"读取最新值"分开

React 19.2 文档里已经有 useEffectEvent

它解决的是一种很常见的闭包问题:

text 复制代码
Effect 只想因为 roomId 变化而重连,但连接成功后的回调又想读取最新 theme / muted / count。

如果直接把所有值都塞进依赖数组,可能会导致频繁重连。比如:

jsx 复制代码
function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(roomId);

    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });

    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);
}

theme 变化也会重连聊天室,这通常不是我们想要的。

React 19.2 可以写成:

jsx 复制代码
function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(roomId);

    connection.on('connected', onConnected);

    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
}

React 文档里的解释是:Effect Event 可以读取最新的 props 和 state,但不会让 Effect 因为这些值重新同步。

源码里 useEffectEvent 的思路也能看出来:

js 复制代码
var ref = { impl: callback };

return function () {
  return ref.impl.apply(void 0, arguments);
};

更新时,React 会把新的 callback 放进这个 ref 对应的实现里:

js 复制代码
useEffectEventImpl({ ref: ref, nextImpl: callback });

所以它和 useRef 的思路有点像:闭包持有稳定入口,真正执行时再走最新实现。

但它不是"依赖数组作弊器"。React 文档也说得很明确:如果某个值应该触发 Effect 重新同步,就应该放进依赖数组;useEffectEvent 只适合那种"在 Effect 触发的事件里读取最新值,但不希望它改变订阅生命周期"的逻辑。

React 里的闭包应该怎么判断

写 React 时,可以按下面这几条判断:

  1. 事件处理函数通常就是闭包,捕获当前 render 的 props 和 state。
  2. useEffect 的依赖数组,决定这个 effect 闭包什么时候更新。
  3. useCallback 只稳定函数身份,不会自动修复 stale closure。
  4. 只更新状态时,优先考虑函数式更新:setCount((c) => c + 1)
  5. 需要长期订阅,又要读最新值,可以考虑 useRef 或 React 19.2 的 useEffectEvent
  6. 如果某个值变化应该重新连接、重新订阅、重新请求,就把它放进依赖数组。

for 循环里的闭包问题

以前最经典的闭包坑,是 var 和循环:

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

输出:

text 复制代码
3
3
3

原因是 var 没有块级作用域,三个回调引用的是同一个 i。等回调执行时,循环已经结束,i 变成了 3

换成 let

js 复制代码
for (let i = 0; i < 3; i++) {
  setTimeout(function () {
    console.log(i);
  }, 0);
}

输出:

text 复制代码
0
1
2

letfor 循环里会为每次迭代创建新的词法环境,所以每个回调拿到的是各自那一轮的 i

如果不用 let,以前常见的写法是 IIFE:

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

这段代码本质上是手动为每一轮循环创建一个新的函数作用域。

闭包和内存:为什么可能泄漏

闭包本身不是内存泄漏。

真正的问题是:闭包让某些变量继续可达,如果这些变量引用了很大的对象、DOM 节点、定时器或缓存,就可能让它们一直无法被回收。

比如:

js 复制代码
let clickHandler = null;

function bindPanel() {
  const panel = document.querySelector('#panel');

  clickHandler = function () {
    console.log(panel.textContent);
  };

  window.addEventListener('click', clickHandler);

  panel.remove();
}

bindPanel();

panel.remove() 只是把节点从 DOM 树上移除了。

clickHandler 还被 window 的事件监听持有,而 clickHandler 又通过闭包引用了 panel。所以这个 DOM 节点虽然离开了页面树,但仍然被 JavaScript 引用着。

在 Chrome DevTools 的内存文档里,这类节点通常会被称为 Detached DOM tree:节点已经从 DOM 树移除,但还有 JavaScript 引用保留着它。

修复思路也很直接:

js 复制代码
window.removeEventListener('click', clickHandler);
clickHandler = null;

或者从一开始就避免在长生命周期闭包里捕获很重的对象:

js 复制代码
function bindPanel() {
  const panel = document.querySelector('#panel');
  const text = panel.textContent;

  window.addEventListener('click', function () {
    console.log(text);
  });

  panel.remove();
}

回调只保留字符串 text,不再保留整个 DOM 节点。

怎么判断闭包该不该担心

日常写代码不需要看到闭包就紧张。大多数闭包都是正常的。

真正需要注意的是下面几类:

  1. 闭包生命周期很长,比如挂在 window、模块单例、全局缓存上。
  2. 闭包捕获了很大的对象,比如大数组、大 Map、大 JSON。
  3. 闭包捕获了 DOM 节点,而且节点后续会被移除。
  4. 闭包被定时器、事件监听、Promise 链长时间持有。

对应的处理方式也不复杂:

  1. 不用了就 removeEventListener
  2. 不用了就 clearIntervalclearTimeout
  3. 长生命周期闭包里尽量只捕获必要字段,不要顺手捕获整个对象。
  4. 确认外部引用断开后,再让垃圾回收接管。

关键不是"销毁闭包",而是"让闭包不再可达"。只要没有地方能再访问这个函数,它引用的词法环境也就有机会被回收。

回到最开始那道题

现在再看一遍:

js 复制代码
var name = 'global';

function outer() {
  var name = 'outer';

  return function inner() {
    console.log(name);
  };
}

var fn = outer();

fn();

为什么输出 outer

可以按下面几步理解:

  1. inner 定义在 outer 里面,所以它的外层词法环境是 outer
  2. fn = outer() 后,fn 指向 inner
  3. inner 通过 fn 逃到了外部。
  4. 只要 fn 还存在,inner 引用的那部分 outer 词法环境就不能释放。
  5. 执行 fn() 时,查找 name 会先沿着 inner -> outer -> global 的链路找。
  6. outer 的环境里找到 name = 'outer',所以输出 outer

所以,闭包不是为了"强行让变量不销毁"而设计出来的技巧,它是词法作用域自然带来的结果。

总结

  1. 作用域解决的是变量存放和变量查找的问题。
  2. JavaScript 是词法作用域,函数能访问哪些变量由定义位置决定。
  3. 作用域链就是从当前环境一路沿 outer 往外找变量。
  4. 规范里的 Lexical Environment 可以理解为"变量记录 + 外层引用"。
  5. 闭包是函数和其创建时词法环境的组合。
  6. 闭包保留的是变量绑定,不是简单复制值。
  7. V8 里需要逃逸的变量会进入堆上的 Context,闭包函数持有这个 Context
  8. React 函数组件每次 render 都会产生新的词法环境,所以事件回调、Effect、useCallback 都和闭包有关。
  9. 闭包本身不是内存泄漏,长生命周期闭包捕获大对象或 DOM 节点才容易出问题。
text 复制代码
函数在哪里定义,就沿着哪里的作用域链查变量;函数被带到外面继续用,就会把需要的外层环境一起带出去。

参考文章和规范

相关推荐
天蓝色的鱼鱼2 小时前
React Router v8 来了:react-router-dom 没了,老项目该怎么迁移?
前端·react.js
Bolt3 小时前
TypeScript 7.0 来了:当 tsc 用 Go 重写之后
javascript·typescript·go
闪闪发光得欧3 小时前
前端提效新思路:Gemini 3.5 自动化定位 CSS 异常
前端·css
yingyima4 小时前
掌握正则表达式的核心:贪婪与非贪婪匹配的底层机制
前端
奇奇怪怪的4 小时前
文档摄入与 Chunking 策略全对决
前端
阳火锅4 小时前
😭测试小姐姐终于不骂我了!这个提BUG神器太香了...
前端·javascript·面试
道友可好5 小时前
AI 是最好的混乱放大器:代码熵管理实战
前端·人工智能·后端
猩猩程序员6 小时前
前端学习 AI Agent 开发
前端
Younglina6 小时前
打了3年羽毛球球才发现:我对自己的装备和胜率一无所知
前端·后端