我们花点时间分析一下代码,看看是否能用ECMAScript 6(ES6)的新功能来改进。
我们创建了一个可以当作类来使用的Stack
函数。JavaScript函数都有构造函数,可以用来模拟类的行为。我们声明了一个私有的items
变量,它只能被Stack
函数/类访问。然而,这个方法为每个类的实例都创建一个items
变量的副本。因此,如果要创建多个Stack
实例,它就不太适合了。
看看如何用ES6新语法声明Stack
类,并和前面的做法比较一下优缺点。
用ES6语法声明Stack类
首先来分析下面的代码:
class Stack { constructor { this.items = ; //{1} } push(element){ this.items.push(element); } //其他方法}
我们只是用ES6的简化语法把Stack
函数转换成Stack
类。这种方法不能像其他语言(Java、C++、C#)一样直接在类里面声明变量,只能在类的构造函数constructor
里声明(行{1}
),在类的其他函数里用this.nameofVariable
就可以引用这个变量。
尽管代码看起来更简洁、更漂亮,变量items
却是公共的。ES6的类是基于原型的。虽然基于原型的类比基于函数的类更节省内存,也更适合创建多个实例,却不能够声明私有属性(变量)或方法。而且,在这种情况下,我们希望Stack
类的用户只能访问暴露给类的方法。否则,就有可能从栈的中间移除元素(因为我们用数组来存储其值),这不是我们希望看到的。
看看ES6语法有没有其他的方法可以创建私有属性。
用ES6的限定作用域
Symbol
实现类ES6新增了一种叫作
Symbol
的基本类型,它是不可变的,可以用作对象的属性。看看怎么用它来在Stack
类中声明items
属性:let _items = Symbol; //{1} class Stack { constructor { this[_items] = ; //{2} } //Stack方法}
在上面的代码中,我们声明了
Symbol
类型的变量_items
(行{1}
),在类的constructor
函数中初始化它的值(行{2}
)。要访问_items
,只需把所有的this.items
都换成this[_items]
。这种方法创建了一个假的私有属性,因为ES6新增的
Object.getOwnPropertySymbols
方法能够取到类里面声明的所有Symbols
属性。下面是一个破坏Stack
类的例子:let stack = new Stack;stack.push(5);stack.push(8);let objectSymbols = Object.getOwnPropertySymbols(stack);console.log(objectSymbols.length); // 1console.log(objectSymbols); // [Symbol]console.log(objectSymbols[0]); // Symbolstack[objectSymbols[0]].push(1);stack.print; //输出 5, 8, 1
从以上代码可以看到,访问
stack[objectSymbols[0]]
是可以得到_items
的。并且,_items
属性是一个数组,可以进行任意的数组操作,比如从中间删除或添加元素。我们操作的是栈,不应该出现这种行为。还有第三个方案。
用ES6的
WeakMap
实现类有一种数据类型可以确保属性是私有的,这就是
WeakMap
。我们会在第7章深入探讨Map
这种数据结构,现在只需要知道WeakMap
可以存储键值对,其中键是对象,值可以是任意数据类型。如果用
WeakMap
来存储items
变量,Stack
类就是这样的:const items = new WeakMap; //{1} class Stack { constructor { items.set(this, ); //{2} } push(element) { let s = items.get(this); //{3} s.push(element); } pop { let s = items.get(this); let r = s.pop; return r; } //其他方法}
行
{1}
,声明一个WeakMap
类型的变量items
。行
{2}
,在constructor
中,以this
(Stack
类自己的引用)为键,把代表栈的数组存入items
。行
{3}
,从WeakMap
中取出值,即以this
为键(行{2}
设置的)从items
中取值。
现在我们知道,
items
在Stack
类里是真正的私有属性了,但还有一件事要做。items
现在仍然是在Stack
类以外声明的,因此谁都可以改动它。我们要用一个闭包(外层函数)把Stack
类包起来,这样就只能在这个函数里访问WeakMap
:let Stack = (function { const items = new WeakMap; class Stack { constructor { items.set(this, ); } //其他方法 } return Stack; //{5}});
当
Stack
函数里的构造函数被调用时,会返回Stack
类的一个实例(行{5}
)。关于JavaScript闭包,请阅读http://www.w3schools.com/js/js_function_closures.asp。
现在,
Stack
类有一个名为items
的私有属性。虽然它很丑陋,但毕竟实现了私有属性。然而,用这种方法的话,扩展类无法继承私有属性。鱼与熊掌不可兼得!把上面的代码跟本章最初实现的
Stack
类做个比较,我们会发现有一些相似之处:function Stack { let items = ; //其他方法}
事实上,尽管ES6引入了类的语法,我们仍然不能像在其他编程语言中一样声明私有属性或方法。有很多种方法都可以达到相同的效果,但无论是语法还是性能,这些方法都有各自的优点和缺点。
哪种方法更好?这取决于你在实际项目中如何使用本书中这些算法,要处理的数据量,要创建的实例个数,以及其他约束条件。最终,还是取决于你。
在本书提供下载的代码中,所有的数据结构都会包含简单的函数类,以及ES6的
WeakMap
和闭包的创建方法。