JS作用域、执行上下文、递归与闭包

目录

 

作用域

全局作用域

函数作用域

执行上下文

函数执行上下文

执行上下文栈

作用域与执行上下文的区别

递归

闭包

产生闭包的条件

闭包的作用

使用注意 

内存泄漏

内存溢出(一种程序运行出现的错误)


作用域

作用域指一个变量的作用范围。它是静态的(相对于上下文对象), 在编写代码时就确定了。

作用:隔离变量,不同作用域下同名变量不会有冲突。

全局作用域

直接编写在script标签中的JS代码,都在全局作用域。

在全局作用域中:

  • 在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用。

  • 创建的变量都会作为window对象的属性保存。

<script>
    var a = 10;
</script>
  • 创建的函数都会作为window对象的方法保存。

<script>
    function say(){
    }
</script>

全局作用域中的变量都是全局变量,在页面的任意的部分都可以访问到。

 

函数作用域

调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁。每调用一次函数就会创建一个新的函数作用域,他们之间是互相独立的。

在函数作用域中可以访问到全局作用域的变量,在全局作用域中无法访问到函数作用域的变量。简单讲就是里面可以访问外面,但是外面不能访问里面。

在函数中要访问全局变量可以使用window对象。

<script>
    var a = 10;
    function say(){
        console.log(window.a);
    }
</script>

提醒1:

在函数作用域也有声明提前的特性:

  • 使用var关键字声明的变量,会在函数中所有的代码执行之前被声明

  • 函数声明也会在函数中所有的代码执行之前执行

因此,在函数中,没有var声明的变量都会成为全局变量,而且并不会提前声明。

<script>
    var a = 10;//全局
    function say(){
        var a = 5;//局部变量
        b = 20;//全局
        console.log(window.a);
    }
</script>

提醒2:定义形参就相当于在函数作用域中声明了变量。

<script>
    var a = 10;//全局
    function say(b){
        //b为局部
    }
</script>

执行上下文

函数执行上下文

在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)。

(1)对局部数据进行预处理:

  • 形参变量==>赋值(实参)==>添加为执行上下文的属性

  • arguments==>赋值(实参列表), 添加为执行上下文的属性

  • var定义的局部变量==>undefined, 添加为执行上下文的属性

  • function声明的函数 ==>赋值(fun), 添加为执行上下文的方法

  • this==>赋值(调用函数的对象)

(2)开始执行函数体代码

执行上下文栈

  • 1.在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象

  • 2.在全局执行上下文(window)确定后, 将其添加到栈中(压栈)

  • 3.在函数执行上下文创建后, 将其添加到栈中(压栈)

  • 4.在当前函数执行完后,将栈顶的对象移除(出栈)

  • 5.当所有的代码执行完后, 栈中只剩下window

作用域与执行上下文的区别

区别1:

  • 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时

  • 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建

  • 函数执行上下文是在调用函数时, 函数体代码执行之前创建

区别2:

  • 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化

  • 执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放

联系:

  • 执行上下文(对象)是从属于所在的作用域

  • 全局上下文环境==>全局作用域

  • 函数上下文环境==>对应的函数使用域

递归

一个函数通过名字调用自身的情况,好处是代码简洁。在树的前序,中序,后序遍历算法中,递归的实现明显要比循环简单得多。

注意:

  • 找规律,很容易找到规律的使用递归特别方便

  • 出口,必须得有个已知的,没有出口就会一直循环

缺点:

  • 函数调用导致的时间和空间消耗大

  • 存在重复计算,效率低

  • 调用栈可能会溢出

<script>
//递归实现阶乘
    function sayhi(num) {
        if (num <= 1){
            return 1;
        }else {
            return num * sayhi(num-1);
            }
        }
        console.log(sayhi(5));
</script>
<script>
	//递归:一个函数可不可以自己调用自己
	 function f1() {
	 	console.log("hello");
	 	f1();

	 }
	 
	//递归就是老和尚讲故事,死循环了,必须要给一个结束的条件才有意义
	 var i = 0;
	 function f1() {
		
	 	console.log("hello");
	 	i++;
	 	if (i < 10) {
	 		f1();
	 	}
		

	// }
	// f1();
	// 
	//求n个数的累加,我们可以采取递归来替代循环
	 function getSum(n) {
	 	if (n === 1) {
	 		return 1;
	 	}

	 	return n + getSum(n-1);

	 }
	 console.log (getSum(100));
	 	
	//递归调用比较占用资源,循环次数太多不适合使用
	//
	//输入一个数,求这个数的各位数字之和
	//先取余再相除在取整
	function getSum(n) {
		//结束条件
		if (n < 10) {
			return n;
		}
		
		return n % 10 + getSum(parseInt(n/10));
	}
	console.log(getSum(140));
		 
</script>

闭包

有权访问另外一个函数作用域中的变量的函数。当内部函数被保存在外部时,将会生成闭包。

产生闭包的条件

  • 1.函数嵌套

  • 2.内部函数引用了外部函数的数据(变量/函数)。

缺点:闭包导致原有作用链不释放,导致内存泄露(漏得越多,等价占用的内存越多,剩下的可用内存就少了)。

全局变量和局部变量,该特殊之处在于函数内部可以读取全局变量,但是函数外部是不能读取局部变量的。函数内部声明变量的时候,一定要使用var命令。这就导致一个问题,如果我们需要从外部读取函数内的局部变量的时候,就必须采用一种特殊的方式,在函数内部再定义一个函数。

<script>
 function f1(){
    var n=22;
    function f2(){
      alert(n); // 22
    }

  }
</script>

我们只需要将f2作为返回值,就可以读取f1的内部变量了。

上述f2就是闭包,也就是说闭包是一个可以读取其他函数内部变量的函数;即一个函数内部的函数。

闭包的作用

  • 读取函数内部的变量,实现公有变量;
  • 可以做缓存,使得变量的值始终保持在内存中;
  • 可以实现封装,属性私有化;
  • 模块化开发,防止污染全局变量

举例1:

    <script>
        function f1() {
            var a = 1;
            function f2() {
                a ++;
                console.log(a);
            }
            return f2;

        }
        var f = f1();
        f();//2
        f();//3
        f();//4
        f();//5
    </script>

ps:外部函数f1执行完毕后,变量a并没有消失,而是保存在了内存中。

内部采用函数表达式,没有产生闭包的原因如下:

    <script>
        function f1() {
            var re = [];
            for (var i=0; i < 10; i++){
                re[i] = function () {
                    console.log(i);
                };
            }
            return re;
        }
        var f = f1();
        for (var j=0; j < 10; j++){
            f[j]();
        }
        // 输出为10个10
        
    </script>

ps:f1执行完了之后外部i值已经是10了,函数内部只是引用 ,只有外部调用的时候才会运行函数,那时候i就已经是10了,而且10个函数都指向同一个AO。

举例2:将所有的数据和功能都封装在一个函数内部(私有的),只向外暴露一个包含n个方法的对象或函数。

<script>        
    function myModule() {
        //私有数据
        var msg = 'hello'

        //操作私有数据的函数
        function doSomething() {
            console.log('doSomething() '); 
        }

        function doOtherthing() {
            console.log('doOtherthing() ')) 
        }

        //通过【对象字面量】的形式进行包裹,向外暴露多个函数
        return {
            doSomething1: doSomething,
            doOtherthing2: doOtherthing
        }
    }
</script>

使用注意 

  • 内存消耗大,理性使用
  • 不要随便改变父函数内部变量的值

内存泄漏

内存泄漏:占用的内存没有及时释放。内存泄露积累多了就容易导致内存溢出。

常见的内存泄露:

  • 1.意外的全局变量

  • 2.没有及时清理的计时器或回调函数

  • 3.闭包

内存溢出(一种程序运行出现的错误)

内存溢出:当程序运行需要的内存超过了剩余的内存时,就出抛出内存溢出的错误。

posted @ 2018-09-10 09:32  沉浮乡土  阅读(455)  评论(0编辑  收藏  举报