讲清楚之 javascript 变量对象

变量对象

这一节聊一下变量对象。都是干货(^▽^)javascript

变量对象是函数运行时数据的集合,存储了在上下文中定义的变量和函数,不一样的函数的变量对象稍有不一样。java

仍是从上下文提及,javascript 引擎执行到函数的时候会向上下文栈中压入一个上下文。
上下文中包含:es6

name -
变量对象(VO, variable object) 当前函数定义的变量、函数、参数
做用域链(Scope chain) 源代码定义时造成的做用域链
this

伪代码:浏览器

// 全局上下文的伪代码
windowEC = {
    VO: Window,
    scopeChain: {},
    this: Window
}
  • 做用域链为当前函数提供上层可访问变量和函数的有序集合;
  • this为函数提供运行时对象环境;
  • 变量对象提供当前函数定义时的变量和函数;

上下文生成时包含的做用域链的造成,和this的指向原则在前面的章节已经梳理过。
javascript 中主要有全局上下文、函数上下文。先了解一下函数调用时上下文栈的变化。函数

上下文栈状态

当函数被调用的时候,一个新的上下文会被加入到上下文栈的顶部, 然后会运行函数内部的代码块。this

因此一个执行上下文或者说函数的生命周期分为上下文建立阶段、函数运行阶段,不一样阶段上下文栈状态和变量对象属性会不同。spa

示例代码:code

function foo (a) {
    var b = 1
    function c () {
        console.log(a + b) // 100
    }
    c()
}
foo(99)

执行上下文建立阶段

在这个阶段上下文对象会生成,并建立变量对象、建立做用域链、肯定 this 的指向。对象

说一千道一万还不如来张图干脆。blog

foo 函数上下文建立完成后的栈状态示意图:

图片描述

注意:此时函数内表达式和语句未执行,变量对象属性值是根据规则被设置为初始值的。

运行阶段

上下文生成完成后,进入函数运行阶段。依次按照代码顺序执行函数内代码(变量的赋值、表达式计算、语句的执行,其余函数调用等)。

在该阶段会访问或设置变量对象(活动对象)属性的值。当执行完 foo 函数内第一行代码var b = 1,此时栈状态:

图片描述

以此类推,每次函数表达式执行时都会在执行上下文长中获取标识符的值,经过运算后又将结果保存在指定的标识符里。所以执行上下文为函数提供一个相似于寄存器的概念来管理数据的功能。

当函数执行完后,对应的执行上下文被销毁。JavaScript 执行器会返回父函数或依据源代码顺序跳转到其余函数。

函数上下文

在函数上下文中,咱们用活动对象(activation object, AO)来表示变量对象。

活动对象和变量对象实际上是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,因此才叫 activation object ,而只有被激活的变量对象,也就是活动对象上的各类属性才能被访问。

活动对象也是在进入函数上下文时刻被建立的,活动对象变量对象的一种激活状态。因此当你把变量对象活动对象记混淆了没关系,由于他们本质上对咱们理解函数调用时的细节没有影响。我在大多数时候也直接用变量对象来表述。

变量对象的建立

变量对象的建立主要是进行标识符值类型的申明和初始化,听从下面这3条原则:

  1. 生成 arguments 对象。检查当前上下文的形参,生成属性与属性值对象(key: value)

    • 当形参没有被赋值时, 属性值被设置为 undefined
  2. 在变量对象上创建函数索引。检查当前做用域定义的 function,在变量对象中以函数名为 key, 以函数所在内存地址为 value 创建索引。

    • 若是函数名已经在变量对象中,则该函数名对应的函数会被新的函数替换。因此函数能够被重复定义,后定义的函数会覆盖掉先前定义的。
  3. 申明变量。检查当前做用域定义的变量,在变量对象中以变量名为 key, 以 undefined 为值挂载内部变量。

    • 若是新申明的变量名与已经申明的形参名、函数名相同,则申明会被抛弃。

因此须要注意的是函数申明比变量申明优先级高,一旦函数申明占用了某一个标识符,后续的变量申明若是使用的是先前使用过的函数标识符, 则该变量申明无效。

栗子1:

function foo () {
    function too() {
    }
    var too
    console.log(typeof too)
}
foo()
// function
// 变量 too 的申明无效

咱们把上面的栗子稍做修改, 栗子2:

function foo () {
    function too() {
    }
    var too = 1
    console.log(typeof too)
}
foo()
// number
// 变量 too 的值类型为 number

变量 too 的值类型为 number, 看到这个栗子你们可能会疑惑, 由于根据上面的3条规则, too 的第二次申明应该是无效的且 too 的类型应该为 function。 其实 too 的值类型在上下文建立阶段确实是 function, 因为 javascript 是动态弱类型语言, 在上下文执行阶段 var too = 1 实质是在给 too 赋值而且发生了隐式类型转换, 因此在执行阶段 too 变成了 number 类型。es6 语法中已经不建议使用var 来申明变量了, 而是使用let 来申明局部变量,从语法层面强制避免了重复的变量申明, 这样栗子2中的状况会直接报错。

将上面的栗子再次修改,进一步探索:

function foo () {
    function too() {
    }
    console.log(typeof too) // function
    var too = 1
    console.log(typeof too) // number
}
foo()

foo 函数运行时会先打印 ‘function’,而后打印 ‘number’。首先表达式console.log(typeof too)执行时标识符too在上下文建立阶段被初始化为一个函数。var too = 1执行后标识符too被赋值为 1,因此第二次console.log(typeof too)的时候输出的是number.

再再举一个例子:

function foo () {
    console.log(a)
    console.log(bar)
    var a = 1
    function bar() {
        return 2
    }
}
foo()
// undefind
// ƒ bar() {
//      return 2
//  }

上下文建立阶段解析函数内代码块后,会在变量对象上添加 ‘a’, ‘bar’ 两个标识符,并填充相应的值结束上下文的建立阶段进入foo 函数的执行阶段。

在执行阶段 foo 函数体第一行表达式要求打印输出 a 的值, 因为 console.log(a) 以前没有对 a 进行任何赋值操做,根据规则此时 a 的值为 undefind 因此输出 'undefind'。函数体内第二行要求打印输出 bar 的值,根据规则标识符 'bar' 对应的是函数,因此 'bar' 的值为函数实体,且在 console.log(bar) 以前也未对 bar 作赋值操做,因此打印出来的是该函数。

变量对象建立完,函数运行前的变量对象是这样的:

// VO 为 Variable Object的缩写,即变量对象
VO = {
    arguments: {...},
    bar: <bar reference>  // 表示bar的地址引用
    a: undefined
}

变量对象(活动对象)的建立过程实质上就是咱们常常提起的函数变量提高,这里3条原则才是变量提高的本质。

若是没有理解透彻能够回头看看前面的内容。

全局上下文

在浏览器中,全局对象就是 window ,也是浏览器提供的预约义变量对象,能够经过 thisself 引用。全局对象提供浏览器预置对象 Array、Object、console.log、alert 等,全局上下文的生命周期和函数的生命周期同样,只要程序运行不结束全局上下文就一直存在。其余全部的上下文环境,都能直接访问全局上下文的属性。

全局对象是预约义的对象,做为 JavaScript 的全局函数和全局属性的占位符。经过使用全局对象,能够访问全部其余全部预约义的对象、函数和属性。在顶层 JavaScript 代码中,能够用关键字 this 引用全局对象。由于全局对象是做用域链的头(最外层做用域),这意味着全部非限定性的变量和函数名都会做为该对象的属性来查询。例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是做用域链的头,还意味着在顶层 JavaScript 代码中声明的全部变量都将成为全局对象的属性。

经过thisseif访问全局对象:

console.log(this)
console.log(self)
相关文章
相关标签/搜索