Html

彻底搞懂JavaScript函数执行机制

2020-06-07  本文已影响0人  zxhnext

当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.创建一个堆(存储代码字符串和对应的键值对)

  1. 初始化当前函数的作用域(没错,创建时就定义好了)
  2. [[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,具体做了以下几件事:

  1. 创建一个新的执行,上下文EC (压缩到栈ECStack里执行)
  2. 初始化THIS的指向
  3. 初始化作用域链[[scopeChain]] : xxx
  4. 创建AO变量对象用来存储变量

这里我们先来简单看下存储变量时的步骤:

  1. 找形参和变量声明(变量提升),将变量和形参名作为AO属性名,值为undefined
  2. 将实参值和形参统一
  3. 在函数体里面找函数声明,值赋予函数体

举个例子:

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() {}
}

注意:

  1. 函数提升优先级比变量高
  2. 当函数声明的函数名被重新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();
a函数定义时 a函数执行时 b函数创建时 b函数执行时

我们来修改一下原来的代码,

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没有被销毁掉。这就是闭包,符合以下两个条件就是闭包:

  1. fn 外部对内部有引用
  2. 在另一个作用域访问到 fn 作用域中的局部成员

5. this

this对象是在运行时基于函数的执行环境绑定的,在全局函数中this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。

每个函数在被调用时都会自动取得两个特殊变量:this 和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。

注意,只要函数前没有调它的东西,该函数就指向window
如下图:


image.png
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就指向谁

上一篇 下一篇

猜你喜欢

热点阅读