这次啃一个 GitHub repo 上的几篇文章,虽然 Blogger 讲得算很易懂了,但还是要花时间好好吸收一下。

作用域

JavaScript 采用词法作用域(静态作用域),函数的作用域在函数定义时就已经确定。与之相反的动态作用域,函数的作用域是在函数调用时才决定。

// example-1
var value = 1;

function foo() {
    console.log(value);
    // 定义: 函数内无局部变量 value,获取全局变量 value = 1
}

function bar() {
    var value = 2;
    foo();
    // 调用 foo(): 不再获取 bar() 内的局部变量 value
}

bar();
// 1

如果是动态作用域,则是:执行 foo(),依然先从 foo() 内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar() 内部查找 value,结果为 2。

接下来是两段代码,虽然执行结果相同,但在执行上却有何差异?

// example-2 Code
var scope = "global scope";
function checkscope() {
    var scope = "local scope";
    function f() {
        return scope;
    }
    return f();
}
checkscope(); // "local scope"

//example-3 Code
var scope = "global scope";
function checkscope() {
    var scope = "local scope";
    function f() {
        return scope;
    }
    return f;
}
checkscope()(); //"local scope"

找到了 SegmentFault 上的一个提问,解释的较为详细了。

从代码的行文来看,会发现:

example-2checkscope() 返回的是内部函数 f() 的执行结果; example-3checkscope() 返回的是内部函数 f,然后再执行返回的函数。

順序執行?

JavaScript 引擎并非以行来分析和执行代码,而是以段来分析执行。当执行一段代码的时候,会进行一个「准备工作」,比如下例的变量提升和函数提升:

// example-4 Code
var foo = function () {
    console.log('foo1');
}
foo();  // foo1

var foo = function () {
    console.log('foo2');
}
foo(); // foo2

// example-5 Code
function foo() {
    console.log('foo1');
}
foo();  // foo2

function foo() {
    console.log('foo2');
}
foo(); // foo2
// example-4 Real 变量提升
var foo;

foo = function() {
    console.log('foo1');
}
foo(); //foo1

foo = function() {
    console.log('foo2');
}
foo(); //foo2

//example-5 Real 函数提升
var foo;
foo = function () {
    console.log('foo1');
}
foo = function () {
    console.log('foo2');
}
foo(); // foo2
foo(); // foo2
var a = "global var";

function foo(a) {
    a = "function var";
    console.log(a);
}

foo("function arg"); // function var
// function var > function arg > global var

执行上下文栈

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文,可以定义它为一个数组:ECStack = [ ];

当 JS 需要解释执行代码的时候,最先遇到全局代码,所以在初始化时先向栈内压入一个全局执行上下文,表示为 globalContext。只有当整个应用程序结束时,ECStack 才会被清空。所以程序结束之前,ECStack 底部永远有 globalContext

// example-6 Code
function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

当上例的代码执行时,执行上下文栈的处理为:

// example-6  Execute
ECStack.push(<fun1> functionContext); // fun1()
ECStack.push(<fun2> functionContext); // fun1() 调用fun2(),创建 fun2() 的执行上下文
ECStack.push(<fun3> functionContext); // fun2() 调用fun3(),创建 fun3() 的执行上下文
ECStack.pop(); // fun3() 执行完毕
ECStack.pop(); // fun2() 执行完毕
ECStack.pop(); // fun1() 执行完毕
// JavaScript 接着执行下面的代码,但是 ECStack 底层永远有个 globalContext

最后我们回到 example-2example-3 中,模拟执行上下文栈。此时能看出两段代码的区别:

// example-2 Execute
ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop(<f>);
ECStack.pop(<chechscope>);

// example-3 Execute
ECStack.push(<checkscope> functionContext);
ECStack.pop(<checkscope>);
ECStack.push(<f> functionContext);
ECStack.pop(<f>);

对于每个执行上下文,都有三个重要属性:

  • 变量对象 (Variable object,VO)
  • 作用域链 (Scope chain)
  • this

下面具体讨论上面提出的三个属性。

变量对象

变量对象存储上下文中定义的变量和函数声明。下面分情况讨论全局上下文的变量对象,和函数上下文的变量对象。

全局上下文

全局上下文中的变量对象,就是全局对象。是由 Object 构造函数实例化出的对象。

console.log(this instanceof Object);

全局对象预定义了一些函数和属性,还可以作为全局变量的宿主。

console.log(Math.random());
console.log(this.Math.random());

var a = 1;
console.log(this.a);
console.log(window.a);

函数上下文

在函数上下文中,通常使用活动对象(activation object, AO)表示变量对象。

活动对象无法直接通过 JS 进行引用,只有当 AO 进入执行上下文中才会被创建,通过函数的 arguments 属性初始化。

执行过程

接下来以下面的代码为例,解释执行上下文的过程。

// example-7 Code
function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};
  b = 3;
}

foo(1);
  1. 进入执行上下文,会依次导入:
    • 导入函数的所有形参
    • 函数声明
    • 变量声明
// example-7 Step-1
VO = {
    arguments: {
        0: 1,
        length: 1
    },                              // 形参
    a: 1,                           // 形参,值为实参或 undefined
    b: undefined,                   // 变量声明
    c: reference to function c(){}, // 函数声明 地址引用
    d: undefined                    // 变量声明
}
  1. 代码执行,给变量声明赋值
// example-7 Step-2
AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,                                  // 变量声明 赋值
    c: reference to function c(){},
    d: reference to FunctionExpression "d" // 变量声明 引用
}

思考:下面的代码将输出何内容?

// example-8 Code
console.log(foo);

function foo(){
    console.log("foo");
}

var foo = 1;

// foo() {
//   console.log("foo");
// }

思路:函数提升 > 变量提升

//example-8 Hint
function foo(){ // 函数提升
    console.log("foo");
}
var foo; // 变量提升
console.log(foo); // foo
foo = 1;

作用域链

大概过程如下:

  1. 创建 ECStack = []
  2. 首先压入 globalContext
  3. 初始化 globalContext 的执行上下文(VO),获取变量对象
  4. globalContext 代码执行(AO)
  5. 函数checkscope() 被创建,初始化内部属性 [[scope]],值为 globalContext.AO
  6. 函数提升,执行 checkscope() 代码,checkscopeContext 压入ECStack
  7. 初始化 checkscope() 的执行上下文,复制 checkscope.[[scope]]scopeChain
  8. 进入执行上下文 VO
  9. 代码执行 AO
  10. ...

更详细的就不写了:关于 example-2example-3 的具体分析

this

该部分待编辑