文章目录
本文章使用代码
文章的讲解跟着这段代码走的。大概先瞄一眼,不用记,要看的时候滑上来看就好了。
//作用域A
var a = 1;
function funB() {
//作用域B
var b = 2;
function funC() {
//作用域C
var c = 3;
console.log(a, b, c);// 1 2 3
}
function funD() {
//作用域D
console.log(a, b);// 1 2
console.log(c);//c is not defined
}
funC();
funD();
}
funB();
作用域
概念:作用域管理着变量和函数的可访问范围,哪个位置可以访问哪些函数或变量都由它把关。
正文开始了
代码是自上而下运行的,会跟着你的书写顺序运行,那它在运行的时候,也需要像人脑一样记录运行到该处时候,有什么变量,有几个函数,叫啥,值是多少。我们用来记录的叫脑子,它们用来记录的叫 执行环境,在 javaScript 中也叫 执行上下文。
既然使用脚本语言,我们就先不去纠结它在二进制的世界里是什么样的了。我们就用简单的形式来说明。
举例说明。我们把执行环境当成一台电脑。当我们有一堆的文件的时候,我们会创建文件夹,对这些文件进行分类存放(没这么做的同学,怕是没有遇到文件找不到,搜索要半天的绝望)。作用域就是相当于文件夹,而变量和函数名就是文件夹中的文件。
- 执行环境 == 电脑
- 作用域 == 文件夹
- 变量和函数 == 文件夹中的文件
我们在看小说之前,会先瞄一眼,“容老夫先看看,这个小说更新到第几话了”。执行环境也是一样,代码在运行之前会有一个查看代码的过程,把出现的变量和函数先进行预声明。
我们把执行过程分成 声明阶段 跟 执行阶段,也就是我们的“瞄一眼”和"认真看"。
声明阶段,关于赋值的操作啊什么的,都不管,就记住一个变量和函数名。执行阶段才会去细看对这个变量做了什么,这个函数干嘛用的。
开始运行
第一步:作用域A在 声明阶段 时,执行环境的心理:
var a = 1; //可以,你要创建一个变量叫a,干嘛用的我先不管
function funB(){...}; //要创建一个函数叫funB是吧,好,我先记着
类似于
是吧,干嘛用的我先不管,先创好放着。
注意,函数被执行的时候会在当前作用域下,创建出一个新的作用域,也就是本文例子中的作用域B。
第二步:作用域A在 执行阶段 时,执行环境的心理:
a = 1; // 给变量a赋值1。妥
funB(); // 然后执行funB函数,创建作用域B
类似于
接下去进入到了作用域B中,重复第一步跟第二步。最终形成如下图
上诉内容就是作用域的生成过程,而 作用域链 呢,就是整条路径
当我们在某个作用域中,需要获取某个变量或者函数的时候。
首先会在当前的作用域中查找是否存在该变量,如果没有,它根据它的 原型链 向上一级作用域查找,依次类推,直到找到该变量后返回或者找不到该变量后抛出异常。
分析一下
问题1:在作用域C中访问变量a,b,c是怎样的过程?
先看看 在作用域C中访问变量c:
- 先查找当前作用域中有没有变量c。
- 哇,找到了。找到就用它了。
再来看看 在作用域C中访问变量b:
- 先查找当前作用域中有没有变量b。
- 没有啊,那我看看我的原型链,根据原型链向上查找,所以我要去作用域B里看看。
- 我来到作用域B了,可以,找到变量b了。就用它了。
最后看看 在作用域C中访问变量a:
- 上面说的够明白了,找到就用,找不到看原型链换个地方找,我不写了。打字手疼
问题2:为什么在作用域C和在作用域D中访问变量c,结果会不一样?
作用域C的原型链:
作用域D的原型链:
根据问题1,动手试一下,如果作用域D中可以访问到c,请扣我。
问题3:经典问题之面试必考题—变量提升
不管你信不信,你把代码改成如下,还可以正常运行
//作用域A
var a = 1;
funB();
function funB() {...}
咦,前面我们不是说好了,代码自上而下运行的吗,为何在funB函数声明之前调用它,还可以用呢?
以这段代码跟着上诉教程再走一遍,你会发现这问题背后的本质。
有些东西没必要全讲完,自己动手看看才是王道,学完不用就感觉关注一下,等着回头看吧。
这里点几个 注意事项 就可以了。(划重点,要考)
- 声明阶段 函数名和变量名相同时,会以函数名覆盖变量名。
- 以向变量赋值函数的形式,在 声明阶段 当成变量处理。
//作用域A
var a;
function a(){};
console.log(a);// 你以为我是函数?对,我就是函数。
--------------------------
function a(){};
var a;
console.log(a);// 你以为我是变量?不,我是函数。
--------------------------
var a = function(){...};// 你这样玩的话,我在声明阶段就当你是一个变量哦
静态作用域和动态作用域
在编译器阶段,作用域分为两种,静态作用域和动态作用域。
与之关联较深的是JS的 闭包 概念,学习是循环渐进的,没必要一锅端。
本章不对其做出相关的讲解,我会在讲解 闭包 时提到,请看后记。
全局作用域和局部作用域
不管是静态作用域还是动态作用域,它们都细分为全局作用域和局部作用域。
全局作用域 就是最最最外面的那层作用域。
在实例中,作用域A就是全局作用域。在javaScript中,全局作用域就是window对象。
也就是说,实例中的作用域A的真实身份就是!! window对象
顺带点一个很有意思的东西。 使用 var 声明的变量,会跑到全局作用域上,也就是说
var a == window.a
于是乎,优雅的网友们忍不住了,“这不是玷污我的作用域吗!”。
于是有了ES6的 let 关键字,由它声明的变量,会形成一个暂时性死区,它会绑定当前的作用域,而不是变到全局作用域上。
局部作用域 也被称为 块级作用域 。
在javascript中,只要是位于 “{ }” 符号之中,就会形成一个局部作用域。
实例中的作用域B,C,D就是局部作用域。
注意事项
根据变量查找的规则。全局作用域在最后一层,如果把局部作用域中的变量都放到全局变量中,那么就会出现执行效率低的情况,因为查找变量的时候总是跑到最后一层才找到。
好吧,有的人会说,现在的硬件这么强,提升这样一点的执行效率对于前端而言微忽其忽,那我们从代码的简易性来说。
如果把变量的使用放在一个局部作用域中,后续修改的人只需要关注这个作用域的逻辑就可以了。
而如果把变量的使用放到全局作用域中,那不得了,不管是开发的人还是后续修改的人,在使用的时候都要注意,该变量是不是在某个时候被意外的修改成其他样子,或者是在审查代码的时候,需要查看整个逻辑。
当技术在同一水平上时,比的就是那一份简洁易懂的代码。
后记一下
我看了很多大佬的文章。关于作用域的文章中都提到了 [[scope]] 、AO、VO、this指针之类的,为何我闭口不谈呢。
因为我在看《你不知道的javaScript》的时候,也没有明文提到这些东西。。回想起来自己一开始学习作用域的知识时,这些属性除了让我更难理解之外,对后续也没啥帮助(目前而言并没有用上)。。
对于作用域的基本理解,本文章已经足够了。如果要深入理解作用域,就要从编译的角度上来讲了。
后续我会再讲解 js编译原理 和 this指针。请看后记。
人不可能懂得世间万物的,每个人掌握一条路,团队配合才是王道。
我要做的,只是让大家能够基础的学会它,然后你想往哪个方向走,或深入或背离,取决于你们。
对了。有一张画了半天又不舍得放弃的图,想想还是放出来吧。作用域链是 从内到外 的。
对this指向性问题详解,可以跳转至以下链接
<div v-if="作者有写相应链接">
<a href="作者写的链接">点击跳转this指向详解</a>
</div>
对于静动态作用域和闭包的详解,可以跳转至以下连接
<div v-if="作者有写相应链接">
<a href="作者写的链接">点击跳转深入闭包</a>
</div>
js编译原理,可以跳转至以下连接
<div v-if="作者有写相应链接">
<a href="作者写的链接">点击跳转js编译原理</a>
</div>
跪求点评中。。。