前言:
通常来说,我们在没有任何目的性的组织代码,所有的代码逻辑都是根据程序员理解到哪一步业务就写到哪一步的代码写法,称之为面向过程的编程。面向过程的编程,是纯粹的以程序员的代码流程,来控制整个项目的业务实现,这样的代码通常具有比较强的耦合性,通常涉及到修改代码,很可能就是一个比较繁杂的过程,需要修改很多的代码,需要思维重现编写该代码的程序员的思考流程。不得不说,这样的代码组织方式不利于后期维护以及修改。
什么是面向对象的编程?
面向对象的编程,理论核心就是将每一个功能模块进行抽离,然后封装成对象,赋予每个功能模块抽离的对象相应的属性,包括该模块需要的数据部分,以及该模块需要与外界进行联动沟通所需要的方法部分。
1 var obj = { 2 name: `name1`, 3 id: `id2`, 4 dosomeThing: function() { 5 console.log(`${this.id}:${this.name}`); 6 }, 7 dosomeThing2: function(num) { 8 console.log(`外部的变量${num} : 对象自带的属性${this.name}`); 9 } 10 } 11 12 13 obj.dosomeThing(); //id2 : name1 14 15 obj.dosomeThing2(); //外部的变量undefined : 对象自带的属性name1 16 17 obj.dosomeThing2(5); //外部的变量5: 对象自带的属性name1
我们可以使用这种方式,将所有的模块抽离并封装成对象,降低模块与模块之间的耦合性,提交代码的可维护性与可读性,我们每个模块都有自己的实例化对象,所有的逻辑我们只需要在对象的方法中进行处理。
但是,这种程度的封装,也有一定的缺陷,如果项目中存在多个重复的模块,很有可能,我们可能需要多次重复生成基本一致的对象来进行操作,会导致我们的代码中存在很多的重复代码,这些冗余的代码,也会导致代码的可读性变差,同时增加维护成本。
面向对象的封装、继承、多态
面向对象有三个最重要的部分,就是封装、继承、多态。以上的用法不难发现,只能支持封装部分,而无法实现继承与多态,如果需要自己去实现深拷贝,也是能够达到这样的效果,但是太过繁琐。
1 var objCopy = function(obj) { 2 var tmp_obj; 3 if(typeof obj == 'object') { 4 if(obj instanceof Array) { 5 tmp_obj = []; 6 } else { 7 tmp_obj = {}; 8 } 9 } else { 10 return obj; 11 } 12 for (var i in obj) { 13 if (typeof obj[i] != 'object') { 14 tmp_obj[i] = obj[i]; 15 } else if (obj[i] instanceof Array) { 16 tmp_obj[i] = []; 17 for (var j in obj[i]) { 18 if (typeof obj[i][j] != 'object') { 19 tmp_obj[i][j] = obj[i][j]; 20 } else { 21 tmp_obj[i][j] = ObjCopy(obj[i][j]); 22 } 23 } 24 } else { 25 tmp_obj[i] = ObjCopy(obj[i]); 26 } 27 } 28 return tmp_obj; 29 } 30 31 var obj2 = objCopy(obj); 32 33 obj2.dosomeThing3 = function() { 34 console.log(`test`); 35 }
不难发现,这种方式确实能够达到目的,但是这种继承的方式,毕竟比较怪异,而且因为通过深拷贝生成的子对象,实际上并没有在原型链上有直接关联。我们通常会使用另外一种方式来定义对象。
function parent(name, id) { this.name = name; this.id = id; } parent.prototype.dosomeThing = function() { console.log(`${this.name}:${this.id}`); } var obj1 = new parent(`name1`, `id2`); obj1.dosomeThing(); //name1:id2 var obj2 = new parent(`name11`, `id22`); obj2.dosomeThing(); //name11:id22 obj2.dosomeThing3 = function() { console.log(`test`); } obj2.dosomeThing3(); //test function child(name, id) { parent.call(this, name, id); } child.prototype = Object.create(parent.prototype); child.prototype.dosomeThing4 = function() { console.log(`hehe`) } obj2.dosomeThing4(); //hehe var obj3 = new child(`dy`, `001`); obj3.dosomeThing4(); //hehe obj3.dosomeThing5 = function() { console.log(`hehe2`) } obj2.dosomeThing5; //undefined obj3.dosomeThing5(); //hehe2
这种方式通过原型链来进行继承以及通过new来生成实例化对象,可以提取公共代码,同时通过继承,并添加私有的方法来达到多态
但是这种方法也有其不足之处:
child.prototype.constructor = parent; //true var a = new child('name', 'id'); a.constructor == parent; //true
这显然不合理,我们通过child构造的对象,其构造函数指向居然是parent,所以我们需要手动的修改constructor的指向
child.prototype.constructor = child; var a = new child('name', id); a.constructor == child; //true
这样就显得合理很多,我们可以将这个过程封装为一个继承方法
var extend = function(child, parent) { child.prototype = Object.create(parent.prototype); child.prototype.constructor = child; }
使用这个方法的时候,我们可以不需要使用apply或者call才进行调用parent方法进行处理,因为我们需要继承的实际上是一些固有的属性,及固化在原型链上的方法行为,以及一些固定的属性,而通过调用临时生成的可变的属性,我们可以在child子类中再次申明,这样,我们通过__proto__属性构建了一整条原型链,原型链里面的属性以及方法都是使用的同一份内存,我们可以减少很多不必要的内存
var parent = function(name, id) { this.name = name; this.id = id; } parent.prototype.class = 'people'; parent.prototype.say = function() { console.log(this.name, this.id, this.class); } var child = function(name, id) { this.name = name; this.id = id; } extend(child, parent); var stu = new child('Lilei', '0001'); stu.say(); //Lilei,0001,people;
我们所有需要固定下来的属性,都可以放到prototype上,然后通过extend方法继承到子类,而直接写在类上的可变的方法是不会进行继承的。
在es6出现以后,我们可以用更直接的方法进行类的封装继承:
class parent{ constructor(name, id){//constructor构造函数 this.name=name; this.id = id; this.class = 'people'; } say(){//定义在原型上的函数 console.log(this.name, this.id, this.class); } } class child extends parent{ constructor(name, id){ super(); this.name=name; this.id = id; } } var stu =new child('Lilei','研发0001'); stu.say(); //Lilei 研发0001 people
需要注意的是,子类中使用构造函数必须要调用super()方法,否则会报错,因为子类没有this指针,调用super后,会调用父类的构造然后构建this指针,然后就可以对this进行操作。否则会报错。