0°

JavaScript闭包

JavaScript预解析

JS的解析和执行过程

代码案例 – 思考:下列代码是否会报错,区别于其他语言。


1
2
3
4
5
1console.log(a);
2var a = 2;
3conosle.log(a);
4
5

该代码的输出结果为undefined 2(在c++中这样的代码是会报错的,但是在JavaScript中不会报错,由于发生了预解析的过程)


1
2
3
4
5
6
7
1// 从解析器角度看到的代码
2var a;
3console.log(a);//undefined
4a = 2;
5console.log(a);// 2
6
7

JS的解析和执行过程

  • 全局预解析阶段(全局变量和函数声明前置)

  • 全局顺序执行阶段(变量赋值、函数调用等操作)

  • 当遇到函数调用时,在执行函数内代码前,进行函数范围内的预解析

  • 当存在函数嵌套时,以此类推,会进行多次函数预解析


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
1// example1
2console.log(a, b); //undefined undefined
3var b = 23;
4console.log(a, b); //undefined 23
5var a = b;
6console.log(a, b); //23 23
7
8// example2
9console.log(obj1, obj2); //undefined undefined
10var obj1 = { x: 23 };
11console.log(obj1, obj2); //{x:23} undefined
12var obj2 = obj1;
13console.log(obj1, obj2); //{x:23} {x:23}
14obj2.x = 25;
15console.log(obj1, obj2); //{x:25} {x:25}
16
17// example3
18function foo() {
19    console.log("j:", j); // undefined
20    var j = 10;
21    console.log("j:", j); // 10
22}
23foo();
24console.log("j:", j); // error
25
26

预解析主要工作(变量声明和函数声明提升)

  • 解析器在执行代码前的进行代码扫描(var、function)

  • 将变量和函数声明在当前作用域(全局、函数)内进行提升

在这里插入图片描述

注:ES5中函数及变量声明重复的话,后面的函数或者变量会将前面的进行覆盖

同时有var和function关键字

  • 情形1:函数表达式


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1// 当function前有运算符的话,认定为表达式,不提升
2foo();
3var foo = function() {
4    console.log("foo");
5};
6# Uncaught TypeError: foo is not a function
7#    at <anonymous>:1:1
8
9// 上述代码等价于
10var foo;
11foo();// error
12foo = function(){
13      console.log("foo");
14};
15
16

练习:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1console.log(foo);// undefined
2var foo = function() {
3    console.log("foo");
4};
5foo();// foo
6
7// 上述代码等价于
8var foo;
9console.log(foo);// undefined
10foo = function(){
11      console.log("foo");
12};
13foo();// 函数会将变量进行覆盖 foo
14
15
  • 情形2:变量名同函数名


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1AA();
2function AA() {
3    console.log("AA_1");
4}
5var AA = function() {
6    console.log("AA_2");
7};
8AA();
9
10// 上述代码等价于
11function AA() {
12    console.log("AA_1");
13}
14var AA;// 在最顶端和在这是等效的 函数会将变量进行覆盖
15AA();// AA_1;
16AA = function(){
17      console.log("AA_2");
18};
19AA();// AA_2
20
21

预解析与作用域

静态词法作用域(重要)


1
2
3
4
5
6
7
8
9
10
11
1var name = "Jack";
2function echo() {
3    console.log(name); // 此时的name指向了Jack之后不会再改变
4}
5function foo() {
6    var name = "Bill";
7    echo();
8}
9foo(); //Jack
10
11

JS采用的是静态词法作用域,代码完成后作用域链就已形成,与代码的执行顺序无关

全局变量与局部变量

  • 全局变量:拥有全局作用域的变量(JS代码中任何地方都可以访问)

  • 全局变量是跨域了所有函数自身作用域的自由变量,可以在函数内和函数外直接访问

  • 局部变量:函数内声明的变量,只在函数体内有定义,作用域是局部性的

  • 在函数外不能直接访问函数的局部变量,但可以通过闭包来访问

  • 函数内访问同名变量时,局部变量会覆盖全局变量


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
1//example1 访问全局变量 x === window.x
2var x = "outside f1";
3var f1 = function() {
4     console.log(x);
5};
6f1(); // outside f1
7console.log(x); //  outside f1
8
9//example2 局部变量会覆盖全局变量
10var x = "outside f1";
11var f1 = function() {
12  var x = "inside f1";
13      console.log(x);
14};
15f1(); // inside f1
16console.log(x); //  outside f1
17
18//example3 局部变量的作用域就在局部
19var f2 = function() {
20    var y = "局部";
21    console.log(y);
22};
23f2(); // '局部'
24console.log(y); //报错
25
26//example4 在函数里不加var声明的变量默认都是全局变量
27var f2 = function() {
28    y = "全局";
29    console.log(y);
30};
31f2(); // "全局"
32console.log(y); //"全局"
33
34

声明前置与作用域的关系(全局作用域、函数作用域)


1
2
3
4
5
6
7
8
9
10
11
12
13
1// 声明前置与作用域的关系
2if (true) {
3    var i = 0;
4}
5console.log(i);
6// 等价于
7var i;
8if (true) {
9    i = 0;
10}
11console.log(i); //ES5 中没有块级作用域 0
12
13

JS作用域及其特点

JS作用域

  • 作用域就是变量与函数的可访问范围(变量生效的区域范围,即在何处可以被访问到)

  • 作用域控制着变量与函数的可见性和生命周期,它也是根据名称查找变量的一套规则

在这里插入图片描述

上述实例(嵌套作用域)中:

变量d只能在bar作用域中被访问到,变量c只能在fn和bar作用域中被访问到

在bar中访问a时为500(覆盖性)在bar中访问c时为200(链式关系


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1var a = 10,
2    b = 20;
3
4function fn() {
5    var a = 100,
6        c = 200;
7    //console.log(a,b,c,d); 报错由于此时的d还没有定义
8    function bar() {
9        //bar局部作用域
10        var a = 500,
11            d = 600; // 若此句该为var a = 500;b = 600;则此时的b相当于是全局变量
12        console.log(a, b, c, d); // 500 20 200 600
13    }
14    bar();
15}
16fn();
17//console.log(a,b,c,d); // 报错 c d此时都没有定义
18
19

JS作用域特点

词法作用域

  • JS采用的是词法作用域(静态性),这种静态结构决定了一个变量的作用域

  • 词法作用域不会被函数从哪里调用等因素影响,与调用形式无关(体现了静态性)

注意:词法作用域主要确定变量的具体内容

在这里插入图片描述

  • 通过new Function创建的函数对象不一定遵从静态词法作用域

  • 对比下边两个例子(通过不同形式定义的函数对象,访问到的scope的区别)

在这里插入图片描述
关于块作用域

  • 大多数语言都有块级作用域,变量“存活”在最近的代码块中,比如Java中

  • JS(ES5)采用的是函数级作用域,没有块作用域

  • 无块作用域的问题(变量污染、变量共享问题)变量污染、变量共享问题,尤其是异步执行的情况下。如果是两个单独的文件的情况下,更容易造成变量污染

在这里插入图片描述

JS执行上下文与调用栈(Call Stack)

执行上下文

  • 执行上下文指代码执行时的上下文环境(包括局部变量、相关的函数、相关自由变量等)

  • JS运行时会产生多个执行上下文,处于活动状态的执行上下文环境只有一个

在这里插入图片描述

理解执行上下文

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
JS运行时会产生多个执行上下文,处于活动状态的执行上下文环境只有一个

练习:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1//理解执行上下文(通俗的例子),嵌套的情况
2var xx = ["书桌","书包","铅笔盒"];//小明家中
3console.log("在家-做作业中 1 ...");
4function goToStore(){
5    var yy = ["文具店老板","出售的文具"];//文具商店中
6    console.log("在文具店-买文具中  ...");
7    function goToBank(){
8        var zz = ["银行职员","柜员机"];//银行中
9        console.log("在银行-取钱 ... 返回文具店");
10    }
11    console.log("在文具店-买文具中  ... 发现没带钱");
12    goToBank();
13    console.log("在文具店-买好文具  ... 返回家");
14}
15console.log("在家-做作业中 2 ... 发现笔没油了");
16goToStore();//笔没油了,去商店买笔
17console.log("在家-继续做作业...");
18
19

在这里插入图片描述

调用栈(Call Stack)

  • 代码执行时JS引擎会以栈的方式来处理和追踪函数调用(函数调用栈 Call Stack)

  • 栈底对应的是全局上下文环境,而栈顶对应的是当前正在执行的上下文环境

在这里插入图片描述

在这里插入图片描述

作用域链与执行上下文

理解代码执行时形成的作用域链(继续小明的例子)

  • 如果有多个文具店和多个银行,那么执行就有多种可能,形成不同的链式关系

  • 依然要遵从静态词法作用域(在A文具店,应该有A店老板,而不应有B店老板)

在这里插入图片描述

作用域链与执行上下文

  • 执行时,当前执行上下文,对应一个作用域链环境来管理和解析变量和函数(动态性)

  • 变量查找按照由内到外的顺序(遵循词法作用域),直到完成查找,若未查询到则报错

  • 当函数执行结束,运行期上下文被销毁,此作用域链环境也随之被释放

在这里插入图片描述

JS的立即执行表达式IIFE

什么是IIFE以及其使用方式

  • IIFE英文全称:Immediately-InvokedFunction Expression即立即执行的函数表达式

  • IIFE的作用(建立函数作用域,解决ES5作用域缺陷所带来的问题,如:变量污染、变量共享等问题)

IIFE的写法

使用小括号的写法(最常见的两种)

  • (function foo( x,y){ … }(2,3)); //2,3为传递的参数

  • (function foo(x,y){ … })(2,3);


1
2
3
4
5
6
7
8
9
10
1(function count(arr){
2   console.log(arr.length);
3})([1,2,3,4]); // 4
4
5// 括号位置的不同
6(function count(arr){
7   console.log(arr.length);
8}(1,2,3,4));// 4
9
10

注意:IIFE是表达式,要注意使用分号结尾,否则可能出现错误


1
2
3
4
5
6
7
8
9
10
1(function() {
2    console.log("111");
3})()//没有分号的话会报错
4(function () {
5    console.log("222");
6})()
7# Uncaught TypeError: (intermediate value)(...) is not a function
8#    at <anonymous>:4:1
9
10

与运算符结合的写法(执行函数、进行运算)


1
2
3
4
5
6
1var i = function( ){ return 10; }( ); //i为10
2true&& function( ){  ... }( );
3~function(arg1,arg2){ ... }(x,y); //x,y为传递参数 位运算非操作符
4!function( ){ ...  }( );//思考!function(){return 2; }( ); 与 !function(){return 0; }( );
5
6

在上述的式子中,IIFE是一个表达式,类似于我们所说的a=2;执行的时候要注意是立即执行,因此function( ){ … }( );的结果就是返回值


1
2
3
4
1! function() { return2; }();  // fasle
2! function() { return0; }(); // true
3
4

练习下列例子:

在这里插入图片描述

通过IIFE来解决的问题(JS缺陷)

变量污染问题

  • JS(ES5)中没有块作用域,容易造成js文件内或文件间的同名变量互相污染

  • 我们往往会通过IIFE引入一个新的作用域来限制变量的作用域,来避免变量污染

在这里插入图片描述

变量共享错误

  • 当程序运行到变量所在作用域时,变量被创建,JS(ES5)没有块作用域,变量可能会共享

如下例:

在函数作用域中创建的变量 i 只有一个,出现了变量 i 共享问题,可通过IIFE解决

在这里插入图片描述

在程序的执行过程中,程序会先执行循环,之后i的值就变为了10,再每一次调用函数数组的函数时,输出的结果都是10,因此需要使用立即执行表达式来解决这个问题

代码调试过程图:

在这里插入图片描述

通过立即执行表达式解决问题:


1
2
3
4
5
6
7
8
9
10
11
12
13
1function f(){
2    var getNumFuncs = [];//函数数组
3    for(var i=0;i<10;i++){
4        (function (j) {
5            getNumFuncs[j] = function(){return j;};
6        })(i);
7    }
8    return getNumFuncs;//设置断点,查看变量共享问题
9}
10var tmp = f();
11tmp[3]();
12
13

注意:上述的代码中的也是先执行循环,但是在执行循环的时候,程序将每一个i的值都传入到了创建的是个作用域的一个中,实现了i值的传入,i为实参,j为形参。

IIFE实际应用案例

案例一


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1var tabs = document.getElementsByClassName('tabs')[0].children;
2var contents = document.getElementsByClassName('show')[0];
3
4for(var i=0;i<tabs.length;i++) {
5    //(function (i) {  //IIFE start
6        tabs[i].onclick=function(){
7            for (var j = 0; j < tabs.length; j++) {
8                tabs[j].className = '';
9            }
10            this.className = "active";
11            contents.innerHTML = "导航" + i + "内容";
12        };
13    //}(i));          //IIFE end
14}
15
16

tab的length为4,由于变量共享在同一个作用域下,所以变量 i 只有一个,并最终i为4,所以点击任何标签,都输出“点击了4”

在这里插入图片描述

避免闭包中非期望的变量共享问题,解决方式 IIFE


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1var tabs = document.getElementsByClassName('tabs')[0].children;
2var contents = document.getElementsByClassName('show')[0];
3
4for(var i=0;i<tabs.length;i++) {
5    (function (i) {    //IIFE start
6        tabs[i].onclick=function(){
7            for (var j = 0; j < tabs.length; j++) {
8                tabs[j].className = '';
9            }
10            this.className = "active";
11            contents.innerHTML = "导航" + i + "内容";
12        };
13    }(i));            //IIFE end
14}
15
16

tab的length为4,立即执行了4次函数,有4个函数作用域,所以变量 i 生成了4次,所以点击时能正常输出1到4

在这里插入图片描述

案例二

在这里插入图片描述

通过立即执行表达式可以实现


1
2
3
4
5
6
7
8
9
1for (var i = 0; i < 5; i++) {
2    (function(j) {  // j = i
3        setTimeout(function() {
4            console.log(new Date, j);
5        }, 1000*i);
6    })(i);
7}
8
9

JS闭包

闭包的概念

闭包(引入案例)

思考:函数内的局部变量,是否能在函数外得到(按照常理来说是没有办法进行访问的)

有什么方法能读写函数内部的局部变量?

在这里插入图片描述

闭包(closure)的概念

  • 闭包是由函数和与其相关的引用环境组合而成的实体(函数和它所在的作用域的变量)

  • 闭包是词法作用域中的函数和其相关变量的包裹体


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1function createInc(startValue){
2   return function(step){
3       startValue+=step;
4       return startValue;
5   }
6}
7var inc = createInc(5);
8console.log(inc(1));//输出多少?
9console.log(inc(2));//输出多少?
10inc = createInc(5);
11console.log(inc(1));//输出多少?
12
13# 输出结果
146
158
166
17
18

在这里插入图片描述


1
2
3
4
5
6
7
8
9
10
11
12
13
14
1function createInc(startValue){
2   return function(step){
3       startValue+=step;
4       return startValue;
5   }
6}
7var inc = createInc(5);
8console.log(inc(1));//输出多少? 6
9console.log(inc(2));//输出多少? 8
10var inc2 = createInc(5);
11console.log(inc(1));//输出多少? 9
12console.log(inc2(1));//输出多少? 6
13
14

根据上述代码,可以写出下列的图示:

在这里插入图片描述

若一个函数离开了它被创建时的作用域,它还是会与这个作用域的变量相关联

闭包是一个函数外加上该函数创建时所建立的作用域


1
2
3
4
5
6
7
8
9
10
11
12
13
14
1function foo() {
2    var i = 0;
3    function bar() {
4        console.log(++i);
5    }
6    return bar;
7}
8var a = foo();
9var b = foo();
10a();//输出多少? 1
11a();//输出多少? 2
12b();//输出多少? 1
13
14
  1. 函数bar和其相关词法上下文中的变量i,构成了一个闭包
  2. 返回的函数bar,依然能够访问到变量i(藕断丝连)

a和b对应的是否为同一个闭包? // a 和 b对应的不是同一个闭包

思考:foo和它相关作用域的变量是否形成闭包?//foo和它相关的作用域的变量也会形成一个闭包

闭包的常见形式

以函数对象形式返回


1
2
3
4
5
6
7
8
9
10
11
12
13
1var tmp = 100;// 注意:词法作用域,形成的闭包是否包含此行的tmp 不包含
2function foo(x){
3   var tmp = 3;// 思考:若屏蔽此行,则又会输出多少?
4   return function(y){
5       console.log(x + y + (++tmp));
6   }
7}
8var fee = foo(2);
9fee(10);// 16
10fee(10);// 17
11fee(10);// 18
12
13

上述代码中fee 形成了一个闭包,闭包包含一个函数对象以及tmp(其中tmp是3,不是100,tmp = 100是自由变量,全局变量)和x变量

  • 思考:若屏蔽此行,则又会输出多少?


1
2
3
4
5
1113
2114
3115
4
5
  • 思考:此实例中fee函数对象相关作用域的变量都有哪些?形成的闭包是否包含foo函数之外(即第一行)的自由变量tmp?foo中的tmp是否调用后就释放?


1
2
3
1答:包含tmp和x 不包含自由变量tmp
2
3

在这里插入图片描述


1
2
3
4
5
6
7
8
9
10
11
12
13
14
1function foo(x){
2   var tmp = 3;
3   return function(y){
4       x.count = x.count ? x.count+1 :1;
5       console.log(x + y + tmp,x.count);
6   }
7}
8var age = new Number(2);
9var bar = foo(age);
10bar(10);// 15 1
11bar(10);// 15 2
12bar(10);// 15 3
13
14
  • 思考:此实例中bar函数对象相关作用域的变量都有哪些?(tmpx)foo中的tmp是否调用后就释放?(不会释放,会常驻内存中)

作为对象的方法返回

在这里插入图片描述

  • 对于c来说的count和reset和n都是构成了两个闭包

  • 对于d来说的count和reset和n都是构成了两个闭包

闭包的作用及常用场景

闭包的作用

  • 可通过闭包来访问隐藏在函数作用域内的局部变量

  • 使函数中的变量被保存在内存中不被释放(单例模式)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1var n = 10;
2function f1(){
3   var n = 999;
4   nAdd = function(){n+=1;};
5   function f2(){
6       console.log(n);
7   }
8   return f2;
9}
10var result = f1();
11result();// 999
12nAdd();
13result();// 999
14
15

上述实例中,无法在f1函数外直接得到局部变量n的999的值,可以通过闭包间接的在f1函数外访问和修改n

注意:上述中nAdd不在result对应的闭包里面!

因此将代码进行修改可得:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1var n = 10;
2function f1(){
3   var n = 999;
4   nAdd = function(){n+=1;};
5   function f2(){
6       console.log(n);
7   }
8   return f2;
9}
10var result = f1();
11var result1 = f1();
12result();// 999
13nAdd();
14result1();// 1000
15result();// 999
16
17

其中nAdd不在f1所对应的闭包里面,因此在对result1()进行调用的时候,则会对n的值进行改变,但是对于result()的调用结果不会发生改变。

在这里插入图片描述

闭包的实际应用案例

  • 案例分析一:创建结点(单例模式)

比如说我现在的需求是这样的,在网页中有时候会需要遮罩层,调用的时候我就创建一个,但是你不可能每次调用创建,所以如果存在就用以前的,如果不存在就创建新的


1
2
3
4
5
6
7
8
9
10
1function fn(){
2   var a;
3   return function(){
4       return a || (a = document.body.appendChild(document.createElement('div')));
5   }
6};
7var f = fn();
8f();
9
10

定时与节点,闭包应用案例 2秒后执行,由于闭包所以objID此时还存在

虽然有时没有直接用,但闭包无时无刻不存在


1
2
3
4
5
6
7
8
1function closureExample(objID,text,timedelay){
2   setTimeout(function(){
3       console.log(objID,text);
4   },timedelay);
5}
6closureExample("mydiv","Closure is Create",2000);
7
8

闭包的注意事项

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包

  • 使用闭包时要注意不经意的变量共享问题,可以通过立即执行表达式来解决

在这里插入图片描述

「点点赞赏,手留余香」

    还没有人赞赏,快来当第一个赞赏的人吧!