JavaScript中的面向对象
面向对象
技术一般水平有限,有什么错的地方,望大家指正。
面向对象只是一种编程模式并不是某一种语言特有的在很多种语言上都有体现。面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式(很官方的一个定义),它有四个基本特性抽象、继承、封装、多态。在JS中我们运用的最多的就是封装和继承。
面向对象编程即以对象为核心通过对象调用方法来完成一套处理流程,假如我们如下需求:生成两个随机数,计算这个两个随机数的和,我们通常这样写:
function calculate(){ var num1 = Math.floor(Math.random()*100); var num2 = Math.floor(Math.random()*100); var result = num1+num2; return result; } console.log(calculate());
用面向对象的方式来进行改写:
var factory = { create:function(){ return Math.floor(Math.random()*100); } } var tool = { add:function(a,b){ return a+b } } function calculate(){ var num1 = factory.create(); var num2 = factory.create(); var rerult = tool.add(num1,num2); return rerult; } console.log(calculate());
我们将生成随机数和计算过程由两个对象来完成,不过看起来更麻烦了,这是因为代码逻辑较为简单,在一个复杂的处理过程中它的优势就会显出来。
面向对象最大的好处就是易于维护,假如我们现在要对产生的随机数加一个约束:产生的随机数自动加10,此时我们只需要在factory中改写create函数即可,calculate函数的处理过程不需要有任何更改,假如calculate里面创建10个随机数用原来的写法就需要改10处,而面向对象的写法只需要改一处即可。
封装的一大好处就是进行复用,直接使用不用管它的内部处理流程,只要把参数传递进行就能达到我们预想的结果。
JS中的构造函数
我们可以直接使用一个对象,为这个对象附加一些属性来达到面向对象的效果。但是用的最多的还是构造函数的形式,构造函数即用来生产对象的一个工厂,基本实现方式为:
function Person(name,age){ this.name = name; this.age = age; } Person.prototype.say = function(){ console.log("你好,我是"+this.name) } Person.prototype.show = function(){ console.log("年龄"+this.age+"一名小学生!"); }
我们将属性写在构造函数Person里面,将函数写在原型上面,这样在执行new操作时新创建出来的对象既有自己的属性也有自己的方法。new的一个处理流程:创建一个对象,将构造函数里面的this指向这个对象,同时新建对象的原型链指向构造函数的原型,最后执行构造函数。
既然new的时候会将构造函数里面的this指向新创建的对象,为什么不将函数也定义在构造函数里面:this.say = function(){}?事实是为了避免新建对象变得太过臃肿,一般实例化对象的属性是不同的,而函数是相同的,函数会形成一个独立的作用域不会互相干扰,而JS通过原型链让实例化的对象使用原型上的函数,所以我们一般将属性定义在构造函数里面,函数定义在原型上。
原型链
实例化出来的对象有一个__proto__属性指向构造函数的原型(Person.prototype)上面。
对象的“.”操作就表示存取,比如obj.name = "zt",表示设置属性,在对象obj上创建一个属性,如果当前对象存在就重新赋值,如果不存在就进行创建并赋值。与存对应的就是取:obj.name,当取操作时原型链的作用就显示出来了,对象obj会优先在自身下面查找name属性,如果属性不存在就会沿着原型链查找,一直找到Object.prototype上面(原型链的末端),如果还是不存在就会返回undefined。
函数的继承
如果我们需要一个新的函数,而现在存在一个相似度80%的函数,我们就可以继承该函数,然后在改写一部分内容,节约我们的开发时间。
假如我们有一个构造函数Person:
function Person(name,age){ this.name = name; this.age = age; } Person.prototype.say = function(){ console.log("我是"+this.name); }
现在我们需要一个动物类:Animal它name和age属性say函数,另外动物还有一个毛色color的属性,同时不用工作(work),现在我们用继承来实现Animal:
function Animal(name,age,color){ Person.call(this,name,age); this.color = color; } Animal.prototype = Object.create(Person.prototype); Animal.prototype.work = function(){ console.log("我不用工作"); }
现在测试一下结果:
var me = new Person("zt",23); me.say();//我是zt me.work();//报错提示不存在work方法 console.log(me.age)//23 var dog = new Animal("旺财",2,"黑毛"); dog.say();//我是旺财 dog.work();//我不用工作 console.log(dog.age);//2
哪个都能正常使用说明继承是成功的。
Person构造函数分为两个部分:属性和方法,所以继承的时候我们分别继承属性和方法即可。
属性继承我们采用对象属性扩展的方式来实现,我们把Person函数里面的this通过call函数改为Animal函数里面的this,这样Person函数执行完成的时候就会为Animal函数的this添加name和age属性,然后Animal函数在为自己添加一个color属性。
方法的继承我们采用Object.create()来实现,Object.create(arg)该函数会创建一个对象,并且创建对象的原型链指向参数arg,看我们上面的代码,Animal的原型的原型链指向Person的原型,实例化对象dog原型链指向Animal的原型:
实例化Animal会产生一个dog对象,dog对象原型链指向Animal.prototype,Animal.prototype对象的原型链指向Person.prototype,这样就将这几个对象串联起来了,比如dog.say(),dog对象优先在自身下面查找该属性,不存在就会沿着原型链去Animal.prototype上查找,还是不存在继续沿着原型链查找,直到找到该属性,或者查找到尽头还是不存在就会返回undefined。
上面的继承方式还存在一个缺陷,我们看上面的代码:
dog.constructor;//Person函数
所以我们还要修复这个问题,手动控制改变dog对象的构造器,在继承完原型函数之后修正构造器指向即可解决:
Animal.prototype.constructor = Animal;
另外常用的一种函数的继承就是将Person.prototype上面的方法拷贝到Animal.prototype上。