首页 > 代码库 > JavaScript面向对象轻松入门之封装(demo by ES5、ES6、TypeScript)
JavaScript面向对象轻松入门之封装(demo by ES5、ES6、TypeScript)
本章默认大家已经看过作者的前一篇文章 《JavaScript面向对象轻松入门之抽象》
为什么要封装?
封装(Encapsulation)就是把对象的内部属性和方法隐藏起来,外部代码访问该对象只能通过特定的接口访问,这也是面向接口编程思想的一部分。
封装是面向对象编程里非常重要的一部分,让我们来看看没有封装的代码是什么样的:
1 function Dog(){ 2 this.hairColor = ‘白色‘;//string 3 this.breed = ‘贵宾‘;//string 4 this.age = 2;//number 5 } 6 var dog = new Dog(); 7 console.log(dog.breed);//log: ‘贵宾‘
看似没有什么问题,但如果breed属性名修改了怎么办?比如换成this.type = ‘贵宾’,那所有使用Dog类的代码都要改变。
如果类的代码和使用类的代码都是你写的,并且使用这个类的地方不多,你这么写无所谓。
但如果使用这个类的地方比较多,或者协同开发时其它人还要使用你的类,那这样做就会让代码很难维护,正确的做法是:
1 function Dog(){ 2 this.hairColor = ‘白色‘;//string 3 this.age = 2;//number 4 this._breed = ‘贵宾‘;//string 5 } 6 Dog.prototype.getBreed = function(){ 7 return this._breed; 8 } 9 Dog.prototype.setBreed = function(val){ 10 this._breed = val; 11 } 12 var dog = new Dog(); 13 console.log(dog.getBreed());//log: ‘贵宾‘ 14 dog.setBreed(‘土狗‘);
getBreed()就是接口,如果内部的属性变化了,比如breed换成了type ,那只需要改变getBreed()里的代码就可以了,并且你可以监听到所有获取这个属性的操作。
所以封装有很多好处:
1、只要接口不改变,内部的实现可以任意改变;
2、使用者使用起来很方便,不用关系内部是如何实现;
3、降低代码之间的耦合;
4、满足大型应用程序和多人协同开发;
用getter/setter来封装私有属性
其实还有另一种封装属性的方法,那就是用getter/setter,如下demo,本章不讲原理,只讲使用,原理可自行查资料:
1 function Dog(){ 2 this.hairColor = ‘白色‘;//string 3 this.age = 2;//number 4 this._breed = ‘贵宾‘;//string 5 Object.defineProperty(this, ‘breed‘, {//传入this和属性名 6 get : function () { 7 console.log(‘监听到了有人调用这个get breed‘) 8 return this._breed; 9 }, 10 set : function (val) { 11 this._breed = val; 12 /* 13 如果不设置setter的话默认这个属性是不可设置的 14 但有点让人诟病的是,浏览器并不会报错 15 所以即使你想让breed是只读的,你也应该设置一个setter让其抛出错误: 16 throw ‘attribute "breed" is read only!‘; 17 */ 18 } 19 }); 20 } 21 var dog = new Dog(); 22 console.log(dog.breed); 23 /*log: 24 ‘监听到了有人调用这个get breed接口‘ 25 ‘贵宾‘ 26 */ 27 dog.breed = ‘土狗‘; 28 console.log(dog.breed); 29 /*log: 30 ‘监听到了有人调用这个get breed接口‘ 31 ‘土狗‘ 32 */
但这种方法写起来比较繁琐,作者一般是用getBreed()这种方法,getter/setter一般用在readonly的属性和一些比较重要的接口,以及重构没有封装接口的属性操作。
还可以用闭包封装私有属性,是最安全的,但会产生额外的内存开销,所以作者不是很喜欢用,大家可自行了解。
公有/私有概念
前两小节我们简单的了解了下封装,但这些肯定是不够用的,下面的我们先来了解下几个概念:
私有属性:即只能在类的内部调获取、修改的属性,不允许外部访问。
私有方法:仅供类内部调用的方法,禁止外部调用。
公有属性:可供类外部获取、修改的属性。理论上讲类的所有属性都应该是私有属性,只能通过封装的接口访问,但一些比较小的类,或者使用次数比较少的类,你觉得比较方便的话也可以不封装接口。
公有方法:可供外部调用的方法,实现接口的方法如getBreed()就是公有方法,以及对外暴露的行为方法。
静态属性、静态方法:类本身的属性和方法。这个就没必要区分公有私有了,所有的静态属性、静态方法都必须是私有的,一定要通过封装接口访问,这也是上一章中作者为什么要用getInstanceNumber()来访问Dog.instanceNumber属性。
ES5 demo如下:
1 function Dog(){ 2 /*公有属性*/ 3 this.hairColor = null;//string 4 this.age = null;//number 5 /*私有属性,人们共同约定私有属性、私有方法前面加上_以便区分*/ 6 this._breed = null;//string 7 this._init(); 8 /*属性的初始化最好放一个私有方法里,构造函数最好只用来声明类的属性和调用方法*/ 9 Dog.instanceNumber++; 10 } 11 /*静态属性*/ 12 Dog.instanceNumber = 0; 13 /*私有方法,只能类的内部调用*/ 14 Dog.prototype._init = function(){ 15 this.hairColor = ‘白色‘; 16 this.age = 2; 17 this._breed = ‘贵宾‘; 18 } 19 /*公有方法:获取属性的接口方法*/ 20 Dog.prototype.getBreed = function(){ 21 console.log(‘监听到了有人调用这个getBreed()接口‘) 22 return this._breed; 23 } 24 /*公有方法:设置属性的接口方法*/ 25 Dog.prototype.setBreed = function(breed){ 26 this._breed = breed; 27 return this; 28 /*这是一个小技巧,可以链式调用方法,只要公有方法没有返回值都建议返回this*/ 29 } 30 /*公有方法:对外暴露的行为方法*/ 31 Dog.prototype.gnawBone = function() { 32 console.log(‘这是本狗最幸福的时候‘); 33 return this; 34 } 35 /*公有方法:对外暴露的静态属性获取方法*/ 36 Dog.prototype.getInstanceNumber = function() { 37 return Dog.instanceNumber;//也可以this.constructor.instanceNumber 38 } 39 var dog = new Dog(); 40 console.log(dog.getBreed()); 41 /*log: 42 ‘监听到了有人调用这个getBreed()接口‘ 43 ‘贵宾‘ 44 */ 45 /*链式调用,由于getBreed()不是返回this,所以getBreed()后面就不可以链式调用了*/ 46 var dogBreed = dog.setBreed(‘土狗‘).gnawBone().getBreed(); 47 /*log: 48 ‘这是本狗最幸福的时候‘ 49 ‘监听到了有人调用这个getBreed()接口‘ 50 */ 51 console.log(dogBreed);//log: ‘土狗‘ 52 console.log(dog);
ES6 demo(新手可不看ES6和TypeScrpt实现部分):
1 class Dog{ 2 constructor(){ 3 this.hairColor = null;//string 4 this.age = null;//number 5 this._breed = null;//string 6 this._init(); 7 Dog.instanceNumber++; 8 } 9 _init(){ 10 this.hairColor = ‘白色‘; 11 this.age = 2; 12 this._breed = ‘贵宾‘; 13 } 14 get breed(){ 15 /*其实就是通过getter实现的,只是ES6写起来更简洁*/ 16 console.log(‘监听到了有人调用这个get breed接口‘); 17 return this._breed; 18 } 19 set breed(breed){ 20 /*跟ES5一样,如果不设置的话默认breed无法被修改,而且不会报错*/ 21 console.log(‘监听到了有人调用这个set breed接口‘); 22 this._breed = breed; 23 return this; 24 } 25 gnawBone() { 26 console.log(‘这是本狗最幸福的时候‘); 27 return this; 28 } 29 getInstanceNumber() { 30 return Dog.instanceNumber; 31 } 32 } 33 Dog.instanceNumber = 0; 34 var dog = new Dog(); 35 console.log(dog.breed); 36 /*log: 37 ‘监听到了有人调用这个get breed接口‘ 38 ‘贵宾‘ 39 */ 40 dog.breed = ‘土狗‘;//log:‘监听到了有人调用这个set breed接口‘ 41 console.log(dog.breed); 42 /*log: 43 ‘监听到了有人调用这个get breed接口‘ 44 ‘土狗‘ 45 */
ES5、ES6中虽然我们把私有属性和方法用“_”放在名字前面以区分,但外部还是可以访问到属性和方法的。
TypeScrpt中就比较规范了,可以声明私有属性,私有方法,并且外部是无法访问私有属性、私有方法的:
1 class Dog{ 2 public hairColor: string; 3 readonly age: number;//可声明只读属性 4 private _breed: string;//虽然声明了private,但还是建议属性名加_以区分 5 static instanceNumber: number = 0;//静态属性 6 constructor(){ 7 this._init(); 8 Dog.instanceNumber++; 9 } 10 private _init(){ 11 this.hairColor = ‘白色‘; 12 this.age = 2; 13 this._breed = ‘贵宾‘; 14 } 15 get breed(){ 16 console.log(‘监听到了有人调用这个get breed接口‘); 17 return this._breed; 18 } 19 set breed(breed){ 20 console.log(‘监听到了有人调用这个set breed接口‘); 21 this._breed = breed; 22 } 23 public gnawBone() { 24 console.log(‘这是本狗最幸福的时候‘); 25 return this; 26 } 27 public getInstanceNumber() { 28 return Dog.instanceNumber; 29 } 30 } 31 let dog = new Dog(); 32 console.log(dog.breed); 33 /*log: 34 ‘监听到了有人调用这个get breed接口‘ 35 ‘贵宾‘ 36 */ 37 dog.breed = ‘土狗‘;//log:‘监听到了有人调用这个set breed接口‘ 38 console.log(dog.breed); 39 /*log: 40 ‘监听到了有人调用这个get breed接口‘ 41 ‘土狗‘ 42 */ 43 console.log(dog._breed);//报错,无法通过编译 44 dog._init();//报错,无法通过编译
注意事项:
1、暴露给别人的类,多个类组合成一个类时,所有属性一定都要封装起来;
2、如果你来不及封装属性,可以后期用getter/setter弥补;
3、每个公有方法,最好注释一下含义;
4、在重要的类前面最好用注释描述所有的公有方法;
后话
如果你喜欢作者的文章,记得收藏,你的点赞是对作者最大的鼓励;
作者会尽量每周更新一章,下一章是讲继承;
大家有什么疑问可以留言或私信作者,作者尽量第一时间回复大家;
如果老司机们觉得那里可以有不恰当的,或可以表达的更好的,欢迎指出来,我会尽快修正、完善。
JavaScript面向对象轻松入门之封装(demo by ES5、ES6、TypeScript)