这次啃一个 GitHub repo 上的几篇文章,虽然 Blogger 讲得算很易懂了,但还是要花时间好好吸收一下。
作用域
JavaScript 采用词法作用域(静态作用域),函数的作用域在函数定义时就已经确定。与之相反的动态作用域,函数的作用域是在函数调用时才决定。
// example-1var 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 Codevar scope = "global scope";function checkscope() { var scope = "local scope"; function f() { return scope; } return f();}checkscope(); // "local scope"
//example-3 Codevar scope = "global scope";function checkscope() { var scope = "local scope"; function f() { return scope; } return f;}checkscope()(); //"local scope"
找到了 SegmentFault 上的一个提问,解释的较为详细了。
从代码的行文来看,会发现:
example-2
:checkscope()
返回的是内部函数 f()
的执行结果;
example-3
:checkscope()
返回的是内部函数 f
,然后再执行返回的函数。
順序執行?
JavaScript 引擎并非以行来分析和执行代码,而是以段来分析执行。当执行一段代码的时候,会进行一个「准备工作」,比如下例的变量提升和函数提升:
// example-4 Codevar foo = function () { console.log('foo1');}foo(); // foo1
var foo = function () { console.log('foo2');}foo(); // foo2
// example-5 Codefunction 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(); // foo2foo(); // 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 Codefunction fun3() { console.log('fun3')}
function fun2() { fun3();}
function fun1() { fun2();}
fun1();
当上例的代码执行时,执行上下文栈的处理为:
// example-6 ExecuteECStack.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-2
和 example-3
中,模拟执行上下文栈。此时能看出两段代码的区别:
// example-2 ExecuteECStack.push(<checkscope> functionContext);ECStack.push(<f> functionContext);ECStack.pop(<f>);ECStack.pop(<chechscope>);
// example-3 ExecuteECStack.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 Codefunction foo(a) { var b = 2; function c() {} var d = function() {}; b = 3;}
foo(1);
- 进入执行上下文,会依次导入:
- 导入函数的所有形参
- 函数声明
- 变量声明
// example-7 Step-1VO = { arguments: { 0: 1, length: 1 }, // 形参 a: 1, // 形参,值为实参或 undefined b: undefined, // 变量声明 c: reference to function c(){}, // 函数声明 地址引用 d: undefined // 变量声明}
- 代码执行,给变量声明赋值
// example-7 Step-2AO = { arguments: { 0: 1, length: 1 }, a: 1, b: 3, // 变量声明 赋值 c: reference to function c(){}, d: reference to FunctionExpression "d" // 变量声明 引用}
// example-8 Codeconsole.log(foo);
function foo(){ console.log("foo");}
var foo = 1;
// foo() {// console.log("foo");// }
思路:函数提升 > 变量提升
//example-8 Hintfunction foo(){ // 函数提升 console.log("foo");}var foo; // 变量提升console.log(foo); // foofoo = 1;
作用域链
大概过程如下:
- 创建
ECStack = []
- 首先压入
globalContext
- 初始化
globalContext
的执行上下文(VO),获取变量对象 globalContext
代码执行(AO)- 函数
checkscope()
被创建,初始化内部属性[[scope]]
,值为globalContext.AO
- 函数提升,执行
checkscope()
代码,checkscopeContext
压入ECStack
- 初始化
checkscope()
的执行上下文,复制checkscope.[[scope]]
到scopeChain
中 - 进入执行上下文 VO
- 代码执行 AO
- …
更详细的就不写了:关于 example-2
和 example-3
的具体分析
this
该部分待编辑