这次啃一个 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-2
:checkscope()
返回的是内部函数 f()
的执行结果;
example-3
:checkscope()
返回的是内部函数 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-2
和 example-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);
- 进入执行上下文,会依次导入:
- 导入函数的所有形参
- 函数声明
- 变量声明
// example-7 Step-1
VO = {
arguments: {
0: 1,
length: 1
}, // 形参
a: 1, // 形参,值为实参或 undefined
b: undefined, // 变量声明
c: reference to function c(){}, // 函数声明 地址引用
d: undefined // 变量声明
}
- 代码执行,给变量声明赋值
// 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;
作用域链
大概过程如下:
- 创建
ECStack = []
- 首先压入
globalContext
- 初始化
globalContext
的执行上下文(VO),获取变量对象 globalContext
代码执行(AO)- 函数
checkscope()
被创建,初始化内部属性[[scope]]
,值为globalContext.AO
- 函数提升,执行
checkscope()
代码,checkscopeContext
压入ECStack
- 初始化
checkscope()
的执行上下文,复制checkscope.[[scope]]
到scopeChain
中 - 进入执行上下文 VO
- 代码执行 AO
- ...
更详细的就不写了:关于 example-2
和 example-3
的具体分析
this
该部分待编辑