JavaScript——闭包

83 阅读7分钟

在理解闭包之前需要理解相关几个知识:作用域和作用域链执行上下文本垃圾回收机制

定义

闭包是指引用了其他函数作用域中变量的函数,参数和变量不会被垃圾回收机制收回,通常是在嵌套函数中实现的。

从技术的角度讲,所有的JavaScript函数都是闭包。

从实践角度来看,满足以下两个条件的函数算闭包

  • 即使创建它的上下文被销毁了,它依然存在。(比如从父函数中返回)
  • 在代码中引用了自由变量

自由变量: 在函数中使用的既不是函数参数也不是函数局部变量的变量称作自由变量

==========================下面是详细解释=============================

MDN对闭包的定义为:闭包是指那些能够访问自由变量的函数。 由MDN对闭包的定义,我们很容易得出:闭包 = 函数 + 函数能访问的自由变量;

let a = 1;
function fun() {
    console.log(a);
}
fun();

上面代码中fun函数能访问a变量,但a既不是fun的局部变量,也不是fun的参数,那a就是fun的自由变量;那么fun+fun能访问的自由变量a就形成了一个闭包,所以说,从技术的角度讲,所有的JavaScript函数都是闭包。 但MDN定义的闭包与我们平时使用的闭包是不一样的,ECMAScript中,闭包是指:

从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。

从实践角度:以下函数才算是闭包:

  • 即使创建它的上下文被销毁了,它依然存在。(比如从父函数中返回)
  • 在代码中引用了自由变量

本文只从实践角度讲解闭包。

分析

function fun2() {
    let x = 1;
    let y = 2;
    return function () {
        return x + y;
    }
}
let fun3 = fun2();
console.log(fun3());

image.png

如上图,函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只在函数执行期间存在。在上面代码中定义fun2()函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链。接着会创建函数的活动对象(用作变量对象)并将其推入作用域链的前端。

作用域链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。

函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域,而闭包是不一样的,在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中

因此,当匿名函数执行时,fun2的执行上下文虽然已销毁,但被内部函数(匿名函数)引用x变量和y变量,也就是说匿名函数的作用域链中实际上包含 fun2()的活动对象,内部函数的作用域链包含fun2()函数的作用域;在fun2()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁。

闭包中的this对象

如果内部函数没有使用箭头函数定义,则 this 对象会在运行时绑定到执行函数的上下文。

如果在全局函数中调用,则 this 在非严格模式下等于 window,在严格模式下等于 undefined。

如果作为某个对象的方法调用,则 this 等于这个对象。匿名函数在这种情况下不会绑定到某个对象,则 this 在非严格模式下等于 window,在严格模式下等于 undefined。

window.name = 'window';
let obj = {
    name: 'obj',
    getName(){
        console.log(this.name)//obj
        return function () {
            return this.name;
        }
    }
};
console.log(obj.getName()());//window

每个函数在被调用时都会自动创建两个特殊变量:this 和 arguments。内部函数永远不可能直接访问外部函数的这两个变量,上面例子中调用obj对象的getName()方法,返回一个匿名函数,因为返回的是一个函数,所以obj.getName()()会立即调用返回的这个函数,又因为返回的匿名函数中this未指向obj,且在非严格模式下的全局函数中调用,故this指向window,所已最终打印出‘windom’。

若想this指向obj对象,可把把 this 保存到闭包可以访问的另一个变量中,如下所示:

window.name = 'window';
let obj = {
    name: 'obj',
    getName(){
        let that = this;
        return function () {
            return that.name;
        }
    }
};
console.log(obj.getName()());//obj

在一些特殊情况下,this 值可能并不是我们所期待的值。比如下面这个修改后的例子

window.name = 'window';
let obj = {
   name: 'obj',
   getName(){
       return this.name;
   }
};
console.log(obj.getName())//obj
console.log((obj.getName)())//obj
console.log((obj.getName = obj.getName)());//window

第一种调用 obj.getName()是正常调用,会返回"obj",

第二种调用时baobj.getName()放在立即执行表达式中,相当于obj.getName()的正常调用,,返回"obj"

第三种调用执行了一次赋值,然后再调用赋值后的结果。因为赋值表达式的值是函数本身,this 值不再与任何对象绑定,所以返回的是"window"。

闭包的优缺点

闭包会保留它们包含函数的作用域,所以比其他函数更占用内存。过度使用闭包可能导致内存过度占用,造成内存泄漏,需要手动回收变量(置空null)

延长变量的生命周期,一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的。

可以定义私有变量和特权方法,以隐藏不能被直接修改的数据,如下:

可以访问私有变量的公共方法叫作特权方法

function Person(name) {
    this.getName = function() {
        return name;
    };
    this.setName = function (value) {
        name = value;
    };
}
let person = new Person('小明');
console.log(person.getName()); // '小明'
person.setName('小红');
console.log(person.getName()); // '小红'

这段代码中的构造函数定义了两个特权方法:getName()和 setName()。每个方法都可以在构造函数外部调用,并通过它们来读写私有的 name 变量。在 Person 构造函数外部,没有别的办法访问 name。因为两个方法都定义在构造函数内部,所以它们都是能够通过作用域链访问 name 的闭包。私有变量name 对每个 Person 实例而言都是独一无二的,因为每次调用构造函数都会重新创建一套变量和方法。不过这样也有个问题:必须通过构造函数来实现这种隔离

应用场景

定时器、柯里化函数,回调函数、延迟调用、防抖节流

防抖实现

定义:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

/**
 * 模拟ajax请求
 * @param data
 */
function ajax(data) {
    console.log(data);
}

/**
 *
 * @param fun 要执行的函数
 * @param delay  触发时长
 * @returns {Function}
 */
function bounce1(fun,delay) {
    return function (args) {
        let that = this;
        let _args = args;
        clearTimeout(fun.id);
        fun.id = setTimeout(function () {
            fun.call(that,_args);
        },delay)
    }
}
let input = document.getElementById('myInput');
let bounce = bounce1(ajax,500);
input.addEventListener('keyup',function (e) {
    bounce(e.target.value);
})

节流实现

定义:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。

/**
 * 模拟ajax请求
 * @param data
 */
function ajax(data) {
    console.log(data);
}

/**
 *
 * @param fun 要执行的函数
 * @param delay  触发时长
 * @returns {Function}
 */
function bounce2(fun,delay) {
    let time = 0;
    let timer = null;
    return function (args) {
        let that = this;
        let _args = args;
        let now = new Date().getTime();
        if(time && now - time < delay){
            if (timer) clearTimeout(timer);
            timer = setTimeout(function () {
                time = now;
                fun.call(this,_args);
            },delay)
        }else {
            time = now;
            fun.call(that,_args)
            
        }
    }
}
let input = document.getElementById('myInput');
let bounce = bounce2(ajax,500);
input.addEventListener('keyup',function (e) {
    bounce(e.target.value);
})