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

发生了什么?

  1. add 函数被调用时,它返回了一个函数。
  2. 这个返回的函数关闭上下文环境,记住参数 x 在那个时候是什么(例如,上面代码的 5)。
  3. 当调用 add 函数的返回结果被分配给变量 add5 时,add5 函数总是知道 x 是什么,当 add5 被初始化创建时。
  4. add5 变量引用一个函数,它总是把传给它的参数加 5
  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),但可以通过 getNamesetName 函数访问,因为,它们创建了闭包,在里边引用了 name 变量。

 

其他


 

下载 Demo

posted @ 2013-10-08 21:41  船长&CAP  阅读(635)  评论(0编辑  收藏  举报
免费流量统计软件