Skip to content

作用域和闭包

环境和作用域

环境存在的价值是被需要,环境有其服务的范围,这个范围就是作用域

环境存在的前提是有人需要他,当不需要的时候,这个环境一定会被回收,被破坏掉,每个环境有其的作用域

js中,环境当中会有很多资源,有变量,有函数等等

js
let name = "jlc";   // 创建了一个全局的环境,在全局中定义的所有数据,都会被保留起来的

全局的环境是不会被回收的(因为全局环境会被很多地方依赖),除非将标签关闭和浏览器关闭,是人为的回收,否则全局环境是不会被回收的

全局的作用域不是在当前的平面中有效,在函数中也是有效的(波及范围更广)

函数的环境和作用域

当函数只是定义声明时,如果没有被调用,就不会开辟出内存空间(只是一个计划)

如果函数调用多次,函数会重新创建多个内存地址,每一次都是全新的内存地址

函数执行(调用)之后,会创建一个新的环境(给他一个新的内存地址,来存放函数定义的数据,这个数据默认情况下只在函数体内可以使用(后续有方法可以让它在函数体外面使用)),当函数执行完之后,在函数体内定义的变量就会被清理掉,后续再调用,会重新给你一个函数环境

js
function hd(){
    let n = 1;
    function sum(){
        console.log(++n);
    };
    sum();
}

// 如果函数调用多次,函数会重新创建多个内存地址,每一次都是全新的内存地址
hd();  // 2
hd();  // 2

如何将函数中的数据给外界一直使用?可以将函数中的内容(只能是引用类型,如函数)进行return返回出去,在用一个变量进行接收,数据只要有一个被使用(return出去的数据进行赋值表示被用),其函数环境就不会被清除

js
function hd(){
    let n = 1;
    return function sum(){
        console.log(++n);
    };
}

// 函数执行一次后,就会创建一个新的空间
let a = hd();  // 将return出来的内容赋值给a(只能返回引用类型),但是其函数环境不会被清除
a();  // 2
a();  // 3
let b = hd();
b();  // 2
js
function hd(){
    let n = 1;
    return function sum(){
        let m = 1;
        function show(){
            console.log(++m);
        }
        show()
    };
}

let a = hd();  
// 只是引用了函数中的sum(),但是sum()中的内存空间会被重新创建的,sum()里面的空间没有被外部引用
a();  // 2
a();  // 2
js
function hd(){
    let n = 1;
    return function sum(){
        let m = 1;
        return function show(){
            console.log(++m);
        };
    };
}

let a = hd()();  
a();  // 2
a();  // 3
构造函数中作用域的使用形态

在构造函数时环境的体现形式

js
function hd(){
    let n = 1;
    this.sum = function(){
        console.log(++n);
    };
}

// 上述函数可以理解为:
function hd(){
    let n = 1;
    function sum(){
        console.log(++n);
    };
    return {
        sum: sum
    };
}

let a = new hd();  // 实例化出一个对象
a.sum();  // 2   // 函数被使用了,同作用域下的数据就被保留
a.sum();  // 3

块作用域

js中新增了块级作用域,使用{}进行包裹,在全局中开辟一个块级的作用域

js
{
    let a = 1;  // const也有块级作用域
    // 在同个块级作用域可以使用a
}
// 在外界不能使用a

可以在不同的块作用域中用相同的名称命名,他们是不影响的,他们两个是不同的数据

注意:var声明变量是没有块级作用域的

js
{
    var a = 1;  // var没有块级作用域的概念,在{}中声明直接会放到全局中
    // 在同个块级作用域可以使用a
}
// 在外界也能使用a

letconstvarfor循环中的差异:

for循环是有块级作用域的特性的

js
for(var i = 1; i <= 3; i++){
    console.log(i);
}
console.log(i);
// 结果显示
1
2
3
4  // 最后一次i打印4
js
for(let i = 1; i <= 3; i++){
    console.log(i);
}
console.log(i);  // 访问不到,报错
// 结果显示
1
2
3

在绝大多数的情况下,使用for循环是不会进行改变全局的,所以一般都是使用let的情况

var虽然没有块级作用域,但是它是有函数作用域的

js
let arr = [];
for(let i = 1; i <= 3; i++){
    // console.log(i);   // 执行完之后,i是不进行保留的
    arr.push(function(){  // 将函数都放到了数组当中,那么该环境的数据都要进行保留
        return i;
    })
}
console.log(arr.length);  // 3
console.log(arr[0]);  // 返回的是一个函数
console.log(arr[0]());  // 执行函数,返回的是1

闭包

一个函数可以访问到其他函数作用域当中的数据,这个现象就是闭包,如子函数可以访问父级(上级)作用域的数据,js中存在闭包的特性

js
let arr = [1, 6, 11, 22, 8, 7];
function between(a, b){
    return function(v){  // 子函数可以访问父级的变量,会一级一级的往上找
        return v >= a && v <= b;
    };
}
console.log(arr.filter(between(3, 9)));

使用闭包的特性完成小动画,点击按钮使按钮水平向右移动:

html
<body>
    <style>
        button {
            position: absolute;  //设置按钮绝对定位
        }
    </style>
    <button>name</button>
    <button>age</button>
</body>

<script>
    let btns = document.querySelectorAll("button");  // 找到所有的按钮
    btns.forEach(function(item){
        item.addEventListener("click", function(){  // 给元素加上点击事件
            let left = 1;
            setInterval(function(){  // 涉及到闭包,向父级找item
                item.style.left = left++ + "px";
            },100);
        });
    });
</script>

上述情况在时间设置为100ms时,点击按钮后,再次点击该按钮,会出现抖动问题

因为执行该函数时,会产生一个环境,由于还有一个定时器,在该环境的内部还会产生一个函数环境,当我们再点击一次按钮时,会在产生一个新的环境,新环境中包括定时器函数环境,每次点击的时候left都变成了1,所以又从1这个位置向右偏移,这就形成了抖动

为了解决这种情况,可以将left变量放到外层函数的作用域中

html
<script>
    let btns = document.querySelectorAll("button");  // 找到所有的按钮
    btns.forEach(function(item){
        let left = 1;  // 放到外层的作用域
        item.addEventListener("click", function(){  // 给元素加上点击事件
            setInterval(function(){  // 涉及到闭包,向父级找item
                item.style.left = left++ + "px";
            },100);
        });
    });
</script>

上述改进可以使按钮动画不进行抖动了,但是会使按钮加速运动(之前抖动时该情况也存在)

因为当我们点击一次后,会产生一个新的环境,每个环境中包含了定时器的环境,点击十次后,定时器也会执行十次,这样原先的100ms执行一次,变成了10ms执行一次

html
<script>
    let btns = document.querySelectorAll("button");  // 找到所有的按钮
    btns.forEach(function(item){
        let left = 1;
        let interval = false;  
        item.addEventListener("click", function(){  // 给元素加上点击事件
            if(!interval){  // 只让定时器执行一次
                interval = true;
                setInterval(function(){  // 涉及到闭包,向父级找item
                    item.style.left = left++ + "px";
                },100);
            }
        });
    });
</script>

闭包的内存泄露

闭包可以给我们带来很多的便利,但是也会带来相应的问题,如内容泄露

点击<div>,弹出其属性值:

html
<body>
    <div desc="jlc">name</div>
    <div desc="24">age</div>
</body>
<script>
    let divs = document.querySelectorAll("div");
    divs.forEach(function(item){
        item.addEventListener("click", function(){
            console.log(item.getAttribute("desc")); // item表示所有的div对象
        });
    });
</script>

我们的目的仅仅是想获取div中的属性desc值,上述代码为了这个功能要将所有的div对象都保存在内存当中,会导致大量内存的浪费,可以通过以下的方式进行改进:

js
let divs = document.querySelectorAll("div");
divs.forEach(function(item){
    let desc = item.getAttribute("desc");
    item.addEventListener("click", function(){
        console.log(decs);  // 属性值能正常打印
        console.log(item);  // 对象的元素值就打印不了了,优化了内存
    });
    item = null;
});

this在闭包中的历史遗留问题

js
let hd = {
    name: "jlc",
    get: function(){
        // return this.name;  // 指向函数的对象hd
        return function(){  // 将结果用一个函数返回出去
            return this.user;  // 这个时候this指向的是window
            // 按理说,这是一个闭包,this应该可以访问父级的作用域,但是this特殊
        };
    }
};

// this是指当前调用函数的对象
let a = hd.get();  // 在全局中调用
console.log(a());  // 显示undefined

解决这个问题,我们可以通过两种方法:

js
// 1.this赋值
let hd = {
    name: "jlc",
    get: function(){
        let This = this;
        return function(){
            return This.user;
        };
    }
};

// 2.使用箭头函数
let hd = {
    name: "jlc",
    get: function(){
        return ()=>{
            return this.user;
        };
    }
};

let a = hd.get();  
console.log(a());   // jlc

Released under the MIT License.