javascript 之 闭包

闭包
MDN 对闭包的定义:
闭包是指那些能够访问独立(自由)变量的函数(变量在本地使用,但定义在一个封闭的作用域中)。换句话说,这些函数可以「记忆」它被创建时候的环境。
《JavaScript 权威指南(第6版)》对闭包的定义:
函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内,这种特性在计算机科学文献中称为闭包。
《JavaScript 高级程序设计(第3版)》对闭包的定义:
闭包是指有权访问另一个函数作用域中的变量的函数。
上面这些定义都比较晦涩难懂,阮一峰的解释稍微好理解一些:
由于在 Javascript 语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成定义在一个函数内部的函数。

闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是可以读取函数内部的变量(作用域链)另一个就是让这些变量的值始终保持在内存中。怎么来理解这句话呢?请看下面的代码。

function fun() {   
    var n = 1;

    add = function() {
        n += 1
    }

    function fun2(){
        console.log(n);
    }

    return fun2;
}

var result = fun();  
result(); // 1
add();
result(); // 2

在这段代码中,result 实际上就是函数 fun2。它一共运行了两次,第一次的值是 1,第二次的值是 2。这证明了,函数 fun 中的局部变量 n 一直保存在内存中,并没有在 fun调用后被自动清除。
为什么会这样呢?原因就在于 fun 是 fun2 的父函数,而 fun2 被赋给了一个全局变量,这导致 fun2 始终在内存中,而 fun2 的存在依赖于 fun,因此 fun 也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是 add = function() { n += 1 } 这一行。首先,变量add 前面没有使用 var 关键字,因此 add 是一个全局变量,而不是局部变量。其次,add的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,和 fun2处于同一作用域,所以 add 相当于是一个 setter,可以在函数外部对函数内部的局部变量进行操作。
计数器的困境
我们再来看一个经典例子「计数器的困境」,假设你想统计一些数值,且该计数器在所有函数中都是可用的。你可以定义一个全局变量 counter 当做计数器,再定义一个 add() 函数来设置计数器递增。代码如下:

var counter = 0;
function add() {
    return counter += 1;
}

console.log(add());
console.log(add());
console.log(add());
// 计数器现在为 3

计数器数值在执行 add() 函数时发生变化。但问题来了,页面上的任何脚本都能改变计数器counter,即便没有调用 add() 函数。如果我们将计数器 counter 定义在 add() 函数内部,就不会被外部脚本随意修改到计数器的值了。代码如下:

function add() {
    var counter = 0;
    return counter += 1;
}

console.log(add());
console.log(add());
console.log(add());
// 本意是想输出 3, 但事与愿违,输出的都是 1

因为每次调用 add() 函数,计数器都会被重置为 0,输出的都是 1,这并不是我们想要的结果。闭包正好可以解决这个问题,我们在 add() 函数内部,再定义一个 plus() 内嵌函数(闭包),内嵌函数 plus() 可以访问父函数的 counter 变量。代码如下:

function add() {
    var counter = 0;
    var plus = function() {counter += 1;}
    plus();
    return counter; 
}

接下来,只要我们能在外部访问 plus() 函数,并且确保 counter = 0 只执行一次,就能解决计数器的困境。代码如下:

var add = function() {
    var counter = 0;
    var plus = function() {return counter += 1;}
    return plus;
}

var puls2 = add();
console.log(puls2());
console.log(puls2());
console.log(puls2());
// 计数器为 3

计数器 counter 受 add() 函数的作用域保护,只能通过 puls2 方法修改。

使用闭包的注意点
● 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在 IE 中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除或设置为 null,断开变量和内存的联系。
● 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(public method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
JavaScript 闭包是一种强大的语言特性。通过使用这个语言特性来隐藏变量,可以避免覆盖其他地方使用的同名变量,理解闭包有助于编写出更有效也更简洁的代码。
this 关键字
谈到作用域和闭包就不得不说 this 关键字,虽然它们之间关联不大,但是它们一起使用却容易让人产生疑惑。下面列出了使用 this 的大部分场景,带大家一探究竟。
this 是 JavaScript 的关键字,指函数执行时的上下文,跟函数定义时的上下文无关。随着函数使用场合的不同,this 的值会发生变化。但是有一个总的原则,那就是 this 指代的是调用函数的那个对象。

全局上下文
在全局上下文中,也就是在任何函数体外部,this 指代全局对象。

// 在浏览器中,this 指代全局对象 window
console.log(this === window);  // true

函数上下文
在函数上下文中,也就是在任何函数体内部,this 指代调用函数的那个对象。
函数调用中的 this

function f1(){
    return this;
}

console.log(f1() === window); // true

如上代码所示,直接定义一个函数 f1(),相当于为 window 对象定义了一个属性。直接执行函数 f1(),相当于执行 window.f1()。所以函数 f1() 中的 this 指代调用函数的那个对象,也就是 window 对象。

function f2(){
    "use strict"; // 这里是严格模式
    return this;
}

console.log(f2() === undefined); // true

如上代码所示,在「严格模式」下,禁止 this 关键字指向全局对象(在浏览器环境中也就是 window 对象),this 的值将维持 undefined 状态。
对象方法中的 this

var o = {
    name: "stone",
    f: function() {
        return this.name;
    }
};

console.log(o.f()); // "stone"

如上代码所示,对象 o 中包含一个属性 name 和一个方法 f()。当我们执行 o.f() 时,方法 f() 中的 this 指代调用函数的那个对象,也就是对象 o,所以 this.name 也就是o.name。
注意,在何处定义函数完全不会影响到 this 的行为,我们也可以首先定义函数,然后再将其附属到 o.f。这样做 this 的行为也一致。如下代码所示:

var fun = function() {
    return this.name;
};

var o = { name: "stone" };
o.f = fun;

console.log(o.f()); // "stone"

类似的,this 的绑定只受最靠近的成员引用的影响。在下面的这个例子中,我们把一个方法g() 当作对象 o.b 的函数调用。在这次执行期间,函数中的 this 将指向 o.b。事实上,这与对象本身的成员没有多大关系,最靠近的引用才是最重要的。

o.b = {
    name: "sophie"
    g: fun,
};

console.log(o.b.g()); // "sophie"

eval() 方法中的 this
eval() 方法可以将字符串转换为 JavaScript 代码,使用 eval() 方法时,this 指向哪里呢?答案很简单,看谁在调用 eval() 方法,调用者的执行环境中的 this 就被 eval() 方法继承下来了。如下代码所示:

// 全局上下文
function f1(){
    return eval("this");
}
console.log(f1() === window); // true

// 函数上下文
var o = {
    name: "stone",
    f: function() {
        return eval("this.name");
    }
};
console.log(o.f()); // "stone"

call() 和 apply() 方法中的 this
call() 和 apply() 是函数对象的方法,它的作用是改变函数的调用对象,它的第一个参数就表示改变后的调用这个函数的对象。因此,this 指代的就是这两个方法的第一个参数。

var x = 0;  
function f() {    
    console.log(this.x);  
}  
var o = {};  
o.x = 1;
o.m = f;  
o.m.apply(); // 0

call() 和 apply() 的参数为空时,默认调用全局对象。因此,这时的运行结果为 0,证明this 指的是全局对象。如果把最后一行代码修改为:
o.m.apply(o); // 1
运行结果就变成了 1,证明了这时 this 指代的是对象 o。
bind() 方法中的 this
ECMAScript 5 引入了 Function.prototype.bind。调用 f.bind(someObject) 会创建一个与 f 具有相同函数体和作用域的函数,但是在这个新函数中,this 将永久地被绑定到了 bind 的第一个参数,无论这个函数是如何被调用的。如下代码所示:

function f() {
    return this.a;
}

var g = f.bind({
    a: "stone"
});
console.log(g()); // stone

var o = {
    a: 28,
    f: f,
    g: g
};
console.log(o.f(), o.g()); // 28, stone

DOM 事件处理函数中的 this
一般来讲,当函数使用 addEventListener,被用作事件处理函数时,它的 this 指向触发事件的元素。如下代码所示:

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <button id="btn" type="button">click</button>
    <script>
        var btn = document.getElementById("btn");
        btn.addEventListener("click", function(){
            this.style.backgroundColor = "#A5D9F3";
        }, false);
    </script>
</body>
</html>

但在 IE 浏览器中,当函数使用 attachEvent ,被用作事件处理函数时,它的 this 却指向window。如下代码所示:

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>test</title>
</head>
<body>
    <button id="btn" type="button">click</button>
    <script>
        var btn = document.getElementById("btn");
        btn.attachEvent("onclick", function(){
            console.log(this === window);  // true
        });
    </script>
</body>
</html>

内联事件处理函数中的 this
当代码被内联处理函数调用时,它的 this 指向监听器所在的 DOM 元素。如下代码所示:

<button onclick="alert(this.tagName.toLowerCase());">
  Show this
</button>

上面的 alert 会显示 button,注意只有外层代码中的 this 是这样设置的。如果 this 被包含在匿名函数中,则又是另外一种情况了。如下代码所示:

<button onclick="alert((function(){return this})());">
  Show inner this
</button>

在这种情况下,this 被包含在匿名函数中,相当于处于全局上下文中,所以它指向 window对象。

猜你喜欢

转载自blog.csdn.net/daicooper/article/details/79525590