执行上下文
执行顺序
js代码是按上下顺序执行的,但不是一行一行执行,而是一段段代码分析执行,当执行一段代码的时候 就会碰到一些事情,比如:1. 变量提升 2. 函数提升 等问题
function foo () {
console.log('foo1')
}
foo() // foo2
function foo () {
console.log('foo1')
}
foo() // foo2
可执行代码(executable code)
一段段代码首先要判断是否可以执行 js 中有一下代码类型
- 全局代码
- 函数代码
- eval 代码 遇到这些代码 就会进入”准备工作“ 中,专业名字叫执行上下文
执行上下文
代码繁多杂乱 js如何管理这么多的执行上下文?
// 数组来模拟执行上下文栈的行为
ESCtack = [];
// JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,初始化的时候向push一个全局的globalContext
// 只有当整个应用程序结束之后,globalContext才会被清空
ESCtack = [globalContext];
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
// 执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,函数执行完毕,就会将函数的执行上下文从栈中弹出
// 假设
// func1()
ECStack.push(<fun1> functionContext);
// fun1调用了func2
ECStack.push(<fun2> functionContext);
// fun2又调用了func3
ECStack.push(<fun3> functionContext);
// func3 执行完毕
ECStack.pop()
// func2 执行完毕
ECStack.pop()
// func1 执行完毕
ECStack.pop()
// 当前执行上下完毕 接着执行后续的代码,但是globalContext永远存在 知道js运行结束
作用域
作用域是指代码定义变量的区别
规定了如何查找变量,确定了当前执行的代码能否访问到这个变量
js是静态作用域
静态作用域和动态作用域
静态作用域: 定义时决定 动态作用域: 调用时决定
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
// 静态时
// 执行foo--> foo查找局部变量value ---> 没有就按照书写位置找上一层 ---> 有value 打印1
// 动态时
// 执行foo--> foo查找局部变量value ---> 没有就按照调用函数的作用域找 ----> foo 在 bar函数中调用 ---> bar函数中有value ---> 打印2
作用域链
js 查找变量时,会从当前执行上下文中找,找不到会一层一层往上找,一直到全局对象,这个链表叫作用域链
函数创建
函数的作用域在函数定义的时候决定了。
函数有一个内部属性[scope]。函数创建的时候,会保存所有父变量对象到里面,是所有父变量对象的层级链
function foo() {
function bar() {
...
}
}
// 函数创建的时候
foo.[[scope]] = [
globalContext.VO
];
BAR.[[SCOPE]] = [
fooContext.AO,
globalContext.VO
]
函数激活
函数激活时,进入函数上下文,创建VO/AO后,就会将活动对象添加到作用域的前端。
这时候执行上下文的作用域链,我们命名为 Scope
Scope = [AO].concat([[scope]])
举个例子
实在看不明白 我举个例子
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
// 执行过程:
// 1.checkscope 函数被创建,作用域保存到scope 中
checkscope.[[scope]] = {
globalcontext.vo
}
// 2.执行checkscope函数,创建执行上下文,checkscope函数进入栈中
ECStack = [
checkscopeContext,
globalContext
];
// 3.checkscope函数不会立即执行,开始准备工作。
// 第一步复制函数[[scope]]属性创建作用域链
checkscopeContext = {
scope: checkscope.[[scope]],
}
// 第二步,用arguments创建活动对象。随后初始化对象,加入参数,函数和变量的声明
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: checkscope.[[scope]],
}
// 第三步: 将活动搞对象压入checkscope 作用域顶端
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: undefined
},
Scope: [AO, [[Scope]]]
}
// 做完准备工作,开始执行函数,函数执行,修改ao的属性值
checkscopeContext = {
AO: {
arguments: {
length: 0
},
scope2: 'local scope'
},
Scope: [AO, [[Scope]]]
}
// 找到scope2 的值,返回后函数执行完毕,当前函数的上下文从执行栈中删除
ECStack = [
globalContext
];
闭包
闭包是指那些能够访问自由变量的的函数
自由变量
函数中使用的,但是既不是函数的参数,也不是函数局部变量的变量
闭包由两部分组成:函数 + 函数能访问的自由变量
var a = 1;
function foo() {
console.log(a);
}
foo();
// foo 可以访问a 但是a不是foo的局部变量,也不是foo的参数,a时自由变量
// 那么,函数foo + foo 访问的自由变量a就构成了一个闭包
从技术的角度讲,所有的JavaScript函数都是闭包。
理论上: 因为函数都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
实际上:
- 即使创建它的上下文已经销毁,它仍然存在(内部函数从父函数中返回)
- 代码中引用了自由变量
分析
// 还是你
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
首先再来复习一遍执行过程:
- 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈
- 全局执行上下文初始化
- 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈
- checkscope 执行上下文初始化,创建变量对象、作用域链、this等
- checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出
- 执行f函数,创建f上下文,压入执行上下文栈
- 上下文初始化,创建变量对象、作用域等
- f执行完毕,从栈中弹出
那么有个问题: f 执行函数的时候。checkscope的上下文已经被销毁,从栈中弹出,怎么会读取到scope的值?
因为作用域链
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
因为这个作用域链,f 依然可以读到checkscopeContext.AO的值,当函数f 引用了checkscopeContext.AO的值的时候,即使checkscopeContext被销毁,但是js依然能让checkscopeContext.AO存活在内存中,f函数依然能通过作用域链找到,所有有了闭包
一个例子
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
答案都是3
// data[0]之前,全局的vo 如下
globalContext = {
VO: {
data: [...],
i: 3
}
}
// 执行到data[0]的时候
data[0]Context = {
Scope: [AO, globalContext.VO]
}
data0没有i值,会从全局找 i等于
改成闭包看看:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();
// 执行函数0的时候,作用域链发生改变
data[0]Context = {
Scope: [AO, 匿名函数Context.AO globalContext.VO]
}
全局上下文的ao改成
匿名函数Context = {
AO: {
arguments: {
0: 0,
length: 1
},
i: 0
}
}