JavaScript学习笔记六

118 阅读10分钟

six

一:作用域(Scope)

1.1 什么是作用域?

概念:作用域是一个变量或函数的作用范围。作用域是在 JS 文件加载解析时,就已经被确定了。

  • JavaScript中的作用域是指变量或函数的可访问或可见性。

目的:为了提高程序的可靠性(安全),减少命名冲突。

  • 作用域(Scope)的主要好处是安全性。变量或函数只能在程序的某个区域访问调用。可以避免程序其他部分对变量或函数的意外修改。
  • 作用域(Scope)还减少了命名空间冲突。可以在不同的作用域中使用相同的变量名或函数名,因为作用域是相互独立互不干扰。
1.2 作用域的分类(ES5 中)

全局作用域:作用于整个 Script 标签内,或是独立的 js 文件。

局部(函数)作用域:作用于函数内的环境。

块级作用域:这个是 ES6 中的新内容,目前还不在学习范围内。

1.3 全局作用域
1.3.1 创建与销毁的过程

全局作用域什么时候创建:全局作用域在页面打开时创建。

全局作用域什么时候销毁:在页面关闭时销毁。

1.3.2 全局作用域与 GO 的关系

GO 对象是 JavaScript 引擎在程序加载时创建的,它的作用是存储全局作用域中的变量声明、函数声明等。同时,GO 对象是 JavaScript 引擎访问全局作用域资源的唯一途径。 注: 上面说所的加载可以理解为在浏览器环境(browser)中的过程为:浏览器下载 JS 文件 ---> 载入缓存(内存) ---> 解析 ---> 预编译 ,但还未到执行的环节。

全局作用域与 GO 的关系是相互依赖的,因此,全局作用域与 GO 的关系可以总结为以下几点:

  • 全局作用域中的变量声明、函数声明等都存储在 GO 对象中。
  • 全局作用域中的变量声明、函数声明等都是通过 GO 对象来访问的。
  • GO 对象是 JavaScript 引擎访问全局作用域资源的唯一途径。

注:全局作用域创建时,GO 对象会生成一个 JavaScript 内部的隐式属性,但只能被 JavaScript 引擎读取。

1.4 局部(函数)作用域
1.4.1 函数的补充知识点

函数是一种对象类型,同时也是一种引用类型和引用值。

函数是对象类型,那也就是说,它一定就有相应的属性,可以通过 . 语法来获取它属性相应的值。 例如:.name .length .protoytpe

1.4.2 创建与销毁的过程

局部(函数)作用域什么时候创建:函数被定义时,也可以理解为函数声明时;

局部(函数)作用域什么时候销毁:函数调用执行完成后,则局部(函数)作用域就会被销毁。

注:如果函数再次被调用执行时,又会被重新创建一个新的作用域和新的 AO 对象。

1.4.3 局部(函数)作用域 与 AO 的关系

AO 对象是 JavaScript 引擎在函数执行前一刻创建,它用于存储函数执行期上下文信息,包括函数的参数(形参和实参)、局部变量、函数声明等。同时,AO 对象是 JavaScript 引擎访问局部(函数)作用域资源的唯一途径。

局部(函数)作用域与 AO 的关系是相互依赖的,因此,局部(函数)作用域与 AO 的关系可以总结为以下几点:

  • 局部(函数)作用域中的形参与实参存储在 AO 对象中。
  • 局部(函数)作用域中的局部变量声明存储在 AO 对象中。
  • 局部(函数)作用域中的函数声明存储在 AO 对象中。
  • 局部(函数)作用域中的形参与实参、局部变量声明、函数声明等都是通过 AO 对象来访问的。
  • AO 对象是 JavaScript 引擎访问局部作用域资源的唯一途径。

注:局部(函数)作用域创建时,AO 对象会生成一个 JavaScript 内部的隐式属性,但只能被 JavaScript 引擎读取。

二:作用域链(Scope Chain)

2.1 什么是作用域链?

概念理解一: 作用域,就是作用域链的容器。作用域 === [[scope]]

概念理解二: GO 全局的执行期上下文; AO 函数的执行期上下文。

概念理解三: 作用域链,就是把 GO 与 AO 形成链式从上到下排列起来,形成一个链式关系,那么这个链式关系,就是作用域链。

2.2 图解作用域链整个过程
2.2.1 回顾预编译过程

image-20231121013012468.png

2.2.2 a 函数被定义,但还未被调用执行时

当 a 函数被定义时,JavaScript 引擎生成作用域 ( [[scope]] ) 属性,[[scope]]保存该函数的作用域链。 该作用域链的第 0 位存储当前环境下的全局执行期上下文 GO。 GO 里存储全局下的所有对象,其中包含全局 a 函数和全局变量 c。

注:每一个函数在被定义时,它的作用域链中就已经包含了当前环境下的全局执行期上下文 GO。

image-20231121015011160.png

2.2.3 a 函数被执行时(前一刻),也就是预编译的过程

当 a 函数被执行时(前一刻),作用域链的顶端(第0位)存储 a 函数生成的函数执行期上下文 AO,同时第1位存储 GO 。 查找变量时到 a 函数存储的作用域链中从顶端开始依次向下查找。

注:所有函数自身的 AO 都排在当前作用域链的最顶端。

image-20231121015618519.png

2.2.4 当 a 函数被调用执行时,b 函数被定义时。

注意:此时的 a 函数还没有执行完毕,所以是 执行时

注:当函数嵌套环境下,外层函数被执行时,内层函数就一定会被定义,这是一种规律。

当 b 函数被定义时,是在 a 函数环境下,所以 b 函数这时的作用域链就是 a 函数被调用执行时的作用域链。 也可以理解为:此时的 b 函数作用域链跟 a 函数被调用执行时的作用域链一模一样。

image-20231121020524846.png

2.2.5 当 a 函数被调用执行时,b 函数被执行时(前一刻)

当 b 函数被执行时(前一刻),生成函数 b 的 [[scope]],存储函数 b 的作用域链,顶端第0位存储 b 函数的 AO。 a 函数的 AO 和全局的 GO 依次向下排列。

image-20231121193322927.png

2.2.6 当 b 函数被执行结束后

当 b 函数被执行结束后,b 函数的 AO 销毁,b 函数回归到被定义时的状态。

image-20231123055506129.png

2.2.7 当 b 函数被执行结束后,同时也意味着 a 函数也执行结束,此时 a 函数回归被定义状态

image-20231123060004640.png

2.3 为什么在全局环境下,获取不到函数内部变量的值?

因为,在全局环境中,只会在自己的 GO 和 AO 中寻找,并且不会向内寻找,这是 JavaScript 的规则。

  • 通过上述的解答,衍生出,另一个规则,就是由内向外,是可以获取外部变量的值;
  • 通过学习的作用域链知识,就可以知道外部的作用域链是比内部的作用域链先生成;
  • 同时,AO 的销毁,也是按照由内向外逐层销毁,所以内部可以获取外部变量的值;
  • 而外部想获取函数内部变量的值时,此时函数内的 AO 已经被销毁,所以就获取不到。
2.4 手写作用域链过程
//范例
function a(){
    function b(){
        function c(){           
        }
        c();
    }
    b();
}
a();

image-20231117022809626.png

三:闭包

3.1 什么是闭包?

MDN: 闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在 JavaScript 中,闭包会随着函数的创建而被同时创建。

小野: 当内部函数被返回到外部并保存时,一定会产生闭包。 闭包 会产生原来作用域链不释放,过渡的闭包,可能会导致内存泄漏或加载过慢。

个人理解: 闭包,在一个嵌套函数中,内层的函数被返回到外部(全局环境中)并保存时,且该内层函数一定是引用或操作了该外层函数的变量,这种现象,就是闭包。

3.2 验证我的个人理解

在 chrome devtools 的验证图解

图1:有引用外层函数变量,并在右侧的作用域信息栏中,有闭包显示。

图2:没有引用外层函数变量,在右侧的作用域信息栏中,没有闭包显示。

image-20231123063117695.png

3.3 图解闭包现象的过程
3.3.1 回顾预编译过程

image-20231123064121535.png

3.3.2 test1 函数被定义时

image-20231123064257403.png

3.3.3 test1 函数执行前

image-20231123064332735.png

3.3.4 test1 函数执行时,test2 函数被定义时

image-20231123064502324.png

3.3.5 test1 函数执行完,过程包含( test2 函数被抛出,test1 函数回归被定义时), test2 函数执行前。
  • 当 test1 函数执行结束时,正常情况下,此时的 test1 函数的 AO 应要被销毁,而图解中 test1 的 AO 并没有销毁,只是连线被断开。 注:这个连线被断开可以理解成,test1 函数的作用域链与 test1 函数的 AO 映射关系被断开。

  • 此时的 test2 函数并没有被调用执行,因为 test2 函数被 return 返回到外部(全局环境下),且被全局变量 test3 接收,且准备要执行 test2 函数。 注:test1 函数 赋值给全局变量 test3 ,就相当于把 test2 函数赋值给了 test3 。

  • test1 函数回归被定义时,此时的 test1 作用域链中,只存储了 GO 。 注:因为任何一个全局函数只要被定义,其作用域链中都会有一个 GO 。

  • test3 执行前(其实就是 test2 函数),生成了自己的 AO ,存储在作用域链的第 0 位。 注:此时的 test2 函数作用域链中还存储着(映射关系) test1 函数的 AO,所以 test1 的 AO 没有销毁,只是断开了与test1 函数作用域链的映射关系。

image-20231124111310115.png

3.3.6 test3 执行完

注:执行test3 ,就是执行 test2 函数。

  • test3 执行完后,就会销毁 test2 函数的 AO ,这一步没有任何问题。

为什么 test2 的作用域不被销毁?

  • test3 执行完,test2 的作用域,正常来说,也应该一并销毁。
  • 因为 接受的 test2 函数的是全局变量,相当于 test2 函数赋值给了全局变量,则这个全局变量就成为了全局函数,所以作用域没有被销毁。

为什么 test2 的作用域链中第 0 位是 test1 函数的 AO ?

  • 因为 test2 函数在被定义时,就已经包含了 test1 函数的 AO ,同时在 test1 函数执行时,就把 test2 函数给 return 出去,连带着把作用域也一起给返回到全局中,同时 函数的作用域是相互独立互不干扰。

为什么 会保存 GO ?

  • 全局,全局,全局。上述两个结论。

image-20231124120258988.png

四:闭包实例代码

4.1 累加减

代码功能:实现累加减

实例学习:

1:n 的值是可以缓存起来,在程序运行期间,实现在原有的基础上自增自减。

2:数组的用法,返回多个函数。

function count(){
    var n = 100;
    function add(){
        n++;
        console.log(n);
    }
    function reduce(){
        n--;
        console.log(n);
    }
    return [add, reduce];
}
var result = count();
result[0]();
result[1]();
4.2 面包管理

代码功能:一个基础的面包进货、销售的功能。

实例学习:

同 4.1 ,巩固下。

function breadMgr(num){
    var breadNum = num || 10;
    function supply(){
        breadNum += 10;
        console.log(breadNum);
    }
    function sale(){
        breadNum--;
        console.log(breadNum);
    }
    return [supply, sale];
}
var breadMgr = breadMgr(50);
breadMgr[0]();
4.3 时间管理

代码功能:时间安排。

实例学习:

1:对象的用法,本实例中是返回一个对象,通过 . 语法,在调用对象方法,来实现函数的多次调用执行。

function sunSched(){
    var sunSched = '';
    
    var option = {
        setSched:function(thing){
            sunSched = thing
        },
        showSched:function(){
            console.log('My Schedule on Sunday is' + ' ' + sunSched )
        }
    }
    return option;
}
var sunSched = sunSched();
sunSched.setSched('working');
sunSched.showSched();