使用闭包和工厂函数模拟 JavaScript 中的“私有”变量 什么是私有变量? 闭包 工厂函数 使用闭包来访问私有变量 编程范式的奇特组合

2025-06-10

使用闭包和工厂函数模拟 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

将变量设置为私有的原因有很多,从安全性到封装性。在这种情况下,私有变量只能通过传统的getter 和 setter 方法间接访问和操作。

闭包

在 JavaScript 中,当函数执行完毕后,其内部声明的任何变量都会被“垃圾回收”。换句话说,它会从内存中删除。这就是为什么 JavaScript 中存在局部变量,也正是因为如此,函数内部的变量才无法被外部访问。

// dev is NOT accessible here
function someFunc() {
  // dev is accessible here
  const dev = 'to';
}
// dev is NOT accessible here
Enter fullscreen mode Exit fullscreen mode

当函数内部的某些内容依赖于被删除的变量时,会发生特殊异常。例如,下面的函数返回另一个依赖于父函数变量的函数。

// 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;
  };
}
Enter fullscreen mode Exit fullscreen mode

注意:上面的示例利用了函数式编程中一个叫做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'
Enter fullscreen mode Exit fullscreen mode

在这种情况下,prefix即使子函数已被垃圾回收,它仍然可以被子函数使用,因为子函数创建了自己的闭包。闭包就像函数执行时所处环境的“快照”。它的闭包是它自身环境的内部副本。

从技术角度来说,闭包中的任何变量都只能由拥有它的子函数访问。只有当前执行上下文引用了该闭包,才能对这些变量执行操作。在这种情况下,子函数拥有的“快照”就是对该闭包的引用,因此它可以访问其变量。

parent函数执行完毕后,该prefix变量将被删除。然而,在此之前,子函数会对其当​​前环境(包含parent其依赖的函数的所有变量)进行“快照”。子函数现在拥有了该变量的副本prefix,可以访问和操作。这就是闭包最基本的用法。MDN 提供了一个更专业的定义

工厂函数

工厂函数是任何返回对象的函数。没错,就是这样。不要将其与类和构造函数混淆。类和构造函数需要new关键字来实例化对象,而工厂函数则返回实例化的对象本身。

function factory(name) {
  return { name };
}

const obj = factory('Some Dood');
console.log(obj.name); // 'Some Dood'
Enter fullscreen mode Exit fullscreen mode

使用闭包作为私有变量

现在,我们已经掌握了在 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;
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

然后,我们可以调用工厂函数来创建动物对象的新实例。需要注意的是,每次调用工厂函数时,都会创建一个新的闭包。因此,每个返回的对象都可以访问它自己的闭包。

const presto = createAnimal('Presto', 'Digger');
const fluffykins = createAnimal('Fluffykins', 'Jumper');
Enter fullscreen mode Exit fullscreen mode

那么我们这样做得到了什么呢?嗯,借助闭包的力量,我们本质上模拟了 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'
Enter fullscreen mode Exit fullscreen mode

编程范式的奇怪组合

这种变通方法确实有点奇怪,但它却实现了面向对象语言中一个看似简单的特性。但如果仔细分析,就会发现它其实很有妙处。首先,它巧妙地将两种截然不同且相互冲突的编程范式——面向对象编程函数式编程——结合在一起。

这种方法的面向对象特性涉及工厂函数、可变性和封装的使用。另一方面,函数式方法则涉及闭包的使用。JavaScript 确实是一种多范式语言,它不断模糊不同范式之间的界限。

有人可能会说,把这两种范式粘在一起既混乱又奇怪。但在我看来,这种说法并不完全正确。即使范式的融合不符合惯例和设计模式,但我发现,为了在 JavaScript 中实现面向对象特性,必须使用函数式编程的特性,这一点非常令人着迷。这两种相互矛盾的范式和谐共存,就像阴阳一样。尽管它们之间存在差异,但总有办法让事情奏效。或许,这可以作为人生的一个比喻?

链接地址:https://dev.to/somedood/emulating-private-variables-in-javascript-with-closures-and-factory-functions-2314
PREV
x++ 和 ++x 的区别 程序员的懒惰与务实 前缀 vs. 后缀 什么时候该用哪个?需要记住的事情
NEXT
🔴 使用 HTML 和 CSS 构建 Twitter 克隆 - 教程