彻底搞懂JavaScript函数执行机制
当Javascript代码执⾏的时候会将不同的变量存于内存中的不同位置:堆(heap)
和栈(stack)
中来加以区分。其中,堆⾥存放着⼀些对象。⽽栈中则存放着⼀些基础类型变量以及对象的指针。 但是我们这⾥说的执⾏栈和上⾯这个栈的意义却有些不同。
这里的栈除了执行代码外,基础类型的值也会存储在栈中,引用类型存储在堆中。
null的作用,null不会开辟内存,而是把之前的指针指向空指针。
null和0区别:0需要开启一个内存,null不需要
null和undefined的区别:
null想要赋值,但是还没有赋值,拿null占位
undefined:未赋值
js 在执⾏可执⾏的脚本时,⾸先会创建⼀个全局可执⾏上下⽂(globalContext)
,每当执⾏到⼀个函数调⽤时都会创建⼀个可执⾏上下⽂(execution context)
EC。当然可执⾏程序可能会存在很多函数调⽤,那么就会创建很多EC,所以 JavaScript 引擎创建了执⾏上下⽂栈(Execution contextstack,ECS)
来管理执⾏上下⽂。
当函数调⽤完成,js会退出这个执⾏环境并把这个执⾏环境销毁
,回到上⼀个⽅法的执⾏环境...这个过程反复进⾏,直到执⾏栈中的代码全部执⾏完毕,如下是以上的⼏个关键词,我们来⼀次分析⼀下:
执⾏栈(Execution Context Stack)
全局对象(GlobalContext)
活动对象(Activation Object)
变量对象(Variable Object)
函数执行的阶段可以分文两个:函数建立阶段、函数执行阶段
1. 函数创建阶段
当调用函数时,还没有执行函数内部的代码,会创建一个创建执行上下文对象
fn.EC = {
variableObject: // 函数中的 arguments、参数、局部成员
scopeChains: // 当前函数所在的父级作用域中的活动对象
this: {} // 当前函数内部的 this 指向
}
创建阶段做了三件事:
1.创建一个堆(存储代码字符串和对应的键值对)
- 初始化当前函数的作用域(没错,创建时就定义好了)
- [[scope]] = 所在上下文EC中的变量对象VO/AO
这里的变量对象VO是与执⾏上下⽂相关的特殊对象,⽤来存储上下⽂的函数声明,函数形参和变量。
我们编写的代码是无法访问VO的
VO分为全局上下⽂的变量对象VO,函数上下⽂的变量对象VO
VO(globalContext) === global;
我们以下面这个函数为例,来分析一下:
var a = 10;
function test(x) {
var b = 20;
};
test(30);
// 全局上下⽂的变量对象
VO(globalContext) = {
a: 10,
test: <reference to function>
};
// test函数上下⽂的变量对象
VO(test functionContext) = {
x: 30,
b: 20
};
2. 函数执行阶段
fn.EC = {
activationObject: // 函数中的 arguments、参数、局部成员
scopeChains: // 当前函数所在的父级作用域中的活动对象
this: {} // 当前函数内部的 this 指向
}
函数执行阶段将变量对象VO变为了活动对象AO,具体做了以下几件事:
- 创建一个新的执行,上下文EC (压缩到栈ECStack里执行)
- 初始化THIS的指向
- 初始化作用域链[[scopeChain]] : xxx
- 创建AO变量对象用来存储变量
这里我们先来简单看下存储变量时的步骤:
- 找形参和变量声明(变量提升),将变量和形参名作为AO属性名,值为undefined
- 将实参值和形参统一
- 在函数体里面找函数声明,值赋予函数体
举个例子:
function fn(a) {
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function() {}
console.log(b);
function d() {}
}
fn(1);
储存变量过程:
// 1
AO: {
a: undefined,
b: undefined
}
// 2
AO: {
a: 1,
b: undefined
}
// 3
AO: {
a: function a() {},
b: undefined
d: function d() {}
}
注意:
- 函数提升优先级比变量高
- 当函数声明的函数名被重新var,但是还未赋值时,该变量还是指向该函数
下面我们来正式看一下AO
// 1.在函数执⾏上下⽂中,VO是不能直接访问的,此时由活动对象扮演VO的⻆⾊。
// 2.Arguments对象它包括如下属性:callee 、length
// 3.内部定义的函数
// 4.以及绑定上对应的变量环境;
// 5.内部定义的变量
VO(functionContext) === AO;
function test(a, b) {
var c = 10;
function d() {}
var e = function _e() {};
(function x() {});
}
test(10); // call
// 当进⼊带有参数10的test函数上下⽂时,AO表现为如下:
// AO⾥并不包含函数“x”。这是因为“x” 是⼀个函数表达式(FunctionExpression, 缩写为FE) ⽽不是函数声明,函数表达式不会影响VO
AO(test) = {
a: 10,
b: undefined,
c: undefined,
d: <reference to FunctionDeclaration "d">
e: undefined
};
3. 完整过程分析
我们以下面代码为例,来完整分析一下函数执行过程:
let x = 1;
function A(y) {
let x = 2;
function B(z) {
console.log(x + y + z);
}
return B;
}
let C = A(2);
C(3);
函数执行过程模拟:
/*第一步:创建全局执行上下文,并将其压入ECStack中*/
ECStack = [
//=>全局执行上下文
EC(G) = {
//=>全局变量对象
VO(G): {
... //=>包含全局对象原有的属性
x = 1;
A = function (y) {...};
A[[scope]] = VO(G); // =>创建函数的时候就确定了其作用域
}
}
];
/*第二步:执行函数A(2)*/
ECStack = [
//=>A的执行上下文
EC(A) = {
//=>链表初始化为:AO(A)->VO(G)
[scope]: VO(G)
scopeChain: <AO(A), A[[scope]]>
// =>创建函数A的活动对象
AO(A): {
arguments: [0: 2],
y: 2,
x: 2,
B: function (z) {...},
B[[scope]] = AO(A);
this: window;
}
},
//=>全局执行上下文
EC(G) = {
//=>全局变量对象
VO(G): {
... //=>包含全局对象原有的属性
x = 1;
A = function (y) {...};
A[[scope]] = VO(G); //=>创建函数的时候就确定了其作用域
}
}
];
/*第三步:执行B/C函数 C(3)*/
ECStack = [
//=>B的执行上下文
EC(B) {
[scope]: AO(A)
scopeChain: < AO(B), AO(A), B[[scope]]>
//=>创建函数B的活动对象
AO(B): {
arguments: [0: 3],
z: 3,
this: window;
}
},
//=>A的执行上下文
EC(A) = {
//=>链表初始化为:AO(A)->VO(G)
[scope]: VO(G)
scopeChain: < AO(A), A[[scope]] >
//=>创建函数A的活动对象
AO(A): {
arguments: [0: 2],
y: 2,
x: 2,
B: function (z) {...},
B[[scope]] = AO(A);
this: window;
}
},
//=>全局执行上下文
EC(G) = {
//=>全局变量对象
VO(G): {
... //=>包含全局对象原有的属性
x = 1;
A = function (y) {...};
A[[scope]] = VO(G); //=>创建函数的时候就确定了其作用域
}
}
]
4. 作用域链
每个javascript函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供javascript引擎存取,[[scope]]就是其中一个。[[scope]]指的就是我们所说的作用域,其中存储了运行期上下文的集合。
作用域链: [[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。
作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所 在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延 续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
函数执行时,会在作用域顶层(0)创建自己的AO,所以查找变量时,也是从作用域链的顶端依次向下查找
我们以下代码为例,来看一下函数的作用域链
function a() {
function b() {
var b = 234;
}
var a=123;
b();
}
var glob = 100 ;
a();
我们来修改一下原来的代码,
function a() {
function b() {
var b = 234;
}
var a=123;
return b
}
var glob = 100 ;
let b = a();
b()
我们将b函数返回出去,此时我们发现,当a函数执行完时,a的AO就已经断开了,但是b依然保留着对它的引用。所以a的AO没有被销毁掉。这就是闭包,符合以下两个条件就是闭包:
- fn 外部对内部有引用
- 在另一个作用域访问到 fn 作用域中的局部成员
5. this
this对象是在运行时基于函数的执行环境绑定的,在全局函数中this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。
每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
注意,只要函数前没有调它的东西,该函数就指向window
如下图:

var length = 10;
function fn() {
console.log(this.length);
}
var yideng = {
length: 5,
method: function() {
fn();
}
};
yideng.method()
不管fn()是回调函数还是在yideng.method()里调用fn(),只要fn()前没有东西调用它,它就指向window
箭头函数没有this,它的this取决于它上一个函数的this,如果没有函数,就是window
其它的谁调用函数,this就指向谁