
一、作用域起点在讲执行上下文之前我们先从作用域Scope入手。作用域简单说就是“变量起作用的范围”。function bar() { var x 10 console.log(x) // 10 } bar() console.log(x) // ReferenceError: x is not definedx只在bar函数体内有效这就是函数作用域。ES6 之后还引入了块级作用域let / constif (true) { let y 20 } console.log(y) // ReferenceError作用域规定了如何查找变量也就是“在哪里找怎么找”。二、执行上下文 代码运行的“环境”执行上下文简称 EC是 JavaScript 引擎在执行代码时创建的一个抽象环境它包含了当前代码运行所需的所有信息变量、函数、this指向、作用域链等。2.1 三种执行上下文1. 全局执行上下文Global EC- 最外层环境在浏览器中就是window对象。- 只存在一个程序启动时创建。2. 函数执行上下文Function EC- 每当调用一个函数时都会为该函数创建一个全新的执行上下文。- 数量不限函数调用结束则销毁闭包除外。3. Eval 执行上下文了解即可2.2 执行上下文栈ECS调用栈JavaScript 是单线程的同一时间只能做一件事。引擎用执行上下文栈也叫调用栈来管理所有的执行上下文。function first() { console.log(first) second() } function second() { console.log(second) third() } function third() { console.log(third) } first()执行过程如下1. 压入全局上下文栈底。2. 调用 first() 压入 first 上下文。3. first 中调用 second()压入 second。4. second 中调用 third()压入 third。5. third 执行完毕弹出接着 second 弹出最后 first 弹出。6. 栈中只剩下全局上下文。这就是我们常说的 “调用栈”栈顶永远是当前正在执行的上下文。三、执行上下文的生命周期创建与执行每个执行上下文都会经历两个阶段1. 创建阶段进入上下文2. 执行阶段代码执行在创建阶段引擎会做三件重要的事- 创建 变量对象Variable ObjectVO- 建立 作用域链- 确定 this 的指向下面我们逐一拆解。3.1 变量对象VO与活动对象AO变量对象 是执行上下文中用于存储变量、函数声明和函数形参的容器。- 在全局上下文中变量对象就是全局对象本身浏览器中为 window。- 在函数上下文中变量对象被称为 活动对象Activation ObjectAO因为它额外包含了 arguments 对象和形参。创建阶段VO/AO 会进行变量提升function test(a) { console.log(a) // 1 console.log(b) // undefined console.log(c) // undefined var b 2 function c() {} } test(1) // 在创建阶段AO 大致如下 AO { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c() {} }注意函数声明会被完整提升赋值而变量声明只提升声明赋值留在原地。3.2 作用域链 查找变量的“路线图”作用域链 是一个由当前上下文及其所有父级上下文的变量对象按顺序组成的链表。当访问一个变量时引擎会沿着这条链从顶端当前 AO开始查找找不到则向上层查找直到全局对象如果还找不到则抛出 ReferenceError。3.2.1 函数的 [[Scope]] 属性每个函数在定义声明时就会创建一个内部属性 [[Scope]]它保存了当前作用域链的快照。当函数被调用时新生成的执行上下文的作用域链 [[Scope]] 自己的 AO。这正是作用域链是静态词法作用域的原因 —— 它取决于函数定义的位置而非调用位置。var x 10 function foo() { console.log(x) // 输出 10 还是 20 } function bar() { var x 20 foo() } bar() // 10因为 foo 定义在全局其 [[Scope]] 中只有全局变量 x即使被 bar 调用也访问不到 bar 的局部变量。这就是词法作用域的精髓。3.2.2 作用域链的完整构建过程我们用一个综合性例子来走一遍var globalVar global function outer(outerParam) { var outerVar outer function inner(innerParam) { var innerVar inner console.log(globalVar, outerParam, outerVar, innerParam, innerVar) } inner(innerArg) } outer(outerArg)步骤拆解1. 全局创建阶段全局 VO { globalVar: undefined, outer: function }作用域链 [全局VO]。2. 执行 outer(outerArg)- 创建 outer 的 AO{ arguments: {0:outerArg}, outerParam: outerArg, outerVar: undefined, inner: function }- outer 的作用域链 [[Scope]]即全局VO 自己的 AO [全局VO, outer AO]- 执行代码赋值 outerVarouter并定义 inner 函数。此时 inner 的 [[Scope]] outer 当前的作用域链即 [全局VO, outer AO]。3. 执行 inner(innerArg)- 创建 inner 的 AO{ arguments: {0:innerArg}, innerParam: innerArg, innerVar: undefined }- inner 的作用域链 [[Scope]]即 [全局VO, outer AO] 自己的 AO [全局VO, outer AO, inner AO]- 执行 console.log沿链查找各变量找到后输出。运行结果global outerArg outer innerArg inner四、经典闭包作用域链的“保鲜”闭包的定义函数 其词法环境的组合。更直白说闭包是指一个函数能够记住并访问其定义时的作用域即使该函数在其作用域之外执行。闭包的产生正是因为函数定义时的 [[Scope]] 被保留了下来即使外层函数执行完毕并弹出栈其 AO 依然被内部函数的 [[Scope]] 引用不会被垃圾回收。function createCounter() { let count 0 return function() { count console.log(count) } } const counter createCounter() counter() // 1 counter() // 2分析- createCounter 执行后返回匿名函数该函数的 [[Scope]] 包含了 createCounter 的 AO其中count0。- createCounter 执行完毕其 EC 弹出栈但它的 AO 因被内部函数引用而不被销毁。- 每次调用 counter都会沿着作用域链找到那个 count并修改它。这就是闭包的本质 —— 作用域链的持久化引用。五、进阶块级作用域与 let / const 的“特殊”行为ES6 引入了块级作用域let 和 const 没有变量提升实际上是有提升但存在暂时性死区TDZ。console.log(a) // undefined var a 1 console.log(b) // ReferenceError: Cannot access b before initialization let b 2在块级作用域的执行上下文中let/const 声明的变量会绑定到块级作用域的“环境记录”中但初始化为 uninitialized进入 TDZ直到赋值语句才变为可访问。这并不影响作用域链的结构只是查找规则多了一个“是否已初始化”的判断。六、分析题目一var arr [] for (var i 0; i 3; i) { arr.push(function() { console.log(i) }) } arr[0]() // ? arr[1]() // ?/ arr[2]() // ?答案3 3 3所有函数都打印 3解析var i 是全局变量循环结束后 i3。三个函数共享同一个 i执行时从全局 VO 中找到 i3。若想打印 0,1,2可用闭包或 let- 闭包版给每个函数绑定一个独立的作用域。- let 版for (let i0; ...) 每次迭代会创建新的块级作用域保存当时的 i。题目二function Foo() { this.a 1 return {} } var f new Foo() console.log(f.a) // undefined解析构造函数如果显式返回一个对象则 new 的结果为该对象而非 this。这里 return {} 覆盖了默认的 this。题目三闭包与循环function test() { var x 10 function inner() { console.log(x) } x 20 return inner } var fn test() fn() // 20解析inner 的 [[Scope]] 捕获的是 test 的 AO 的引用x 在 test 执行阶段被修改为 20所以输出 20闭包保存的是引用不是快照。七、总结概念核心执行上下文代码运行的环境分为全局、函数、eval由栈管理变量对象/活动对象存储变量、函数声明、形参等创建阶段完成提升作用域链由当前 VO 及所有外层 VO 构成查找变量时自上而下词法作用域作用域由函数定义位置决定与调用位置无关闭包函数保留其定义时的作用域链引用使得外层 AO 得以持久this与作用域链无关由调用方式决定在此不展开理解执行上下文和作用域链你就掌握了 JavaScript 的“内力”。后续无论学习异步、原型链还是框架源码都会事半功倍。核心代码在编写时确定作用域在运行时创建上下文在栈中切换在链中查找。