闭包、立即执行函数、this

一、闭包

1.变量的作用域

变量的作用域分为全局变量和局部变量。

(1)全局变量与局部变量

全局变量:

  • 函数以外定义的变量是全局变量
  • 函数内部可以直接读取全局变量
// 全局变量
        var n = 2022;
        function f1() {
            console.log(n);
        }
        f1();//2022

局部变量

  • 函数内部定义的(只在该函数作用域中)变量
  • 在函数外部无法读取函数内的局部变量
 // 局部变量
 function f2() {
            var n = 2022;
        }
        console.log(n);//错误
        var m = 12;
        function f3() {
            var m = 34;
            console.log(m);
        }
        console.log(m);//12
        f3();//34
  • 作用域最大的用处就是隔离变量,不同作用域下相同变量不会有冲突。
  • 有上下级关系
    在这里插入图片描述

注意:

  • 不用var声明的变量不管在哪里都为全局变量。
  • 由于JS不存在块级作用域,对于forif中定义的变量隶属于函数作用域
(2)作用域链
  • 一般情况下,变量取值是到创建这个变量的函数的作用域中取值
  • 但是如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这样的一个查找过程形成的链条就叫做作用域链
        var a = 10;//全局变量
        function fn() {
            var b = 20;

            function bar() {
                console.log(a + b);//10+20=30
            }
            return bar;
        }
        var x = fn(),
            b = 200;
        x();

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

(3)如何从外部读取函数内部的局部变量

解决方法:在函数内部,再定义一个函数。
如:在函数f1内部,再定义一个f2函数,将f2作为返回值,这样就能在f1的外部读取其内部变量了。

 function f1() {
            var n = 2022;
            function f2() {
                console.log(n);
            }
            return f2;
        }
        var result = f1();
        result(); // 2022

2.闭包的概念

上面代码中的f2函数就是闭包。

  • 闭包就是有权访问另一个函数作用域中的变量的函数,可以理解为定义在一个函数内部的函数。
  • 闭包是将函数内部和函数外部连接起来的桥梁
  • 创建闭包的最常见的方式就是在一个函数内(如f1)创建另一个函数(f2),通过该函数(f2)访问这个函数(f1)的局部变量

3.闭包的特性

  • 函数嵌套函数,让外部访问函数内部变量成为可能
  • 可以避免使用全局变量,防止全局变量污染
  • 局部变量会常驻在内存中,会造成内存泄漏(即函数内部可以引用外部的参数和变量,这些参数和变量不会被垃圾回收机制回收)

Javascript的垃圾回收机制

  • Javascript的垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要某块内存时这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存。
    即在 Javascript中,如果一个对象不再被引用,那么这个对象就会被垃圾回收机制回收。

4.使用闭包的好处

  • 读取函数内部的变量
  • 让变量的值始终保存在内存中
//全局变量的累加
var a = 1;
function f1(){
	a ++;
	console.log(a);
}
f1();//2
f1();//3
//局部变量则不行
function f1(){
	var a = 1;
	a ++;
	console.log(a);
}
f1();//2
f1();//2
//因为每次执行时,a被重新赋值为1
//如果想让局部变量累加该怎么办呢?这时我们就可以用到闭包
function f1(){
	var a = 1;
	function f2(){   //函数嵌套函数
		a ++;
		console.log(a)
	}
	return f2
}
var result = f1();//将返回的函数赋给f2
result(); //2
result(); //3
  • 这也证明了函数f1中的局部变量a一直保存在内存中,并没有在f1调用后自动清除。
  • 原因:f1是f2的父函数,f2被赋给了一个全局变量,这样f2就始终在内存中,而2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制回收。

5.使用闭包的注意事项

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

二、立即执行函数

1.JS中的函数定义方式

(1)函数的声明式写法

function foo(){...}

这种写法会导致函数提升,即所有的function关键字都会被解释器优先编译,即不管声明在什么位置,都可以调用它,但它本身不会被执行。定义该函数只是让解释器知道它的存在,只有在调用该函数时,函数才会被执行。
在这里插入图片描述

(2)函数的表达式写法

var foo=function(){...}

该写法不会导致函数提升,因此必须先声明,再调用,否则会出错。
在这里插入图片描述

2.立即执行函数

(1)定义

立即执行函数:声明一个函数,并马上调用这个匿名函数。

匿名函数:即使用function关键字声明一个函数,但未给函数命名,倘若需要传值,直接将参数写到括号内即可。

在这里插入图片描述

(2)立即执行函数的官方写法
  • 定义好函数之后,立即调用该函数,此时并不能在函数的定义后面直接加(),这将导致语法错误
//error
function foo(){} ()
  • 原因:function关键字既可以当做语句,也能当做表达式,即
//语句
function foo() {};

//表达式
var foo = function (){};

为了避免解析上的歧义,JS引擎规定:如果function关键字出现在行首,就一律解析成语句。
所以当JS引擎看到行首是function关键字后,认为这一段都是函数定义,不应该以()结尾,所以就会报错。

  • 解决办法:不要让function关键字出现在行首,让JS引擎将其理解为一个表达式,因此立即执行函数有以下两种官方写法
//第一种:用括号把整个函数定义和调用包裹起来,w3c建议第一种
(function (){}());

//第二种:用括号把函数定义包裹起来,后面再加括号
(function (){})();

这两种写法均是以圆括号开头,JS引擎明白圆括号后面的是表达式,而非一个函数定义语句,这样就不会再报错。

(3)立即执行函数的原理

在这里插入图片描述
立即执行函数一般也写成匿名函数,匿名函数不能单独使用,至少要用()包裹起来,否则会报错。因此,立即执行函数还有其他的扩展写法:

(function foo(){console.log("Hello World!")}())//用括号把整个表达式包起来,正常执行
(function foo(){console.log("Hello World!")})()//用括号把函数包起来,正常执行
!function foo(){console.log("Hello World!")}()//使用!,求反,这里只想通过浏览器语法检查。
+function foo(){console.log("Hello World!")}()//使用+,正常执行
-function foo(){console.log("Hello World!")}()//使用-,正常执行
~function foo(){console.log("Hello World!")}()//使用~,正常执行
(4)立即执行函数的作用

立即执行函数会形成一个单独的作用域,可以封装一些临时变量或者局部变量,避免污染全局变量。

例题一:创建单独的作用域
<body>
    <ul id="list">
        <li></li>
        <li></li>
        <li></li>
        <li></li>
    </ul>
    <script>
      var list = document.getElementById("list");
      var li = list.children;
      for(var i = 0 ;i<li.length;i++){
        li[i].onclick=function(){
         console.log(i);  // 结果总是4.而不是0,1,2,3
        }
      }
     </script>  
</body>

这是一道非常经典的例题,为什么结果总是4呢?

var声明的变量是全局有效的,这样变量i是贯穿整个作用域的,而不是给每个li分配一个i,用户触发onclick事件之前,for循环已经执行结束了。

异步事件的执行:浏览器是事件驱动的,浏览器中很多行为是异步(Asynchronized)的,很容易有事件被同时或者连续触发。当异步事件发生时,会创建事件并放入执 行队列中,等待当前代码执行完成之后再执行这些代码,如鼠标点击事件发生、定时器触发事件发生、XMLHttpRequest完成回调这些事件,都会被放入执行队列中等待。

当用户触发onclick事件时,onclick那个函数才被调用。调用时,需要对变量i求值。根据作用域链的原理,解析程序首先会在事件处理程序内部查找i,但点击事件内部并没有定义i;所以又往上找,找到var i=0,此时有定义,但此时for循环已经结束,而循环结束时i=4,存储在作用域链中i的值为4。所以,不管点击第几个li,打印出来的都为4

那么,如何才能输出0,1,2,3呢?
——使用立即执行函数,给每个li创建一个独立的作用域。在立即执行函数执行的时候,i的值被赋给j,此后j的值一直不变。i的值从0到3,对应四个立即执行函数,这四个立即执行函数里边的j分别是0,1,2,3,即点击第几个li,就输出几

<body>
    <ul id="list">
        <li></li>
        <li></li>
        <li></li>
        <li></li>
    </ul>
    <script>
      var list = document.getElementById("list");
      var li = list.children;
      for (var i = 0; i < li.length; i++) {
            (function(j) {
                li[j].onclick = function() {
                    console.log(j); //0,1,2,3
                };
            })(i); // 把实参i赋值给形参j
        }
     </script>  
</body>

另外,也可以用let关键字解决这个问题,let 声明的变量只在 let 命令所在的代码块{}内有效,使用的是块级作用域。即每个点击事件都会进入一个不同的块,四次点击对应四个不同的块,所以也会输出0123

<body>
    <ul id="list">
        <li></li>
        <li></li>
        <li></li>
        <li></li>
    </ul>
    <script>
      var list = document.getElementById("list");
      var li = list.children;
      for(let i = 0 ;i<li.length;i++){
        li[i].onclick=function(){
         console.log(i);  // 结果0,1,2,3
        }
      }
     </script>  
</body>
例题二:封装临时变量
(function() {
            var todayDom = document.getElementById("today");
            var days = [
                "星期日",
                "星期一",
                "星期二",
                "星期三",
                "星期四",
                "星期五",
                "星期六",
            ];
            var today = new Date();//声明日期对象
            var year = today.getFullYear();//获取完整年,例如2022
            var month = today.getMonth() + 1;//获取月,注意月是从0开始计算,需要在后面+1
            var date = today.getDate();
            var day = today.getDay();
            var dateTime =
                "今天是" + year + "年" + month + "月" + date + "日" + days[day];
            todayDom.innerHTML = dateTime;
        })();

如果上面的代码没有被包裹在立即执行函数中,那么这些临时变量todayDom,days,today,year,month,date,day,dateTime都将成为全局变量(初始化代码遗留的产物)。用立即执行函数之后,这些变量都不会在全局变量中存在,以后也不会其他地方使用,有效的避免了污染全局变量。

所以,如果有些变量是初始化过程结束之后不再被使用,或者某些工作只需要执行一次,就可以用立即执行函数。

同时,立即执行函数也能像其他函数一样可以有返回值,返回任何类型的值,比如对象,函数。

  var result = (function() {
            var todayDom = document.getElementById("today");
            var days = [
                "星期日",
                "星期一",
                "星期二",
                "星期三",
                "星期四",
                "星期五",
                "星期六",
            ];
            var today = new Date(); //声明日期对象
            var year = today.getFullYear(); //获取完整年,例如2022
            var month = today.getMonth() + 1; //获取月,注意月是从0开始计算,需要在后面+1
            var date = today.getDate();
            var day = today.getDay();
            var dateTime =
                "今天是" + year + "年" + month + "月" + date + "日" + days[day];
            return function() {
                return dateTime;
            };
        })();
        console.log(result());

三、this关键字

this表示当前对象的一个引用,它并不是固定不变的,其会随着执行环境的改变而改变。

  • 在方法中,this 表示该方法所属的对象。
  • 如果单独使用,this 表示全局对象window
  • 在函数中,this 表示全局对象window
  • 在函数中,在严格模式下,this 是未定义的(undefined)。
  • 在事件中,this 表示接收事件的元素。
  • 类似 call()apply() 方法可以将 this 引用到任何对象。
1.全局作用域或者普通函数中 this指向全局对象 window
console.log(this===window) //true
//声明式
 function foo() {
            console.log(this);
        }
        foo(); //window
//函数式
 var foo = function() {
            console.log(this);
        };
        foo(); //window
//立即执行函数
(function() {
            console.log(this);
        })(); //window
2.方法调用中谁调用 this指向谁
//事件绑定
var btn = document.querySelector("button")
btn.onclick = function () {
  console.log(this) // btn
}
//事件监听谁,this就指向谁(只要不使用bind,不使用箭头函数)
//事件监听
var btn = document.querySelector("button")
btn.addEventListener('click', function () {
  console.log(this) //btn
})

//对象方法调用
 var time = {
            down: function() {
                console.log(this);
            },
        };
        time.down(); // person
3.在构造函数或者构造函数原型对象中 this指向构造函数的实例
//不使用new指向window
function Person(name) {
  console.log(this) // window
  this.name = name;
}
Person('inwe');
//使用new
function Person(name) {
  this.name = name
  console.log(this) //people
}
var people = new Person('iwen')
//这里new改变了this指向,将this由window指向Person的实例对象people

练习题

(1)

 function test(){
    	var num =100;
    	function a(){
    		num++;
    		console.log(num);
    	}
    	function b(){
    		num--;
    		console.log(num);
    	}
    	return [a,b];//返回一个函数数组
    }
    var myArr=test();
    myArr[0] (); //执行函数a 101
    myArr[1] (); //执行函数b 100

分析:这里的function ab是并集的关系,属于同一等级,函数ab共用同一个function test()的作用域,所以一个值的改变必然会导致另一个值的改变。 myArr[0] ()执行函数a,num初始值为100,再num++,此时num值为101
接着 myArr[1] ()执行函数b,function test()的作用域中的num值为101,再num--,所以输出结果为100

(2)

  var obj = {
            a: function() {
                console.log(this.b);
            },
            b: 1,
        };
        var foo = obj.a;
        var b = 2;
        obj.a();//1,这条语句表示this在obj这个对象中引用,则指向obj对象。而obj对象中又有变量b的定义和取值,所以结果为1
        foo();//2,将obj对象中的a方法赋给foo,在全局环境中调用foo函数,此时的this指向window,所以结果为全局变量b=2
  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值