淺談JavaScript的面向對象和它的封裝、繼承、多態(tài)
寫在前面
既然是淺談,就不會從原理上深度分析,只是幫助我們更好地理解...
面向對象與面向過程
面向對象和面向過程是兩種不同的編程思想,剛開始接觸編程的時候,我們大都是從面向過程起步的,畢竟像我一樣,大家接觸的***門計算機語言大概率都是C語言,C語言就是一門典型的面向過程的計算機語言。
面向過程主要是以動詞為主,解決問題的方式是按照順序一步一步調用不同的函數(shù)。
面向對象是以名詞為主,將問題抽象出具體的對象,而這個對象有自己的屬性和方法,在解決問題的時候,是將不同的對象組合在一起使用。
- //面向過程裝大象
- 1.開(冰箱)
- 2.(大象)裝進(冰箱)
- 3.關(冰箱)
- //面向對象裝大象
- 1. 冰箱.開門()
- 2. 冰箱.裝進(大象)
- 3. 冰箱.關門()
從這個例子可以看出,面向對象是以主謂為主,將主謂堪稱一個一個的對象,然后對象有自己的屬性和方法。
面向對象是以功能來劃分問題的,而不是步驟。功能上的統(tǒng)一保證了面向對象設計的可擴展性,解決了代碼重用性的問題。
這也是在漫長的程序設計的發(fā)展過程中得到的驗證結果,面向對象的編程思想較之于面向過程較好一點
封裝
面向對象有封裝、繼承和多態(tài)三大特性。
封裝:就是把事物封裝成類,隱藏事物的屬性和方法的實現(xiàn)細節(jié),僅對外公開接口。
在ES5中,并沒有class的概念,但是由于js的函數(shù)級作用域(函數(shù)內部的變量函數(shù)外訪問不到)。所以我們可以模擬class。在es5中,類其實就是保存了一個函數(shù)的變量,這個函數(shù)有自己的屬性和方法。將屬性和方法組成一個類的過程就是封裝。
1.通過構造函數(shù)添加
JavaScript提供了一個構造函數(shù)(Constructor)模式,用來在創(chuàng)建對象時初始化對象。構造函數(shù)其實就是普通的函數(shù),只不過有以下的特點
- ①首字母大寫(建議構造函數(shù)首字母大寫,即使用大駝峰命名,非構造函數(shù)首字母小寫)
- ②內部使用this
- ③使用new生成實例
通過構造函數(shù)添加屬性和方法實際上也就是通過this添加的屬性和方法。因為this總是指向當前對象的,所以通過this添加的屬性和方法只在當前對象上添加,是該對象自身擁有的。所以我們實例化一個新對象的時候,this指向的屬性和方法都會得到相應的創(chuàng)建,也就是會在內存中復制一份,這樣就造成了內存的浪費。
- 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的方式添加屬性和方法會導致內存浪費的現(xiàn)象,有什么辦法可以讓實例化的類所使用的屬性和方法 直接使用指針 指向同一個屬性和方法。
這就是原型的方法
- JavaScript規(guī)定,每一個構造函數(shù)都有一個prototype屬性,指向另一個對象。這個對象的所有屬性和方法,都會被構造函數(shù)的實例繼承。
- 也就是說,對于那些不變的屬性和方法,我們可以直接將其添加在類的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聲明一個類
①構造器:構造器內創(chuàng)建自有屬性
②方法:聲明類實例具有的方法
- class Cat {
- //等價于Cat構造器
- constructor(name) {
- this.name = name;
- }
- //更加簡單的聲明類的內部函數(shù)
- //等價于 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類是一個具有構造函數(shù)行為的函數(shù),其中內部方法eat實際上就是Cat.prototype.eat()
所以說es6的class封裝類,本質上是es5實現(xiàn)方式的語法糖
最主要的區(qū)別在于,class類的屬性是不可重新賦值和不可枚舉的,Cat.prototype就是一個只讀屬性
class和自定義類型的區(qū)別
(1)class的聲明不會提升,與let類似
(2)class的聲明自動運行于嚴格模式之下
(3)class聲明的方法不可枚舉
(4)class的內部方法沒有 constructor 屬性,無法new
(5)調用class的構造函數(shù)必須new
(6)class內部方法不能同名
class類的使用
class作為js中的一級公民,可以被當作值來直接使用
- //1.類名作為參數(shù)傳入函數(shù)
- function createObj (ClassName) {
- return new ClassName()
- }
- //2.立即執(zhí)行,實現(xiàn)單例模式
- 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對象的作用就是為類的原型添加共有的方法的,但是類不能直接訪問這些方法,只有將類實例化之后,新創(chuàng)建的對象復制了父類構造函數(shù)的屬性和方法,并將原型 proto 指向了父類的原型對象。這樣子類就可以訪問父類的屬性和方法,同時,父類中定義的屬性和方法不會被子類繼承。
but使用類繼承的方法,如果父類的構造函數(shù)中有引用數(shù)據(jù)類型,就會在子類中被所有實例共用,因此一個子類的實例如果更改了這個引用數(shù)據(jù)類型,就會影響到其他子類的實例。
構造函數(shù)繼承
為了克服類繼承的缺點,才有了構造函數(shù)繼承,構造函數(shù)繼承的核心思想就是SuperClass.call(this, id),直接改變this的指向,使通過this創(chuàng)建的屬性和方法在子類中復制一份,因為是單獨復制的,所以各個實例化的子類互不影響。but會造成內存浪費的問題
- //構造函數(shù)繼承
- //聲明父類
- 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']
組合式繼承
組合式繼承是汲取了兩者的優(yōu)點,既避免了內存浪費,又使得每個實例化的子類互不影響。
- //組合式繼承
- //聲明父類
- 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警告:組合式繼承方法固然好,但是會導致一個問題,父類的構造函數(shù)會被創(chuàng)建兩次(call()的時候一遍,new的時候又一遍)
寄生組合繼承
組合式繼承的缺點的關鍵是 父類的構造函數(shù)在類繼承和構造函數(shù)繼承的組合形式被創(chuàng)建了兩邊,但是在類繼承中我們并不需要創(chuàng)建父類的構造函數(shù),我們只要子類繼承父類的原型即可。
所以我們先給父類的原型創(chuàng)建一個副本,然后修改子類的 constructor 屬性,***在設置子類的原型就可以了
- //原型式繼承
- //原型式繼承其實就是類式繼承的封裝,實現(xiàn)的功能返回一個實例,該實例的原型繼承了傳入的o對象
- function inheritObject(o) {
- //聲明一個過渡函數(shù)
- 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", "韓二虎"]
多態(tài)
多態(tài)實際上是不同對象作用與同一操作產(chǎn)生不同的效果。多態(tài)的思想實際上是把 “想做什么” 和 “誰去做” 分開。
多態(tài)的好處在于,你不必再向對象詢問“你是什么類型”后根據(jù)得到的答案再去調用對象的某個行為。你盡管去調用這個行為就是了,其他的一切可以由多態(tài)來負責。規(guī)范來說,多態(tài)最根本的作用就是通過吧過程化的條件語句轉化為對象的多態(tài)性,從而消除這些條件分支語句。
由于JavaScript中提到的關于多態(tài)的詳細介紹并不多,這里簡單的通過一個例子來介紹就好
- //非多態(tài)
- 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 函數(shù)目前保持了一定的彈性,但這種彈性很脆弱的,一旦需要替換或者增加成其他的animal,必須改動hobby函數(shù),繼續(xù)往里面堆砌條件分支語句。我們把程序中相同的部分抽象出來,那就是吃某個東西。然后再重新編程。
- //多態(tài)
- var hobby = function(animal){
- if(animal.eat instanceof Function){
- animal.eat();
- }
- }
- var cat = {
- eat: function() {
- alert("fish!")
- }
- }
- var dog = {
- eat: function() {
- alert("meat!")
- }
- }
現(xiàn)在來看這段代碼中的多態(tài)性。當我們向兩種 animal 發(fā)出 eat 的消息時,會分別調用他們的 eat 方法,就會產(chǎn)生不同的執(zhí)行結果。對象的多態(tài)性提示我們,“做什么” 和 “怎么去做”是可以分開的,這樣代碼的彈性就增強了很多。即使以后增加了其他的animal,hobby函數(shù)仍舊不會做任何改變。
- //多態(tài)
- 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!