【你不知道的JavaScript】(二)函数作用域和块作用域

(一)函数作用域

1. 函数中的作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

2. 隐藏内部实现

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容,还可以避免同名标识符之间的冲突

规避冲突的方法:

(1) 全局命名空间

一些常使用的第三方库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象。这个对象被用作库的命名空间所有需要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将自己的标识符暴漏在顶级的词法作用域中

var MyReallyCoolLibrary = {
    awesome: "stuff",
    doSomething: function() {
        // ...
    },
    doAnotherThing: function() {
        // ...
    }
};

(2) 模块管理

使用模块管理器工具,任何库都无需将标识符加入到全局作用域中,而是通过依赖管理器的机制将库的标识符显式地导入到另外一个特定的作用域中

3. 函数作用域

(1) 函数声明与函数表达式

区分函数声明和表达式最简单的方法是看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。

简单的说,不以function开头的函数语句就是函数表达式定义。

函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处

// 函数声明
function foo() {}

// 函数表达式
(function bar() {})

// 函数表达式
x = function hello() {}

if (x) {
   // 函数表达式
   function world() {}
}

// 函数声明
function a() {
   // 函数声明
   function b() {}
   if (0) {
      //函数表达式
      function c() {}
   }
}

(2) 匿名和具名

函数表达式可以是匿名的,而函数声明则不可以省略函数名。

  • 匿名函数
setTimeout( function() { // 匿名函数表达式
    console.log("I waited 1 second!");
}, 1000 );

//匿名函数表达式书写起来简单快捷,但也有几个缺点需要考虑
//1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
//2. 如果没有函数名,当函数需要引用自身时只能使用已经过期的arguments.callee 引用,比如在递归中。
//   另一个函数需要引用自身的例子,是在事件触发后事件监听器要解绑自身。
//3. 匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可以让代码不言自明。
  • 具名函数

行内函数表达式非常强大且有用 —— 匿名和具名之间的区别并不会对这点有任何影响。始终给函数表达式命名是一个最佳实践

setTimeout( function timeoutHandler() {  
    //给函数表达式指定一个函数名可以有效解决匿名函数表达式带来的问题
    console.log( "I waited 1 second!" );
}, 1000 );

(3) 立即执行函数表达式

IIFE,代表立即执行函数表达式Immediately Invoked Function Expression)。

  • 函数名对IIFE 当然不是必须的,IIFE 最常见的用法是使用一个匿名函数表达式
// IIFE 两种常见的形式
(function (){ .. })();
// 第一个( ) 将函数变成表达式,第二个( ) 执行了这个函数

// 另一个改进形式
(function(){ .. }());
// 调用的() 括号被移进了用来包装的( ) 括号中

// 以上两种形式在功能上是一致的,选择哪个全凭个人喜好。
  • IIFE 的另一个非常普遍的进阶用法把它们当作函数调用并传递参数进去
var a = 2;
(function IIFE( global ) {
    var a = 3;
    console.log( a ); // 3
    console.log( global.a ); // 2
})( window ); 
console.log( a ); // 2

以上代码中,将window对象的引用传递进去,但将参数命名为global;当然也可以从外部作用域传递任何你需要的东西,并将变量命名为任何你觉得合适的名字

  • IIFE 还有一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当作参数传递进去。这种模式在UMD项目中被广泛使用。
var a = 2;
(function IIFE( def ) {
    def( window );
})(function def( global ) {
    var a = 3;
    console.log( a ); // 3
    console.log( global.a ); // 2
});

函数表达式def 定义在片段的第二部分,然后当作参数(这个参数也叫作def)被传递进IIFE 函数定义的第一部分中。最后,参数def(也就是传递进去的函数)被调用,并window 传入当作global 参数的值。

(二)块作用域

变量的声明应该距离使用的地方越近越好,并最大限度地本地化。

1. with

with 从对象中创建出的作用域仅在with 声明中而非外部作用域中有效。有关with关键字的用法可查看《【你不知道的JavaScript】(一)作用域与词法作用域》

2. try/catch

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

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

3. let

  • ES6 引入了新的let 关键字,提供了除var以外的另一种变量声明方式;
  • let关键字可以将变量绑定到所在的任意作用域中(通常是{ .. } 内部)。
var foo = true;
if (foo) {
    let bar = foo * 2;
    bar = something( bar );
    console.log( bar );
}
console.log( bar ); // ReferenceError

(1) 垃圾收集

另一个块作用域非常有用的原因和闭包及回收内存垃圾的回收机制相关。

(2) let循环

for (var j=0; j<10; j++) {
    console.log( j );
}
console.log( j ); // 10


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

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

4. const

除了let 以外,ES6还引入了const,同样可以用来创建块作用域变量,但其值是固定(常量)。之后任何试图修改值的操作都会引起错误。

var foo = true;
if (foo) {
    var a = 2;
    const b = 3; // 包含在if 中的块作用域常量

    a = 3; // 正常!
    b = 4; // 错误!
}

console.log( a ); // 3
console.log( b ); // ReferenceError!

函数作用域和块作用域的行为是一样的,可以总结为:任何声明在某个作用域内的变量,都将附属于这个作用域。

(三)提升

1. 先有鸡还是先有蛋

正确的思考思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理

a1 = 2;
var a1;
console.log( a1 ); // 2

//↑上面代码会发生以下处理
var a1; // 编译
a = 21; // 执行
console.log( a1 );


console.log( a ); // undefined
var a = 2;

//↑上面代码会发生以下处理
var a; // 编译
console.log( a );
a = 2; // 留在原地等待执行

也就是说,先有蛋(声明)后有鸡(赋值);只有声明本身会被提升,而赋值或其他运行逻辑会留在原地

2. 注意事项

(1) 每个作用域都会进行提升操作;函数内部的声明会被提升到该函数中的最上方,而不是整个程序的最上方;

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

//↑上面代码跟下面等同
function foo() {
    var a;
    console.log( a ); // undefined
    a = 2;
}
foo();

(2) 函数声明会被提升,但是函数表达式却不会被提升。

foo(); // 不是ReferenceError, 而是TypeError!
var foo = function bar() {
    // ...
};

// ↑上面代码中的变量标识符`foo()`被提升并分配给所在作用域(在这里是全局作用域)
// 因此`foo()` 不会导致`ReferenceError`
// 但是`foo`此时并没有赋值(如果它是一个函数声明而不是函数表达式,那么就会赋值)。
// `foo()`由于对`undefined`值进行函数调用而导致非法操作,因此抛出`TypeError` 异常。

即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用。

foo(); // TypeError 
bar(); // ReferenceError 
var foo = function bar() { 
    // ... 
};

这个代码片段经过提升后,实际上会被理解为以下形式:

var foo; 
foo(); // TypeError 
bar(); // ReferenceError 
foo = function() { 
    var bar = ...self... 
    // ... 
}

(3) 函数声明和变量声明都会被提升。函数优先 —— 函数会首先被提升,然后才是变量。

foo(); // 1 

var foo; 
function foo() { 
    console.log( 1 ); 
} 
foo = function() { 
    console.log( 2 ); 
};

↑以上代码片段会被引擎理解为如下形式:

function foo() { 
    console.log( 1 ); 
} 
foo(); // 1 
foo = function() { 
    console.log( 2 ); 
};

var foo 尽管出现在 function foo()...的声明之前,但它是重复的声明(因此被忽略了),因为函数声明会被提升到普通变量之前。尽管重复的 var声明会被忽略掉,但出现在后面的函数声明还是可以覆盖前面的。

小总结

  • 我们习惯将 var a = 2; 看作一个声明,而JavaScript 引擎将 var aa = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。
  • 无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。
  • 形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升
  • 声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升,会留在原地等待执行
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容