Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【进阶1-2期】JavaScript深入之执行上下文栈和变量对象 #13

Open
yygmind opened this issue Dec 4, 2018 · 15 comments
Open

Comments

@yygmind
Copy link
Owner

yygmind commented Dec 4, 2018

本期的主题是调用堆栈,本计划一共28期,每期重点攻克一个面试重难点,如果你还不了解本进阶计划,文末点击查看全部文章。

如果觉得本系列不错,欢迎点赞、评论、转发,您的支持就是我坚持的最大动力。


JS是单线程的语言,执行顺序肯定是顺序执行,但是JS 引擎并不是一行一行地分析和执行程序,而是一段一段地分析执行,会先进行编译阶段然后才是执行阶段。

翠花,上代码

例子一:变量提升

foo;  // undefined
var foo = function () {
    console.log('foo1');
}

foo();  // foo1,foo赋值

var foo = function () {
    console.log('foo2');
}

foo(); // foo2,foo重新赋值

例子二:函数提升

foo();  // foo2
function foo() {
    console.log('foo1');
}

foo();  // foo2

function foo() {
    console.log('foo2');
}

foo(); // foo2

例子三:声明优先级,函数 > 变量

foo();  // foo2
var foo = function() {
    console.log('foo1');
}

foo();  // foo1,foo重新赋值

function foo() {
    console.log('foo2');
}

foo(); // foo1

上面三个例子中,第一个例子是变量提升,第二个例子是函数提升,第三个例子是函数声明优先级高于变量声明。

需要注意的是同一作用域下存在多个同名函数声明,后面的会替换前面的函数声明。

执行上下文

执行上下文总共有三种类型

  • 全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象。
  • 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文。
  • Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使用。

这部分内容在【进阶1-1期】中详细介绍了,点击查看【进阶1-1期】理解JavaScript 中的执行上下文和执行栈

执行上下文栈

因为JS引擎创建了很多的执行上下文,所以JS引擎创建了执行上下文(Execution context stack,ECS)来管理执行上下文。

当 JavaScript 初始化的时候会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,执行栈才会被清空,所以程序结束之前, 执行栈最底部永远有个 globalContext。

ECStack = [		// 使用数组模拟栈
    globalContext
];

具体执行过程如下图所示,这部分内容在【进阶1-1期】中详细介绍了,点击查看【进阶1-1期】理解JavaScript 中的执行上下文和执行栈

找不同

有如下两段代码,执行的结果是一样的,但是两段代码究竟有什么不同?

var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
    var scope = "local scope";
    function f(){
        return scope;
    }
    return f;
}
checkscope()();

答案是 执行上下文栈的变化不一样。

第一段代码:

ECStack.push(<checkscope> functionContext);
ECStack.push(<f> functionContext);
ECStack.pop();
ECStack.pop();

第二段代码:

ECStack.push(<checkscope> functionContext);
ECStack.pop();
ECStack.push(<f> functionContext);
ECStack.pop();

函数上下文

在函数上下文中,用活动对象(activation object, AO)来表示变量对象。

活动对象和变量对象的区别在于

  • 1、变量对象(VO)是规范上或者是JS引擎上实现的,并不能在JS环境中直接访问。
  • 2、当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这时候活动对象上的各种属性才能被访问。

调用函数时,会为其创建一个Arguments对象,并自动初始化局部变量arguments,指代该Arguments对象。所有作为参数传入的值都会成为Arguments对象的数组元素。

执行过程

执行上下文的代码会分成两个阶段进行处理

  • 1、进入执行上下文

  • 2、代码执行

进入执行上下文

很明显,这个时候还没有执行代码

此时的变量对象会包括(如下顺序初始化):

  • 1、函数的所有形参 (only函数上下文):没有实参,属性值设为undefined。
  • 2、函数声明:如果变量对象已经存在相同名称的属性,则完全替换这个属性。
  • 3、变量声明:如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性。

上代码就直观了

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};

  b = 3;
}

foo(1);

对于上面的代码,这个时候的AO是

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

形参arguments这时候已经有赋值了,但是变量还是undefined,只是初始化的值

代码执行

这个阶段会顺序执行代码,修改变量对象的值,执行完成后AO如下

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

总结如下:

  • 1、全局上下文的变量对象初始化是全局对象

  • 2、函数上下文的变量对象初始化只包括 Arguments 对象

  • 3、在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值

  • 4、在代码执行阶段,会再次修改变量对象的属性值

参考

JavaScript深入之执行上下文栈
JavaScript深入之变量对象

进阶系列目录

  • 【进阶1期】 调用堆栈
  • 【进阶2期】 作用域闭包
  • 【进阶3期】 this全面解析
  • 【进阶4期】 深浅拷贝原理
  • 【进阶5期】 原型Prototype
  • 【进阶6期】 高阶函数
  • 【进阶7期】 事件机制
  • 【进阶8期】 Event Loop原理
  • 【进阶9期】 Promise原理
  • 【进阶10期】Async/Await原理
  • 【进阶11期】防抖/节流原理
  • 【进阶12期】模块化详解
  • 【进阶13期】ES6重难点
  • 【进阶14期】计算机网络概述
  • 【进阶15期】浏览器渲染原理
  • 【进阶16期】webpack配置
  • 【进阶17期】webpack原理
  • 【进阶18期】前端监控
  • 【进阶19期】跨域和安全
  • 【进阶20期】性能优化
  • 【进阶21期】VirtualDom原理
  • 【进阶22期】Diff算法
  • 【进阶23期】MVVM双向绑定
  • 【进阶24期】Vuex原理
  • 【进阶25期】Redux原理
  • 【进阶26期】路由原理
  • 【进阶27期】VueRouter源码解析
  • 【进阶28期】ReactRouter源码解析

交流

进阶系列文章汇总:https://github.com/yygmind/blog,内有优质前端资料,欢迎领取,觉得不错点个star。

我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

@icantunderstand
Copy link

对变量对象和激活对象这个有点不明白 这两个是不是ES3的概念呢 在ES5中通过 VariableEnvironment
LexicalEnvironment 这个些概念来表达了

@Dstyleself
Copy link

函数提升的例子写错了啊

@yygmind
Copy link
Owner Author

yygmind commented Feb 15, 2019

函数提升的例子写错了啊

没错啊

@cengjingdefeng
Copy link

函数提升的例子写错了啊

没错

@pengcc
Copy link

pengcc commented Mar 18, 2019

foo(); // foo2
var foo = function() {
console.log('foo1');
}

foo(); // foo1,foo重新赋值

function foo() {
console.log('foo2');
}

foo(); // foo1

######### 我的理解是函数声明提升,变量声明提升,但赋值不提升,所以我觉得这个例子用来说明函数声明优先可能不太准确,程序段可以等同于如下,供参考#########
var foo; // undefined/变量声明提升/
function foo() {
console.log('foo2');
} /函数声明提升/ // foo -> function foo(){}
foo(); // foo2
foo = function() {
console.log('foo1');
} /重新赋值/
foo(); // foo1

@CBGhaha
Copy link

CBGhaha commented Mar 26, 2019

为什么觉得变量对象、活动对象、作用域链和前面1-1 的词法环境和变量环境矛盾了呢?

@wbc0301
Copy link

wbc0301 commented Jun 25, 2019

找不同,
第一部分属于函数的尾调用,所以调用 f() 的时候外层的调用帧已经出栈,和第二个是一样一样的。

@luyaoguang
Copy link

为什么我觉得checkscope两段代码的执行效果是一样的呢
如果checkscope return f 就pop出去了
那checkscope()()应该是glocal scope了把

@jonyteng
Copy link

jonyteng commented Jul 5, 2019

为什么我觉得checkscope两段代码的执行效果是一样的呢
如果checkscope return f 就pop出去了
那checkscope()()应该是glocal scope了把

质疑地有道理。自己在chrome中看堆栈调用,我认为执行上下文栈是一样的。

@SoleilQ
Copy link

SoleilQ commented Jul 9, 2019

function foo(a) {} 进入执行上下文阶段 AO应该写成VO吧 此时还没有AO 执行阶段 VO才会变成AO

@kukudeshiyi
Copy link

为什么觉得变量对象、活动对象、作用域链和前面1-1 的词法环境和变量环境矛盾了呢?

应该差不多是这样,这是之前总结的,但是看完1-1、1-2,我感觉又合不到一块了
1、当函数在被创建时,会创建一个包含全局变量对象的作用域链;
2、当调用函数时,执行上下文被创建,这个期间,完成了变量对象的创建,确定this指向,以及把当前的变量对象推至当前作用域链的前端,形成完整的作用域链;
3、代码执行阶段,完成对变量的赋值,函数的引用,以及执行其他代码;
image

@charmingYouYou
Copy link

对变量对象和活动对象不清楚的推荐去这看看js 中的活动对象 与 变量对象 什么区别?

@abnerliushitong
Copy link

变量提升的例子 foo 应该输出的是function foo { console.log('foo2')} 不应该是undefined吧

@guestccc
Copy link

函数提升的例子写错了啊

没错呀

@ldsyzjb
Copy link

ldsyzjb commented Nov 10, 2020

请问1-2期描述的变量对象和1-1期中描述的环境记录是什么关系,为什么会有两种说法?
@yygmind

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests