[js学习笔记] 函数

[前言] 在整理闭包笔记的时候,对函数部分笔记的简单整理和理解。

函数

我们都知道函数都有自己的私有的作用域,通常情况下函数以外地方是不能访问函数中的变量的,但是函数内部是可以访问其外层的作用域。

1
2
3
4
5
6
var outer=1;
function inner(){
var inner=2;
console.log(outer) // 1
}
console.log(inner) // undefined

函数只取决于被定义的环境,而非被调用时产生的环境。严谨地说,即语法定义时所产生的作用域就是 词法作用域,因而当词法分析器处理代码的时候将会保持作用域不变。
当退出函数的时候,函数中的局部变量就会随即销毁。
对此,JS权威指南中有一句很精辟的描述:JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里.

1
2
3
4
5
6
function test(){
var a=0;
console.log(a++)
}
test() // 0
test() // 0

Execution Context(运行期上下文)、Activation Object(活动对象)

当开始执行函数时,会创建一个 Execution Context的内部对象,该对象定义了函数运行时的作用域环境,可理解为一个记录当前执行的方法 外部描述信息的对象,记录所执行方法的类型,名称,参数和活动对象(activeObject)。(注意这里要和函数创建时的作用域链对象[[scope]]区分,这是两个不同的作用域链对象,这样分开我猜测一是为了保护[[scope]],二是为了方便根据不同的运行时环境控制作用域链。函数每执行一次,都会创建单独的Execution Context,也就相当于每次执行函数前,都把函数的作用域链复制了一份到当前的Execution Context中)
此时,在Execution Context的作用域链的顶部会插入一个新的对象,叫做 Activation Object,可理解为一个记录当前执行的方法 内部执行信息的对象,记录内部变量集(variables)、内嵌函数集(functions)、实参(arguments)、作用域链(scopeChain)等执行所需信息,其中内部变量集(variables)、内嵌函数集(functions)是直接从第一步建立的语法分析树复制过来的
当函数执行结束之后,就会销毁Execution Context,也就会销毁Execution Context的作用域链,当然也就会销毁Activation Object(但如果存在闭包,Activation Object就会以另外一种方式存在,这也是闭包产生的真正原因,具体的我们稍后讨论。)。

同名变量和形参都引用同一个内存地址

我们先现在来看一个栗子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var x=1;
function funcA(i,j){
console.log(i)
console.log(arguments[0])
var i=100;
console.log(i)
console.log(arguments[0])
function funcB(){
var k=200;
console.log(i)
console.log(k);
}
funcB()
}
funcA(300,400)

你的答案是 300 300 100 300 200 100 200 吗?
但正确答案是
// funcA内
300
300
100
100 // 差别在这里,不是300
// funcB内
100
200

这是因为,在一个方法中,同名的实参,形参和变量之间是引用关系,也就是Activation Object中 同名变量和形参都引用同一个内存地址
现在我们简单构造Execution Context和Activation Object的伪代码来理解一下~

  • 运行期上下文

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var ExecutionContext ={
    window:{
    type: "global",
    name: "global",
    body: ActiveObject.window
    },
    funcA:{
    type: "function",
    name: "funcA",
    body: ActiveObject.funcA,
    scopeChain: this.window.body
    },
    funcB:{
    type: "function",
    name: "funcB",
    body: ActiveObject.funcB,
    scopeChain: this.funcA.body
    },
    }
  • 活动对象

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    window.ActiveObject={
    variables:{
    x:{value:1}
    },
    functions:{
    funcA:this.funcA
    },
    };
    funcA.ActiveObject={
    variables:{
    i:{value:100}
    },
    functions:{
    funcB: SyntaxTree.funcB
    },
    parameters:{ // 形参
    i: this.variables.i ,
    j: {value: 400} // 重点
    },
    arguments:[this.parameters.i,this.parameters.j] // 实参
    };
    funcB.ActiveObject={
    variables:{
    k:{value:200}
    },
    functions:{},
    parameters:{},
    arguments:[]
    }
    }

在上文中的ActiveObject对象伪代码分析我们可以看到,parameters中i是指向variables中的i,也就是说形参中的i和局部变量i指向同一块存储空间。
注意的是,每个函数被调用的时候都会生成独立的AO,上面的伪代码仅用于理解。

变量查找规则是首先在当前执行环境的 ActiveObject 中寻找,没找到,则顺着执行环境中属性 ScopeChain 指向的 ActiveObject 中寻找,一直到 Global Object(window) ,方法内变量的生存周期取决于方法实例是否存在活动引用,如没有就销毁活动对象。

最后

以上仅为部分笔记的整理,如果有理解错的地方欢迎指正~

参考链接

文档