作用域和闭包

一. 作用域是什么

1. 定义

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。

2. LHS 查询和RHS 查询

如果查找的目的是对变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,
就会使用 RHS 查询。赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操
作都会导致关联作用域的赋值操作。

var a
LHS: a = 2 
RHS: console.log(a)
3. 作用域嵌套

LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所
需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层
楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。

function foo(a) {
 console.log( a + b ); // 当前作用域没有b,向上一级作用域查找
}
var b = 2;
foo( 2 ); // 4
4. 异常

不成功的 RHS 引用会导致抛出 ReferenceError 异常。不成功的 LHS 引用会导致自动隐式
地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛
出 ReferenceError 异常(严格模式下)

二. 词法作用域

1. 作用域工作模型

作用域共有两种工作模型: 词法作用域和动态作用域

2. 如何预测每个变量的位置

在这个例子中有三个逐级嵌套的作用域。为了帮助理解,可以将它们想象成几个逐级包含
的气泡。


image.png
  1. 包含着整个全局作用域,其中只有一个标识符:foo。
  2. 包含着 foo 所创建的作用域,其中有三个标识符:a、bar 和 b。
  3. 包含着 bar 所创建的作用域,其中只有一个标识符:c。

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段
基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它
们进行查找。

3. 特殊情况

eval(..)和with,这两种机制都将导致代码运行变慢,不要使用它们。

三. 函数作用域和块作用域

函数是 JavaScript 中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会
在所处的作用域中“隐藏”起来,这是有意为之的良好软件的设计原则。

1.隐藏内部实现
image.png

下面两段代码,哪个看起来更舒服?

function doSomething(a) {
 b = a + doSomethingElse( a * 2 );
 console.log( b * 3 );
}
function doSomethingElse(a) {
 return a - 1;
}
var b;
doSomething( 2 ); // 15
function doSomething(a) {
 function doSomethingElse(a) {
 return a - 1;
 }
 var b;
 b = a + doSomethingElse( a * 2 );
 console.log( b * 3 );
}
doSomething( 2 ); // 15
规避冲突
  1. 全局命名空间: 尽可能少使用全局变量。
  2. 模块管理: 按模块管理功能代码,自己管理自己内部变量。
2. 块作用域

函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,
也可以属于某个代码块(通常指 { .. } 内部)。


image.png

我们在 for 循环的头部直接定义了变量 i,通常是因为只想在 for 循环内部的上下文中使
用 i,而忽略了 i 会被绑定在外部作用域(函数或全局)中的事实。
这就是块作用域的用处。变量的声明应该距离使用的地方越近越好,并最大限度地本地
化。

从 ES3 开始,try/catch 结构在 catch 分句中具有块作用域。

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

在 ES6 中引入了 let 关键字(var 关键字的表亲),用来在任意代码块中声明变量。


image.png

四. 提升

1. 先有鸡还是先有蛋
a = 2;
var a;
console.log( a );

你认为 console.log(..) 声明会输出什么呢?

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

这次呢?

当你看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个
声明:var a; 和 a = 2;。第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在
原地等待执行阶段。
我们的第一个代码片段会以如下形式进行处理:

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

其中第一部分是编译,而第二部分是执行。
类似地,我们的第二个代码片段实际是按照以下流程处理的:

var a;
console.log( a );
a = 2;
2. 函数优先
foo(); 
var foo;
function foo() {
 console.log( 1 );
}
foo = function() {
 console.log( 2 );
};

猜猜执行结果?


image.png

会输出 1 而不是 2 !这个代码片段会被引擎理解为如下形式:

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

五. 作用域闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用
域之外执行。

function foo() {
 var a = 2;
 function bar() { 
 console.log( a );
 }
 return bar;
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果。

无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到
闭包。

function foo() {
 var a = 2;
 function baz() {
 console.log( a ); // 2
 }
 bar( baz );
}
function bar(fn) {
 fn(); // 妈妈快看呀,这就是闭包!
}

传递函数当然也可以是间接的。

var fn;
function foo() {
 var a = 2;
 function baz() {
 console.log( a );
 }
 fn = baz; // 将 baz 分配给全局变量
}
function bar() {
 fn(); // 妈妈快看呀,这就是闭包!
}
foo();
bar(); // 2

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用
域的引用,无论在何处执行这个函数都会使用闭包。

循环和闭包

要说明闭包,for 循环是最常见的例子。

for (var i=1; i<=5; i++) {
 setTimeout( function timer() {
 console.log( i );
 }, 0 );
}

正常情况下,我们对这段代码行为的预期是分别输出数字 1~5。
但实际上,这段代码在运行时会输出五次 6。
这里引伸出一个更深入的问题,代码中到底有什么缺陷导致它的行为同语义所暗示的不一
致呢?
缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是
根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,
但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。

解决这个问题的方法如下,它需要有自己的变量,用来在每个迭代中储存 i 的值:

for (var i=1; i<=5; i++) {
 (function() { 
  // 使用函数包裹了一层,每次迭代都有自己的作用域,
  // 使得延迟函数的回调可以将新的作用域封闭在每个迭代内部
   var j = i;
   setTimeout( function timer() {
     console.log( j );
   }, j*1000 );
 })();
}
使用块作用域
for (var i=1; i<=5; i++) {
 let j = i; // 是的,闭包的块作用域!
 setTimeout( function timer() {
   console.log( j );
 }, j*1000 );
}
for (let i=1; i<=5; i++) {
 setTimeout( function timer() {
   console.log( i );
 }, i*1000 );
}

for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声
明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这
个变量。

模块

还有其他的代码模式利用闭包的强大威力,但从表面上看,它们似乎与回调无关。下面一
起来研究其中最强大的一个:模块。

function CoolModule() {
 var something = "cool"; 
 var another = [1, 2, 3];
 function doSomething() { 
 console.log( something );
 }
 function doAnother() {
 console.log( another.join( " ! " ) );
 }
 return {
 doSomething: doSomething, 
 doAnother: doAnother
 };
}
var foo = CoolModule(); 
foo.doSomething(); // cool
foo.doAnother(); // 1 ! 2 ! 3
ES6中的模块

ES6 中为模块增加了一级语法支持。但通过模块系统进行加载时,ES6 会将文件当作独立
的模块来处理。每个模块都可以导入其他模块或特定的 API 成员,同样也可以导出自己的
API 成员。

bar.js
function hello(who) {
 return "Let me introduce: " + who;
}
export hello;
foo.js
// 仅从 "bar" 模块导入 hello()
import hello from "bar";
var hungry = "hippo";
function awesome() {
 console.log(
 hello( hungry ).toUpperCase()
 );
}
export awesome;
baz.js
// 导入完整的 "foo" 和 "bar" 模块
module foo from "foo";
module bar from "bar";
console.log(
 bar.hello( "rhino" )
); // Let me introduce: rhino
foo.awesome(); // LET ME INTRODUCE: HIPPO

模块文件中的内容会被当作好像包含在作用域闭包中一样来处理,就和前面介绍的函数闭
包模块一样。

你可能感兴趣的:(作用域和闭包)