JavaScript基础知识-作用域和闭包

1.理解词法作用域和动态作用域
词法作用域

+  编译器的第一个工作阶段叫作分词,就是把由字符组成的字符串分解成词法单元。这个概念是理解词法作用域的基础

简单地说,词法作用域就是定义在词法阶段的作用域,是由写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变

关系

无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定


function foo(a) {
    var b = a * 2;
    function bar(c) {
        console.log( a, b, c );
    }
    bar(b * 3);
}
foo( 2 ); // 2 4 12
遮蔽
  • 作用域查找从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见第一个匹配的标识符为止
  • 在多层的嵌套作用域中可以定义同名的标识符,这叫作“遮蔽效应”,内部的标识符“遮蔽”了外部的标识符
var a = 0;
function test(){
    var a = 1;
    console.log(a);//1
    console.log(window.a);//0
}
test();

【1】如果处于词法作用域,也就是现在的javascript环境。变量a首先在foo()函数中查找,没有找到。于是顺着作用域链到全局作用域中查找,找到并赋值为2。所以控制台输出2

【2】如果处于动态作用域,同样地,变量a首先在foo()中查找,没有找到。这里会顺着调用栈在调用foo()函数的地方,也就是bar()函数中查找,找到并赋值为3。所以控制台输出3
  两种作用域的区别,简而言之,词法作用域是在定义时确定的,而动态作用域是在运行时确定的
  ** 引用:https://www.cnblogs.com/xiaohuochai/p/5700095.html**

2.理解JavaScript的作用域和作用域链
作用域
  • 作用域就是变量和函数的可访问范围,控制着变量和函数的可见性与生命周期,在JavaScript中变量的作用域有全局作用域和局部作用域。
    全局和局部作用域下面用一张图来解释:
    在这里插入图片描述
作用域链
  • 全局执行环境是最外层的一个执行环境,在web浏览器中全局执行环境是window对象,因此所有全局变量和函数都是作为window对象的属性和放大创建的。每个函数都有自己的执行环境,当执行流进入一个函数的时候,函数的环境会被推入一个函数栈中,而在函数执行完毕后执行环境出栈并被销毁,保存在其中的所有变量和函数定义随之销毁,控制权返回到之前的执行环境中,全局的执行环境在应用程序退出(浏览器关闭)才会被销毁。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)来保证对执行环境有权访问的变量和函数的有序访问。

用一张图来解释作用域链的运行:由里向外执行。

在这里插入图片描述

function a(sum1,sum2){
            var sum=num1+num2;
            return sum;
        }
        var tatal=a(5,10);

在函数add创建时,它的作用域链中会填入一个全局对象,该对象包含了所有全局变量,如下图所示:
在这里插入图片描述

执行此函数时会创建一个称为“运行期上下文(execution context)”的内部对象,运行期上下文定义了函数执行时的环境。每个运行期上下文都有自己的作用域链,用于标识符解析,当运行期上下文被创建时,而它的作用域链初始化为当前运行函数的[[Scope]]所包含的对象。

这些值按照它们出现在函数中的顺序被复制到运行期上下文的作用域链中。它们共同组成了一个新的对象,叫“活动对象(activation object)”,该对象包含了函数的所有局部变量、命名参数、参数集合以及this,然后此对象会被推入作用域链的前端,当运行期上下文被销毁,活动对象也随之销毁。新的作用域链如下图所示:

在这里插入图片描述

  • 在函数执行过程中,没遇到一个变量,都会经历一次标识符解析过程以决定从哪里获取和存储数据。该过程从作用域链头部,也就是从活动对象开始搜索,查找同名的标识符,如果找到了就使用这个标识符对应的变量,如果没找到继续搜索作用域链中的下一个对象,如果搜索完所有对象都未找到,则认为该标识符未定义。
    总结
  • 根据上述讲的作用域链的结构可以看出,定义的标识符的越深,那么读写的速度也就越慢,而全局变量总是处于作用域链的最末端,所以当变量解析的时候,查找全局变量是最慢的,所以在编写代码的时候要尽可能少的使用全局变量,尽可能使用局部变量。
    ** 引用: https://blog.csdn.net/zlts000/article/details/40555671**
3.理解JavaScript的执行上下文栈,可以应用堆栈信息快速定位问题
1、执行上下文
  • 简而言之,执行上下文就是当前JavaScript代码被解析和执行时所在环境的抽象概念,JavaScript中运行任何的代码都是在执行上下文中运行。
2、执行上下文类型:

执行上下文总共有三种类型:

1)全局执行上下文

这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:1. 创建一个全局对象,在浏览器中这个对象就是window对象。2. 将this指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。

2)函数执行上下文

每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用时才会被创建,一个程序中可以存在任意数量的函数执行上下文,每当一个新的执行上下文被创建,它都会按照特定的顺序执行一系列步骤。

  • 函数多了就有多个函数执行上下文,每次调用函数创建一个新的执行上下文,如何管理创建的那么多执行上下文?
  • JavaScript引擎创建了执行上下文栈来管理执行上下文,可以把执行上下文栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
3)Eval函数执行上下文

运行在eval函数中的代码也获得了自己的执行上下文,但是JavaScript中不常用eval函数,这里不再详细叙述。

3、执行上下文生命周期:

执行上下文的生命周期包括三个阶段:创建阶段 -> 执行阶段 -> 回收阶段

1)创建阶段:

当函数被调用,但未执行任何其内部代码之前,会做三件事:
  ① 创建变量对象:首先初始化函数的参数 arguments ,提升函数声明和变量声明。
  ② 创建作用域链:在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,JavaScript始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一级的父作用域中查找,直到找到该变量。
  ③ 确定 this 指向:包含多种情况,下文详细叙述。

  • 在一段JS脚本执行之前,会先解析代码(所以说 JS 是解释执行的脚本语言),解析的时候会先创建一个全局的执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来。变量先赋值为undefined,函数则先声明好可使用,这一步做完了,然后开始正式执行程序。
  • 另外一个函数在执行之前会先创建一个函数执行上下文环境,跟全局上下文差不多,不过函数执行上下文中会出现 this arguments 和函数的参数。
2)执行阶段:

执行变量赋值、代码执行

3)回收阶段:

执行上下文出栈等虚拟机回收执行上下文

4.this的原理以及几种不同使用场景的取值
  • this 的值是在执行的时候才能确认,定义的时候不能确认。因为this是执行上下文环境的一部分,而执行上下文需要在代码执行之前确定,而不是定义的时候:
// 情况1
function foo() {
  console.log(this.a) //1
}
var a = 1
foo()
// 对于直接调用 foo 来说,不管 foo 函数被放在了什么地方,this 一定是 window
// 情况2
function fn(){
  console.log(this);
}
var obj={fn:fn};
obj.fn(); //this->obj
// 对于 obj.fn() 来说,我们只需要记住,谁调用了函数,谁就是 this,所以在这个场景下 fn 函数中的 this 就是 obj 对象
// 情况3
function CreateJsPerson(name,age){
//this是当前类的一个实例p1
this.name=name; //=>p1.name=name
this.age=age; //=>p1.age=age
}
var p1=new CreateJsPerson("尹华芝",48);
// 在构造函数模式中,类中(函数体中)出现的 this.xxx=xxx 中的 this 是当前类的一个实例
// 情况4
function add(c, d){
  return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
// call、apply 和 bind:this 是第一个参数
// 情况5
<button id="btn1">箭头函数this</button>
<script type="text/javascript">
    let btn1 = document.getElementById('btn1');
    let obj = {
        name: 'kobe',
        age: 39,
        getName: function () {
            btn1.onclick = () => {
                console.log(this);//obj
            };
        }
    };
    obj.getName();
</script>
// 箭头函数 this 指向:箭头函数没有自己的 this,看其外层的是否有函数,如果有,外层函数的 this 就是内部箭头函数的 this,如果没有,则 this 是 window。

  • 默认绑定规则
    (1)全局指向window
    (2) 函数独立调用里的this指向的也是window
  • 隐式绑定规则(谁调用指向谁)
    (3) 对象里的调用this指向实例对象
    (每一个函数执行都会有一个自身的this,this指向可能相同,但本身是不一样的,由执行方式决定)
    (4)立即执行函数,this指向window(环境不同,this指向不同)
    箭头函数 > new > 显式 > 隐式 > 默认绑定
5.闭包的实现原理和作用,可以列举几个开发中闭包的实际应用
(1、闭包的概念:

指有权访问另一个函数作用域中的变量的函数,一般情况就是在一个函数中包含另一个函数。

(2、闭包的作用:
  • 访问函数内部变量、保持函数在环境中一直存在,不会被垃圾回收机制处理
  • 因为函数内部声明 的变量是局部的,只能在函数内部访问到,但是函数外部的变量是对函数内部可见的,这就是作用域链的特点了。
  • 子级可以向父级查找变量,逐级查找,找到为止
  • 因此我们可以在函数内部再创建一个函数,这样对内部的函数来说,外层函数的变量都是可见的,然后我们就可以访问到他的变量了。
( 3、闭包的优点:
  • 方便调用上下文中声明的局部变量
    逻辑紧密,可以在一个函数中再创建个函数,避免了传参的问题
(4、闭包的缺点:
  • 因为使用闭包,可以使函数在执行完后不被销毁,保留在内存中,如果大量使用闭包就会造成内存泄露,内存消耗很大
6.理解堆栈溢出和内存泄漏的原理,如何防止
循环引用

一个很简单的例子:一个DOM对象被一个Javascript对象引用,与此同时又引用同一个或其它的Javascript对象,
这个DOM对象可能会引发内存泄露。这个DOM对象的引用将不会在脚本停止的时候被垃圾回收器回收。要想破坏循环引用,
引用DOM元素的对象或DOM对象的引用需要被赋值为null。

闭包

在闭包中引入闭包外部的变量时,当闭包结束时此对象无法被垃圾回收(GC)。

DOM泄露

当原有的COM被移除时,子结点引用没有被移除则无法回收。

Timers计(定)时器泄露

定时器也是常见产生内存泄露的地方:

7.如何处理循环的异步操作

7.1异步操作保存变量(函数封装)
7.2异步操作顺序执行(await、Promise.then)
7.3异步操作同步执行(Promise.all、执行完后调用对应方法)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值