js进阶系列(三):函数作用域和块级作用域

/ 0评 / 0

函数作用域

从之前我们讨论的作用域是负责声明并维护由所有声明的标识符(变量)组成一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。作用域包含着一系列的规则。很多函数嵌套的时候,最里面的函数的作用域包含着对外层函数作用域的引用,这些引用包含着对标识符(变量、函数)的定义。

我们知道。JavaScript有基于函数的作用域,这意味着每一个函数都可以创建属于自己的一套作用域。属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。例如

function foo(){
    var a =1;
    function bar(){
        var b=2;
    }
}

 这里面,函数foo有两个标识符a和bar(),在函数外部是访问不到foo里面的标识符的,但是在foo里面的bar()是可以访问到foo的标识符的,这也是作用域链的体现。这样通过函数作用域将代码类似于‘隐藏’起来了,这是一个很有用的技术。通过声明一个函数,把我们需要的标识符都定义在函数里面,成为了这个函数的私有变量了,这样做有几个好处

1.避免使用过多的全局变量。

2.限制了标识符的访问权限,起到封装的作用

3.避免同名标识符之间的冲突

我们知道做项目有可能不仅仅是一个人写代码。项目是很多人参与的,如果我们的全局变量太多个了,那后面,有可能会造成变量重名了,那后果不用我说大家也知道。当然后期也不好维护。

当然也有很多方法能够避免这种问题,比如命名空间、模块管理等等。

我们已经知道,在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。虽然这种技术可以解决一些问题,但是它并不理想,因为会导致一些额外的问题。首先,必须声明一个具名函数,意味着这个函数 的名称本身“污染”了所在作用域(一般是全局作用域)。其次,必须显式地通过函数名调用这个函数才能运行其中的代码。如果函数不需要函数名(或者至少函数名可以不污染所在作用域),并且能够自动运行,这将会更加理想。

这里我们需要弄清楚一下函数声明和函数表达式的区别

包装函数的声明以(function... 而不仅是以function... 开始。尽管看上去这并不是一个很显眼的细节,但实际上却是非常重要的区别。函数会被当作函数表达式而不是一个标准的函数声明来处理。区分函数声明和表达式最简单的方法是看function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。换句话说,(function foo(){ .. }) 作为函数表达式意味着foo 只能在.. 所代表的位置中被访问,外部作用域则不行。foo 变量名被隐藏在自身中意味着不会非必要地污染外部作用域

一、匿名函数和具名函数

 

简单来说匿名函数就是没有名字的函数,最大的用处就是回调函数了

var timer=setTimeout(function(){
   console.log(123);
},1000);

 因为function().. 没有名称标识符。函数表达式可以是匿名的,而函数声明则不可以省略函数名——在JavaScript 的语法中这是非法的。当然匿名函数还有几个缺点需要自己衡量下

1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。尤其是代码量多的时候。
      2. 如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee 引用,比如在递归中。另一个函数需要引用自身的例子,是在事件触发后事件监听器需要解绑自身。
      3. 匿名函数的可读性比较差,因为没有函数名,没法直观的看清匿名函数起到的作用。

所有始终给一个函数表达式命名是最佳实践(匿名函数具体看需求)。

二、自执行函数

var a = 2;
(function foo() {
    var a = 3;
     console.log( a ); // 3
})();
console.log( a ); // 2

 还有一种方式也很多人推荐

var a = 2;
(function foo() {
    var a = 3;
     console.log( a ); // 3
}());
console.log( a ); // 2

 当然啦,这两种形式在功能上是一致的。选择哪个全凭个人喜好。

由于函数被包含在一对( ) 括号内部,因此成为了一个表达式,通过在末尾加上另外一个( ) 可以立即执行这个函数,比如(function foo(){ .. })()。第一个( ) 将函数变成表达式,第二个( ) 执行了这个函数

另一个非常普遍的进阶用法是把它们当作函数调用并传递参数进去。

例如:
var a = 2;
(function fn( global ) {
    var a = 3;
    console.log( a ); // 3
    console.log( global.a ); // 2
})( window );
console.log( a ); // 2

 我们将window 对象的引用传递进去,但将参数命名为global,因此在代码风格上对全局对象的引用变得比引用一个没有“全局”字样的变量更加清晰。当然可以从外部作用域传递任何你需要的东西,并将变量命名为任何你觉得合适的名字。这对于改进代码风格是非常有帮助的。

块级作用域

首先我们来看哥例子,例如

for(var i=0;i<5;i++){
   console.log(i);
}
alert(i);   //5


我们在for 循环的头部直接定义了变量i,在for{}包含的地方使用了变量i,但是在for{}外部还是可以访问变量i,从而i 会被绑定在外部作用域(函数或全局)中。同样道理

var flag=ture;
if(flag){
   var a=1;
   console.log(a);   //1
}
console.log(a)    //a

 在if语句里面,我们定义了一个变量a,在if里面能够访问,但是在外部还是可能访问a的。

但是with,它不仅是一个难于理解的结构,同时也是块作用域的一个例子(块作用域的一种形式),用with 从对象中创建出的作用域仅在with 声明中而非外部作用域中有效。

还有就是JavaScript 的ES3 规范中规定try/catch 的catch 分句会创建一个块作用域,其中声明的变量仅在catch 内部有效。

try {
	undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
	console.log( err ); // 能够正常执行!
}
console.log( err ); // ReferenceError: err not found

到目前为止,我们知道JavaScript 在暴露块作用域的功能中有一些奇怪的行为。如果仅仅是这样,那么JavaScript 开发者多年来也就不会将块作用域当作非常有用的机制来使用了。

目前,ES6 改变了现状,引入了新的let 关键字,提供了除var 以外的另一种变量声明方式。let 关键字可以将变量绑定到所在的任意作用域中(通常是{ .. } 内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。 

var foo = true;
if (foo) {
	let bar = foo * 2;
bar = something( bar );
	console.log( bar );
}
console.log( bar ); // ReferenceError

 用let 将变量附加在一个已经存在的块作用域上的行为是隐式的。在开发和修改代码的过程中,如果没有密切关注哪些块作用域中有绑定的变量,并且习惯性地移动这些块或者将其包含在其他的块中,就会导致代码变得混乱。

还有一点值得注意的是:只要声明是有效的,在声明中的任意位置都可以使用{ .. } 括号来为let 创建一个用于绑定的块

if (foo) {
{ // <-- 显式的快
    let bar = foo * 2;
    bar = something( bar );
    console.log( bar );
    }
}
console.log( bar ); // ReferenceError

 在这个例子中,我们在if 声明内部显式地创建了一个块,如果需要对其进行重构,整个块都可以被方便地移动而不会对外部if 声明的位置和语义产生任何影响。

我们通过自执行函数还可以模拟块级作用。

(function(){
   //代码
})()

自执行函数里面的标识符在外面是访问不到的。

 js是ES6之前是没有块级作用域的,但是通过一些手段我们可以模拟块级作用域。

学习笔记,欢迎交流探讨~

 

 

 

 

 

发表评论

电子邮件地址不会被公开。 必填项已用*标注