# 四.变量对象

前言

  • 变量对象会保存变量声明(var)、函数参数(arguments)、函数定义(function)
    • 变量对象会首先获得函数的参数变量和值
    • 获取所有function进行函数声明,函数名为变量对象的属性名,值为函数对象,如果属性已经存在,值会用新值覆盖
    • 再依次所有的 var 关键字进行的变量声明,每找到一个变量声明,就会在变量对象上建一个属性,值为undefined,如果变量名已经存在,则会跳过,并不会修改原属性值,let声明的变量并不会在此阶段进行处理
    • 函数声明优先级更高,同名的函数会覆盖函数和变量,但同名var变量并不会覆盖函数,执行阶段重新赋值可以改变原有的值

函数在运行的瞬间,生成一个活动对象(Active Object),简称 AO

  • 分析参数
    • 函数接收形式参数,添加到 AO 的属性,并且这个时候值为 undefined,即 AO.[xxx]=undefined
    • 接收实参,添加到 AO 的属性,覆盖之前的 undefine
  • 分析变量声明
    • 如果上一步分析参数中 AO 还没有xxx属性,则添加 AO 属性为 undefined,即 AO.[xxx]=undefined
    • 如果 AO 上面已经有xxx属性了,则不作任何修改
  • 分析函数声明
    • 如果function [xxx](){}把函数赋值给 AO.[xxx],覆盖上一步分析的值

# 1.全局执行上下文中

  • 全局执行上下文中只有一个,在客户端中一般由浏览器创建,也就是我们熟知的 window 对象,我们能通过 this 直接访问到它
  • window 对象也是 var 声明的全局变量的载体。我们通过 var 创建的全局对象,都可以通过 window 直接访问

# 1.1 var

var 只提前声明,在代码执行的过程中才定义赋值

  • 代码
console.log(a)
var a = 1
1
2
  • 执行过程
// 创建阶段
var globalEC = {
  VO: {
    a: undefined,
  },
}
var ECStack = [globalEC]
// 执行阶段
console.log(globalEC.VO.a)
globalEC.VO.a = 1
1
2
3
4
5
6
7
8
9
10

# 1.2 function

function 声明和定义都完成了

  • 代码
var a = 1
function fn(m) {
  console.log("fn")
}
function fn(m) {
  console.log("new_fn")
}
function a() {
  console.log("fn_a")
}
console.log(a)
fn(1)
var fn = "var_fn"
console.log(fn)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 执行过程
// 创建阶段
var globalEC = {
  VO: {
    a: function a() {
      console.log("fn_a")
    },
    fn: function fn(m) {
      console.log("new_fn")
    },
  },
}
var ECStack = [globalEC]
// 执行阶段
globalEC.VO.a = 1
console.log(globalEC.VO.a)
globalEC.VO.fn(1)
globalEC.VO.fn = "var_fn"
console.log(globalEC.VO.fn)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 函数体中 return 后面的返回值不预解释,但是 return 后面的代码,虽然不执行,但是要预解释
function fn() {
  console.log(num)
  return function () {}
  var num = 12
}
fn()
// - 预解释
//   - function fn(){"console.log(num);return function(){};var num=12"}
// - 代码执行

//   - fn()
//   - 私有作用域下的预解释
//     - function(){""}
//     - num = undefined
//   - 私有作用域下代码的执行

//     - console.log(num)//undefined
//     - num= 12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 全局上下文的变量对象

  • 在浏览器里,全局对象为window
  • 全局上下文的变量对象为window,而且这个变量对象不能激活变成活动对象
  • 只要窗口打开,全局上下文会一直存在,所有的上下文都可以直接访问全局上下文变量对象上的属性
  • 只有全局上下文的变量对象允许通过 VO 的属性名称来间接访问,在函数上下文中是不能直接访问 VO 对象的
  • 未进入执行阶段前,变量对象中的属性都不能访问!但是进入到执行阶段后,变量对象转变成了活动对象,里面的属性都能被访问了,对于函数上下文来讲,活动对象与变量对象其实都是同一个对象,只是处于执行上下文的不同生命周期

# 1.3 语句块

不管条件是否成立,都会进行预解释

if (!("a" in window)) {
  var a = 1
}
console.log(a)
// - 预解释
//   - a=undefined//window.a=undefined(全局作用域下定义的)
// - 代码执行
//   - !('a' in window)//false
//   - console.log(a)
//     3.2 只对"="左边的进行预解释,右边的是值,不进行预解释
1
2
3
4
5
6
7
8
9
10
fn()
var fn = function () {
  console.log(1)
}
// - 预解释
//   - fn = undefined
// - 代码执行
//   - fn()//报错
1
2
3
4
5
6
7
8

# 1.4 自执行函数

自执行函数的定义和执行是一起完成的。但是在执行的过程中,它的私有作用域中是要进行预解释的

;(function (num) {
  console.log(num)
})(100)
// - 预解释
//   - 无
// - 代码执行
//   - console.log(100)
1
2
3
4
5
6
7

# 1.5 变量重名

  • 重命名变量 a 又被声明了一次,浏览器在预解释的时候发现已经有一个同名变量 a 了,就不去重复声明了,并且在声明的时候也没有被赋值,则变量 a 还是保持原来的值不变。
  • 如果预解释的时候发现重名了,不重新的声明,但是需要重新的定义。(即后面发现是重名的变量,如果该重名变量只是声明没有定义时,可以忽略,但是该重名变量声明并赋值,是需要重新声明的。)
function a(x) {}
var a
console.log(a)
// - 预解释
//   - function a(x){""}
// - 代码执行
//   - console.log(a)
1
2
3
4
5
6
7

# 2.函数执行上下文中

  • 在 JS 执行过程会产生多个执行上下文,JS 引擎会有栈来管理这些执行上下文
  • 执行上下文栈也叫做调用栈,执行栈用于存储代码执行期间创建的所有上下文,具有 LIFO(先进后出)的特性
  • 栈底永远是全局上下文,栈顶为当前正在执行的上下文
  • 当开启一个函数执行时会生成一个新的执行上下文并放入调用栈,执行完毕后自动出栈。
function one() {
  var a = 1
  debugger
  function two() {
    var b = 1
    debugger
    function three() {
      var c = 1
    }
    three()
    debugger
  }
  two()
  debugger
}
one()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var globalExecuteContext = {
  VO: {
    setTimeout: "setTimeout",
  },
}
var executeContextStack = [globalExecuteContext]
var oneExecuteContext = {
  VO: { a: 1 },
}
executeContextStack.push(oneExecuteContext)
var towExecuteContext = {
  VO: { b: 2 },
}
executeContextStack.push(twoExecuteContext)
var threeExecuteContext = {
  VO: { c: 3 },
}
executeContextStack.push(threeExecuteContext)
console.log(executeContextStack)
executeContextStack.pop()
executeContextStack.pop()
executeContextStack.pop()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2.1 激活对象

  • 在函数的调用栈中,如果当前执行上下文处于函数调用栈的顶端,则意味着当前上下文处于激活状态,此时变量对象称为活动对象(AO)
  • 活动变量包含变量对象所有的属性,并有包含this指针
function one(m) {
  function two() {
    console.log("two")
  }
}
one(1)
1
2
3
4
5
6

VO 和 AO

// 执行阶段 VO=>AO
let VO = (AO = {
  m: 1,
  two: () => {
    console.log("two")
  },
})
let oneEC = {
  VO,
  this: window,
  scopeChain: [VO, globalVO],
}
1
2
3
4
5
6
7
8
9
10
11
12

# 3.Eval 函数执行上下文中

  • 预解释(变量提升),是指在当前作用域下,JS 代码从上到下执行之前,浏览器会默认先把带 var 和 function 关键字的进行提前声明或者定义。
  • 声明:只声明,没有定义,如 var num,此时 num 的默认值是 undefined
  • 定义:即赋值操作