js函数作用域和块作用域

学习自《你不知道的javascript》

一、函数作用域

1.1 规避冲突

1
2
3
4
5
6
7
8
9
10
function foo() {
function bar(a) {
i = 3; // 修改for循环中的i
console.log(a + i)
}
for(var i = 0; i < 10; i++) {
bar(i * 2) // 无限循环
}
}
foo();

上面的代码会造成无限循环,为什么呢?
      for循环第一次调用bar()时,i值是 0 传入bar()中,而bar()第一句就对i进行了重新赋值为3,然后继续循环调用bar(),i值始终为3不会改变,进入无限循环。

规避冲突的方法

  • 使用第三方库时,为了避免和其他库冲突,通常在全局作用域中声明一个独特名字的变量,一般是对象;这个对象是库的命名空间,所有需要暴露给外界的功能都会成为该对象的属性,而不是把自己的标识符暴露在顶级的词法作用域中。

  • 模块管理。和现代的模块机制很接近,就是从众多的模块管理器中挑选一个来使用。利用作用域的规则强制所有标识符都不能注入到共享作用域中,而是保持在私有、无冲突的作用域中。

1.2 函数作用域深入

函数有两种申明方式,一种是函数声明,另一种是函数表达式。
如何区分呢?
function关键字是不是声明时的第一个词,是的话就是函数申明,不是的话就叫函数表达式

匿名函数和具名函数

1
2
3
setTimeout(function() {
console.log("I wait 1 second")
}, 1000)

以上代码内的函数就是匿名函数表达式。函数表达式可以省略函数名,但是函数申明不能省略函数名。

匿名函数的缺点:

  • 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试困难。
  • 没有函数名,难以引用自身。比如在递归中和事件触发后事件监听器需要解绑自身。
  • 可读性/可理解性差。

立即执行函数表达式

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

      上述代码第一个()将函数变成表达式,第二个()执行了这个函数。这种模式有个专业术语叫IIFE。该模式还有一个变种:

1
2
3
4
5
6
var a = 2;
(function(){
var a = 3;
console.log(a); // 3
}())
console.log(a) // 2

      IIFE普遍的进阶方法是把它们当做函数调用并传参进去。参照下面的代码,其实就是把window对象当做参数传进函数内。

1
2
3
4
5
6
7
var a = 2;
(function IIFE(global) {
var a = 3;
console.log(a) // 3
console.log(global.a) // 2
})(window)
console.log(a) // 2

      IIFE还有另一种进阶方法,即倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行后当作参数传进去。

1
2
3
4
5
6
7
8
var a = 2;
(function IIFE( def ) {
def(window);
})(function def( global ) {
var a = 3;
console.log(a) // 3
console.log(global.a) //2
})

1.2 块作用域

先看一段代码:

1
2
3
4
for(var i = 0; i < 10; i++) {
console.log(i)
}
window.i // 10

      结合上段代码可以看到,定义在for循环中的变量i被绑定到了全局作用域上,成为了全局作用域的属性。
      这就是块作用域的用处。变量的声明应该距离使用的地方越近越好,并最大限度地本地化。


另一个例子:

1
2
3
4
5
6
var foo = true;
if(foo) {
var bar = foo * 2;
bar = something( bar );
console.log(bar)
}

      在上面的例子中,bar变量仅仅在if声明的上下文中使用,因此如果能将它声明在if块内部中会是一个很有意义的事情。但是,当使用var声明变量时,它写在哪里都是一样的,因为它们最终都会属于外部作用域。其实就是无法将bar变量限制在if判断的作用域内,使得外部无法访问。

      开发过程中,一些只在特定作用域内才用到的变量等,可能最终成为了外部甚至全局作用域的属性,这样容易污染全局。如果存在块作用域这些将不是烦恼,但可惜,js并没有像其他语言一样直接提供块作用域,但依然可以通过一些方法来实现块作用域。

1.2.1 with

1.2.2 try/catch

try/catchcatch分句会创建一个块作用域,其中声明的变量仅在catch内部有效。

例如:

1
2
3
4
5
6
try {
undefined() // 执行一个非法操作来强制制造一个异常
} catch (err) {
console.log(err) // 能正常执行
}
console.log(err) // ReferenceError: err is not defined

1.2.3 let

let关键字可以将变量绑定到所在的任意作用域中(通常是{...}内部)。换句话说,let为其声明的变量隐式地劫持了所在的块作用域。

例子:

1
2
3
4
5
6
7
var foo = true;
if(foo) {
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
console.log(bar) // ReferenceError

      用let将变量附加在一个已经存在的块作用域上的行为是隐式的。如果害怕代码变得混乱的话可以显式的创建块。上面的代码改为:

1
2
3
4
5
6
7
8
9
var foo = true;
if(foo) {
{ // 显式的块
let bar = foo * 2;
bar = something(bar);
console.log(bar);
}
}
console.log(bar) // ReferenceError

      但是使用let进行的声明不会在块作用域中进行提升。声明的代码被运行前,声明并不“存在”
例子:

1
2
3
4
{
console.log(bar); // ReferenceError
let bar = 2;
}

      let在循环里也有着非常大的优势。

1
2
3
4
for(let i = 0; i < 10; i++) {
console.log(i);
}
console.log(i); // ReferenceError

for循环头部的let不仅将i绑定到for循环的块中,事实上它将其重新绑定到了循环的每一个迭代中,确保使用上一个循环迭代结束时的值重新进行赋值。

类似于:

1
2
3
4
5
6
7
{
let j;
for(j = 0; j < 10; j++) {
let i = j; // 每个迭代重新绑定
console.log(i)
}
}

1.2.4 const

const也同样可以用来创建块作用域变量,但是其值是固定的。

1
2
3
4
5
6
7
8
9
var foo = true;
if(foo) {
var a = 2;
const b = 3; // 包含在if中的块作用域常量
a = 3;
b = 4; // 错误
}
console.log(a); // 3
console.log(b); // ReferenceError
Newer Post

Eclipse中tomcat启动报错

Caused by: java.lang.ClassNotFoundException: org.springframework.web.context.support.XmlWebApplicati背景:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;由于公司年后将gitl …

继续阅读
Older Post

公司内部Ext框架使用注意事项或者叫开发指南

以下内容不全是我一个人理解分析的,感谢前辈们: 陈德林、徐冬冬、大许磊、沈梁以及黄建雄的帮忙! 公司内部Ext框架是老板在Ext基础上封装好的,使用起来极为方便,基本上就是复制粘贴改数据,但前提是你知道在哪里改。 导航 显示页面 框架学习 OnBeforeFormLoad() tools.Set …

继续阅读