EagleBear2002 的博客

这里必须根绝一切犹豫,这里任何怯懦都无济于事

Web 前端开发-07-2-JavaScript 作用域与闭包

JavaScript 作用域

作用域是当前的执行上下文,值(en-US)和表达式在其中“可见”或可被访问,即作用域指的是有权访问的变量集合。

  • 如果一个变量(en-US)或表达式不在当前的作用域中,那么它是不可用的
  • 作用域也可以堆叠成层次结构,子作用域可以访问父作用域,反过来则不行

JavaScript 的作用域分以下三种:

  • 全局作用域:脚本模式运行所有代码的默认作用域
  • 模块作用域:模块模式中运行代码的作用域
  • 函数作用域:由函数创建的作用域

此外,(ES6)用 letconst 声明的变量属于额外的作用域:

  • 块级作用域:用一对花括号(一个代码块)创建出来的作用域

JavaScript 变量

在 JavaScript 中,对象和函数也是变量。

作用域决定了从代码不同部分对变量、对象和函数的可访问性。

全局变量

在函数之外声明的变量,叫做全局变量,因为它可被当前文档中的任何其他代码所访问。

全局变量的作用域是全局的:网页的所有脚本和函数都能够访问它。

1
2
3
4
5
6
var carName = " Volvo";

// code here can use carName
function myFunction() {
// code here can use carName
}

自动全局

如果为尚未声明的变量赋值,此变量会自动成为全局变量。这段代码将声明一个全局变量 carName,即使在函数内进行了赋值。

1
2
3
4
5
// code here can use carName
function myFunction() {
carName = "Volvo";
// code here can use carName
}

函数作用域

在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的域的内部有定义。

相对应的,一个函数可以访问定义在其范围内的任何变量和函数。

换言之,定义在全局域中的函数可以访问所有定义在全局域中的变量。在另一个函数中定义的函数也可以访问在其父函数中定义的所有变量和父函数有权访问的任何其他变量。

1
2
3
4
5
// code here can not use carName
function myFunction() {
var carName = "Volvo";
// code here can use carName
}

JavaScript 变量的有效期

JavaScript 变量的有效期始于其被创建时。

局部变量会在函数完成时被删除。

全局变量会在您关闭页面是被删除。

函数参数:函数参数也是函数内的局部变量。

HTML 中的全局变量:

  • 通过 JavaScript,全局作用域形成了完整的 JavaScript 环境。
  • 在 HTML 中,全局作用域是 window。所有全局变量均属于 window 对象。

困境

假设想要使用一个变量来计算,并且希望这个计数器对所有函数都可用。

可以使用一个全局变量和一个函数来增加计数器:

1
2
3
4
5
6
7
var counter = 0;
function add() {
counter += 1;
}
add();
add();
add();

但是,如果在函数内部声明了计数器,没有人可以在不调用 add() 的情况下更改它:

1
2
3
4
5
6
7
8
9
function add() {
var counter = 0;
counter += 1;
}

add();
add();
add();
// the counter should now be 3, but it does not work !

JavaScript 嵌套函数

所有函数都可以访问全局作用域

事实上,在 JavaScript 中,所有函数都可以访问它们“上层”的作用域

JavaScript 支持嵌套函数。嵌套函数可以访问它们“上层”的作用域

在这个例子中,内部函数 plus() 可以访问父函数中的 counter 变量

1
2
3
4
5
6
7
8
function add() {
var counter = 0;
function plus() {
counter += 1;
}
plus();
return counter;
}

JavaScript 闭包

给变量 add 分配一个自调用函数的返回值。

自调用函数只运行一次。它将计数器设置为零(0),并返回一个函数表达式。

这样 add 就变成了一个函数。“奇妙的”部分是它可以访问父作用域中的计数器。

这称为 JavaScript 闭包。它使函数具有“私有”变量成为可能。

计数器受匿名函数作用域的保护,只能使用 add 函数进行更改。

闭包是可以访问父作用域的函数,即使父函数已经关闭。

1
2
3
4
5
6
7
8
9
10
var add = (function () {
var counter = 0;
return function () {
return (counter += 1);
};
})();

add();
add();
add();

闭包的定义

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

闭包是函数和执行它的作用域组成的综合体——《JavaScript 权威指南》。所有的函数都是闭包。

函数可以访问它被创建时的上下文环境,称为闭包——《JavaScript 语言精粹》。内部函数比它的外部函数具有更长的生命周期。

更简单的定义:闭包是引用了自由变量的函数。自由变量是作用域可以导出到外部作用域的变量。函数内部变量和函数参数都可以是自由变量;函数参数不包含 this 和 arguments

词法作用域

词法(lexical)一词指的是,词法作用域根据源代码中声明变量的位置来确定该变量在何处可用。嵌套函数可访问声明于它们外部作用域的变量。

1
2
3
4
5
6
7
8
9
10
11
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() {
// displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}

displayName();
}

init();

闭包应用场景

闭包很有用,因为它允许将函数与其所操作的某些数据(环境)关联起来。这显然类似于面向对象编程。

  1. 实现私有成员
  2. 保护命名空间
  3. 避免污染全局变量
  4. 变量需要长期驻留在内存
1
2
3
4
5
6
7
8
9
function a() {
var i = 0;
function b() {
alert(++i);
}
return b;
}
var c = a();
c();

例:用闭包模拟私有方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var Counter = (function () {
var privateCounter = 0;

function changeBy(val) {
privateCounter += val;
}

return {
increment: function () {
changeBy(1);
},
decrement: function () {
changeBy(-1);
},
value: function () {
return privateCounter;
},
};
})();

console.log(Counter.value()); /* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value()); /* logs 2 */
Counter.decrement();
console.log(Counter.value()); /* logs 1 */

作用域链

function 对象同其他对象一样,拥有可以编程访问的属性,和一系列不能通过代码访问而仅供 js 引擎存取的内部属性,其中一个是[[scope]],包含了一个函数被创建的作用域中对象的集合。称为作用域链(Scope chain)。决定了那些数据可以被函数访问。

1
2
3
4
function add(num1, num2) {
var sum = num1 + num2;
return sum;
}

执行 add 函数

执行环境和作用域链。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function initUI() {
var bd = document.body,
links = document.getElementsByTagName("a"),
i = 0,
len = links.length;
while (i < len) {
update(links[i++]);
}
document.getElementById("go-btn").onclick = function () {
start();
};
bd.className = "active";
}

function initUI() {
var doc = document,
bd = doc.body,
links = doc.getElementsByTagName("a"),
i = 0,
len = links.length;
}

改变作用域链-with

不推荐使用 with,在 ECMAScript 5 严格模式中该标签已被禁止。推荐的替代方案是声明一个临时变量来承载你所需要的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function initUI() {
with (document) {
//avoid!
var bd = body,
links = getElementsByTagName("a"),
i = 0,
len = links.length;
while (i < len) {
update(links[i++]);
}
getElementById("go-btn").onclick = function () {
start();
};
bd.className = "active";
}
}

改变作用域链—性能问题

try-catch

1
2
3
4
5
6
7
8
9
10
try {
methodThatMightCauseAnError();
} catch (ex) {
alert(ex.message); //scope chain is augmented here
}
try {
methodThatMightCauseAnError();
} catch (ex) {
handleError(ex); //delegate to handler method
}

闭包、作用域和内存

1
2
3
4
5
6
function assignEvents() {
var id = "xdi9592";
document.getElementById("save - btn").onclick = function (event) {
saveDocument(id);
};
}

闭包执行

提升(Hosting)

引擎会在解释 JavaScript 代码之前首先进行编译,编译过程中的一部分工作就是找到所有的声明,并用合适的作用域将他们关联起来,这也正是词法作用域的核心内容。

JavaScript 变量的另一个不同寻常的地方是,你可以先使用变量稍后再声明变量而不会引发异常。这一概念称为变量提升;JavaScript 变量感觉上是被“提升”或移到了函数或语句的最前面。

但是,提升后的变量将返回 undefined 值。因此在使用或引用某个变量之后进行声明和初始化操作,这个被提升的变量仍将返回 undefined 值。

例子

1
2
3
4
5
6
7
8
9
10
11
/* 例子 1*/
console.log(x === undefined); // true
var x = 3;

/* 例子 2*/
// will return a value of undefined
var myvar = "my value";
(function () {
console.log(myvar); // undefined
var myvar = "local value";
})();
1
2
3
4
5
6
7
8
9
10
11
/* 例子 1*/
var x;
console.log(x === undefined); // true
x = 3;
/* 例子 2*/
var myvar = "my value";
(function () {
var myvar;
console.log(myvar); // undefined
myvar = "local value";
})();

由于存在变量提升,一个函数中所有的 var 语句应尽可能地放在接近函数顶部的地方。这个习惯将大大提升代码的清晰度。

let 和 const 关键字

ES6 新增块级作用域。这个区块对这些变量从一开始就形成了封闭作用域,直到声明语句完成,这些变量才能被访问(获取或设置),否则会报错 ReferenceError。

暂时性死区(英 temporal dead zone,简 TDZ),即代码块开始到变量声明语句完成之间的区域。

通过 let 声明的变量没有变量提升、拥有暂时性死区,作用于块级作用域:

  • 当进入变量的作用域(包围它的语法块),立即为它创建(绑定)存储空间,不会立即初始化,也不会被赋值
  • 访问(获取或设置)该变量会抛出异常 ReferenceError
  • 当执行到变量的声明语句时,如果变量定义了值则会被赋值,如果变量没有定义值,则被赋值为 undefined
1
2
3
4
5
6
7
{
// TDZ starts at beginning of scope
console.log(bar); // undefined
console.log(foo); // ReferenceError
var bar = 1;
let foo = 2; // End of TDZ (for foo)
}

temporal

使用术语“temporal”是因为区域取决于执行顺序(时间),而不是编写代码的顺序(位置)。例如,下面的代码会生效,是因为即使使用 let 变量的函数出现在变量声明之前,但函数的执行是在暂时性死区的外面。

1
2
3
4
5
6
7
{
// TDZ starts at beginning of scope
const func = () => console.log(letVar); // OK
// Within the TDZ letVar access throws `ReferenceError`
let letVar = 3; // End of TDZ (for letVar)
func(); // Called outside TDZ!
}

函数提升

对于函数来说,只有函数声明会被提升到顶部,而函数表达式不会被提升。

1
2
3
4
5
6
7
8
9
10
11
/* 函数声明 */
foo(); // "bar"
function foo() {
console.log("bar");
}

/* 函数表达式 */
baz(); // 类型错误:baz 不是一个函数
var baz = function () {
console.log("bar2");
};
1
2
3
4
5
6
7
8
9
10
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
return function () {
return this.name;
};
},
};
alert(object.getNameFunc()());
1
2
3
4
5
6
7
8
9
10
11
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function () {
var that = this;
return function () {
return that.name;
};
},
};
alert(object.getNameFunc()());