MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript闭包与this的深度理解

2024-08-254.0k 阅读

JavaScript闭包的深度理解

闭包的基本概念

在JavaScript中,闭包是一个函数以及其周围状态(词法环境)的引用捆绑在一起形成的实体。简单来说,闭包使得函数可以访问并操作其外部作用域的变量,即使在外部作用域已经执行完毕之后。

来看一个简单的示例:

function outerFunction() {
    let outerVariable = '我是外部变量';
    function innerFunction() {
        console.log(outerVariable);
    }
    return innerFunction;
}

let closure = outerFunction();
closure(); 

在上述代码中,outerFunction返回了innerFunction。当outerFunction执行完毕后,通常情况下其内部的变量outerVariable应该随着函数执行栈的销毁而被垃圾回收。但由于innerFunction形成了闭包,它持有对outerFunction作用域的引用,所以outerVariable不会被回收,并且innerFunction可以在之后访问并使用outerVariable

闭包的形成条件

  1. 函数嵌套:内部函数必须定义在外部函数内部,这是闭包形成的基础结构。例如:
function parent() {
    function child() {
        // 代码逻辑
    }
    return child;
}
  1. 内部函数访问外部函数的变量:内部函数要使用外部函数作用域中的变量,如前面例子中innerFunction访问outerVariable。如果内部函数不访问外部函数的变量,虽然存在函数嵌套结构,但严格意义上不构成闭包,因为没有对外部作用域变量的“特殊保留”需求。

闭包的作用

  1. 数据封装与隐私保护:通过闭包,可以将一些变量和函数封装在一个特定的作用域内,对外界隐藏。只有通过闭包返回的特定函数才能访问这些“私有”数据。
function counter() {
    let count = 0;
    function increment() {
        count++;
        return count;
    }
    return increment;
}

let myCounter = counter();
console.log(myCounter()); 
console.log(myCounter()); 
// 这里无法直接访问count变量,实现了一定的数据封装
  1. 实现模块模式:在JavaScript中,模块模式是一种常用的设计模式,闭包是实现模块模式的关键。模块模式允许开发者将相关的代码和数据封装在一个对象中,通过返回对象的方式暴露特定的接口,同时隐藏内部实现细节。
let myModule = (function () {
    let privateVariable = '私有变量';
    function privateFunction() {
        console.log('这是私有函数');
    }
    return {
        publicFunction: function () {
            privateFunction();
            console.log(privateVariable);
        }
    };
})();

myModule.publicFunction(); 

在上述代码中,privateVariableprivateFunction在外部无法直接访问,只能通过publicFunction间接访问,实现了模块的封装和接口暴露。

  1. 函数柯里化:柯里化是一种将多参数函数转换为一系列单参数函数的技术,闭包在其中起到关键作用。通过柯里化,可以逐步传递参数,延迟函数的执行,提高函数的灵活性和复用性。
function add(a) {
    return function (b) {
        return function (c) {
            return a + b + c;
        };
    };
}

let add5 = add(5);
let add5And10 = add5(10);
let result = add5And10(15);
console.log(result); 

在这个例子中,add函数返回了一个内部函数,这个内部函数又返回了另一个内部函数。通过逐步传递参数,实现了柯里化。

闭包与内存管理

闭包在带来强大功能的同时,也可能会导致内存泄漏问题。由于闭包会持有对外部作用域的引用,使得外部作用域中的变量无法被垃圾回收机制回收。如果不当使用闭包,可能会导致大量无用的变量占据内存空间。

function createClosure() {
    let largeData = new Array(1000000).fill(1); 
    function inner() {
        console.log('内部函数');
    }
    return inner;
}

let closure = createClosure();
// 这里即使createClosure执行完毕,largeData由于被闭包引用,不会被回收

在实际开发中,要注意及时解除闭包对不必要变量的引用,以避免内存泄漏。例如,当不再需要使用闭包时,可以将其赋值为null,让垃圾回收机制可以回收相关内存。

let closure = createClosure();
closure();
closure = null; 

JavaScript中this的深度理解

this的绑定规则

  1. 默认绑定:当函数独立调用(不是作为对象的方法调用)时,this指向全局对象。在浏览器环境中,全局对象是window;在Node.js环境中,全局对象是global
function defaultBinding() {
    console.log(this);
}

defaultBinding(); 

在浏览器中运行上述代码,会打印出window对象。但需要注意的是,在严格模式下,默认绑定的this会是undefined

function strictDefaultBinding() {
    'use strict';
    console.log(this);
}

strictDefaultBinding(); 
  1. 隐式绑定:当函数作为对象的方法被调用时,this指向该对象。
let person = {
    name: '张三',
    sayName: function () {
        console.log(this.name);
    }
};

person.sayName(); 

在这个例子中,sayName函数作为person对象的方法被调用,所以this指向person对象,会打印出张三

  1. 显式绑定:通过callapplybind方法可以显式地指定函数调用时this的指向。
    • call方法call方法接受多个参数,第一个参数是要绑定的this值,后面的参数是函数执行时需要的参数。
function greet(message) {
    console.log(this.name + ','+ message);
}

let person1 = { name: '李四' };
let person2 = { name: '王五' };

greet.call(person1, '你好'); 
greet.call(person2, '早上好'); 
- **apply方法**:`apply`方法与`call`方法类似,不同之处在于它接受一个数组作为参数列表。
function sum(a, b) {
    return a + b;
}

let numbers = [3, 5];
let resultSum = sum.apply(null, numbers); 
console.log(resultSum); 

这里sum.apply(null, numbers)numbers数组展开作为参数传递给sum函数,null表示this指向全局对象(在非严格模式下)。

- **bind方法**:`bind`方法会创建一个新的函数,新函数的`this`被绑定到指定的值,并且可以预先传递部分参数。
function multiply(a, b) {
    return a * b;
}

let double = multiply.bind(null, 2);
let resultMultiply = double(5); 
console.log(resultMultiply); 

在这个例子中,multiply.bind(null, 2)创建了一个新函数doublethis被绑定为null(在非严格模式下指向全局对象),并且预先传递了参数2

  1. new绑定:当使用new关键字调用函数时,会创建一个新的对象,并且函数中的this会指向这个新创建的对象。
function Person(name) {
    this.name = name;
    this.sayHello = function () {
        console.log('你好,我是'+ this.name);
    };
}

let newPerson = new Person('赵六');
newPerson.sayHello(); 

在上述代码中,通过new Person('赵六')创建了一个新的Person实例,Person函数中的this指向这个新实例,从而可以为实例添加属性和方法。

this绑定的优先级

new绑定 > 显式绑定(callapplybind) > 隐式绑定 > 默认绑定。

function testThis() {
    console.log(this);
}

let obj = {
    method: testThis
};

let newFunc = testThis.bind({ name: '绑定对象' });
let newObj = new newFunc(); 
// new绑定优先级最高,这里this指向newObj

obj.method(); 
// 隐式绑定,this指向obj

testThis(); 
// 默认绑定,在非严格模式下this指向全局对象

箭头函数中的this

箭头函数与普通函数在this绑定上有很大的区别。箭头函数没有自己的this,它的this继承自外层作用域(词法作用域)。

let person = {
    name: '小明',
    arrowFunction: () => {
        console.log(this.name);
    },
    normalFunction: function () {
        console.log(this.name);
    }
};

person.normalFunction(); 
person.arrowFunction(); 

在上述代码中,normalFunction作为person对象的方法,this指向person对象,所以能正确打印出小明。而arrowFunction是箭头函数,它的this继承自外层作用域(这里是全局作用域,在浏览器环境中this指向window,而window没有name属性,所以会打印undefined)。

再看一个更复杂的例子:

function outer() {
    let self = this;
    let innerArrow = () => {
        console.log(this === self);
    };
    let innerFunction = function () {
        console.log(this === self);
    };

    innerArrow(); 
    innerFunction(); 
}

outer.call({ name: '外层对象' }); 

在这个例子中,outer函数通过call方法将this绑定到{ name: '外层对象' }innerArrow箭头函数的this继承自outer函数的作用域,所以this === selftrue。而innerFunction是普通函数,它在调用时this指向全局对象(因为是独立调用,这里未在严格模式下),所以this === selffalse

解决箭头函数this绑定问题的方法

如果需要在箭头函数中访问对象的this,可以通过以下几种方法:

  1. 使用selfthat变量:在外部作用域定义一个变量来保存this,然后在箭头函数中使用该变量。
let person = {
    name: '小红',
    init: function () {
        let self = this;
        document.addEventListener('click', () => {
            console.log(self.name);
        });
    }
};

person.init(); 
  1. 使用bind方法:虽然箭头函数本身不能使用bind来改变this绑定,但可以在定义箭头函数时,通过bind将外层作用域的this绑定到箭头函数内部。
let person = {
    name: '小刚',
    init: function () {
        document.addEventListener('click', (function () {
            console.log(this.name);
        }).bind(this));
    }
};

person.init(); 

这里通过bind(this)init函数中的this绑定到内部函数,从而在事件处理函数中正确访问person对象的name属性。

闭包与this的结合应用

  1. 在闭包中访问正确的this:当闭包与this结合时,需要注意this的正确指向。
function outer() {
    let self = this;
    function inner() {
        console.log(self === this);
    }
    return inner;
}

let closure = outer.call({ name: '闭包外部对象' });
closure(); 

在这个例子中,通过在outer函数中保存thisself,在inner函数(闭包)中可以正确访问到outer函数调用时的this

  1. 箭头函数闭包中的this:箭头函数闭包同样遵循其this继承规则。
let obj = {
    name: '对象',
    createClosure: function () {
        return () => {
            console.log(this.name);
        };
    }
};

let closure = obj.createClosure();
closure(); 

这里箭头函数闭包的this继承自createClosure函数的作用域,所以能正确打印出对象

  1. 实际应用场景:在事件处理和回调函数中,闭包与this的正确结合非常重要。
let button = document.getElementById('myButton');
let app = {
    message: '按钮被点击',
    init: function () {
        button.addEventListener('click', () => {
            console.log(this.message);
        });
    }
};

app.init(); 

在上述代码中,通过使用箭头函数作为事件处理函数,确保了this指向app对象,从而可以正确访问message属性。如果使用普通函数作为事件处理函数,由于普通函数在这种情况下this指向button元素(隐式绑定到事件目标),就无法正确访问app.message

通过深入理解JavaScript的闭包和this,开发者可以编写出更健壮、灵活且符合预期的代码,避免因作用域和this绑定问题导致的各种错误。在实际项目开发中,无论是前端的页面交互逻辑,还是后端的Node.js应用开发,对闭包和this的准确把握都是至关重要的技能。