作用域
在深入学习JavaScript作用域之前,首先要了解一下,究竟什么是作用域。几乎所有的编程语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。
我们先了解一下JavaScript的工作原理,引擎,编译器,作用域三者是如何协同工作来完成javascript代码的执行的。
引擎:从头到尾负责整个JavaScript程序的编译及执行过程。
编译器:负责词法分析及代码生成
作用域:负责收集并维护由所有声明的变量组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些变量的访问权限。
我们看下最简单的var index = 10;了解一下引擎、编译器和作用域是如何协同工作的。
- var index;
- index = 10;
在上面的变量声明的执行时,我们提到了引擎在作用域中查找变量的问题,我们将此分为两种方式,一种为LHS查询,一种为RHS查询,在上面的例子中,引擎在执行index = 10时,进行的是LHS查询。
那么究竟何为LHS查询和RHS查询呢,简单的,当变量出现在左侧时,执行的是LHS查询(L可以认为是Left)。除了LHS查询,剩下的就是RHS查询。作进一步说明,LHS查询是企图找到变量的容器本身,即去寻找赋值操作的目标,而RHS查询是找赋值操作的源头,即获取变量的值。
举例说明:
- index = 10;
- console.log(index);
在执行index = 10时,引擎会进行LHS查询,去寻找index变量的容器,目的是找到赋值操作的目标。而在执行console.log(index)时,引擎会进行RHS查询,目的是去找到index的值。我们之所以进行执行的区分,是因为如果index变量没有声明的情况下,这两种查询方式的结果是完全不同的。
作用域嵌套 引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找,直至到最外层的全局作用域链,不管最终是否找到了变量,查找过程到到此结束。
如下:在执行index = 15时,引擎首先在当前作用域fun函数中查找index变量,没有找到,那么继续向上查找,在par函数中也没有找到,那么继续向上一级查找,最后在全局作用域中找到了该index.
- <script>
- var index = 10;
- function par(){
- function fun(){
- index = 15;
- }
- fun();
- }
- par();
- </script>
当引擎进行RHS查询时,如果查询到作用域链的顶层(全局作用域)依旧未找到index变量,那么引擎就会抛出一个ReferenceError异常。
当引擎进行LHS查询,在全局作用域中也未能找到目标变量(本例中的index),在非严格模式下,会在全局作用域中创建一个该名称的变量。而在严格模式下,会同RHS查询一样,抛出一个ReferenceError异常。
作用域查找会在找到第一个匹配的标识符(变量)时停止,在多层嵌套作用域中可以定义同名的标识符,这也称之为"遮蔽效应",如上面的代码中,如果fun函数中定义了index变量,那么在fun中对index的赋值操作不会影响到全局变量中的index.因为作用域查找始终是从运行时所处的最内部的作用域开始,逐级向上查找,直到找到匹配的标识符为止。
词法作用域是由写代码时将变量和函数写在哪里决定的,而不是由其调用的位置决定,JS提供了两种机制修改词法作用域,即:width和eval,鉴于这两种机制都会导致性能的降低,在此不多作介绍,尽量避免使用即可。
初学者或多或少都会遇到一个问题:命名冲突。
命名冲突会导致变量的值被意外覆盖。而这并非是我们想看到的。那么如何规避冲突呢?
1.全局命名空间(类似于jQuery的实现)
如:我们在全局作用域重视声明了一个名字足够独特的变量,通常是一个变量,如下面的carousel_yve,这个对象被称为库的命名空间,所有需要暴露给外界的功能都会称为这个对象的属性(如:index、defaults、init),避免将自己的标识符暴露在顶级的词法作用域中。
- <script type = "text/javascript">
- var carousel_yve = {
- index: 0,
- defaults: { "1200px",
- height: "500px"},
- init: function(){
- console.log(this.defaults);
- }
- }
- carousel_yve.init(); //Object { "1200px", height: "500px"}
- console.log(carousel_yve.index); //10
- </script>
2.模块模式
模块模式分为两种,一种是每次调用都会创建一个新的模块实例,另一种是单例模式,即只会创建一个实例。
如:
- <script type = "text/javascript">
- function carousel_yve(){
- var index = 0;
- var defaults = { "1200px",
- height: "500px"};
- function init(){
- console.log(defaults);
- }
- function doSomething(){
- console.log(index);
- }
- return {
- init: init,
- doSomething: doSomething
- }
- }
- var example = carousel_yve();
- example.init(); //Object { "1200px", height: "500px"}
- example.doSomething(); //0
- </script>
carousel_yve是一个函数,通过对它的调用来创建一个模块实例。每次调用都会生成一个实例。
我们再来看一下单例模式:
- <script type = "text/javascript">
- var example = (function carousel_yve(){
- var index = 0;
- var defaults = { "1200px",
- height: "500px"};
- function init(){
- console.log(defaults);
- }
- function doSomething(){
- console.log(index);
- }
- return {
- init: init,
- doSomething: doSomething
- }
- })();
- example.init(); //Object { "1200px", height: "500px"}
- example.doSomething(); //0
- </script>
我们将先前的模块函数换成了IIFE,即:立即调用。
模块模式需要具备两个条件:
1.必须有外部的封闭函数,该函数必须至少被调用一次。
2.封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
鉴于本文的目的是研究javaScript的作用域问题,因此对于模块模式不再做更多的扩展说明。
关于IIFE,有时我们想对外隐藏时,也可以简单的使用此方式。通过这样的方式,避免污染所在的作用域。
- <script type = "text/javascript">
- (function carousel_yve(){
- var index = 10;
- var defaults = { "1200px", height: "500px;"};
- var obj = document.getElementById("btn");
- obj.addEventListener("click", function(){/*code*/}, false);
- //code……
- })();
- </script>
包装函数的声明以(function开始,而不是以function开始,看起来区别很小,但是实际上却完全不一样,因为(function开始会当做函数表达式,而function开头是作为标准的函数声明。而函数声明和函数表达式最重要的区别在于它们的名称标识符被绑定在何处。上面的代码中carousel_yve被绑定在自身的函数中,而不是所在的作用域中,其只能在自身函数的内部被访问。
此外,对于匿名函数和具名函数还要做一点说明。
JavaScript中,函数表达式允许匿名,但是函数声明不允许省略函数名,即不允许匿名。尽管匿名函数使用起来简单快捷,但是匿名函数的几个缺点需要考虑:
1.匿名函数在栈追踪中不会显示出有意义的函数名,使得调试困难。
2.如果没有函数名,当函数需要调用自身时,只能使用过期的arguments.callee引用,比如在递归中。
3.匿名函数省略了对于代码可读性/可理解性很重要的函数名,一个描述性的名称可以防止代码不言自明。(代码即注释)
匿名或具名并不会影响函数的功能,因此始终给函数表达式命名是值得推崇的。如我们经常使用的setTimeout、setInterval中。给回调函数命一个形象的名字将使代码的可读性更强。
提升
- <script type = "text/javascript">
- console.log(a);
- var a = 2;
- </script>
- var a;
- console.log(a);
- a = 2;
- <script type = "text/javascript">
- console.log(a);
- a = 2;
- </script>
- <script type = "text/javascript">
- example();
- var example = function(){
- console.log(10);
- };
- function example(){
- console.log(20);
- }
- </script>
- function example(){
- console.log(20);
- }
- example();
- example = function(){
- console.log(10);
- };
- <script type = "text/javascript">
- example();
- var example = function(){
- console.log(10);
- };
- function example(){
- console.log(20);
- };
- function example(){
- console.log(30)
- }
- </script>
- <script type = "text/javascript">
- var a = true;
- if(a == true){
- function example(){
- console.log(10);
- };
- }else{
- function example(){
- console.log(20);
- }} example();
- <html lang="en">
- <head>
- <meta charset="UTF-8" />
- <title>Document</title>
- <style>
- #ullist li { display: block; 40px; height: 40px;
- border:1px solid #ccc; text-align: center;
- line-height: 40px; cursor: pointer; float: left;
- margin:10px;}
- </style>
- </head>
- <body>
- <ul id="ullist">
- <li id="li1">1</li>
- <li id="li2">2</li>
- <li id="li3">3</li>
- <li id="li4">4</li>
- <li id="li5">5</li>
- </ul>
- </body>
- <script>
- window.onload=function(){
- var ullist=document.getElementById("ullist");
- var listE=document.getElementsByTagName("li");
- for (var i=0; i<listE.length; i++){
- listE[i].onclick = function(i){
- alert(listE[i].innerHTML);
- };
- };
- }
- </script>
- </html>
- <script type = "text/javascript">
- window.onload=function(){
- var ullist=document.getElementById("ullist");
- var listE=document.getElementsByTagName("li");
- for (var i=0; i<listE.length; i++){
- listE[i].onclick = (function(i){
- return function(){
- alert(listE[i].innerHTML);
- }
- })(i);
- };
- };
- </script>