在上一篇文章 深刻理解JavaScript 执行上下文 中提到 只有理解了执行上下文,才能更好地理解 JavaScript 语言自己,好比变量提高,做用域,闭包等,本篇文章就来讲一下 JavaScript 的做用域。javascript
这篇文章称为笔记更为合适一些,内容来源于 《你不知道的JavaScript(上卷)》第一部分 做用域和闭包。讲的很不错,很是值得一看。html
做用域是根据名称查找变量的一套规则。java
先来理解一些基础概念:git
接下来来看看下面代码的执行过程:github
var a = 2;
a
是否存在于同一个做用域集合中。若是存在,编译器会忽略声明,继续编译;不然,会要求做用域在当前做用域集合中声明一个新的变量,并命名为 a
a
。若是是,引擎就会使用该变量;若是不存在,引擎会继续查找该变量总结:变量的赋值操做会执行两个动做,首先编译器会在当前做用域中声明一个变量,而后在运行时引擎就会会做用域中查找该变量,若是可以找到就对它赋值。segmentfault
编译器在编译过程的第二步中生成了代码,引擎执行它时,会经过查找变量 a
来判断它是否已声明过。查找的过程当中由做用域进行协助,可是引擎执行怎么样的查找,会影响最终的查找结果。性能优化
在咱们的例子中,引擎会为变量 a 进行 LHS 查询,另一个查找的类型叫作 RHS。 ”L“ 和 "R" 分别表明一个赋值操做左侧和右侧。当变量出如今赋值操做的左侧时进行 LHS 查询,出如今右侧时进行 RHS 查询。闭包
LHS:试图找到变量的容器自己,从而能够对其赋值;RHS: 就是简单地查找某个变量的值。
console.log(a);
对 a 的引用是一个 RHS 引用,由于这里 a 并无赋予任务值,相应地须要查找并取得 a 的值,这样才能将值传递给 console.log(...)函数
a = 2;
这里对 a 的引用是 LHS 引用,由于实际上咱们并不关心当前的值是什么,只是想要为 = 2这个赋值操做找到目标。post
funciton foo(a) { console.log(a) } foo(2);
foo
函数时,2
会被分配给参数 a
,为了给参数 a
(隐式地) 分配值,须要进行一次 LHS
查询。console.log(...)
。console.log(...)
自己也须要一个引用才能执行,所以会对 console对象进行 RHS
查询,而且检查获得的值中是否有一个叫作 log
的方法。RHS查询在全部嵌套的做用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。进行RHS查询找到了一个变量,可是你尝试对这个变量的值进行不合理的操做,好比试图对一个非函数类型的值进行调用,后者引用null或 undefined 类型的值中的属性,那么引擎会抛出一个另一种类型的异常 TypeError。
引擎执行 LHS 查询时若是找不到该变量,则会在全局做用域中建立一个。可是在严格模式下,并非自动建立一个全局变量,而是会抛出 ReferenceError 异常
补充 JS几种常见的错误类型
简单总结以下:
做用域是一套规则,用于肯定在哪里找,怎么找到某个变量。若是查找的目的是对变量进行赋值,那么就会使用 LHS查询; 若是目的是获取变量的值,就会使用 RHS 查询;
JavaScript 引擎执行代码前会对其进行编译,这个过程当中,像 var a = 2 这样的声明会被分解成两个独立的步骤
词法做用域是你在写代码时将变量写在哪里来决定的。编译的词法分析阶段基本可以知道全局标识符在哪里以及是如何声明的,从而可以预测在执行过程当中若是对他们查找。
有一些方法能够欺骗词法做用域,好比 eval, with, 这两种如今被禁止使用,1是严格模式和非严格模式下表现不一样 2是有性能问题, JavaScript引擎在编译阶段会作不少性能优化,而其中不少优化手段都依赖于可以根据代码的词法进行静态分析,并预先肯定全部变量和函数的定义位置,才能在执行过程当中快速找到识别符,eval, with会改变做用域,因此碰到它们,引擎将没法作优化处理。
var a = 1; function foo() { }
变量a 和函数声明 foo 都是在全局做用域中的。
var a = 1; function foo() { b = 2; } foo(); console.log(b); // 2
函数做用域是指在函数内声明的全部变量在函数体内始终是可见的。外部做用域没法访问函数内部的任何内容。
function foo() { var a = 1; console.log(a); // 1 } foo(); console.log(a); // ReferenceError: a is not defined
只有函数的{}
构成做用域,对象的{}
以及if(){}
都不构成做用域;
提高是指声明会被视为存在与其所出现的做用域的整个范围内。
JavaScript编译阶段是找到找到全部声明,并用合适的做用域将他们关联起来(词法做用域核心内容),因此就是包含变量和函数在内的全部声明都会在任何代码被执行前首先被处理。
每一个做用域都会进行提高操做。
function foo() { var a; console.log(a); // undefined a = 2; } foo();
注意,函数声明会被提高,可是函数表达式不会被提高。
关于 块级做用域和变量提高的内容以前在 从JS底层理解var、let、const这边文章中详细介绍过,这里再也不赘述。
咱们来看下面这段代码
for(var i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }) } console.log(`当前的i为${i}`); // 当前的i为5
上面这段代码咱们但愿是输出 0,1, 2, 3, 4 ,可是实际上输出的是 5,5, 5, 5, 5。咱们在 for 循环的头部直接定义了变量 i,一般是由于只想在 for 循环内部的上下文中使用 i,可是实际上 此时的 i 被绑定在外部做用域(函数或全局)中。
,块级做用域是指在指定的块级做用域外没法访问。在ES6以前是没有块级做用域的概念的,ES6引入了 let 和 const。咱们能够改写上面的代码,使它按照咱们想要的方式运行。
for(let i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }) } // 0 1 2 3 4 console.log(`当前的i为${i}`); // ReferenceError: i is not defined
此时 for 循环头部的 let 不只将 i 绑定到了 for 循环的迭代中,事实上将它从新绑定到了循环的每个迭代中,确保使用上一次循环迭代结束的值从新进行赋值。
let声明附属于一个新的做用域而不是当前的函数做用域(也不属于全局做用域)。可是其行为是同样的,能够总结为:任何声明在某个做用域内的变量,都将附属于这个做用域。
const也是能够用来建立块级做用域变量,可是建立的是固定值。
JavaScript是基于词法做用域的语言,经过变量定义的位置就能知道变量的做用域。全局变量在程序中始终都有都定义的。局部变量在声明它的函数体内以及其所嵌套的函数内始终是有定义的。
每一段 JavaScript 代码都有一个与之关联的做用域链(scope chain)。这个做用域链是一个对象列表或者链表。当 JavaScript 须要查找变量 x 的时候(这个过程称为变量解析),它会从链中的第一个变量开始查找,若是这个对象上依然没有一个名为 x 的属性,则会继续查找链上的下一个对象,若是第二个对象依然没有名为 x 的属性,javaScript会继续查找下一个对象,以此类推。若是做用域链上没有任何一个对象包含属性 x, 那么就认为这段代码的做用域链上不存在 x, 并最终抛出一个引用错误 (Reference Error) 异常。
下面做用域中有三个嵌套的做用域。
function foo(a) { var b = a * 2; function bar(c) { console.log(a, b, c) } bar( b * 3); } foo(2);
气泡1
包含着整个全局做用域,其中只有一个标识符:foo;气泡2
包含着foo所建立的做用域,其中有三个标识符:a、bar 和 b;气泡3
包含着 bar所建立的做用域,其中只有一个标识符:c
执行 console.log(...)
,并查找 a,b,c三个变量的引用。下面咱们来看看查找这几个变量的过程.
它首先从最内部的做用域,也就是 bar(..) 函数的做用域气泡开始找,引擎在这里没法找到 a,所以就会去上一级到所嵌套的 foo(...)的做用域中继续查找。在这里找到了a,所以就使用了这个引用。对b来讲也同样,而对 c 来讲,引擎在 bar(..) 中就找到了它。
若是 a,c都存在于 bar(...) 内部,console.log(...)就能够直接使用 bar(...) 中的变量,而无需到外面的 foo(..)中查找。做用域会在查找都第一个匹配的标识符时就中止。
在多层的嵌套做用域中能够定义同名的标识符,这叫”遮蔽效应“。
var a = '外部的a'; function foo() { var a = 'foo内部的a'; console.log(a); // foo内部的a } foo();
JavaScript的执行分为:解释和执行两个阶段
做用域在函数定义时就已经肯定了,而不是在函数调用时肯定,但执行上下文是函数执行以前建立的。