关于javascript:深入理解JS作用域链与执行上下文

42次阅读

共计 3911 个字符,预计需要花费 10 分钟才能阅读完成。

变量晋升:

变量晋升(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>

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

执行上下文:

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

作用域链:

其实,咱们晓得,JS 用的是 词法作用域 的。

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

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

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

外部属性 [[Scope]] 蕴含了一个函数被创立的作用域中对象的汇合。

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

来源于:《高性能 JavaScript》;

我好奇的是,怎样才能看到这个,不能通过代码拜访的属性???通过老夫的钻研得出,能看到这个货色的办法;

关上谷歌浏览器的 console,并输出一下代码:

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

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

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

你须要这样:

而后这样:

好了,你曾经看到了,[[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] :

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

因为,初始化时,[[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] :

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

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

当 须要调用 testValue 变量时;

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

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

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

咱们来做一个乏味的试验,跟方才,依照我形容的办法,你能够找到 [[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 申明前:

编号 2 运行时:

编号 3 执行后:

你可依照我的办法,做很屡次试验,试着嵌套几个函数,在调用它们之前察看作用域链。

作用域链,是在 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) 句时,是程序正在筹备:初始化执行上下文

如上图所示,在函数未调用之前,曾经有 add 函数的 [[Scope]] 属性所保留的 作用域链 外面曾经有这些货色了。

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

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

一旦初始化 执行上下文 胜利,就会创立一个 流动对象,外面会产生 this arguments 以及咱们申明的变量,这个例子外面是 xy

运行执行上下文 阶段:

完结 执行上下文 阶段

好了,然而,这里没有波及到调用其余函数。

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

正文完
 0