在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) | 拦截对目标对象属性的读取操作。 | - - - |
set(target, propKey, value, receiver) | 拦截对目标对象属性的设置操作。 | - - - - |
has(target, propKey) | 拦截对目标对象属性的存在性检查(使用in 操作符)。 | - - |
deleteProperty(target, propKey) | 拦截对目标对象属性的删除操作(使用delete 操作符)。 | - - |
apply(target, thisArg, argumentsList) | 拦截对目标函数的调用操作。 | - - - |
construct(target, argumentsList, newTarget) | 拦截使用new 操作符创建目标对象的实例的操作。 | - - - |
getOwnPropertyDescriptor(target, propKey) | 拦截对目标对象属性的Object.getOwnPropertyDescriptor 方法的调用。 | - - |
defineProperty(target, propKey, descriptor) | 拦截对目标对象属性的Object.defineProperty 方法的调用。 | - - - |
getPrototypeOf(target) | 拦截对目标对象的Object.getPrototypeOf 方法的调用。 | - target :目标对象。 |
setPrototypeOf(target, proto) | 拦截对目标对象的Object.setPrototypeOf 方法的调用。 | - - |
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中两种强大的特性,它们各自具有独特的功能和用途。闭包主要用于数据隐藏与封装、工厂函数以及模拟私有方法和变量等场景;而代理则更适用于增强对象功能、数据验证、日志记录以及访问控制等场景。在实际开发中,可以根据具体需求选择合适的特性来实现所需的功能。