震惊!5分钟封装JavaScript栈数据结构Stack类JavaScript数据结构与算法系列
Posted 狼丶宇先生
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了震惊!5分钟封装JavaScript栈数据结构Stack类JavaScript数据结构与算法系列相关的知识,希望对你有一定的参考价值。
本文目录
一、创建javascript栈结构Stack类
在上一篇文章中,我们了解并学习了JavaScript是怎么来定义的栈数据结构,那么本章就是来创建一个JavaScript的栈数据结构类。
创建一个stack类最简单的方式是使用一个数组来存储其元素。在处理大量数据的时候(这在现实生活中的项目里很常见),我们同样需要评估如何操作数据是最高效的。在使用数组时,大部分方法的时间复杂度是O(n)。
O(n)的意思是,我们需要选代整个数组直到找到要找的那个元素,在最坏的情况下需要选代数组的所有位置,其中的n代表数组的长度。如果数组有更多元素的话,所需的时间会更长。另外,数组是元素的一个有序集合,为了保证元素排列有序,它会占用更多的内存空间。
如果我们能直接获取元素,占用较少的内存空间,并且仍然保证所有元素按照我们的需要排列,那不是更好吗?对于使用JavaScript语言实现栈数据结构的场景,我们也可以使用一个JavaScript对象来存储所有的栈元素,保证它们的顺序并且遵循LIFO原则。
我们来看看如何实现这样的行为。
首先像下面这样声明一个Stack 类(stack.js文件)。
class Stack {
constructor(){
this.count = 0;
this.items = {};
}
//TODO...
}
在这个版本的Stack类中,我们将使用一个count属性来帮助我们记录栈的大小(也能帮助我们从数据结构中添加和删除元素)。
二、封装基本功能与方法
①向栈中插入元素
在基于数组的版本中,我们可以同时向stack类中添加多个元素。由于现在使用了一个对象,这个版本的push方法只允许我们一次插入一个元素。下面是push方法的代码。
class Stack {
constructor(){
this.count = 0;
this.items = {};
}
push(element){
this.items[this.count] = element;
this.count++;
}
}
在JavaScript中,对象是一系列键值对的集合。要向栈中添加元素,我们将使用count变量作为items对象的键名,插人的元素则是它的值。在向栈插入元素后,我们递增count变量。
可以延用之前的示例来使用Stack类,并向其中插入元素5和9。
const stack = new Stack();
stack.push(5);
stack.push(9);
在内部,items包含的值和count属性如下所示。
items = {
0: 5,
1: 9
}
count = 2;
②验证一个栈是否为空和它的大小
count属性也表示栈的大小。因此,我们可以简单地返回count属性的值来实现size方法。
class Stack {
constructor(){
this.count = 0;
this.items = {};
}
push(element){
this.items[this.count] = element;
this.count++;
}
size(){
return this.count;
}
}
要验证栈是否为空,可以像下面这样判断count的值是否为0。
class Stack {
constructor(){
this.count = 0;
this.items = {};
}
push(element){
this.items[this.count] = element;
this.count++;
}
size(){
return this.count;
}
isEmpty(){
return this.count === 0;
}
}
③从栈中弹出元素
由于我们没有使用数组来存储元素,需要手动实现移除元素的逻辑。pop方法同样返回了从栈中移除的元素,它的实现如下。
class Stack {
constructor(){
this.count = 0;
this.items = {};
}
push(element){
this.items[this.count] = element;
this.count++;
}
size(){
return this.count;
}
isEmpty(){
return this.count === 0;
}
pop(){
if(this.isEmpty()){ // {1}
return undefined;
}
this.count--; //{2}
const result = this.items[this.count]; //{3}
delete this.items[this.count]; //{4}
return result; //{5}
}
}
首先,我们需要检验栈是否为空(行{1})。如果为空,就返回 undefined。如果栈不为空的话,我们会将count属性减1(行{2}),并保存栈顶的值(行{3}),以便在删除它(行{4})之后将它返回(行{5})。
由于我们使用的是JavaScript对象,可以用JavaScript的 delete 运算符从对象中删除一个特定的值。
我们使用如下内部的值来模拟pop操作。
items = {
0: 5,
1: 9
}
count = 2;
要访问到栈顶的元素(即最后添加的元素9),我们需要访问键值为1的位置。因此我们将count变量从2减为1。这样就可以访问items[1],删除它,并将它的值返回了。
④查看栈顶的值并将栈清空
上一节我们学习了,要访问栈顶元素,需要将count属性减1。那么我们来看看peek方法的代码。
class Stack {
constructor(){
this.count = 0;
this.items = {};
}
push(element){
this.items[this.count] = element;
this.count++;
}
size(){
return this.count;
}
isEmpty(){
return this.count === 0;
}
pop(){
if(this.isEmpty()){ // {1}
return undefined;
}
this.count--; //{2}
const result = this.items[this.count]; //{3}
delete this.items[this.count]; //{4}
return result; //{5}
}
peek(){
if(this.isEmpty()){
return undefined;
}
return this.items[this.count - 1];
}
}
要清空该栈,只需要将它的值复原为构造函数中使用的值即可。
class Stack {
constructor(){
this.count = 0;
this.items = {};
}
push(element){
this.items[this.count] = element;
this.count++;
}
size(){
return this.count;
}
isEmpty(){
return this.count === 0;
}
pop(){
if(this.isEmpty()){ // {1}
return undefined;
}
this.count--; //{2}
const result = this.items[this.count]; //{3}
delete this.items[this.count]; //{4}
return result; //{5}
}
peek(){
if(this.isEmpty()){
return undefined;
}
return this.items[this.count - 1];
}
clear(){
this.items = {};
this.count = 0;
//当然我们也可以遵循LIFO原则,使用下面的逻辑来移除栈中所有的元素。
// while(1this.isEmpty()){
// this.pop();
// }
}
}
⑤创建tostring方法
在数组版本中,我们不需要关心toString方法的实现,因为数据结构可以直接使用数组已经提供的toString方法。
对于使用对象的版本,我们将创建一个toString方法来像数组一样打印出栈的内容。
class Stack {
constructor(){
this.count = 0;
this.items = {};
}
push(element){
this.items[this.count] = element;
this.count++;
}
size(){
return this.count;
}
isEmpty(){
return this.count === 0;
}
pop(){
if(this.isEmpty()){ // {1}
return undefined;
}
this.count--; //{2}
const result = this.items[this.count]; //{3}
delete this.items[this.count]; //{4}
return result; //{5}
}
peek(){
if(this.isEmpty()){
return undefined;
}
return this.items[this.count - 1];
}
clear(){
this.items = {};
this.count = 0;
//当然我们也可以遵循LIFO原则,使用下面的逻辑来移除栈中所有的元素。
// while(1this.isEmpty()){
// this.pop();
// }
}
toString(){
if(this.isEmpty()){
return '';
}
let objString = `${this.items[0]}`; //{1}
for(let i = 1; i < this.count; i++){ //{2}
objString = `${objString},${this.items[i]}`; //{3}
}
return objString;
}
//5,9
}
如果栈是空的,我们只需返回一个空字符串即可。
如果它不是空的,就需要用它底部的第一个元素作为学符单的初始值(行{1}),然后选代整个栈的键(行{2}),一直到栈顶,添加一个逗号(,)以及下一个元素(行{3})。
如果栈只包含一个元素,行{2}和行{3}的代码将不会执行。
实现了 toString方法后,我们就完成了这个版本的Stack类。这也是一个用不同方式写代码的例子。对于使用Stack 类的开发者,选择使用基于数组或是基于对象的版本并不重要,两者都提供了相同的功能,只是内部实现很不一样。
小提示: 除了toString方法,我们创建的其他方法的复杂度均为O(1),代表我们可以直接找到目标元素并对其进行操作(push、pop或peek)。
三、保护数据结构内部元素
在创建别的开发者也可以使用的数据结构或对象时,我们希望保护内部的元素,只有我们暴露出的方法才能修改内部结构。
对于Stack类来说,要确保元素只会被添加到栈顶,而不是栈底或其他任意位置(比如栈的中间)。不幸的是,我们在Stack类中声明的items和count属性并没有得到保护,因为JavaScript 的类就是这样工作的。
试着执行下面的代码。
const stack = new Stack();
console.log(Object.getOwnPropertyNames(stack)); //{1}
console.log(Object.keys(stack)); //{2}
console.log(stack.items); //{3}
行{1}和行{2}的输出结果是[“count”,“items”]。这表示count和items属性是公开的,我们可以像行{3}那样直接访问它们。根据这种行为,我们可以对这两个属性赋新的值。
本章使用ES2015(ES6)语法创建了Stack类。ES2015类是基于原型的。尽管基于原型的类能节省内存空间并在扩展方面优于基于函数的类,但这种方式不能声明私有属性(变量)或方法。
另外,在本例中,我们希望Stack类的用户只能访问我们在类中暴露的方法。下面来看看其他使用JavaScript来实现私有属性的方法。
①下划线命名约定
一部分开发者喜欢在JavaScipt中使用下划线命名约定来标记一个属性为私有属性。
class Stack {
constructor(){
this._count = 0;
this._items = {};
}
}
下划线命名约定就是在属性名称之前加上一个下划线(_)。不过这种方式只是一种约定,并不能保护数据,而且只能依赖于使用我们代码的开发者所具备的常识。
②用ES6的限定作用域Symbol实现类
ES6新增了一种叫作Symbol的基本类型,它是不可变的,可以用作对象的属性。看看怎么用它在Stack类中声明items属性(我们将使用数组来存储元素以简化代码)。
const _items = Symbol('stackItems'); //{1}
class Stack {
constructor(){
this[_items] = [];
}
//栈的方法
}
在上面的代码中,我们声明了Symbol类型的变量_items(行{1}),在类的constructor函数中初始化它的值(行{2})。要访问_items,只需要把所有的 this.items都换成this[_items]。
这种方法创建了一个假的私有属性,因为ES6新增的 Object.getOwnPropertySymbols方法能够取到类里面声明的所有Symbols属性。
下面是一个破坏Stack类的例子。
const stack = new Stack();
stack.push(5);
stack.push(9);
let objectSymbols = Object.getOwnPropertySymbols(stack);
console.log(objectSymbols.length); //输出1
console.log(objectSymbols); //输出[Symbol()]
console.log(objectSymbols[0]); //输出Symbol()
stack[objectSymbols[0]].push(1);
stack.print(); //输出5,8,1
从以上代码可以看到,访问 stack[objectSymbols[0]]是可以得到_items 的。并且,_items属性是一个数组,可以进行任意的数组操作,比如从中间删除或添加元素(使用对象进行存储也是一样的)。
但我们操作的是栈,不应该出现这种行为。
还有第三个方案。
③用ES6的WeakMap实现类
有一种数据类型可以确保属性是私有的,这就是WeakMap。会在后面章深入探讨Map这种数据结构,现在只需要知道weakMap可以存储键值对,其中键是对象,值可以是任意数据类型。
如果用WeakMap 来存储items属性(数组版本),Stack 类就是这样的:
const items = new WeakMap(); //{1}
class Stack {
constructor(){
items.set(this,[]); //{2}
}
push(element){
const s = items.get(this); //{3}
s.push(element);
}
pop(){
const s = items.get(this);
return s.pop();
}
// 其它方法
}
上面的代码片段解释如下。
- 行{1},声明一个WeakMap类型的变量items。
- 行{2},在constructor中,以this(Stack类自己的引用)为键,把代表栈的数组存人items。
- 行{3},从WeakMap中取出值,即以this为键(行{2}设置的)从items中取值。
现在我们知道了,items在Stack类里是真正的私有属性。采用这种方法,代码的可读性不强,而且在扩展该类时无法继承私有属性。
所以鱼和熊掌不可兼得!
④ECMAScript类属性提案
TypeScript提供了一个给类属性和方法使用的private修饰符。然而,该修饰符只在编译时有用(TypeScript类型和错误检测)。在代码被转移完成后,属性同样是公开的。
事实上,我们不能像在其他编程语言中一样声明私有属性和方法。虽然有很多种方法都可以达到相同的效果,但无论是在语法还是性能层面,这些方法都有各自的优点和缺点。
哪种方法更好呢?这取决于你在实际项目中如何使用本文展示的算法,也取决于你需要处理的数据量、需要构造的实例数量,以及其他约束条件。最终,还是取决于你的选择。
有一个关于在JavaScript类中增加私有属性的提案。通过这个提案,我们能够直接在类中声明JavaScript类属性并进行初始化。下面是一个例子。
class Stack{
#count = 0;
#items = [];
//栈的方法
}
我们可以通过在属性前添加井号(#)作为前缀来声明私有属性。这种行为和WeakMap中的私有属性很相似。所以在不远的未来,我们有希望不使用特殊技巧或牺牲代码可读性,就能使用私有类属性。
要了解更多有关类属性提案的信息,请访问:https://github.com/tc39/proposal-class-fields。
四、本章小结
本系列文章主要讲述如何创建JavaScript栈数据结构(Stack)类,以及方法封装,本文作为栈类第二篇,主要是先讲述如何创建JavaScript栈数据结构(Stack)类,重点是使用不同的方法来实现Stack类,以及如何保护内部的元素。数据栈类的文章共三篇本文是第二篇。
工欲善其事,必先利其器。
五、写在后面
这作为一个JavaScript栈数据结构的第二篇文章,主要目的是使用不同的方法来实现Stack类,本系列也会持续进行更新的。
剧透:下一章讲述栈数据结构能解决啥问题。
有问题请留言或者@博主,谢谢支持o( ̄︶ ̄)o~
感谢您的阅读,如果此文章或项目对您有帮助,若可以的话请给个一键三连吧!
GitHub有开源项目,需要的小伙伴可以顺手star一下!
GitHub: https://github.com/langyuxiansheng
以上是关于震惊!5分钟封装JavaScript栈数据结构Stack类JavaScript数据结构与算法系列的主要内容,如果未能解决你的问题,请参考以下文章
手把手5分钟掌握JavaScript栈数据结构JavaScript数据结构与算法系列
手把手5分钟掌握JavaScript栈数据结构JavaScript数据结构与算法系列
手把手5分钟掌握JavaScript栈数据结构JavaScript数据结构与算法系列
5分钟入门JavaScript队列(Queue)数据结构JavaScript数据结构与算法系列