js的代码执行顺序,很多人都会觉得是顺序执行。但是执行的过程中我们会遇到很多坑,比如下面的代码:
function foo() {
console.log('foo')
}
foo() //foo1
function foo() {
console.log('foo1');
}
foo() //foo1
说好的顺序执行呢?其实是因为函数是会提升的,提升过后,相同函数中,后面的函数会覆盖掉之前的,所以两次执行都会输出foo1。
其实,上面的代码说明了js引擎对代码的分析其实是一段一段的,当执行一段代码的时候,就会进行一个准备工作。
可执行代码
上面说的一段一段的分析是什么呢,这里我们要说到以客概念,就是可执行代码
可执行代码中包含:
- 全局代码
- 函数代码
- eval代码
那准备工作其实就是我们说的执行上下文
执行上下文栈
当函数增多的时候,就会很难管理,这时候,我们就需要使用一个执行上下文栈来进行管理
当执行JavaScript代码的时候,我们最先肯定是执行全局代码,这时候我们将全局上下文压入栈,因为栈的概念是后进先出,所以只有当代码全局完成的时候,ECstack才会被清空。
ECstack [
函数上下文
全局上下文
]
这么说说可能不好理解,我将开始的那段代码进行了一个图形的解析

这里我们将全局上下文用globalContext表示,我们可以看到当进栈的时候,第二个fooContext会覆盖掉之前的fooContext,所以在执行foo()操作的时候,出栈的就是第二个fooContext。
执行上下文属性
执行上下文中其实有三个重要属性,应该也是我们经常听说的:
- 变量对象
- 作用域链
- this
变量对象
变量对象(VO)就是存储了上下文中的定义的变量和函数声明,那其中又有全局上下文中的全局对象和函数上下文中的活动对象(AO)。
全局对象
全局上下文中的变量对象其实就是全局对象,其实就是一个window对象,也是由Object构造函数实例化的一个对象
this instanceOf Object //全局中为true
其实它也是作为一个全局变量的宿主
var a = 1
console.log(this.a)
其实它还有很多属性,我就在这不多说了。
活动对象
函数上下文中用活动对象(AO)来表示变量对象。前面我们所说的全局中的变量对象是可以在任何地方访问,它是在执行任何上下文之前就先创建的对象。但是函数环境的局部变量对象是在函数执行之前创建的,VO是不能直接访问的,所以我们需要与VO相对应的AO来访问函数内部对象。
AO也就是VO激活后的对象,它通过函数的arguments进行初始化,arguments属性值是Arguments对象。Arguments中的属性有:
- callee:可以调用函数自身
- length:实参的长度
- properties-indexes: 属性的值就是函数参数的值
执行过程
执行上下文代码的时候回分为两个阶段进行处理:进入执行上下文和执行代码
-
进入执行上下文,变量对象会包含:
-
函数所有的形参
- 按名称和对应值的形式创建变量对象的属性
- 如果没有传递对应参数,则为undefined
-
所有的函数说明
- 按名称和对应值(函数对象function-object)的形式创建变量对象的属性
- 如果已经存在相同名字的变量(变量没有进行赋值),就进行替代
-
所有的变量声明
- 按名称和对应值(undefined)的形式创建变量对象的属性
- 如果已经存在相同名字(形参或者函数)的属性,不进行替代
-
举个栗子
function foo(x) {
var y = 'friends';
function z() {}
y = 'world';
}
foo('hello');
这个时候所形成的VO为:
VO = {
arguments: {
0:'hello'
length: 1
},
x: 'hello',
y: undefined,
z: reference to function z(){},
}
-
代码执行
- 在代码执行前,很多VO/AO都有了自己的属性,但是大部分属性都是体统的默认值undefined
- 所以在代码执行的时候,会相应的填充属性中的值来计算结果
之前的栗子中的VO就可以为:
VO = {
arguments: {
0:'hello'
length: 1
},
x: 'hello',
y: 'world',
z: reference to function z(){},
}
作用域链
简单来说,作用域链就是查找变量对象的过程,从当前的上下文开始查找,如果没有的话就会从父级执行上下文的变量对象中查找,就这样找呀找呀~~ 直到找到全局上下文的变量对象,也就是全局对象,再没有的话就说明你没有定义啦~~就会报错了。
函数的创建和变化过程
-
函数创建
每个函数的内部其实都有一个
[[scope]]的属性,当函数创建的时候,就会保存所有的父级变量对象到其中,[[scope]]本质上就是一个数组,(我认为就是像栈一样的一个数组),但是它还不是一个完整的作用域链。
栗子栗子又来了
function father() {
function son() {
...
}
}
函数创建的时候,son的[[scope]]为
son.[[scope]]: [
fatherContext.AO,
globalContext.VO
]
-
函数激活
当函数激活后,就会进入到函数上下文中,创建VO/AO后,就会将活动对象添加到作用域的最前端,那
[AO]和之前的[[scope]]组合就形成了作用域链啦!!
Scope = [AO].concat([[scope]])
this
这一块的原理还在学习中...
其实我现在是用一句话行走于this中的: this永远指向最后调用它的那个对象。
var a = 1
var obj = {
a: 2,
foo: function() {
console.log(this.a) //2
}
}
obj.foo()
在这里foo是对象obj调用的,所以指向obj,那么值自然就为2了。
这里就先举一个小栗子,关于this原理会涉及到reference等的规范类型概念,我还没有看透,等我菜鸡归来~~
这里面很多都是看@冴羽的文章,大家可以去看他写的文章,应该更加深入~~
注:此文为本人学习过程中的笔记记录,如果有错误或者不准确的地方请大佬多多指教~