Fork me on GitHub

作用域和闭包

目录

前言

作用域和闭包是前端面试中,最可能考查的知识点。例如下面的题目:

题目:现在有个 HTML 片段,要求编写代码,点击编号为几的链接就alert弹出其编号

1
2
3
4
5
6
7
<ul>
<li>编号1,点击我请弹出1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>

一般不知道这个题目用闭包的话,会写出下面的代码:

1
2
3
4
5
6
var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i++) {
list[i].addEventListener('click', function(){
alert(i + 1)
}, true)
}

实际上执行才会发现始终弹出的是6,这时候就应该通过闭包来解决:

1
2
3
4
5
6
7
8
var list = document.getElementsByTagName('li');
for (var i = 0; i < list.length; i++) {
list[i].addEventListener('click', function(i){
return function(){
alert(i + 1)
}
}(i), true)
}

要理解闭包,就需要我们从「执行上下文」开始讲起。

执行上下文

先讲一个关于 变量提升 的知识点,面试中可能会遇见下面的问题:

题目:说出下面执行的结果(这里笔者直接注释输出了)

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log(a) // undefined
var a = 100
fn('zhangsan') // 'zhangsan' 20
function fn(name) {
age = 20
console.log(name, age)
var age
}
console.log(b); // 这里报错
// Uncaught ReferenceError: b is not defined
b = 100;

在一段 JS 脚本(即一个<script>标签中)执行之前,要先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个 全局执行上下文 环境,先把代码中即将执行的(内部函数的不算,因为你不知道函数何时执行)变量、函数声明都拿出来。变量先暂时赋值为undefined,函数则先声明好可使用。这一步做完了,然后再开始正式执行程序。再次强调,这是在代码执行之前才开始的工作。

我们来看下上面的面试小题目,为什么aundefined,而b却报错了,实际 JS 在代码执行之前,要「全文解析」,发现var a,知道有个a的变量,存入了执行上下文,而b没有找到var关键字,这时候没有在执行上下文提前「占位」,所以代码执行的时候,提前报到的a是有记录的,只不过值暂时还没有赋值,即为undefined,而b在执行上下文没有找到,自然会报错(没有找到b的引用)。

另外,一个函数在执行之前,也会创建一个 函数执行上下文 环境,跟 全局上下文 差不多,不过 函数执行上下文 中会多出this`` arguments和函数的参数。参数和arguments好理解,这里的this咱们需要专门讲解。

总结一下:

范围:一段<script>、js 文件或者一个函数
全局上下文:变量定义,函数声明
函数上下文:变量定义,函数声明,thisarguments

this

先搞明白一个很重要的概念 —— this的值是在执行的时候才能确认,定义的时候不能确认! 为什么呢 —— 因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候。看如下例子

1
2
3
4
5
6
7
8
9
10
var a = {
name: 'A',
fn: function () {
console.log(this.name)
}
}
a.fn() // this === a
a.fn.call({name: 'B'}) // this === {name: 'B'}
var fn1 = a.fn
fn1() // this === window

this执行会有不同,主要集中在这几个场景中

  • 作为构造函数执行,构造函数中
  • 作为对象属性执行,上述代码中a.fn()
  • 作为普通函数执行,上述代码中fn1()
  • 用于call apply bind,上述代码中a.fn.call({name: 'B'})

下面再来讲解下什么是作用域和作用域链。

题目:如何理解 JS 的作用域和作用域链

作用域

1
2
3
4
if (true) {
var name = 'zhangsan'
}
console.log(name)

从上面的例子可以体会到作用域的概念,作用域就是一个独立的地盘,让变量不会外泄、暴露出去。上面的name就被暴露出去了,因此,JS 没有块级作用域,只有全局作用域和函数作用域。

1
2
3
4
5
6
7
var a = 100
function fn() {
var a = 200
console.log('fn', a)
}
console.log('global', a)
fn()

全局作用域就是最外层的作用域,如果我们写了很多行 JS 代码,变量定义都没有用函数包括,那么它们就全部都在全局作用域中。这样的坏处就是很容易撞车、冲突。

1
2
3
4
5
6
// 张三写的代码中
var data = {a: 100}
// 李四写的代码中
var data = {x: true}

这就是为何 jQuery、Zepto 等库的源码,所有的代码都会放在(function(){....})()中。因为放在里面的所有变量,都不会被外泄和暴露,不会污染到外面,不会对其他的库或者 JS 脚本造成影响。这是函数作用域的一个体现。

附:ES6 中开始加入了块级作用域,使用let定义变量即可,如下:

1
2
3
4
if (true) {
let name = 'zhangsan'
}
console.log(name) // 报错,因为let定义的name是在if这个块级作用域

作用域链

首先认识一下什么叫做 自由变量 。如下代码中,console.log(a)要得到a变量,但是在当前的作用域中没有定义a(可对比一下b)。当前作用域没有定义的变量,这成为 自由变量 。自由变量如何得到 —— 向父级作用域寻找。

1
2
3
4
5
6
7
8
var a = 100
function fn() {
var b = 200
console.log(a)
console.log(b)
}
fn()

如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链

1
2
3
4
5
6
7
8
9
10
11
12
var a = 100
function F1() {
var b = 200
function F2() {
var c = 300
console.log(a) // 自由变量,顺作用域链向父作用域找
console.log(b) // 自由变量,顺作用域链向父作用域找
console.log(c) // 本作用域的变量
}
F2()
}
F1()

闭包

讲完这些内容,我们再来看一个例子,通过例子来理解闭包。

1
2
3
4
5
6
7
8
9
10
function F1() {
var a = 100
return function () {
console.log(a)
}
}
var f1 = F1()
var a = 200
f1()

自由变量将从作用域链中去寻找,但是 依据的是函数定义时的作用域链,而不是函数执行时,以上这个例子就是闭包。闭包主要有两个应用场景:

  • 函数作为返回值,上面的例子就是
  • 函数作为参数传递,看以下例子
1
2
3
4
5
6
7
8
9
10
11
12
function F1() {
var a = 100
return function () {
console.log(a)
}
}
function F2(f1) {
var a = 200
console.log(f1())
}
var f1 = F1()
F2(f1)

至此,对应着「作用域和闭包」这部分一开始的点击弹出alert的代码再看闭包,就很好理解了。

闭包的概念

各种专业文献上的”闭包”(closure)定义非常抽象,很难看懂。我的理解是,闭包就是能够读取其他函数内部变量的函数。

由于在Javascript语言中,只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成”定义在一个函数内部的函数”。

所以,在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。

闭包的用途

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

怎么来理解这句话呢?请看下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function f1(){
    var n=999;
    nAdd=function(){n+=1}
    function f2(){
      alert(n);
    }
    return f2;
  }
  var result=f1();
  result(); // 999
  nAdd();
  result(); // 1000

在这段代码中,result实际上就是闭包f2函数。它一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。

为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

这段代码中另一个值得注意的地方,就是”nAdd=function(){n+=1}“这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。

使用闭包的注意事项

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

  • 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。

参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
12
13
14
15
16
var name = "The Window";
  var object = {
    name : "My Object",
    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };
    }
  };
  alert(object.getNameFunc()());

(完)

-------------本文结束感谢您的阅读-------------

发布时间:2018年06月11日 - 19:06

最后更新:2018年06月11日 - 20:06

原始链接:https://xn--v4q63d8za.xn--6qq986b3xl/作用域和闭包/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

谢谢您给我买糖吃~