深入彻底理解原生js的作用域、作用域链(以及浏览器是怎样解析js代码的)

在学习闭包的过程中,我发现,要想真正的理解闭包,光知道什么闭包是不行的,我们还需要知道什么是作用域以及作用域链、函数声明和函数表达式,当然还牵扯到的知识有,垃圾回收机制和内存泄漏等,所以,今天我就多用一点时间把这些知识点都总结一下,为了防止内容太多造成大家眼睛疲劳,我会把这些知识点分开写在不同的页面,如果觉得不错的话,再看后续的就可以。

首先说函数作用域之前,我们先唠唠浏览器是怎么解析js代码的

浏览器在读取HTML文件的时候,只有当遇到<script>标签的时候,才会唤醒所谓的“JavaScript解析器”开始工作。

JavaScript解析器工作步骤

1、“找一些东西”:var、function、参数(也被称之为预解析)。

如果遇到重名分以下两种情况:

  • 遇到变量和函数重名,只留下函数
  • 遇到函数重名了,根据代码的上下文顺序,留下最后一个

2、逐行解读代码(表达式可以修改预解析的值)

js解析器在执行第一步预解析的时候,会从代码的开始搜索知道结尾,找的时候,只会找var、function和参数等内容。一般把第一步称之为“JavaScript的预解析”,而且,当找到这些内容时,所有的变量,在正式运行代码之前,都提前赋了一个值:变量都赋值为undefined,所有函数在正式运行代码之前,都是整个函数块。

强调一点:JavaScript解析器不只是这两个步骤,只是这里我们只介绍了这两步骤,方便我们理解作用域。

作用域:所谓作用域——作用(可以进行读、写)的域(空间、范围、区域):也即可访问对象、变量、函数的集合

其实我们也可以把作用域分为:全局作用域(网页中所有脚本和函数均可使用)和局部作用域(函数内部)

注意:只要是域,就会进行预解析和逐行解读代码这两步

全局变量拥有全局作用域(函数外声明的变量以及没有用var关键字定义的变量都是全局变量)

局部变量(函数内部声明的变量,形参都是局部变量)只能在函数内部访问,因为局部变量只作用于函数内,所以不同的函数可以使用相同名称的变量

全局变量在页面关闭后销毁,局部变量在函数执行完毕销毁。

OK,铺垫完了,现在我们来看几个变态的面试题

alert(a);//function a(){alert(4);}
var a = 1;
alert(a);//1
function a(){alert(2);}
alert(a);//1
var a = 3;
alert(a);//3
function a(){alert(4);}
alert(a);//3
//a();如果我们再添加上这句代码,程序会报错

看完上面的代码,你可知道是为什么吗?还是像我刚看到那样一脸懵逼,这都是what?

哈哈,下来就我就来分析一下呗:

首先我们说过,在执行这段代码之前会先进行预解析,所以当变量和函数重名留下函数,当函数和函数重名留下最后一个函数,所以预解析的结果就是存入了一个代码块:function  a(){alert(4);},所以在逐行解读代码的时候第一句弹出的结果也就没什么疑问了,又因为在代码解读的时候,表达式可以修改预解析的值,所以,执行到最后一行的时候a=3,再然后我们说如果要是再后面加一个a(),会报错,因为此时就相当于3();你说能不报错嘛?

然后,继续看代码

var a = 1;
function fn1(){
  alert(a);//undefined
  var a = 2;
}
fn1();
alert(a);//1

再看

var a = 1;
function fn1(){
  alert(a);//1
   a = 2;
}
fn1();
alert(a);//2

再看

var a = 1;
function fn1(a){
  alert(a);//undefined
  a = 2;
}
fn1();
alert(a);//1

再看

var a = 1;
function fn1(a){
  alert(a);//1
  a = 2;
}
fn1(a);
alert(a);//1

看完上面四段代码,有木有想跳楼的冲动,ok,冷静冷静,现在且听我娓娓道来

首先我们来说一下第一段代码:函数调用时,弹出的是undefined,这个没啥疑问,注意一点就是函数内也要进行预解析,然后函数在预解析的时候会给变量赋值为未定义,alert弹出1,因为局部变量不能改变全局变量的值

然后我们说第二段:过程和第一段差不多,需要注意的是,函数内部的a没有用var关键字定义,所以它就是全局变量,函数调用的时候弹出1(这里其实涉及到了作用域链的问题,有里向外,子级作用域返回父级作用域找到了全局变量a=1),那么当执行到a=2的时候,全局变量a自然就是2了

第三段代码解读:首先我们需要知道的是,形参实际上就是一个局部变量,相当于var a,然后知道了这个,接下来的过程就和第一段一样了

第四段代码:首先我们看到第四段代码和第三段代码唯一的不同之处,就是第四段代码多了一个实参,然后函数内部预解析之后,在逐行解读代码的时候,因为从function fn1(a){}这一行开始,实参把a=1传给了局部变量a,所以函数调用时弹出的a为1。

好了,看完解释有没有豁然开朗,嗯哼,接下来我们再看一段代码,不要怕,这次的很简单,真的很简单

<script>
  alert(a);
</script>
<script>
   var a=1;
</script>//这段代码会报错
<script>
   var a=1; 
</script>
<script>
   alert(a);//弹出1
</script>

这个想必大家都知道答案,但我还是唠叨一下,其实这是因为两个<script></script>其实是两个域,而域是从上而下,一个一个执行的,只有执行完了上面的才会开始执行下面的,这也就是为什么第一个报错,而第二段代码可以正常弹出1了。

下面我们来说一下什么是作用域链:

当代码在一个环境中执行时,都会创建一个作用域链。 作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。整个作用域链的本质是一个指向变量对象的指针列表。作用域链的最前端,始终是当前正在执行的代码所在环境的变量对象。 

  如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,就是函数内部的arguments对象。作用域链中的下一个变量对象来自该函数的包含环境,而再下一个变量对象来自再下一个包含环境。这样,一直延续到全局执行环境,全局执行环境的变量对象始终是作用域链中的最后一个对象。

    不同的层级作用域上对象的分布

  • 在javascript的最顶层(也就是不包含任何函数定义内的代码),作用域链由一个全局对象组成。
  • 在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。
  • 在一个嵌套的函数体内,作用域链上至少有三个对象。当调用这个函数时,它创建一个新的对象来存储它的局部变量,它实际上保存在同一个作用域链。
   对于嵌套函数来讲,事情更有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数每次定义的时候都有微妙的差别——在每次调用外部函数时,内部函数的代码都是不相同的,而且关联这段代码的作用域链也不相同。

终于把关于作用域的知识想唠的唠完了,接下来如果有兴趣看函数声明和函数表达式的请点击js函数声明和函数表达式

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值