JavaScript 作用域和闭包——另一个角度:扩展你对作用域和闭包的认识【翻译+整理】
——这篇文章有点意思,可以扩展你对作用域和闭包的认识。
本文内容
- 背景
- 作用域
- 闭包
- 臭名昭著的循环问题
- 自调用函数(匿名函数)
- 其他
我认为,尝试向别人解释 JavaScript 作用域和闭包是很纠结的事情。
背景
有很多文章和博客都在试图解释的作用域(scope)和关闭(closure),但总体来说,我认为大多数都不是很清楚。此外,一些人想当然地认为,之前,每个人都已经大概用15种其他语言开发,而我的经验是,很多这样编写 JavaScript 代码的人都具有 HTML 和 CSS 背景,而不是 C 和 Java。
因此,本文的目标是,让每个人掌握什么是作用域和闭包,他们如何工作,特别是你如何可以从中受益。在阅读本文前,你需要理解变量和函数这些基本概念。
作用域(Scope)
作用域是指变量和函数可访问性在哪里,以及执行环境是什么。最基本的是,一个变量或函数可以定义在全局或局部作用域。变量具有所谓的函数作用域,函数跟变量具有相同的作用域。
全局(Global)作用域
当是全局的时候,意味着,在你的代码中,可以从任何地方访问。如下代码所示:
var monkey = "Gorilla";
function greetVisitor() {
return alert("Hello dear blog reader!");
}
如果该代码执行在 Web 浏览器中,那么,函数的作用域是 window,因此,对运行在 Web 浏览器窗口中所有东西,该函数都是可用的。
局部(Local)作用域
与全局作用域相反,局部作用域只是在你某部分的代码中定义并访问,如一个函数。代码如下所示:
function talkDirty() {
var saying = "Oh, you little VB lover, you";
return alert(saying);
}
alert(saying); // Throws an error
上面代码,变量 saying 只在 talkDirty 函数内可用。而在外边,更本没有定义。注意:如果声明 saying 前边没有 var 关键字,那么该变量将自动变成一个全局变量。
这也意味着,如果你有嵌套(nested)函数,那么,这个嵌套函数将可以访问包含其函数内的变量和函数:
function saveName(firstName) {
function capitalizeName() {
return firstName.toUpperCase();
}
var capitalized = capitalizeName();
return capitalized;
}
alert(saveName("Robert")); // Returns "ROBERT"
正如上面看到的,嵌套函数 capitalizeName 不需要任何参数传递,就可以完全访问它外边的 saveName 函数的参数 firstName。清楚起见,让我们看下面的例子:
function siblings() {
var siblings = ["John", "Liza", "Peter"];
function siblingCount() {
var siblingsLength = siblings.length;
return siblingsLength;
}
function joinSiblingNames() {
return "I have " + siblingCount() + " siblings:\n\n" + siblings.join("\n");
}
return joinSiblingNames();
}
alert(siblings()); // Outputs "I have 3 siblings: John Liza Peter"
上面代码,那两个嵌套函数都可以访问包含其函数内的 siblings 数组,并且,每个嵌套函数都可以同等地访问另一个嵌套函数(在这种情况下,joinSiblingNames 可以访问 siblingCount)。但是, siblingCount 函数中的变量 siblingsLength 只在它所在的函数可用,也就是作用域。
闭包(Closures)
现在,当你希望获得更好地掌握作用域是什么时,让我们向组合添加闭包。闭包是表达式,通常是函数,再一个某个上下文环境内进行变量设置。或者,为了尝试并使其更容易,涉及局部变量的内部函数创建闭包。例如:
function add(x) {
return function (y) {
return x + y;
};
}
var add5 = add(5);
var no8 = add5(3);
alert(no8); // Returns 8
发生了什么?
- 当 add 函数被调用时,它返回了一个函数。
- 这个返回的函数关闭上下文环境,记住参数 x 在那个时候是什么(例如,上面代码的 5)。
- 当调用 add 函数的返回结果被分配给变量 add5 时,add5 函数总是知道 x 是什么,当 add5 被初始化创建时。
- add5 变量引用一个函数,它总是把传给它的参数加 5。
- 这意味着,当用 3 调用 add5 时,它应该返回 3 加 5,等于 8。
因此,在 JavaScript 世界里,事实上,add5 函数实际像如下所示:
function add5(y) {
return 5 + y;
}
臭名昭著的循环问题
有多少次你创建一个循环,想以某种方式分配 i 的值,例如:给一个元素,有没有发现它只是返回的最后一个 i 的值?
-
错误的引用
让我们看下错误的代码,它创建五个链接元素,每个元素的文本值为 i,并且每个元素的单击事件为弹出内容为 i 的 alert。把这五个元素追加到 document body:
function addLinks() {
for (var i = 0, link; i < 5; i++) {
link = document.createElement("a");
link.innerHTML = "Link " + i;
link.onclick = function () {
alert(i);
};
document.body.appendChild(link);
}
}
window.onload = addLinks;
每个元素都获得了正确的文本,例如“Link 0”、“Link 1”等等,但是你单击的每个链接,alert 中的值都是 5,这显然不对,为什么?原因在于,变量 i 以 1 递增,可 onclick 事件没有被执行,仅仅是应用到了元素,i 再增加 1。
因此,循环继续直到等于 5,这是 i 的最后一个值,此时,函数 addLinks 退出。之后,当 onclick 事件实际被触发时,只得到 i 的最后一个值。
-
正确的引用
你所需要做的就是创建一个闭包,这样,当你把 i 值应用到元素的 onclick 事件时,就会在触发那个时刻得到 i 的准确值。如下所示:
function addLinks() {
for (var i = 0, link; i < 5; i++) {
link = document.createElement("a");
link.innerHTML = "Link " + i;
link.onclick = function (num) {
return function () {
alert(num);
};
} (i);
document.body.appendChild(link);
}
}
window.onload = addLinks;
上面代码就没有问题了。单击第一个创建的链接元素,alter 为 0;第二个为 1 等等。该解决方案是,应用到 onclick 事件的内部函数创建了一个闭包,在那里引用参数 num,也就是 i 的值。
自调用函数(Self-Invoking Functions)
自调用函数是那些自动执行的函数(匿名函数),并且创建它们自己的闭包,如下代码所示:
(function () {
var dog = "German Shepherd";
alert(dog);
})();
alert(dog); // Returns undefined
dog 变量只在其作用域内可用。它可以解决我们上面的循环问题,而且这也是 Yahoo JavaScript Module Pattern 的基础。
Yahoo JavaScript Module Pattern
Yahoo JavaScript 模块模式的要点是,它使用了一个自调用函数来创建一个封闭,因此,使具有私有和公共的属性和方法成为可能。一个简单的例子,如下所示:
var person = function () {
// Private
var name = "Robert";
return {
getName: function () {
return name;
},
setName: function (newName) {
name = newName;
}
};
} ();
alert(person.name); // Undefined
alert(person.getName()); // "Robert"
person.setName("Robert Nyman");
alert(person.getName()); // "Robert Nyman"
这个代码就很优雅了。现在,你可以自己决定公开什么,隐藏什么了。变量 name 就是对外部隐藏的(在外部引用为 undefined),但可以通过 getName 和 setName 函数访问,因为,它们创建了闭包,在里边引用了 name 变量。
其他