Bootstrap

JS宏进阶:闭包与代理

在JavaScript中,闭包和代理是两种重要的概念,它们各自具有独特的功能和用途。闭包,它指的是一个函数能够访问并操作其父函数作用域中的变量,即使父函数已经执行完毕。它允许内部函数访问外部函数的变量,从而提供了强大的功能。而代理(Proxy)则是另一种高级功能,它允许开发者定义基本操作的自定义行为,如属性查找、赋值、枚举、函数调用等。

一、闭包

1、定义

闭包是一个函数及其相关的引用环境的组合。在JavaScript中,当一个函数在另一个函数的内部定义,并且这个内部函数引用了外部函数的变量时,就形成了一个闭包。即使外部函数已经执行完毕,内部函数仍然可以访问外部函数的变量。

2、工作原理

2.1、词法作用域

词法作用域(也称为静态作用域),JavaScript采用这种方式就意味着函数的作用域在函数定义时就已经确定,而不是在函数执行时确定。

当一个函数在另一个函数内部定义时,内部函数会捕获外部函数的词法作用域,包括外部函数的参数和变量。这样,就实现访问外部函数变量或参数的目的。

2.2、执行上下文和作用于链

当一个函数被调用时,JavaScript引擎会为其创建一个执行上下文,并构建作用域链。作用域链是一个对象列表,用于解析函数中的变量。而内部函数的作用域链包含了外部函数的作用域,因此内部函数可以访问外部函数的变量。

2.3、闭包的持久化

由于内部函数持有了对外部函数作用域的引用,即使外部函数执行完毕,这些作用域也不会被销毁。只要内部函数存在,外部函数的变量就会保留在内存中,从而实现“持久化”。

3、闭包使用实例

function createCounter(){
    let count = 0;
    return function() {
        count++;
        return count;
    };
}

let counter = createCounter();
console.log(counter()); // 输出: 1
console.log(counter()); // 输出: 2

上述示例代码是在本专栏第一张中的闭包函数示例,当时主要介绍函数,并没有深入分析他的原理。现在我们理解了闭包的定义,相信你能很轻松的解释其中原理:

createCounter是一个外部函数,它定义了一个局部变量count,随后,内部返回一个匿名函数,在这个函数中,访问并更改了外部函数的变量count,随后将其作为返回值返回给用户。当createCounter()执行后就会将其赋值给counter,由于 createCounter的返回值是一个函数,所以counter也是一个函数,而且该函数的作用于在createCounter里面,所以每次调用counter时,createCounter里面的变量count都会被修改(这里是递增)。

4、闭包的应用场景

4.1、数据的封装和隐藏
function Counter() {
    var count = 0; // 私有变量

    function increment() {
        count++;
        console.log(count);
    }

    return {
        increase: increment, // 返回的对象通过increment方法访问count,形成闭包
    };
}

var counter = new Counter();
counter.increase(); // 输出: 1

闭包可以用来模拟类的私有属性和方法。通过在构造函数中定义并返回一个对象,该对象的方法利用闭包访问构造函数内部的私有变量。

4.2、回调函数和事件处理器

JS中,回调函数和事件处理器经常需要访问外部函数的变量。闭包使得这些函数即使在异步执行时也能访问外部变量。例如,new XMLHttpRequest 对象中,有一个事件函数onreadystatechange函数,在使用它时就有可能利用到闭包:

var xhr = new XMLHttpRequest();
var count = 0;

xhr.open('GET', 'https://xxx.xxx.com/api', true);

xhr.onreadystatechange = function() {
    if (xhr.readyState === 4 && xhr.status === 200) {
        count++; // 访问外部变量
        var innerVariable = "Response received";
        console.log(innerVariable);
    }
};

xhr.send();

再如,在数组的forEach、map等高级函数中利用闭包

const numbers = [1, 2, 3, 4, 5];

function createCounter() {
    let count = 0; // 私有变量
    return function() {
        count++;
        return count;
    };
}

const counter = createCounter();

numbers.forEach(function(number) {
    console.log(`Number: ${number}, Count: ${counter()}`);
});
4.3、数据缓存

利用闭包存储先前计算的结果,避免重复计算,提高性能。

function expensiveCalculation(value) {
    let cache = {};

    return function cachedCalculation(newValue) {
        if (newValue in cache) {
            return cache[newValue];
        } else {
            let result = performExpensiveOperation(newValue); // 假设这是一个耗时的操作
            cache[newValue] = result;
            return result;
        }
    };
}

let memoizedCalc = expensiveCalculation();
memoizedCalc(10); // 第一次计算
memoizedCalc(10); // 第二次从缓存中获取结果

二、代理

1、定义

JS中代理是ES6(ECMAScript 2015)中引入的一个新特性。它提供了一种机制,通过该机制可以拦截和修改对目标对象的访问和操作。

2、工作原理

代理对象充当了一个“中间人”的角色,它拦截了对目标对象的操作,并在操作执行前后添加自定义的行为。代理对象通过构造函数Proxy来创建,该构造函数接受两个参数:目标对象和一个处理器对象(称为handler)。

3、代理对象简介

3.1、创建语法

let proxy = new Proxy(target, handler);

target:被代理的目标对象。

handler:定义代理行为的对象。处理器对象中可以定义各种陷阱函数(trap),用于拦截对目标对象的操作。

3.2、处理器对象handler介绍

处理器对象中可以定义多种陷阱函数,每种陷阱函数对应一种基本操作。如下表所示(标红是常用的):

陷阱函数描述参数
get(target, propKey, receiver)拦截对目标对象属性的读取操作。

target:目标对象。

propKey:要读取的属性名。

receiver:最初接收调用的对象(通常是Proxy实例本身)。

set(target, propKey, value, receiver)拦截对目标对象属性的设置操作。

target:目标对象。

propKey:要设置的属性名。

value:要设置的新属性值。

receiver:最初接收调用的对象(通常是Proxy实例本身)。

has(target, propKey)拦截对目标对象属性的存在性检查(使用in操作符)。

target:目标对象。

propKey:要检查的属性名。

deleteProperty(target, propKey)拦截对目标对象属性的删除操作(使用delete操作符)。

target:目标对象。

propKey:要删除的属性名。

apply(target, thisArg, argumentsList)拦截对目标函数的调用操作。

target:目标函数。

thisArg:函数调用时使用的this值。

argumentsList:函数调用时传递的参数列表(类数组对象)。

construct(target, argumentsList, newTarget)拦截使用new操作符创建目标对象的实例的操作。

target:目标构造函数。

argumentsList:调用new时传递的参数列表(类数组对象)。

newTarget:最初接收调用的构造函数(通常是Proxy实例本身)。

getOwnPropertyDescriptor(target, propKey)拦截对目标对象属性的Object.getOwnPropertyDescriptor方法的调用。

target:目标对象。

propKey:要获取描述符的属性名。

defineProperty(target, propKey, descriptor)拦截对目标对象属性的Object.defineProperty方法的调用。

target:目标对象。

propKey:要定义的属性名。

descriptor:属性描述符对象。

getPrototypeOf(target)拦截对目标对象的Object.getPrototypeOf方法的调用。target:目标对象。
setPrototypeOf(target, proto)拦截对目标对象的Object.setPrototypeOf方法的调用。

target:目标对象。

proto:要设置的新原型。

isExtensible(target)拦截对目标对象的Object.isExtensible方法的调用。target:目标对象。
preventExtensions(target)拦截对目标对象的Object.preventExtensions方法的调用。target:目标对象。
getOwnPropertyNames(target)拦截对目标对象的Object.getOwnPropertyNames方法的调用。target:目标对象。
getOwnPropertySymbols(target)拦截对目标对象的Object.getOwnPropertySymbols方法的调用。target:目标对象。

4、代理的应用场景

数据验证:可以拦截赋值操作,并在赋值前检查值的类型和范围等。确保数据的准确性

懒加载:也就是延迟加载,当需要访问某个属性时才加载该属性的值。这可以减少初始加载时间,提高应用性能。

对象封装:它可以提供统一的访问接口,隐藏对象的内部实现细节。例如,可以拦截对目标对象的所有操作,并在操作执行前后添加日志记录或权限检查。

日志:用于记录对对象的所有操作,以便进行调试和监控。通过拦截和记录对对象的各种操作,可以方便地跟踪对象的状态变化。

数据绑定:代理可以用于实现数据绑定。当数据对象的属性发生变化时,可以自动更新到界面上,提高开发效率和代码的可维护性。

权限控制:用于控制对对象的访问权限。例如,可以拦截对目标对象的属性读取操作,并根据用户的权限决定是否允许读取。(在JS宏中没啥用,因为允不允许访问,数据始终在表格里面,安全性不高)

5、示例代码

let target = {
    name: 'Alice',
    age: 25
};

let handler = {
    get(target, prop, receiver) {
        console.log(`Reading ${prop}`);
        return Reflect.get(target, prop, receiver);
    },
    set(target, prop, value, receiver) {
        console.log(`Writing ${prop}`);
        return Reflect.set(target, prop, value, receiver);
    }
};

let proxy = new Proxy(target, handler);

console.log(proxy.name); // 输出: Reading name, Alice
proxy.age = 30; // 输出: Writing age
console.log(proxy.age); // 输出: Reading age, 30

上述是一个简单的代理示例,展示了如何拦截和修改对目标对象的属性读取和赋值操作。

三、闭包与代理的比较

闭包代理
定义能够访问另一个函数作用域中的变量的函数拦截对象上的操作,并在它们被执行之前或之后执行自定义代码的对象
特点函数嵌套函数,访问外部变量,变量持久化拦截对象操作,自定义行为
优点封装数据,避免全局变量污染,实现私有方法和变量增强对象功能,数据验证,日志记录
缺点内存消耗,性能问题性能开销
应用场景数据隐藏与封装,工厂函数,模拟私有方法和变量数据验证与格式化,访问控制,事件监听

四、总结

闭包和代理是JavaScript中两种强大的特性,它们各自具有独特的功能和用途。闭包主要用于数据隐藏与封装、工厂函数以及模拟私有方法和变量等场景;而代理则更适用于增强对象功能、数据验证、日志记录以及访问控制等场景。在实际开发中,可以根据具体需求选择合适的特性来实现所需的功能。

;