震惊!5分钟封装JavaScript栈数据结构Stack类(二)【JavaScript数据结构与算法系列】

本文详细介绍了如何使用JavaScript创建栈数据结构的类,包括使用对象而非数组来存储元素以提高效率,并探讨了如何保护内部元素,如使用下划线命名约定、ES6的Symbol和WeakMap。此外,还讨论了即将推出的类属性提案以实现真正的私有属性。文章通过示例代码展示了Stack类的push、size、isEmpty、pop、peek、clear和toString等方法的实现。
摘要由CSDN通过智能技术生成

一、创建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

评论 57
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狼丶宇先森

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值