浅谈JavaScript的面向对象和它的封装、继承、多态
写在前面
既然是浅谈,就不会从原理上深度分析,只是帮助我们更好地理解...
面向对象与面向过程
面向对象和面向过程是两种不同的编程思想,刚开始接触编程的时候,我们大都是从面向过程起步的,毕竟像我一样,大家接触的第一门计算机语言大概率都是C语言,C语言就是一门典型的面向过程的计算机语言。
面向过程主要是以动词为主,解决问题的方式是按照顺序一步一步调用不同的函数。
面向对象是以名词为主,将问题抽象出具体的对象,而这个对象有自己的属性和方法,在解决问题的时候,是将不同的对象组合在一起使用。
//面向过程装大象 1.开(冰箱) 2.(大象)装进(冰箱) 3.关(冰箱) //面向对象装大象 1. 冰箱.开门() 2. 冰箱.装进(大象) 3. 冰箱.关门()
从这个例子可以看出,面向对象是以主谓为主,将主谓堪称一个一个的对象,然后对象有自己的属性和方法。
面向对象是以功能来划分问题的,而不是步骤。功能上的统一保证了面向对象设计的可扩展性,解决了代码重用性的问题。
这也是在漫长的程序设计的发展过程中得到的验证结果,面向对象的编程思想较之于面向过程较好一点
封装
面向对象有封装、继承和多态三大特性。
封装:就是把事物封装成类,隐藏事物的属性和方法的实现细节,仅对外公开接口。
在ES5中,并没有class的概念,但是由于js的函数级作用域(函数内部的变量函数外访问不到)。所以我们可以模拟class。在es5中,类其实就是保存了一个函数的变量,这个函数有自己的属性和方法。将属性和方法组成一个类的过程就是封装。
1.通过构造函数添加
JavaScript提供了一个构造函数(Constructor)模式,用来在创建对象时初始化对象。构造函数其实就是普通的函数,只不过有以下的特点
①首字母大写(建议构造函数首字母大写,即使用大驼峰命名,非构造函数首字母小写) ②内部使用this ③使用new生成实例
通过构造函数添加属性和方法实际上也就是通过this添加的属性和方法。因为this总是指向当前对象的,所以通过this添加的属性和方法只在当前对象上添加,是该对象自身拥有的。所以我们实例化一个新对象的时候,this指向的属性和方法都会得到相应的创建,也就是会在内存中复制一份,这样就造成了内存的浪费。
function Cat(name,color){ this.name = name; this.color = color; this.eat = (() => { console.log("fish!") }) } //生成实例 var cat1 = new Cat("tom", "gray")
通过this定义的属性和方法,我们实例化对象的时候斗湖重新复制一份
2.通过原型prototype封装
在类上通过this的方式添加属性和方法会导致内存浪费的现象,有什么办法可以让实例化的类所使用的属性和方法 直接使用指针 指向同一个属性和方法。
这就是原型的方法
JavaScript规定,每一个构造函数都有一个prototype属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。 也就是说,对于那些不变的属性和方法,我们可以直接将其添加在类的prototype对象上。 function Cat(name,color){ this.name = name; this.color = color; } Cat.prototype.type = "英短"; Cat.prototype.eat = ( () => { alert("fish!") } ) //生成实例 var cat1 = new Cat('Tom', 'gray'); var cat2 = new Cat('Kobe', 'purple'); console.log(cat1.type); //英短 cat2.eat(); //fish!
这时所有实例的type属性和eat()方法,其实都是同一个内存地址,指向prototype对象,因此就提高了运行效率。
但是这样做也有弊端,因为实例化的对象的原型都是指向同一内存地址,改动其中一个对象的属性可能会影响到其他的对象
es6中的类和封装
es6声明一个类
①构造器:构造器内创建自有属性
②方法:声明类实例具有的方法
class Cat { //等价于Cat构造器 constructor(name) { this.name = name; } //更加简单的声明类的内部函数 //等价于 Cat.prototype.eat eat() { console.log("fish!"); } } //生成实例 var cat1 = new Cat("tom"); cat1.eat(); //fish! console.log(cat1 instanceof Cat); //true console.log(cat1 instanceof Object); //true console.log(typeof Cat); //function console.log(typeof Cat.prototype.eat); //function
从上面class声明的Cat为例:Cat类是一个具有构造函数行为的函数,其中内部方法eat实际上就是Cat.prototype.eat()
所以说es6的class封装类,本质上是es5实现方式的语法糖
最主要的区别在于,class类的属性是不可重新赋值和不可枚举的,Cat.prototype就是一个只读属性
class和自定义类型的区别
(1)class的声明不会提升,与let类似
(2)class的声明自动运行于严格模式之下
(3)class声明的方法不可枚举
(4)class的内部方法没有 constructor 属性,无法new
(5)调用class的构造函数必须new
(6)class内部方法不能同名
class类的使用
class作为js中的一级公民,可以被当作值来直接使用
//1.类名作为参数传入函数 function createObj (ClassName) { return new ClassName() } //2.立即执行,实现单例模式 let cat1 = new class{ constructor (name) { this.name = name } eat() { console.log("fish!") } }("tom”) cat1.eat() //fish!
继承
继承就是子类可以使用父类的所有功能,并且对这些功能进行扩展。继承的过程,就是从一般到特殊的过程。
1.类式继承
所谓的类式继承就是使用的原型的方式,将方法添加在父类的原型上,然后子类的原型是父类的一个实例化对象。
//声明父类 var SuperClass = function(){ let id = 1; this.name = ['java']; this.superValue = function() { console.log('this is superValue!') } } //为父类添加共有方法 SuperClass.prototype.getSuperValue = function () { return this.superValue(); }; //声明子类 var SubClass = function() { this.subValue = (() => { console.log('this is subValue!') }) } //继承父类 SubClass.prototype = new SuperClass(); //为子类添加共有方法 SubClass.prototype.getSubValue = function() { return this.subValue() } //生成实例 var sub1 = new SubClass(); var sub2 = new SubClass(); sub1.getSuperValue(); //this is superValue! sub1.getSubValue(); //this is subValue! console.log(sub1.id); //undefined console.log(sub1.name); //["java"] sub1.name.push("php"); console.log(sub1.name); //["java", "php"] console.log(sub2.name); //["java", "php"]
其中最核心的是SubClass.prototype = new SuperClass();
类的原型对象prototype对象的作用就是为类的原型添加共有的方法的,但是类不能直接访问这些方法,只有将类实例化之后,新创建的对象复制了父类构造函数的属性和方法,并将原型 proto 指向了父类的原型对象。这样子类就可以访问父类的属性和方法,同时,父类中定义的属性和方法不会被子类继承。
but使用类继承的方法,如果父类的构造函数中有引用数据类型,就会在子类中被所有实例共用,因此一个子类的实例如果更改了这个引用数据类型,就会影响到其他子类的实例。
构造函数继承
为了克服类继承的缺点,才有了构造函数继承,构造函数继承的核心思想就是SuperClass.call(this, id),直接改变this的指向,使通过this创建的属性和方法在子类中复制一份,因为是单独复制的,所以各个实例化的子类互不影响。but会造成内存浪费的问题
//构造函数继承 //声明父类 var SuperClass = function(id){ var name = 'java' this.languages = ['java', 'php', 'ruby']; this.id = id } //声明子类 var SubClass = function(id){ SuperClass.call(this, id) } //生成实例 var sub1 = new SubClass(1); var sub2 = new SubClass(2); console.log(sub2.id); // 2 console.log(sub1.name); //undefined sub1.languages.push("python"); console.log(sub1.languages); // ['java', 'php', 'ruby', 'python'] console.log(sub2.languages); // ['java', 'php', 'ruby']
组合式继承
组合式继承是汲取了两者的优点,既避免了内存浪费,又使得每个实例化的子类互不影响。
//组合式继承 //声明父类 var SuperClass = function(name){ this.languages = ['java', 'php', 'ruby']; this.name = name; } //声明父类原型方法 SuperClass.prototype.showLangs = function () { console.log(this.languages); } //声明子类 var SubClass = function(name){ SuperClass.call(this, name) } //子类继承父类(链式继承) SubClass.prototype = new SuperClass(); //生成实例 var sub1 = new SubClass('python'); var sub2 = new SubClass('go'); sub2.showLangs(); //['java', 'php', 'ruby'] sub1.languages.push(sub1.name); console.log(sub1.languages);//["java", "php", "ruby", "python"] console.log(sub2.languages);//['java', 'php', 'ruby']
but警告:组合式继承方法固然好,但是会导致一个问题,父类的构造函数会被创建两次(call()的时候一遍,new的时候又一遍)
寄生组合继承
组合式继承的缺点的关键是 父类的构造函数在类继承和构造函数继承的组合形式被创建了两边,但是在类继承中我们并不需要创建父类的构造函数,我们只要子类继承父类的原型即可。
所以我们先给父类的原型创建一个副本,然后修改子类的 constructor 属性,最后在设置子类的原型就可以了
//原型式继承 //原型式继承其实就是类式继承的封装,实现的功能返回一个实例,该实例的原型继承了传入的o对象 function inheritObject(o) { //声明一个过渡函数 function F() {} //过渡对象的原型链继承父对象 F.prototype = o; //返回一个过渡对象的实例,该实例的原型继承了父对象 return new F(); } //寄生式继承 //寄生式继承就是对原型继承的第二次封装,使得子类的原型等于父类的原型。并且在第二次封装的过程中对继承的对象进行了扩展 function inheritPrototype(subClass, superClass){ //复制一份父类的原型保存在变量中,使得p的原型等于父类的原型 var p = inheritObject(superClass.prototype); //修正因为重写子类原型导致子类constructor属性被修改 p.constructor = subClass; //设置子类的原型 subClass.prototype = p; } //定义父类 var SuperClass = function(name) { this.name = name; this.languages = ["java", "php", "python"] } //定义父类原型方法 SuperClass.prototype.showLangs = function() { console.log(this.languages); } //定义子类 var SubClass = function(name) { SuperClass.call(this,name) } inheritPrototype(SubClass, SuperClass); var sub1 = new SubClass('go');
es6中的继承
class SuperClass { constructor(name) { this.name = name this.languages = ['java', 'php', 'go']; } showLangs() { console.log(this.languages); } } class SubClass extends SuperClass { constructor(name) { super(name) } //重写父类中的方法 showLangs() { this.languages.push(this.name) console.log(this.languages); } } //生成实例 var sub = new SubClass('韩二虎'); console.log(sub.name); //韩二虎 sub.showLangs(); //["java", "php", "go", "韩二虎"]
多态
多态实际上是不同对象作用与同一操作产生不同的效果。多态的思想实际上是把 “想做什么” 和 “谁去做” 分开。
多态的好处在于,你不必再向对象询问“你是什么类型”后根据得到的答案再去调用对象的某个行为。你尽管去调用这个行为就是了,其他的一切可以由多态来负责。规范来说,多态最根本的作用就是通过吧过程化的条件语句转化为对象的多态性,从而消除这些条件分支语句。
由于JavaScript中提到的关于多态的详细介绍并不多,这里简单的通过一个例子来介绍就好
//非多态 var hobby = function(animal){ if(animal == 'cat'){ cat.eat() }else if(animal == 'dog'){ dog.eat() } } var cat = { eat: function() { alert("fish!") } } var dog = { eat: function() { alert("meat!") } } console.log(123); hobby('cat'); //fish! hobby('dog'); //meat!
从上面的例子能看到,虽然 hobby 函数目前保持了一定的弹性,但这种弹性很脆弱的,一旦需要替换或者增加成其他的animal,必须改动hobby函数,继续往里面堆砌条件分支语句。我们把程序中相同的部分抽象出来,那就是吃某个东西。然后再重新编程。
//多态 var hobby = function(animal){ if(animal.eat instanceof Function){ animal.eat(); } } var cat = { eat: function() { alert("fish!") } } var dog = { eat: function() { alert("meat!") } }
现在来看这段代码中的多态性。当我们向两种 animal 发出 eat 的消息时,会分别调用他们的 eat 方法,就会产生不同的执行结果。对象的多态性提示我们,“做什么” 和 “怎么去做”是可以分开的,这样代码的弹性就增强了很多。即使以后增加了其他的animal,hobby函数仍旧不会做任何改变。
//多态 var hobby = function(animal){ if(animal.eat instanceof Function){ animal.eat(); } } var cat = { eat: function() { alert("fish!") } } var dog = { eat: function() { alert("meat!") } } var aoteman = { eat: function(){ alert("lil-monster!") } } hobby(cat); //fish! hobby(dog); //meat! hobby(aoteman); //lil-monster!