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

贫道,感受,JS的坑,不是通常地大。javascript

变量提高:

变量提高( hoisting )。java

我可恨的 var 关键字:

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

<script type="text/javascript">

    var str = 'Hello JavaScript hoisting';

    console.log(str);	// Hello JavaScript hoisting
    
</script>
复制代码

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

如今,我将这一段代码,改一改,将 调用 放在前面, 声明 放在后面。浏览器

不少语言好比说 C 或者 C++ 都是不容许的,可是 javaScript 容许bash

大家试着猜猜获得的结果:闭包

<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' 删除后,试试思考这段代码的结果:post

<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 引擎是如何管理,多个函数之间的 执行上下文 ???

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

参考与鸣谢:

  • 此文章主要参考自《高性能 JavaScript》
相关文章
相关标签/搜索