使用闭包和工厂函数模拟 JavaScript 中的“私有”变量
什么是私有变量?
闭包
工厂函数
使用闭包作为私有变量
编程范式的奇怪组合
尽管JavaScript最近实现了类,但从未有过原生的方式来控制对象属性的可见性。具体来说,从未有过真正将变量变为私有变量的方法。目前,变通方法是我们最好的选择。最常见的变通方法之一是使用下划线符号。这只是在变量名前添加下划线 ( ) 的惯例_
。这样做是为了表明变量是私有的,不应被随意修改。例如,存储敏感信息(例如密码)的“私有”变量将被_password
明确命名以表明它是“私有的”。但是,仍然可以通过编写 来访问和修改它someObj._password
。它就像任何其他可以更改的对象属性一样。下划线仅仅是添加到某个标识符前面的符号。坦率地说,前缀下划线只是出于惯例,对那些可能想访问和修改“私有”变量的人进行非强制性的威慑。
什么是私有变量?
在许多面向对象编程语言中,都有一种方法可以限制变量在其作用域之外的可见性。换句话说,有些编程语言允许变量只能由“拥有”它的对象访问。更专业地说,私有变量仅对当前类可见。它无法在全局作用域或其任何子类中访问。例如,我们可以在 Java(以及大多数其他编程语言)中通过private
在声明变量时使用关键字来实现这一点。尝试在拥有它的类之外访问私有变量将引发错误。
// Example Class
class Example {
// hiddenVariable CAN only be accessed here
private String hiddenVariable;
public Example(String websiteName) {
hiddenVariable = websiteName;
}
}
// Main Method
public class Main {
public static void main(String[] args) {
// Instantiate class
Example website = new Example("DEV.to");
// This will throw an error
// error: hiddenVariable has private access in Example
System.out.println(website.hiddenVariable);
}
}
将变量设置为私有的原因有很多,从安全性到封装性。在这种情况下,私有变量只能通过传统的getter 和 setter 方法间接访问和操作。
闭包
在 JavaScript 中,当函数执行完毕后,其内部声明的任何变量都会被“垃圾回收”。换句话说,它会从内存中删除。这就是为什么 JavaScript 中存在局部变量,也正是因为如此,函数内部的变量才无法被外部访问。
// dev is NOT accessible here
function someFunc() {
// dev is accessible here
const dev = 'to';
}
// dev is NOT accessible here
当函数内部的某些内容依赖于被删除的变量时,会发生特殊异常。例如,下面的函数返回另一个依赖于父函数变量的函数。
// Parent function
function parent() {
// Local variable of the parent function
const prefix = 'I am a ';
// Child function
return function(noun) {
// The child function depends on the variables of the parent function.
return prefix + noun;
};
}
注意:上面的示例利用了函数式编程中一个叫做currying的概念。如果你感兴趣的话,可以阅读更多相关内容。
// Store the returned child function
const getSentence = parent();
// At this point, `parent()` has finished executing.
// Despite that, the `prefix` variable is still
// accessible to the child function. More on that later.
const job = getSentence('programmer');
// What is the value of `job`?
console.log(job); // 'I am a programmer'
在这种情况下,prefix
即使子函数已被垃圾回收,它仍然可以被子函数使用,因为子函数创建了自己的闭包。闭包就像函数执行时所处环境的“快照”。它的闭包是它自身环境的内部副本。
从技术角度来说,闭包中的任何变量都只能由拥有它的子函数访问。只有当前执行上下文引用了该闭包,才能对这些变量执行操作。在这种情况下,子函数拥有的“快照”就是对该闭包的引用,因此它可以访问其变量。
当parent
函数执行完毕后,该prefix
变量将被删除。然而,在此之前,子函数会对其当前环境(包含parent
其依赖的函数的所有变量)进行“快照”。子函数现在拥有了该变量的副本prefix
,可以访问和操作。这就是闭包最基本的用法。MDN 提供了一个更专业的定义。
工厂函数
工厂函数是任何返回对象的函数。没错,就是这样。不要将其与类和构造函数混淆。类和构造函数需要new
关键字来实例化对象,而工厂函数则返回实例化的对象本身。
function factory(name) {
return { name };
}
const obj = factory('Some Dood');
console.log(obj.name); // 'Some Dood'
使用闭包作为私有变量
现在,我们已经掌握了在 JavaScript 中模拟“私有”变量所需的所有知识。我们可以先编写一个工厂函数,返回一个包含 getter 和 setter 方法的对象。该工厂函数接受两个参数,分别对应于返回对象的“私有”属性。
function createAnimal(name, job) {
// "Private" variables here
let _name = name;
let _job = job;
// Public variables here
return {
// Getter Methods
getName() {
return _name;
},
getJob() {
return _job;
},
// Setter Methods
setName(newName) {
_name = newName;
},
setJob(newJob) {
_job = newJob;
}
};
}
然后,我们可以调用工厂函数来创建动物对象的新实例。需要注意的是,每次调用工厂函数时,都会创建一个新的闭包。因此,每个返回的对象都可以访问它自己的闭包。
const presto = createAnimal('Presto', 'Digger');
const fluffykins = createAnimal('Fluffykins', 'Jumper');
那么我们这样做得到了什么呢?嗯,借助闭包的力量,我们本质上模拟了 JavaScript 中的“私有”变量。
// These properties will be inaccessible
console.log(presto._name); // undefined
console.log(presto._job); // undefined
console.log(fluffykins._name); // undefined
console.log(fluffykins._job); // undefined
// Getter methods have access to the closure
console.log(presto.getName()); // 'Presto'
console.log(presto.getJob()); // 'Digger'
console.log(fluffykins.getName()); // 'Fluffykins'
console.log(fluffykins.getJob()); // 'Jumper'
// Setter methods can mutate the variables in the closure
presto.setName('Quick');
presto.setJob('Bone Finder');
fluffykins.setName('Mittens');
fluffykins.setJob('Fish Eater');
console.log(presto.getName()); // 'Quick'
console.log(presto.getJob()); // 'Bone Finder'
console.log(fluffykins.getName()); // 'Mittens'
console.log(fluffykins.getJob()); // 'Fish Eater'
编程范式的奇怪组合
这种变通方法确实有点奇怪,但它却实现了面向对象语言中一个看似简单的特性。但如果仔细分析,就会发现它其实很有妙处。首先,它巧妙地将两种截然不同且相互冲突的编程范式——面向对象编程和函数式编程——结合在一起。
这种方法的面向对象特性涉及工厂函数、可变性和封装的使用。另一方面,函数式方法则涉及闭包的使用。JavaScript 确实是一种多范式语言,它不断模糊不同范式之间的界限。
有人可能会说,把这两种范式粘在一起既混乱又奇怪。但在我看来,这种说法并不完全正确。即使范式的融合不符合惯例和设计模式,但我发现,为了在 JavaScript 中实现面向对象特性,必须使用函数式编程的特性,这一点非常令人着迷。这两种相互矛盾的范式和谐共存,就像阴阳一样。尽管它们之间存在差异,但总有办法让事情奏效。或许,这可以作为人生的一个比喻?
链接地址:https://dev.to/somedood/emulating-private-variables-in-javascript-with-closures-and-factory-functions-2314