Javascript基於對象的三大特征和C++,Java面向對象的三大特征一樣,都是封裝(encapsulation)、繼承(inheritance )和多態(polymorphism )。只不過實現的方式不同,其基本概念是差不多的。其實除三大特征之外,還有一個常見的特征叫做抽象(abstract),這也就是我們在一些書上有時候會看到面向對象四大特征的原因了。
一、封裝性
封裝就是把抽象出來的數據和對數據的操作封裝在一起,數據被保護在內部,程序的其它部分只有通過被授權的操作(成員方法),才能對數據進行操作。
JS封裝只有兩種狀態,一種是公開的,一種是私有的。
案例:
function Person(name,sal){ this.name=name; //公開 var sal=sal; //私有 this.showInfo=function(){ //公開 window.alert(this.name+" "+sal); } function showInfo2(){ //把函數私有化 window.alert("你好"+this.name+" "+sal); } } var p1 = new Person('Cece', 20, 10000); window.alert(p1.name + " is " +p1.age); //Cece is undefined p1.showInfo();//Cece 20 p1.showInfo2();//VM302:1 Uncaught TypeError: p1.showInfo2 is not a function(…)
構造函數方式與原型方式給對象添加方法的區別:
//1.通過構造函數方式給對象添加方法 function Dog(name){ this.name=name; this.shout=function(){ window.alert("小狗尖叫"+this.name); } } var dog1=new Dog("aa"); var dog2=new Dog("bb"); if(dog1.shout==dog2.shout){ window.alert("相等"); }else{ window.alert("不相等"); } //會輸出“不相等”
//2.通過原型方式給對象添加方法 function Dog(name){ this.name=name; } Dog.prototype.shout=function(){ window.alert("小狗尖叫"+this.name); } var dog1=new Dog("aa"); var dog2=new Dog("bb"); if(dog1.shout==dog2.shout){ window.alert("相等"); }else{ window.alert("不相等"); } //會輸出“相等”
說明通過構造函數來分配成員方法,給每個對象分配一份獨立的代碼。這樣的弊端就是如果對象實例有很多,那函數的資源占用就會很大,而且有可能造成內存泄漏。
而原型法是大家共享同一份代碼,就不會有那種弊端。
因此,通過構造函數添加成員方法和通過原型法添加成員方法的區別:
1.通過原型法分配的函數是所有對象共享的;
2.通過原型法分配的屬性是獨立的;(如果你不修改屬性,他們是共享)
3.如果希望所有的對象使用同一個函數,最好使用原型法添加方法,這樣比較節省內存。
特別強調:我們前面學習的通過prototype給所有的對象添加方法,但是這種方式不能去訪問類的私有變量和方法。案例:
function Person(){ this.name="Cece"; var age=18; this.abc=function(){ //公開 window.alert("abc"); } function abc2(){ //私有 window.alert("abc"); } } Person.prototype.fun1=function(){ window.alert(this.name);//Cece //window.alert(age);//Uncaught ReferenceError: age is not defined(…) //abc2(); //Uncaught ReferenceError: abc2 is not defined(…) this.abc(); //abc } var p1=new Person(); p1.fun1();
二、繼承性
繼承可以解決代碼復用,讓編程更加靠近人類思維。當多個類存在相同的屬性(變量)和方法時,可以從這些類中抽象出父類,在父類中定義這些相同的屬性和方法,所有的子類不需要重新定義這些屬性和方法,只需要通過繼承父類中的屬性和方法。
JS中實現繼承的方式:
1.類繼承:
(1)對象冒充
案例:
//1.把子類中共有的屬性和方法抽取出,定義一個父類Stu function Stu(name, age){ this.name = name; this.age = age; this.show = function(){ window.alert(this.name + " " + this.age); } } function MidStu(name, age) { this.stu = Stu; // 通過對象冒充來實現繼承的 // 對象冒充的意思就是獲取那個類的所有成員,因為js是誰調用那個成員就是誰的,這樣MidStu就有了Stu的成員了 this.stu(name, age); this.payFee = function(){ window.alert("繳費" + money * 0.8); } } function Pupil(name, age) { this.stu = Stu; // 通過對象冒充來實現繼承的 this.stu(name, age); this.payFee = function(){ window.alert("繳費" + money * 0.5); } } var midStu = new MidStu("zs", 13); midStu.show(); var pupil = new Pupil("ls", 10); pupil.show();
(2)通過call或者apply實現
案例:
//1.把子類中共有的屬性和方法抽取出,定義一個父類Stu function Stu(name,age){ //window.alert("確實被調用."); this.name=name; this.age=age; this.show=function(){ window.alert(this.name+"年齡是="+this.age); } } //2.通過call或者apply來繼承父類的屬性的方法 function MidStu(name,age){ //這里這樣理解: 通過call修改了Stu構造函數的this指向, //讓它指向了調用者本身. Stu.call(this,name,age); //如果用apply實現,則可以 //Stu.apply(this,[name,age]); //說明傳入的參數是 數組方式 //可以寫MidStu自己的方法. this.pay=function(fee){ window.alert("你的學費是"+fee*0.8); } } function Pupil(name,age){ Stu.call(this,name,age);//當我們創建Pupil對象實例,Stu的構造函數會被執行,當執行后,我們Pupil對象就獲取從 Stu封裝的屬性和方法 //可以寫Pupil自己的方法. this.pay=function(fee){ window.alert("你的學費是"+fee*0.5); } } //測試 var midstu=new MidStu("zs",15); var pupil=new Pupil("ls",12); midstu.show(); midstu.pay(100); pupil.show(); pupil.pay(100);
2.原型繼承
原型繼承是js中最通用的繼承方式,不用實例化對象,通過直接定義對象,並被其他對象引用,這樣形成的一種繼承關系,其中引用對象被稱為原型對象。
function A(){ this.color = 'red'; } function B(){} function C(){} B.prototype = new A(); C.prototype = new B(); // 測試原型繼承 var c = new C(); console.log(c.color); // red
原型繼承顯得很簡單,不需要每次構造都調用父類的構造函數,也不需要通過復制屬性的方式就能快速實現繼承。但它也存在一些缺點:
③ 占用內存多,每次繼承都需要實例化一個父類,這樣會存在內存占用過多的問題。
3.復制繼承(知道就好)
function A(){ this.color = 'red'; } A.prototype.say = function() { console.log(this.color); } var a = new A(); var b = {}; // 開始拷貝 for(var item in a) { b[item] = a[item]; } // 開始測試 console.log(b.color); // red b.say(); // red
封裝后:
Function.prototype.extend = function(obj){ for(item in obj){ this.constructor.prototype[item] = obj[item]; } } function A(){ this.color = 'green'; } A.prototype.say = function(){ console.log(this.color); } // 測試 var b = function(){}; b.extend(new A()); b.say(); // green
復制繼承實際上是通過反射機制復制類對象中的可枚舉屬性和方法來模擬繼承。這種可以實現多繼承。但也有缺點:
⑥ 復制繼承僅僅是簡單的引用賦值,如果父類成員包含引用類型,那么也會帶來很多副作用,如不安全,容易遭受污染等。
4.混合繼承(構造+原型)
而寫在類中的方法,實例化的時候會在每個實例中再復制一份,所以消耗的內存更高。
所以沒有特殊原因,我們一般把屬性寫到類中,而行為寫到原型中。
2、構造函數中定義的屬性和方法要比原型中定義的屬性和方法的優先級高,如果定義了同名稱的屬性和方法,構造函數中的將會覆蓋原型中的。
function A(x,y){ this.x = x; this.y = y; } A.prototype.add = function(){ return (this.x-0) + (this.y-0); } function B(x,y){ A.call(this,x,y); } B.prototype = new A(); // 測試 var b = new B(2,1); console.log(b.x); // 2 console.log(b.add()); // 3
5.多重繼承
function A(x){ this.x = x; } A.prototype.hi = function(){ console.log('hi'); } function B(y){ this.y = y; } B.prototype.hello = function(){ console.log('hello'); } // 給Function增加extend方法 Function.prototype.extend = function(obj) { for(var item in obj) { this.constructor.prototype[item] = obj[item]; } } // 在類C內部實現繼承 function C(x,y){ A.call(this,x); B.call(this,y); }; C.extend(new A(1)); C.extend(new B(2)); // 通過復制繼承后,C變成了一個對象,不再是構造函數了,可以直接調用 C.hi(); // hi C.hello(); // hello console.log(C.x); // 1 console.log(C.y); // 2
在js中實現類繼承,需要設置3點:
類的構造函數中的成員,一般稱之為本地成員,本地成員繼承可以用call 和 apply。而類的原型成員就是類的原型中的成員。
關於繼承更多知識參考:面向對象在javascript中的三大特征之繼承
三、多態性
JS的函數重載
這個是多態的基礎,在之前的Javascript入門已經說過了,JS函數不支持多態,但是事實上JS函數是無態的,支持任意長度,類型的參數列表。如果同時定義了多個同名函數,則以最后一個函數為准。
案例1:js不支持重載
/*****************說明js不支持重載*****/ function Person(){ this.test1=function (a,b){ window.alert('function (a,b)'); } this.test1=function (a){ window.alert('function (a)'); } } var p1=new Person(); //js中不支持重載. //但是這不會報錯,js會默認是最后同名一個函數,可以看做是后面的把前面的覆蓋了。 p1.test1("a","b"); p1.test1("a");
案例2:js如何實現重載
//js怎么實現重載.通過判斷參數的個數來實現重載 function Person(){ this.test1=function (){ if(arguments.length==1){ this.show1(arguments[0]); }else if(arguments.length==2){ this.show2(arguments[0],arguments[1]); }else if(arguments.length==3){ this.show3(arguments[0],arguments[1],arguments[2]); } } this.show1=function(a){ window.alert("show1()被調用"+a); } this.show2=function(a,b){ window.alert("show2()被調用"+"--"+a+"--"+b); } function show3(a,b,c){ window.alert("show3()被調用"); } } var p1=new Person(); //js中不支持重載. p1.test1("a","b"); p1.test1("a");
1、多態基本概念
多態是指一個引用(類型)在不同情況下的多種狀態。也可以理解成:多態是指通過指向父類的引用,來調用在不同子類中實現的方法。
案例:
// Master類 function Master(name){ this.nam=name; //方法[給動物喂食物] } //原型法添加成員函數 Master.prototype.feed=function (animal,food){ window.alert("給"+animal.name+" 喂"+ food.name); } function Food(name){ this.name=name; } //魚類 function Fish(name){ this.food=Food; this.food(name); } //骨頭 function Bone(name){ this.food=Food; this.food(name); } function Peach(name){ this.food=Food; this.food(name); } //動物類 function Animal(name){ this.name=name; } //貓貓 function Cat(name){ this.animal=Animal; this.animal(name); } //狗狗 function Dog(name){ this.animal=Animal; this.animal(name); } //猴子 function Monkey(name){ this.animal=Animal; this.animal(name); } var cat=new Cat("貓"); var fish=new Fish("魚"); var dog=new Dog("狗"); var bone=new Bone("骨頭"); var monkey=new Monkey("猴"); var peach=new Peach("桃"); //創建一個主人 var master=new Master("zs"); master.feed(dog,bone); master.feed(cat,fish); master.feed(monkey,peach);
多態利於代碼的維護和擴展,當我們需要使用同一類樹上的對象時,只需要傳入不同的參數就行了,而不需要再new 一個對象。
以上就是Javascript基於對象三大特性。
附錄:js中創建對象的各種方法(現在最常用的方法是組合模式)。
1)原始模式
//1.原始模式,對象字面量方式 var person = { name: 'Jack', age: 18, sayName: function () { alert(this.name); } }; //1.原始模式,Object構造函數方式 var person = new Object(); person.name = 'Jack'; person.age = 18; person.sayName = function () { alert(this.name); };
顯然,當我們要創建批量的person1、person2……時,每次都要敲很多代碼,資深copypaster都吃不消!然后就有了批量生產的工廠模式。
2)工廠模式
//2.工廠模式,定義一個函數創建對象 function creatPerson (name, age) { var temp = new Object(); person.name = name; person.age = age; person.sayName = function () { alert(this.name); }; return temp; }
工廠模式就是批量化生產,簡單調用就可以進入造人模式。指定姓名年齡就可以造一堆小寶寶啦,解放雙手。但是由於是工廠暗箱操作的,所以你不能識別這個對象到底是什么類型(instanceof 測試為 Object),另外每次造人時都要創建一個獨立的temp對象,代碼臃腫。
3)構造函數
//3.構造函數模式,為對象定義一個構造函數 function Person (name, age) { this.name = name; this.age = age; this.sayName = function () { alert(this.name); }; } var p1 = new Person('Jack', 18); //創建一個p1對象 Person('Jack', 18); //屬性方法都給window對象,window.name='Jack',window.sayName()會輸出Jack
構造函數與C++、JAVA中類的構造函數類似,易於理解,另外Person可以作為類型識別(instanceof 測試為 Person 、Object)。但是所有實例依然是獨立的,不同實例的方法其實是不同的函數。這里把函數兩個字忘了吧,把sayName當做一個對象就好理解了,就是說張三的 sayName 和李四的 sayName是不同的存在,但顯然我們期望的是共用一個 sayName 以節省內存。
4)原型模式
//4.原型模式,直接定義prototype屬性 function Person () {} Person.prototype.name = 'Jack'; Person.prototype.age = 18; Person.prototype.sayName = function () { alert(this.name); }; //4.原型模式,字面量定義方式 function Person () {} Person.prototype = { name: 'Jack', age: 18, sayName: function () { alert(this.name); } };
var p1 = new Person(); //name='Jack' var p2 = new Person(); //name='Jack'
這里需要注意的是原型屬性和方法的共享,即所有實例中都只是引用原型中的屬性方法,任何一個地方產生的改動會引起其他實例的變化。
5)混合模式(構造+原型)
//5. 原型構造組合模式, function Person (name, age) { this.name = name; this.age = age; } Person.prototype = { hobby: ['running','football']; sayName: function () { alert(this.name); }, sayAge: function () { alert(this.age); } }; var p1 = new Person('Jack', 20); //p1:'Jack',20; __proto__: ['running','football'],sayName,sayAge var p2 = new Person('Mark', 18); //p1:'Mark',18;__proto__: ['running','football'],sayName,sayAge
做法是將需要獨立的屬性方法放入構造函數中,而可以共享的部分則放入原型中,這樣做可以最大限度節省內存而又保留對象實例的獨立性。