你真的了解 JavaScript 中的作用域么?
大家都知道闭包是js中非常重要的知识点,很多初学者觉得这玩意太难了,很难理解…其实刚开始我也是这样的,
但是,闭包却非常重要!非常重要!非常重要!《在你不知道的JavaScript》中甚至这样说“对于那些有一点 JavaScript 使用经验但从未 真正理解闭包概念的人来说,理解闭包可以看作是某种意义上的重生”,我想大家是不是非常迫切的想深入了解一下闭包呢?但是不要着急,不能一口气吃一个胖子,但是,对于想真正理解闭包有一个非常重要的知识点,那就是作用域与词法作用域,如果你没能好好理解作用域,那么闭包是肯定理解不了的!那么接下来就好好的理解一下作用域吧。
变量提升
在详细阐述作用域前我们需要先了解一下变量提升
- 变量提升: 当栈内存(作用域)形成,JS代码自上而下执行之前,浏览器会把所有带“var”,“function”关键词的进行提前 “声明” 或者 “定义” ,这种预先处理机制称之为 “变量提升”.
声明(declare):var a (默认值undefined)
定义(defined):a=12 (定义其实就是赋值操作)
console.log(a);//=>undefined
var a = 12;
- [变量提升阶段]
- 带“VAR”的只声明未定义
- 带“FUNCTION”的声明和赋值都完成了
- 变量提升只发生在当前作用域(例如:开始加载页面的时候只对全局作用域下的进行提升,因为此时函数中存储的都是字符串而已)
- 在全局作用域下声明的函数或者变量是“全局变量”,同理,在私有作用域下声明的变量是“私有变量” [带VAR/FUNCTION的才是声明]
- 浏览器很懒,做过的事情不会重复执行第二遍,也就是,当代码执行遇到创建函数这部分代码后,直接的跳过即可(因为在提升阶段就已经完成函数的赋值操作了)
什么是作用域
可以这样理解,“作用域就是一套规则,用于确定在何处以及如何查找变量(表示符)的规则”。这句话中读到了一个关键点,查找变量(表示符),那么我们从查找变量开始说起吧。
如下代码
function foo(){
var a = 1,
console.log(a)//输出1
}
foo()
在函数执行前 会形成一个私有作用域,在foo函数执行的时候,会输出一个变量a,那么这个a变量是来自哪里的呢,是私有作用域foo中定义的代码 var = 1.
那么我们再看如下代码
var b = '2';
function foo() { console.log(b); // 输出2 }
foo()
同样的道理,在输出 b 的时候,自己函数内部没有找到变量 b,那么就在外层 的全局中查找,找到了就停止查找并输出了。
注意以上两行代码都有查找变量,第一段代码是在函数中找到的a变量,第二段代码是在全局中找大的b变量。
- 函数作用域,全局作用域,第一段代码就是在函数作用域中找到的a变量,第2段代码就是在全局作用域中找到的b变量。
- 所以 我们可以这样理解,作用域就是查找变量的地方,。在某函数中找到 该变量,就可以说在该函数作用域中找到了该变量;在全局中找到该变量,就 可以说在全局作用域中找到了该变量!
作用域链
当我们在查找一个一个变量时,现在函数中查找,如果没有找到,再去去阿奴作用域中查找,有个往外层查找的过程,我们就好像是顺着一条链条从里往外查找变量,这条连链条,我们称之为作用域链。
作用域嵌套
在还没有接触到 ES6 的 let、const 之前,只有函数作用域和全局作用域,函数 作用域肯定是在全局作用域里面的,而函数作用域中又可以继续嵌套函数作用
以上两张图可以很直观的看出作用域的嵌套关系了吧。查找变量也是顺着红色 的箭头走的,从里到外,这从里到外的各层作用域就组成了作用域链。
- 我们再看下面的代码
var n = 10;
function fn() {
var n = 20;
function f() {
n++;
console.log(n);
}
f();
return f;
}
var x = fn();
x();
x();
console.log(n);
作用域中变量(标识符)的查找规则
- 首先声明一点,JavaScript 是有编译过程的,不要惊讶,真的有!也就是说 *var name = ‘iceman’*这段代码,其实这是有两个动作的:
- 编译器在当前作用域中声明一个变量 name
- 运行时引擎在作用域中查找该变量,找到了 name 变量并为其赋值
- 证明以上的说法:console.log(name); // 输出 undefined var name = ‘iceman’;
在 var name = 'iceman’的上一行输出 name 变量,并没有报错,输出 undefined,说明输出的时候该变量已经存在了,只是没有赋值而已。 - 其实编译器是这样工作的,在代码执行之前从上到下的进行编译,当遇到某个 用 var 声明的变量的时候,先检查在当前作用域下是否存在了该变量。如果存 在,则忽略这个声明;如果不存在,则在当前作用域中声明该变量。也就是我们所说的预编译
- 上面的这段简单的代码包含两种查找类型:输出变量的值的时候的查找类型是 RHS,找到变量为其赋值的查找类型是 LHS。
- 我猜各位同学一定可以猜到“L”和“R”的含义,这里的左侧和右侧指的是在 赋值操作的左侧和右侧。也就是说,变量出现在赋值操作的左侧时进行 LHS 查 询,出现在右侧时进行 RHS 查询。用一句通俗的话来讲,RHS 就是取到它的源值。
- 注意:“赋值操作的左侧和右侧”,并不意味着只是“=”,实际上赋值操作还 有好几种形式。
- 在作用域中查找变量都是 RHS,并且查找的规则是从当前作用域开始找,如果 没找到再到父级作用域中找,一层层往外找,如果在全局作用域如果还没找到 的话,就会报错了:ReferenceError: 某变量 is not defined
- 所有的赋值操作中查找变量都是 LHS。其中 a=4 这类赋值操作,也是会从当前 作用域中查找,如果没有找到再到外层作用域中找,如果到全局变量这个变量,在非严格模式下会创建一个全局变量 a。不过,非常不建议这么做,因为 轻则污染全局变量,重则造成内存泄漏(比如:a = 一个非常大的数组,a 在 全局变量中,一直用有引用,程序不会自动将其销毁)
词法作用域
- 在上面的作用域介绍中,我们将作用域定义为一套规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查 找。
- 我们在前面有抛出一个概念:“词法作用域是作用域的一种工作模型”,作用 域有两种工作模型,在 JavaScript 中的词法作用域是比较主流的一种,另一种 动态作用域(比较少的语言在用)。
- 所谓的词法作用域就是在你写代码时将变量和块作用域写在哪里来决定,也就是词法作用域是静态的作用域,在你书写代码时就确定了。
请看以下代码:
function fn1(x) {
var y = x + 4;
function fn2(z) {
console.log(x, y, z);
}
fn2(y * 5);
}
fn1(6); // 6 10 50
这个例子中有个三个嵌套的作用域,如图:
- A 为全局作用域,有一个标识符:fn1
- B 为 fn1 所创建的作用域,有三个标识符:x、y、fn2
- C 为 fn2 所创建的作用域,有一个标识符:z
- 作用域是由期代码写在哪里决定的,并且是逐级包含的。
- 在此强调,词法作用域就是作用域是由书写代码时函数声明的位置来决定的。 编译阶段就能够知道全部标识符在哪里以及是如何声明的,所以词法作用域是静态的作用域,也就是词法作用域能够预测在执行代码的过程中如何查找标识符