【问题标题】:Is using closures to emulate encapsulation a bad idea?使用闭包来模拟封装是个坏主意吗?
【发布时间】:2016-10-20 22:08:37
【问题描述】:

例如,看一下我对堆栈的简单实现:

var MyStack = (function() {
    var min;
    var head;

    // Constructor
    function MyStack() {
        this.size = 0;
    }

    MyStack.prototype.push = function(val) {
        var node = new Node(val);

        if (typeof min === 'undefined' || val < min) {
            min = val;
        }

        ++this.size;
        if (typeof head === 'undefined') {
            head = node;
        } else {
            node.next = head;
            head = node;
        }
    };

    MyStack.prototype.pop = function() {
        if (typeof head === 'undefined') {
            throw new Error('Empty stack');
        }

        --this.size;

        var data = head.data;
        head = head.next;

        return data;
    };

    MyStack.prototype.min = function() {
        if (typeof min === 'undefined') {
            throw new Error('Min not defined');
        }

        return min;
    };

    MyStack.prototype.peek = function() {
        if (typeof head === 'undefined') {
            throw new Error('Empty stack');
        }

        return head.data;
    };

    function Node(data) {
        this.data = data;
        this.next;
    }

    return MyStack;
})();

通过使用这种方法,我可以确保没有人能够(无意或有意)操纵“私有”字段,例如 min 和 head。我还可以使用不需要公开的私有函数,例如 Node()。

我已经读到这将使用更多内存,因为它必须为为 MyStack 创建的每个新对象维护一个额外的范围。它是否需要如此多的额外内存以至于这种方式是个坏主意?

我确实尝试通过使用原型而不是每次创建新对象时都创建函数来优化它。换句话说,我没有将函数包含在 MyStack 的构造函数中。

我的问题是,这是糟糕的设计吗?这种方法有什么重大缺陷吗?

【问题讨论】:

  • “模拟封装”是指“实现封装”吗?因为我看不出这不是封装。

标签: javascript closures


【解决方案1】:

使用闭包来模拟封装是个坏主意吗?

没有。虽然我不认为这是“模拟”,但闭包正在实现封装。

我确实尝试通过使用原型而不是每次创建新对象时都创建函数来优化它。换句话说,我没有将函数包含在 MyStack 的构造函数中。

我的问题是,这是糟糕的设计吗?这种方法有什么重大缺陷吗?

是的,这实际上是错误的。您的minhead(以及MyStackNode)变量本质上是静态的。它们只定义一次,将由所有实例共享。您不能创建两个不同的堆栈,它们都将具有相同的 head 引用。

要封装每个实例的状态,您需要在构造函数中声明变量,以便使用每个新对象创建它们。为此,您还必须在构造函数范围内声明所有需要访问它们的方法(“特权”)。

var MyStack = (function() {
    function MyStack() {
        var size = 0;
        var head = undefined;

        function checkNonEmpty() {
            if (typeof head === 'undefined') {
                throw new Error('Empty stack');
            }
        }
        this.push = function(val) {
            size++;
            head = new Node(val, head);
        };
        this.pop = function() {
            checkNonEmpty();
            this.size--;
            var data = head.data;
            head = head.next;
            return data;
        };
        this.peek = function() {
            checkNonEmpty();
            return head.data;
        };
        this.getSize = function() {
            return size;
        };
    }

    function Node(data, next) {
        this.data = data;
        this.next = next;
    }

    return MyStack;
})();

如果您想将这些方法放在原型上,您需要将headsize 值作为实例的属性提供。

【讨论】:

  • 哇哦,我不知道我实际上是在创建静态字段。难怪我在使用我的实现时遇到了一些问题。现在您已经重构了我的代码,这对我来说更有意义。所以本质上,以下是两难境地吗?要么拥有“私有”字段但每次创建新对象时都重新创建方法,要么公开字段但享受原型的内存优势?
  • @gjvatsalya 是的,完全正确。虽然创建方法实际上相当便宜,但您通常不必担心它。
  • 太棒了,那太好了。感谢您指出我犯的严重错误。
【解决方案2】:

考虑到封装是面向对象编程的原则之一,我不认为这是一种不好的编码习惯。 JavaScript 中的闭包是封装变量和限制对应用程序其他部分的访问的方法。

在很多方面,JavaScript 中没有什么是真正安全的。即使使用闭包,您希望保持私有的那些变量也可以在不同的浏览器中通过不同的方式访问。例如,在 Google Chrome 中,您可以在调试器中设置断点并访问活动外壳中的任何变量。尽管我们可以在浏览器中做很多事情来确保安全性,但我们仍然在处理一种在运行代码的机器上编译的解释性语言。

考虑到你的例子: var MyStack = (function(){...})(); 如果您曾经使用过 typescript 或任何快速开发框架,您会看到这是在框架编译/转译时用于命名对象的输出编码方法。

【讨论】:

  • 我明白了,这是有道理的。关于使用调试器,这意味着它们只有只读访问权限,对吗?还是他们仍然可以通过设置断点来修改值?
  • 另外,你能解释一下你在上一段中的意思吗?您是说那些框架使用的模式与我在示例中使用的模式相同?
  • @gjvatsalya 他们可以修改调试器中的任何内容。当您在断点处停止时,您可以双击 Scope 字段中的任何变量,它会让您为它输入一个新值。
  • 至于最后一段,是的。我的意思是那些框架使用相同的编码风格。就调试器而言,是的,您可以动态更改变量并操纵应用程序。你真的无能为力。您还询问了性能。基于这种编码风格的性能开销可以忽略不计。
  • 感谢您的回答。如果性能影响可以忽略不计,那么我会很乐意用这种方法编写更多的 JS 代码。
猜你喜欢
  • 2014-12-23
  • 1970-01-01
  • 1970-01-01
  • 2010-09-22
  • 2010-09-28
  • 2010-11-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多