Lexical Environment 词法环境

531 阅读6分钟

Lexical Environment 词法环境

JavaScript 中,每个运行的函数,代码块,和整个程序都有一个叫做 Lexical Environment (词法环境)的隐藏的内部关联对象。

Lexical Environment 对象由两部分组成:

  • Environment Record,一个把所有局部变量作为属性的对象(还包括一些其他信息比如this值)
  • outer lexical environment 的引用,指向当前代码块(花括号)之外的词法环境

变量就是 Environment Record 这个特殊对象的属性,“修改或访问一个变量” 意味着“修改或访问 Environment Record 对象的一个属性”。

浏览器中,最顶层的词法环境叫 global Lexical Environment,它没有 outer 引用,指向null.

Lexical Environment 是一个存在于规范里的理论对象,不能通过代码直接操作它

小结:

  1. 变量就是一个特殊内部对象的属性,跟当前运行的代码块/函数/脚本有关
  2. 操作变量实际上就是操作对象的属性

函数声明和变量声明

当一个 Lexical Environment 被创建时,函数声明立刻生效,函数立即可用;而 let 变量声明直到运行到那一行时才生效

say('Tom') 
// cannot access 'phrase' before initialization 

let phrase = "Hello"

// say('Tom')
// Hello,Tom

function say(name) {
    console.log(`${phrase}, ${name}`)
}

脚本启动的那一刻 lexical environment 便创建完成,函数声明立刻生效,变量phrase还未声明,变量为 uninitialized 状态


let phrase 
say('Tom')
// undefined,Tom

phrase = 'Hello'
function say(name) {
    console.log(`${phrase}, ${name}`)
}

变量声明了但未赋值,所以为 undefined

闭包

每个函数在被创建时便有一个隐藏属性 [[Environment]], 指向该函数诞生时所在的环境。

当函数运行时,一个新的 Lexical Environment 被创建,并且引用隐藏属性 [[Environment]] 指向的外部 Lexical Environment

function makeCounter() {
    let count = 0
    return function () {
        console.log(count++)
    }
}

let counter = makeCounter() 
let counter1 = makeCounter()
counter() // 0
counter() // 1
counter() // 2
counter1() // 0

counter 变量保存了返回函数的 [[Environment]] 属性,即其诞生环境,即 makeCounter 函数的 Lexical Environment。 当counter被调用时,新 Lexical Environment 生成,并指向外部环境。 操作count变量时,先搜索自身的 Lexical Environment,未找到,再向上搜索makeCounter 的 Lexical Environment,找到count变量并修改,变量在其诞生的环境中被修改并被保存。变量counter1保存了一个新调用的makeCounter函数的 Lexical Environment,所以count值重新为0。

垃圾回收

通常来说,一个函数调用完毕后,其 Lexical Environment 会被销毁,因为没有东西引用它。JavaScript 中,只有被引用的对象才能保存在内存中。保存 Lexical Environment 的唯一方法就是创建内嵌函数引用它。

function f() {
    let value = 123
    return function() {
        console.log(value)
    }
}
let g = f()
g = null

变量g将函数f的 Lexical Environment 保存在内存中。

随后g指向null,函数f的 Lexical Environment 被回收。

V8 引擎的优化

理论上来讲,一个内嵌函数被保存,其诞生环境中的所有变量也被保存。但 V8(Chrome,Opera) 引擎会对其优化,如果外部变量没有被使用,那么它也会被销毁。 在 Chorme 浏览器中调试如下代码

function f() {
    let value = Math.random()
    
    function g() {
        debugger // in console: type alert(value); No such variable!
    }
    return g
}
let g = f()
g()

调试上述代码时,value变量不存在,因为它没有被使用, V8 引擎将它优化了。

使用 Chorme/Opera debug 时可能会遇到上述问题。这不是 bug,而是 V8 引擎的一个特性。

例1

function makeArmy() {
  let shooters = []
  
  for (var i = 0; i < 10; i++) {
    let shooter = function() {
      console.log(i)
    }
    shooters.push(shooter)
  }
  return shooters
}
let army = makeArmy()
army[0]() // 10
army[1]() // 10
army[2]() // 10
function makeArmy() {
  let shooters = []

  var i = 0
  while (i < 10) {
    let shooter = function() {
      console.log(i)
    }
    shooters.push(shooter)
    i++
  }
  return shooters
}
let army = makeArmy()
army[0]() // 10
army[1]() // 10
army[2]() // 10

以上代码中,shooters 列表中储存了9个 shooter 闭包函数的地址,调用 makeArmy() 后,循环完成,存在于循环的 Lexical Environment 中的变量 i 的值为10。所以再调用shooter函数时打印出的i均为10。

解决办法,:

function makeArmy() {
  let shooters = []
  
  for (let i = 0; i < 10; i++) {
    let shooter = function() {
      console.log(i)
    }
    shooters.push(shooter)
  }
  return shooters
}
let army = makeArmy()
army[0]() // 0
army[1]() // 1
army[2]() // 2

上述代码中,在每一次for循环中,let 语句都会新建一个 Lexical Environment,并且会在其中新建一个i变量,且i++在第二个新环境创建之后执行。第一个环境包含i = 0shooter函数;第二个环境包含i++shooter函数,以此类推...

shooter函数执行时,会打印其相应的 Lexical Environment 中的i变量。所以依次输出0,1,2

function makeArmy() {
  let shooters = []
  
  var i = 0
  while (i < 10) {
    let j = i
    let shooter = function() {
      console.log(j)
    }
    shooters.push(shooter)
    i++
  }
  return shooters
}
let army = makeArmy()
army[0]() // 0
army[1]() // 1
army[2]() // 2

上述代码逻辑更加清晰,可视为for循环的内部逻辑的实现

例2:

let i = 0
for (i = 0; i < 6; i++) {
  setTimeout(() => {
    console.log(i)
  })
}
// 6,6,6,6,6,6

所有循环执行完后变量i = 6,再执行setTimeout()异步任务,输出6次i,即6个6

for (let i = 0; i < 6; i++) {
  setTimeout(() => {
    console.log(i)
  })
}
// 0,1,2,3,4,5

第一次循环let 语句新建第一个 Lexical Environment,包含初始值i = 0setTimeout()异步任务;第二次循环let创建第二个环境,先copy上一个i变量的值新建一个新的i,再执行主任务i++,此时i = 1setTimeout()任务在队列中未执行;以此类推,第三个环境中i = 2... 循环完成后,执行队列中的任务,输出相应环境中的i值,即0,1,2,3,4,5

其他方法:

let i 
for (i = 0; i < 6; i++) {
    let j = i
    setTimeout(() => {
        console.log(j)
    })
}
// 0, 1, 2, 3, 4, 5

// 或者
function helper(n) {
  setTimeout(() => {
    console.log(n)
  })
}
let i
for (i = 0; i < 6; i++) {
  helper(i)
}
// 0, 1, 2, 3, 4, 5

小结:

  • 利用let在循环内创建新的 Lexical Environment 和新的i变量保存不同的值
  • 也可通过调用新函数的方式创建不同的 Lexical Environment

终极变态例子:

for (let i = 0; i < 6; i++) {
  setTimeout(() => {
    console.log(i)
  })
  i++
}
// 1,3,5
for (let i = (setTimeout(() => console.log(i)), 0); i < 6; i++) {
    i++
    console.log(i)
}
// 1, 3, 5, 0

(setTimeout(() => console.log(i)), 0)返回值为0,let 新建第一个 Lexical Environment 中包含:i = 0,和setTimeout()任务;然后再新建第二个环境,包含一个新的i = 0变量,再执行{}内的主任务i++,所以第二个环境中i = 1,输出为1;第三个环境再 copy 上一个 i 值,执行两次i++,输出为3,以此类推...

主任务执行完后,执行setTimeout(),输出第一个环境中的i,即为0。

来源