《JavaScript 闯关记》之作用域和闭包

作用域和闭包是 JavaScript 最紧要的定义有,想如果尤其读书
JavaScript,就务须理解 JavaScript 作用域和闭包的行事规律。

作用域

其余程序设计语言都生作用域的定义,简单的说,作用域就是变量和函数的可看范围,即作用域控制正在变量和函数的可见性和生命周期。在
JavaScript 中,变量的意域有全局作用域和组成部分作用域两栽。

全局作用域(Global Scope)

每当代码中任何地方还能够访问到之对象有全局作用域,一般的话以下三种情形有全局作用域:

1.尽外层函数和当绝外层函数外面定义之变量拥有全局作用域,例如:

var global = "global";     // 显式声明一个全局变量
function checkscope() {
    var local = "local";   // 显式声明一个局部变量
    return global;         // 返回全局变量的值
}
console.log(global);       // "global"
console.log(checkscope()); // "global"
console.log(local);        // error: local is not defined.

上面代码中,global 是全局变量,不管是当 checkscope()
函数内部或外部,都能顾到全局变量 global

2.享有末定义直接赋值的变量自动声明也具备全局作用域,例如:

function checkscope() {
    var local = "local"; // 显式声明一个局部变量
    global = "global";   // 隐式声明一个全局变量(不好的写法)
}
console.log(global);     // "global"
console.log(local);      // error: local is not defined.

地方代码中,变量 global 未用 var
关键字定义就是直接赋值,所以隐式的创导了全局变量
global,但这种写法容易导致误解,应竭尽避免这种写法。

3.所有 window 对象的特性拥有全局作用域

貌似情况下,window 对象的放开属性都兼备全局作用域,例如
window.namewindow.locationwindow.top 等等。

一部分作用域(Local Scope)

跟全局作用域相反,局部作用域一般不过以固定的代码有外可看到。最广泛的是于函数体内定义的变量,只能当函数体内使用。例如:

function checkscope() {
    var local = "local";   // 显式声明一个局部变量
    return local;         // 返回全局变量的值
}
console.log(checkscope()); // "local"
console.log(local);        // error: local is not defined.

面代码中,在部数体内定义了变量
local,在函数体内是足以拜了,在函数外聘就报错了。

全局与一部分作用域的关联

于函数体内,局部变量的先期级高于同名的全局变量。如果以函数内声明的一个有的变量或者函数参数中寓的变量和全局变量重名,那么全局变量就受部分变量所掩盖。

var scope = "global";      // 声明一个全局变量
function checkscope() {
    var scope = "local";   // 声明一个同名的局部变量
    return scope;          // 返回局部变量的值,而不是全局变量的值
}
console.log(checkscope()); // "local"

尽管在大局作用域编写代码时可无写 var
语句,但扬言局部变量时虽然必须使用 var
语句子。思考一下若无这样做会怎么样:

scope = "global";           // 声明一个全局变量,甚至不用 var 来声明
function checkscope2() {
    scope = "local";        // 糟糕!我们刚修改了全局变量
    myscope = "local";      // 这里显式地声明了一个新的全局变量
    return [scope, myscope];// 返回两个值
}
console.log(checkscope2()); // ["local", "local"],产生了副作用
console.log(scope);         // "local",全局变量修改了
console.log(myscope);       // "local",全局命名空间搞乱了

函数定义是可嵌套的。由于每个函数都发她和谐的作用域,因此会面面世几乎个组成部分作用域嵌套的状态,例如:

var scope = "global scope";         // 全局变量
function checkscope() {
    var scope = "local scope";      //局部变量 
    function nested() {
        var scope = "nested scope"; // 嵌套作用域内的局部变量
        return scope;               // 返回当前作用域内的值
    }
    return nested();
}
console.log(checkscope());          // "nested scope"

函数作用域和声明提前

当局部类 C
语言的编程语言中,花括号内的诸一样段落代码都抱有各自的作用域,而且变量在声明其的代码段之外是不可见的,我们誉为块级作用域(block
scope),而 JavaScript 中从来不块级作用域。JavaScript
取而代之地采取了函数作用域(function
scope),变量在声明其的函数体以及这函数体嵌套的任意函数体内还是来定义之。

当如下所出示之代码中,在不同职务定义了变量 ij
k,它们都于与一个企图域内,这三独变量在函数体内都是生定义的。

function test(o) {
    var i = 0; // i在整个函数体内均是有定义的
    if (typeof o == "object") {
        var j = 0; // j在函数体内是有定义的,不仅仅是在这个代码段内
        for (var k = 0; k < 10; k++) { // k在函数体内是有定义的,不仅仅是在循环内
            console.log(k); // 输出数字0~9
        }
        console.log(k); // k已经定义了,输出10
    }
    console.log(j); // j已经定义了,但可能没有初始化
}

JavaScript
的函数作用域是据在函数内声明的持有变量在函数体内始终是可见的。有意思的凡,这意味变量在声明前还是就可用。JavaScript
的这个特点深受非正式地称之为声明提前(hoisting),即 JavaScript
函数里声称的有变量(但切莫干赋值)都受「提前」至函数体的顶部,看一下之类代码:

var scope = "global";
function f() {
    console.log(scope);  // 输出"undefined",而不是"global"
    var scope = "local"; // 变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
    console.log(scope);  // 输出"local"
}

你或会见误以为函数中之率先执行会输出 "global",因为代码还不曾实施及
var
语句声明局部变量的地方。其实不然,由于函数作用域的特色,局部变量在全方位函数体始终是发定义的,也就是说,在部数体内有些变量遮盖了同名全局变量。尽管如此,只有当程序执行到
var
语词的当儿,局部变量才会受真正赋值。因此,上述过程等价于:将函数内之变量声明“提前”至函数体顶部,同时变量初始化留在原的职务:

function f() {
    var scope;          // 在函数顶部声明了局部变量
    console.log(scope); // 变量存在,但其值是"undefined"
    scope = "local";    // 这里将其初始化并赋值
    console.log(scope); // 这里它具有了我们所期望的值
}

当备块级作用域的编程语言中,在小的作用域里给变量声明和动用变量的代码尽可能接近彼此,通常来讲,这是一个老大正确的编程习惯。由于
JavaScript
没有块级作用域,因此有程序员特意用变量声明放在函数体顶部,而休是用宣示靠近放在使用变量的处在。这种做法让他们之源代码非常鲜明地反映了实际的变量作用域。

企图域链

现代码在一个环境被实施时,会创变量对象的一个作用域链(scope
chain)。作用域链的用途,是承保对实践环境发生且访问的有变量和函数的有序访问。作用域链的前端,始终都是眼下执行之代码所在环境之变量对象。如果这个环境是函数,则用那动目标(activation
object)作为变量对象。活动目标在最开头经常只有含一个变量,即 arguments
对象(这个目标在大局环境中凡是免存的)。作用域链中的生一个变量对象来包含(外部)环境,而再度下一个变量对象则出自下一个富含环境。这样,一直累及全局执行环境;全局执行环境之变量对象始终犹是图域链中的最后一个目标。

标识符解析是挨作用域链一级一级地搜寻标识符的长河。搜索过程始终打图域链的前端开始,然后逐级地于后回顾,直至找到标识符为止(如果找不至标识符,通常会促成错误有)。

吁圈下的言传身教代码:

var color = "blue";

function changeColor(){
    if (color === "blue"){
        color = "red";
    } else {
        color = "blue";
    }
}

console.log(changeColor());

于这大概的事例中,函数 changeColor()
的来意域链包含两只目标:它和谐的变量对象(其中定义着 arguments
对象)和全局环境的变量对象。可以于函数内部访问变量
color,就是坐好以是作用域链中找到它们。

另外,在有些作用域中定义的变量可以当有的环境中以及全局变量互换使用,如下面这事例所示:

var color = "blue";

function changeColor(){
    var anotherColor = "red";

    function swapColors(){
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;

        // 这里可以访问color、anotherColor和tempColor
    }

    // 这里可以访问color和anotherColor,但不能访问tempColor
    swapColors();
}

// 这里只能访问color
changeColor();

上述代码共关系3只实施环境:全局环境、changeColor() 的片段环境暨
swapColors() 的一部分环境。全局环境中有一个变量 color 和一个函数
changeColor()changeColor() 的组成部分环境遭到发生一个称呼吧 anotherColor
的变量和一个名叫吧 swapColors() 的函数,但它们吗堪拜全局环境面临的变量
colorswapColors() 的片环境遭受有一个变量
tempColor,该变量只能以这个环境被做客到。无论全局环境或
changeColor() 的组成部分环境还无权访问 tempColor。然而,在
swapColors()
内部则足以看其他两只条件受到的有所变量,因为那片个条件是它们的爸爸行环境。下图形象地展示了面前是例子的意图域链。

齐图中的矩形表示一定的实施环境。其中,内部环境可以经过作用域链访问具有的外部环境,但外部环境不克看中环境被的旁变量和函数。这些条件间的牵连是线性、有次序的。每个环境都可发展搜索作用域链,以询问变量和函数叫作;但其它条件都未可知通过为下寻找作用域链而进入其他一个实践环境。对于这事例中的
swapColors() 而言,其意图域链中富含3单对象:swapColors()
的变量对象、changeColor() 的变量对象和全局变量对象。swapColors()
的一部分环境开始经常会见事先以大团结之变量对象中追寻变量和函数名为,如果找未顶则另行找找上一级作用域链。changeColor()
的意图域链中只含两独对象:它自己之变量对象以及全局变量对象。这吗就是,它不能够看
swapColors()
的环境。函数参数也于看成变量来对比,因此该访问规则与履行环境面临的另外变量相同。

闭包

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 调用后被活动清除。

何以会如此吧?原因就是在于 funfun2 的父函数,而 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

计数器 counteradd() 函数的作用域保护,只能通过 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 对象。

关卡

细琢磨,下面代码块会输出什么结果为?

// 挑战一
function func1() {
    function func2() {
        console.log(this)
    }
    return func2;
}
func1()();  // ???

// 挑战二
scope = "stone";

function Func() {
    var scope = "sophie";

    function inner() {
        console.log(scope);
    }
    return inner;
}

var ret = Func();
ret();    // ???

// 挑战三
scope = "stone";

function Func() {
    var scope = "sophie";

    function inner() {
        console.log(scope);
    }
    scope = "tommy";
    return inner;
}

var ret = Func();
ret();    // ???

// 挑战四
scope = "stone";

function Bar() {
    console.log(scope);
}

function Func() {
    var scope = "sophie";
    return Bar;
}

var ret = Func();
ret();    // ???

// 挑战五
var name = "The Window";  
var object = {    
    name: "My Object",
    getNameFunc: function() {      
        return function() {        
            return this.name;      
        };    
    }  
};  
console.log(object.getNameFunc()());    // ???

// 挑战六
var name = "The Window";  
var object = {    
    name: "My Object",
    getNameFunc: function() {      
        var that = this;      
        return function() {        
            return that.name;      
        };    
    }  
};  
console.log(object.getNameFunc()());    // ???

更多

关注微信公众号「劼哥舍」回复「答案」,获取关卡详解。
关注
https://github.com/stone0090/javascript-lessons,获取最新动态。