JavaScript进阶-执行上下文栈和变量对象(一周一更)

前言

在阅读本篇文章之前, 请先了解执行上下文执行栈的基础知识点, 移步《JavaScript进阶-执行上下文(理解执行上下文一篇就够了)》

本篇文章是接着介绍执行上下文的要点和讲解变量提升.

变量提升

在使用javascript编写代码的时候, 我们知道, 声明一个变量用var, 定义一个函数用function.那你知道程序在运行它的时候, 都经历了什么吗?

变量声明提升

首先是用var定义一个变量的时候, 例如:

var a = 10;

大部分的编程语言都是先声明变量再使用, 但是javascript有所不同, 上面的代码, 实际相当于这样执行:

var a;
a = 10;

因此有了下面这段代码的执行结果:

console.log(a); // 声明,先给一个默认值undefined;
var a = 10; // 赋值,对变量a赋值了10
console.log(a); // 10

上面的代码👆在第一行中并不会报错Uncaught ReferenceError: a is not defined, 是因为声明提升, 给了a一个默认值.

这就是最简单的变量声明提升.

函数声明提升

定义函数也有两种方法:

  • 函数声明: function foo () {};
  • 函数表达式: var foo = function () {}.

第二种函数表达式的声明方式更像是给一个变量foo赋值一个匿名函数.

那这两种在函数声明的时候有什么区别吗?

案例一🌰:

console.log(f1) // function f1(){}
function f1() {} // 函数声明
console.log(f2) // undefined
var f2 = function() {} // 函数表达式

可以看到, 使用函数声明的函数会将整个函数都提升到作用域(后面会介绍到)的最顶部, 因此打印出来的是整个函数;

而使用函数表达式声明则类似于变量声明提升, 将var f2提升到了顶部并赋值undefined.


我们将案例一的代码添加一点东西:

案例二🌰:

console.log(f1) // function f1(){...}
f1(); // 1
function f1() { // 函数声明
    console.log('1')
}
console.log(f2) // undefined
f2(); // 报错: Uncaught TypeError: f2 is not a function
var f2 = function() { // 函数表达式
    console.log('2')
}

虽然f1()function f1 () {...}之前,但是却可以正常执行;

f2()却会报错, 原因在案例一中也介绍了是因为在调用f2()时, f2还只是undifined并没有被赋值为一个函数, 因此会报错.

声明优先级: 函数大于变量

通过上面的介绍我们已经知道了两种声明提升, 但是当遇到函数和变量同名且都会被提升的情况时, 函数声明的优先级是要大于变量声明的.

  • 变量声明会被函数声明覆盖
  • 可以重新赋值

案例一🌰:

console.log(f1); // f f1() {...}
var f1 = "10";
function f1() {
  console.log('我是函数')
}
// 或者将 var f1 = "10"; 放到后面

案例一说明了变量声明会被函数声明所覆盖.

案例二🌰:

console.log(f1); // f f1() { console.log('我是新的函数') }
var f1 = "10";

function f1() {
  console.log('我是函数')
}

function f1() {
  console.log('我是新的函数')
}

案例二说明了前面声明的函数会被后面声明的同名函数给覆盖.

如果你搞懂了, 来做个小练习?

练习✍️

function test(arg) {
  console.log(arg);
  var arg = 10;
  function arg() {
    console.log('函数')
  }
  console.log(arg)
}
test('LinDaiDai');

答案📖

function test(arg) {
  console.log(arg); // f arg() { console.log('函数') }
  var arg = 10;
  function arg() {
    console.log('函数')
  }
  console.log(arg); // 10
}
test('LinDaiDai');
  1. 函数里的形参arg被后面函数声明arg给覆盖了, 所以第一个打印出的是函数;
  2. 当执行到var arg = 10的时候, arg又被赋值了10, 所以第二个打印出10.

执行上下文栈的变化

先来看看下面两段代码, 在执行结果上是一样的, 那么它们在执行的过程中有什么不同吗?

var scope = "global";
function checkScope () {
  var scope = "local";
  function fn () {
    return scope;
  }
  return fn();
}
checkScope();
var scope = "global"
function checkScope () {
  var scope = "local"
  function fn () {
    return scope
  }
  return fn;
}
checkScope()();

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

在第一段代码中, 栈的变化是这样的:

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

可以看到fn后被推入栈中, 但是先执行了, 所以先被推出栈;


而在第二段中, 栈的变化为:

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

由于checkscope是先推入栈中且先执行的, 所以在fn被执行前就被推出了.

VO/AO

接下来要介绍两个概念:

  • VO(变量对象), 也就是variable object, 创建执行上下文时与之关联的会有一个变量对象,该上下文中的所有变量和函数全都保存在这个对象中。

  • AO(活动对象), 也就是``activation object`,进入到一个执行上下文时,此执行上下文中的变量和函数都可以被访问到,可以理解为被激活了。

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

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

上面似乎说的比较难理解😢, 没关系, 我们慢慢来看.

执行过程

首先来看看一个执行上下文(EC) 被创建和执行的过程:

  1. 创建阶段:
  • 创建变量、参数、函数arguments对象;

  • 建立作用域链;

  • 确定this的值.

  1. 执行阶段:

变量赋值, 函数引用, 执行代码.

进入执行上下文

在创建阶段, 也就是还没有执行代码之前

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

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

一起来看下面的例子🌰:

function fn (a) {
  var b = 2;
  function c () {};
  var d = function {};
  b = 20
}
fn(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: 20,
  c: reference to function c() {},
  d: reference to function d() {}
}

在此阶段, 前面的变量对象中的值就会被赋值了, 此时变量对象处于激活状态.

总结

  • 全局上下文的变量对象初始化是全局对象, 而函数上下文的变量对象初始化只有Arguments对象;

  • EC创建阶段分为创建阶段和代码执行阶段;

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

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

后语

参考文章:

《聊一聊javascript执行上下文》

《木易杨前端进阶-JavaScript深入之执行上下文栈和变量对象》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容