使用闭包来模拟封装是一个不好的想法吗?

6
例如,看一下我实现的一个简单栈的例子:
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构造函数的一部分。

我的问题是,这是不良设计吗?这种方法是否存在任何重大缺陷?


1
你是不是指“实现封装”而不是“模拟封装”?因为我看不出这种方式不是封装。 - ruakh
2个回答

4
使用闭包来模拟封装是一个不错的主意。虽然我不认为这是“模拟”,但是闭包确实实现了封装。
我试图通过使用原型来优化它,而不是每次创建新对象时创建函数。换句话说,我没有将函数作为MyStack的构造函数的一部分包含进去。
我的问题是,这是不好的设计吗?这种方法有什么重大缺点吗?
是的,那实际上是错误的。你的min和head变量(以及MyStack和Node)本质上是静态的。它们只被定义一次,并将被所有实例共享。你不能创建两个不同的堆栈,它们都将具有相同的头引用。
要封装每个实例状态,您需要在构造函数中声明变量,以便它们将随每个新对象一起创建。为此,您还需要在构造函数范围内声明所有需要访问它们的方法(“特权”)。
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
@gjvatsalya 是的,完全正确。虽然创建方法实际上相当便宜,但通常您不需要担心它。 - Bergi
太棒了,那太好听了。感谢您指出我犯的重大错误。 - gjvatsalya

2
考虑到封装是面向对象编程原则之一,我认为这不是一种糟糕的编码实践。JavaScript中的闭包是封装变量和限制应用程序其他部分访问的一种方式。
在很多方面,JavaScript中没有真正的安全性。即使使用闭包,您想要保持私有的变量也可以通过不同浏览器中的不同方法访问。例如,在Google Chrome中,您可以在调试器中设置断点并访问活动封闭内的任何变量。尽管我们可以在浏览器中做很多事情来确保安全性,但我们仍然处理在运行代码的机器上编译的解释语言。
考虑到您的示例: var MyStack = (function(){...})(); 如果您曾经使用typescript或任何快速开发框架,则会看到这是框架编译/转换时用于命名空间对象的输出编码方法。

我明白了,这很有道理。关于使用调试器,这意味着他们只能拥有只读访问权限,对吗?或者他们仍然可以通过设置断点来修改值? - gjvatsalya
另外,你能解释一下你在最后一段所说的意思吗?你是不是只是说那些框架使用了和我在例子中使用的相同模式? - gjvatsalya
@gjvatsalya 他们可以在调试器中修改任何内容。当您在断点处停止时,可以双击范围字段中的任何变量,并允许您为其输入新值。 - Barmar
关于最后一段,是的。我的意思是那些框架使用相同的编码风格。至于调试器,是的,你可以在运行时更改变量并操纵应用程序。这方面真的没有太多可以做的。你还问到性能问题。基于这种编码风格的开销是可以忽略不计的。 - msinnes
感谢您的回答。如果性能影响可以忽略不计,那么我会更愿意用这种方法编写更多的JS代码。 - gjvatsalya

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接