理解 JS 作用域链与执行上下文

image

贫道,感觉,JS的坑,不是一般地大。

变量提升:

变量提升( hoisting )。

我可恨的 var 关键字:

你读完下面内容就会明白标题的含义,先来一段超级简单的代码:

<script type="text/javascript">

    var str = 'Hello JavaScript hoisting';

    console.log(str);   // Hello JavaScript hoisting
    
</script>

这段代码,很意外地简单,我们的到了想要的结果,在控制台打印出了:Hello JavaScript hoisting

现在,我将这一段代码,改一改,将 调用 放在前面, 声明 放在后面。

很多语言比如说 C 或者 C++ 都是不允许的,但是 javaScript 允许

你们试着猜猜得到的结果:

<script type="text/javascript">

    console.log(str);       // undefined

    var str = 'Hello JavaScript hoisting';

    console.log(str);       // Hello JavaScript hoisting

</script>

你会觉得很奇怪,在我们调用之前,为什么我们的 str = undefined ,而不是报错:未定义???

我将 var str = 'Hello JavaScript hoisting' 删除后,试试思考这段代码的结果:

<script type="text/javascript">

    console.log(str);       // Uncaught ReferenceError: str is not defined

</script>

现在得到了,我们想要的,报错:未定义。

事实上,在我们浏览器会先解析一遍我们的脚本,完成一个初始化的步骤,它遇到 var 变量时就会先初始化变量为 undefined

这就是变量提升(hoisting ),它是指,浏览器在遇到 JS 执行环境的 初始化,引起的变量提前定义。

在上面的代码里,我们没有涉及到函数,因为,我想让代码更加精简,更加浅显,显然我们应该测试一下函数。

<script type="text/javascript">
    
    console.log(add);           // ƒ add(x, y) { return x + y; }
    
    function add(x, y) {
        return x + y;
    }

</script>

在这里,我们并没有调用函数,但是这个函数,已经被初始化好了,其实,初始化的内容,比我们看到的要多。

如何避免变量提升:

使用 letconst 关键字,尽量使用 const 关键字,尽量避免使用 var 关键字;

<script type="text/javascript">
    
    // console.log(testvalue1);     // 报错:testvalue1 is not defined
    
    // let testvalue1 = 'test';
    
    /*---------我是你的分割线-------*/
    
    console.log(testvalue2);        // 报错:testvalue1 is not defined

    const testvalue2 = 'test';

</script>

但,如果为了兼容也就没办法喽,哈哈哈,致命一击!!!

image

执行上下文:

执行上下文,又称为执行环境(execution context),听起来很厉害对不对,其实没那么难。

image

作用域链:

其实,我们知道,JS 用的是 词法作用域 的。

关于 其他作用域 不了解的童鞋,请移步到我的《谈谈 JavaScript 的作用域》,或者百度一下。

每一个 javaScript 函数都表示为一个对象,更确切地说,是 Function 对象的一个实例。

Function 对象同其他对象一样,拥有可编程访问的属性。和一系列不能通过代码访问的 属性,而这些属性是提供给 JavaScript 引擎存取的内部属性。其中一个属性是 [[Scope]] ,由 ECMA-262标准第三版定义。

内部属性 [[Scope]] 包含了一个函数被创建的作用域中对象的集合。

这个集合被称为函数的 作用域链,它能决定哪些数据能被访问到。

来源于:《 高性能JavaScript 》;

我好奇的是,怎样才能看到这个,不能通过代码访问的属性???经过老夫的研究得出,能看到这个东西的方法;

打开谷歌浏览器的 console ,并输入一下代码:

function add(x, y) {
  return x + y;
}

console.log( add.prototype );   // 从原型链上的构造函数可以看到,add 函数的隐藏属性。

可能还有其他办法,但,我只摸索到了这一种。

你需要这样:

image

然后这样:

image

好了,你已经看到了,[[Scope]] 属性下是一个数组,里面保存了,作用域链,此时只有一个 global

思考以下代码,并回顾 词法作用域,结合 [[Scope]] 属性思考,你就能理解 词法作用域 的原理,

var testValue = 'outer';

function foo() {
  console.log(testValue);       // "outer"
  
  console.log(foo.prototype)    // 编号1
}

function bar() {
  var testValue = 'inner';
  
  console.log(bar.prototype)    // 编号2
  
  foo();
}

bar();

以下是,执行结果:

编号 1 的 [[Scope]] 属性:Scopes[1] :

image

编号 2 的 [[Scope]] 属性:Scopes[1]

image

因为,初始化时,[[Scope]] 已经被确定了,两个函数无论是谁,如果自身的作用域没找到的话,就会在全局作用域里寻找变量。

再思考另外一段代码:

var testValue = 'outer';

function bar() {
  var testValue = 'inner';
  
  foo();
  
  console.log(bar.prototype)    // 编号 1
  
  function foo() {
    console.log(testValue);     // "inner"
    
    console.log(foo.prototype); // 编号 2 
  }
}

bar();

编号 1 的 [[Scope]] 属性:Scopes[1] :

image

编号 2 的 [[Scope]] 属性:Scopes[2] :

image

这就解释了,为什么结果是,testValue = "inner"

当 需要调用 testValue 变量时;

先找本身作用域,没有,JS 引擎会顺着 作用域链 向下寻找 [0] => [1] => [2] => [...]。

在这里,找到 bar 函数作用域,另外有趣的是,Closure 就是闭包的意思 。

image

证明,全局作用域链是在 全局执行上下文初始化时 就已经确定的:

我们来做一个有趣的实验,跟刚才,按照我描述的方法,你可以找到 [[Scope]] 属性。

那这个属性是在什么时候被确定的呢???

很显然,我们需要从,函数声明前,函数执行时,和函数执行完毕以后三个方面进行测试:

console.log(add.prototype);     // 编号1 声明前

function add(x, y) {

  console.log(add.prototype);   // 编号2 运行时
  return x + y;
}

add(1, 2);
console.log(add.prototype);     // 编号3 执行后

编号1 声明前:

image

编号2 运行时:

image

编号3 执行后:

image

你可按照我的方法,做很多次实验,试着嵌套几个函数,在调用它们之前观察作用域链。

作用域链,是在 JS 引擎 完成 初始化执行上下文环境,已经确定了,这跟我们 变量提升 小节讲述得一样。

它保证着 JS 内部能正常查询 我们需要的变量!。

我的一点疑惑

注意:在这里,我无法证明一个问题。

  1. 全局执行上下文初始化完毕之后,它是把所有的函数作用域链确定。
  2. 还是,初始化一个执行上下文,将本作用域的函数作用域链确定。

这是我的疑惑,我无法证明这个问题,但是,我更倾向于 2 的观点,如果知道如何证明请联系我。至少,《高性能JavaScript》中是这样描述的。

知道作用域链有什么好处?

试想,我们知道作用域链,有什么用呢???

我们知道,如果作用域链越深, [0] => [1] => [2] => [...] => [n],我们调用的是 全局变量,它永远在最后一个(这里是第 n 个),这样的查找到我们需要的变量会引发多大的性能问题?JS 引擎查找变量时会耗费多少时间?

所以,这个故事告诉我们,尽量将 全局变量局部化 ,避免,作用域链的层层嵌套,所带来的性能问题。

理解 执行上下文:

将这段代码,放置于全局作用域之下。这一段代码,改编自《高性能JavaScript》。

function add(x, y) {
    return x + y;
}

var result = add(1, 2);

这段代码也很简洁,但在 JavaScript 引擎内部发生的事情可并不简单。

正如,上一节,变量提升 所论述,JS 引擎会初始化我们声明 函数 和 变量 。

那么在 add(1, 2) 执行前,我们的 add 函数 [[Scope]] 内是怎样的呢???

这里有三个时期:初始化 执行上下文、运行 执行上下文、结束 执行上下文

很显然,执行到 var result = add(1, 2) 句时,是程序正在准备:初始化执行上下文

image

如上图所示,在函数未调用之前,已经有 add 函数的[[Scope]]属性所保存的 作用域链 里面已经有这些东西了。

当执行此函数时,会建立一个称为 执行上下文 (execution context) 的内部对象。

一个 执行上下文 定义了一个函数执行时的环境,每次调用函数,就会创建一个 执行上下文 ;

一旦初始化 执行上下文 成功,就会创建一个 活动对象 ,里面会产生 this arguments 以及我们声明的变量,这个例子里面是 xy

运行执行上下文 阶段:

image

结束 执行上下文 阶段

image

好了,但是,这里没有涉及到调用其他函数。

其实,还有,我们的 JavaScript 引擎是如何管理,多个函数之间的 执行上下文 ???

管理多个执行上下文,其实是用的 上下文执行栈 具体请参考链接:请猛戳这里,大佬写的文章。

参考与鸣谢:

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

推荐阅读更多精彩内容