面向對象是一種編程思想,咱們經過類(構造函數)和對象實現的面向對象編程,知足下述三個特定:封裝、繼承和多態。
封裝即把實現一個功能的代碼封裝到一個函數中,之後實現這個功能,只須要執行該函數便可。實現低耦合,高內聚。javascript
如今咱們把屬性和方法封裝成一個對象:java
//建立一個對象 var person = new Object(); //添加屬性和方法 person.name = "鋼鐵俠"; person.sex = "男"; person.showName = function(){ alert("個人名字叫" + this.name);//個人名字叫鋼鐵俠 } person.showSex = function(){ alert("個人性別是" + this.sex);//個人性別是男 } person.showName(); person.showSex();
若是咱們想建立一個不一樣性別不一樣姓名的對象,就須要再寫一遍上述代碼:程序員
//建立一個對象 var person2 = new Object(); //添加屬性和方法 person2.name = "猩紅女巫"; person2.sex = "女"; person2.showName = function(){ alert("個人名字叫" + this.name);//個人名字叫猩紅女巫 } person2.showSex = function(){ alert("個人性別是" + this.sex);//個人性別是女 } person2.showName(); person2.showSex();
若是咱們想要建立多個對象的話,寫起來就很是麻煩,因此要去封裝建立對象的函數解決代碼重複的問題。編程
function createPerson(name, sex){ var person = new Object(); person.name = name; person.sex = sex; person.showName = function(){ alert("我叫" + this.name); } person.showSex = function(){ alert("我是" + this.sex + "的"); } return person; }
而後生成實例對象,就等因而在調用函數:設計模式
var p1 = createPerson("鋼鐵俠", "男"); p1.showName();//我叫鋼鐵俠 p1.showSex();//我是男的 var p2 = createPerson("猩紅女巫", "女"); p2.showName();//我叫猩紅女巫 p2.showSex();//我是女的
上述過程能夠類比爲開工廠生產酸奶:第一步:須要原料;第二步:加工酸奶;第三步:出廠售賣;咱們經過var聲明空對象的這一步就至關於第一步原料,添加屬性和函數就至關於第二步加工,經過return返回對象就至關於第三步出廠。這種符合上述一、二、3步驟的函數叫作工廠函數,這種設計函數的思路,叫作工廠設計模式。數組
官方函數建立對象的方法是經過new的方法,當咱們不使用new建立對象的時候,函數內部的this會指向窗口。app
function show(){ alert(this);//[object Window] } show();
因此當咱們在函數內部給this.name賦值爲xxxx時,能夠經過window.name輸出xxxx,由於若是這個函數沒有主人的話它的主人就是window對象。iphone
function show(){ alert(this);//[object Window] this.name = "xxxx"; } show(); alert(window.name);//xxxx
可是若是這個函數經過new運算符去調用,那麼這個函數中的this,就會指向新建立的對象。函數
function show(){ alert(this);//[object Object] } var obj = new show();
當咱們經過new運算符去調用函數的時候,它首部和尾部會自動的生成如下兩步:一、原料操做:強制改變this指向this = new Object();
三、出廠操做:將this返回return this;
。性能
function show(){ // this = new Object(); alert(this);//[object Object] this.name = "xxxx"; // return this; } var obj = new show(); alert(obj.name);//xxxx
因此如今咱們改造一下以前建立的函數,調用的時候所有都經過new去調用,而且將函數中的person改爲this。
function createPerson(name, sex){ this.name = name; this.sex = sex; this.showName = function(){ alert("我叫" + this.name); } this.showSex = function(){ alert("我是" + this.sex + "的"); } } var p1 = new createPerson("鋼鐵俠", "男"); p1.showName();//我叫鋼鐵俠 p1.showSex();//我是男的 var p2 = new createPerson("猩紅女巫", "女"); p2.showName();//我叫猩紅女巫 p2.showSex();//我是女的
咱們把這種能夠建立對象的函數,叫作構造函數。(功能就是用來構造對象)
function Person(name, sex){ this.name = name; this.sex = sex; }
爲了和別的函數,進行區分,咱們把構造函數首字母大寫。官方的構造函數:Array、Object、Date。
咱們經過typeof能夠看到官方經過new建立的Object、Array、Date本質上都是function函數。並且全部被該函數,建立的對象,對象的方法都是一套,arr1.push === arr2.push
返回值是true。
var arr = new Array(); var obj = new Object(); var d = new Date(); alert(typeof Array);//類型 function函數
可是經過調用函數生成的對象方法,彼此之間沒有聯繫,不能反映出它們是同一個原型對象的實例。alert(p1.showName === p2.showName);
返回值爲false。
var arr1 = new Array(10, 20, 30); var arr2 = new Array(40, 50, 60); alert(arr1.push === arr2.push); //true
咱們聲明兩個數組
var arr1 = [10, 20, 30, 40, 50]; var arr2 = [60, 70, 80, 90, 100];
給數組添加求和的函數
arr1.sum = function(){ var res = 0; for(var i = 0; i < this.length; i++){ res += this[i]; } return res; }
調用arr1.sum能夠輸出arr1的和爲150,可是調用arr2.sum會系統報錯,提示arr1.sum不是一個函數。由於arr1和arr2是單獨的兩個對象,給arr1添加一個方法,arr2並不會擁有這個方法。因此咱們以前經過new調用函數生成對象後,他們的方法是相互獨立的。
alert(arr1.sum == arr2.sum);//false
每個實例對象,都有本身的屬性和方法的副本。這不只沒法作到數據共享,也是極大的資源浪費。
prototype對象的引入:全部實例對象須要共享的屬性和方法,都放在這個對象中,那些不須要共享的屬性和方法,就放在構造函數中。以此來模擬類。
因此想讓arr2也擁有求和函數就須要再從新寫一個arr2.sum,這樣就會形成浪費,咱們想讓對象共用一個方法,這時候就須要引入原型prototype。在JS中一切皆對象,函數也是對象。 每個被建立的函數,都有一個官方內置的屬性,叫作prototype(原型)對象 ,咱們輸出一下show.protoype,獲得結果[object Object]
。
function show(){ } alert(show.protoype);//[object Object]
全部實例對象須要共享的屬性和方法,都放在這個對象裏面;那些不須要共享的屬性和方法,就放在構造函數裏面。
若是,咱們想要讓該函數建立出來的對象,公用一套函數,那麼咱們應該將這套函數,添加該函數的prototype原型。 因此咱們若是想讓兩個數組都擁有求和的方法,就須要將這個方法添加在Array的原型上。
Array.prototype.sum = function(){ var res = 0; for(var i = 0; i < this.length; i++){ res += this[i]; } return res; }
如今arr1和arr2均可以使用這個函數,而且arr1.sum == arr2.sum,他們使用的這個函數都是原型上的同一個方法。
alert(arr1.sum());//150 alert(arr2.sum());//400 alert(arr1.sum == arr2.sum);//true
咱們能夠經過混合法,讓用戶自定義構造函數,封裝一個能夠建立對象的函數,而且調用的是同一個方法。
function Person(name, sex){ //this = new Object(); this.name = name; this.sex = sex; //return this; } //函數,必須,添加在這個函數的prototype原型 Person.prototype.showName = function(){ alert("我叫" + this.name); } Person.prototype.showSex = function(){ alert("我是" + this.sex + "的"); } var p1 = new Person("鋼鐵俠", '男'); var p2 = new Person("猩紅女巫", "女"); p1.showName();//我叫鋼鐵俠 p1.showSex();//我是男的 p2.showName();//我叫猩紅女巫 p2.showSex();//我是女的 alert(p1.showName == p2.showName); //true
如今咱們要測試100輛不一樣品牌的汽車,記錄他們在道路上行駛的性能指數。
建立一個能夠構造各式各樣車的構造函數
function Car(type, name, speed){ this.type = type; this.name = name; this.speed = speed; }
在Car的原型上添加功能:讓車跑在路上,計算時速。
Car.prototype.run = function(road){ alert(`一輛${this.type}品牌的${this.name}系列,時速爲${this.speed}km/h的車,跑在長度爲${road.length}km的${road.name},最終的成績是${road.length / this.speed}小時`); }
建立一個能夠構造各式各樣馬路的構造函數
function Road(name, length){ this.name = name; this.length = length; }
添加第一個測試用例car1:
var kuahaidaqiao = new Road("跨海大橋", 1000); var car1 = new Car("大衆", "PASSAT", 100); car1.run(kuahaidaqiao);//一輛大衆品牌的PASSAT系列,時速爲100km/h的車,跑在長度爲1000km的跨海大橋,最終的成績是10小時
這就是面向對象編程,只寫一遍代碼,以後再要建立對象,只須要調用封裝好的函數。類和對象是面向對象編程的兩個語法,是面向對象實現的基礎,可是在JS中沒有類的概念,一切皆對象,全部的實例都是由Object構造函數構造出來的。因此當咱們有類的需求的時候,咱們自創了一個構造函數,來替代類的存在,因此構造函數的本質就是類。
爲了配合prototype
屬性,Javascript定義了一些關鍵字,幫助咱們使用它。
instanceof
格式:對象 instanceof 構造函數
功能:判斷這個對象是不是後面這個構造函數構造的。若是是,返回true;不然,返回false。
alert(car1 instanceof Car);//true alert(car1 instanceof Road);//false alert(car1 instanceof Object);//true
isPrototypeOf()
格式:構造函數.prototype.isPrototypeOf(對象)
功能:判斷某個proptotype
對象是否擁有某個實例,若是是,返回true;不然,返回false。
alert(Car.prototype.isPrototypeOf(car1)); //true alert(Road.prototype.isPrototypeOf(kuahaidaqiao)); //true
hasOwnProperty()
格式:對象.hasOwnProperty("屬性")
功能:實例對象一旦建立,將自動引用prototype對象的屬性和方法。也就是說,實例對象的屬性和方法,分紅兩種,一種是本地的,另外一種是引用的。每一個實例對象都有一個hasOwnProperty()
方法,用來判斷某一個屬性究竟是本地屬性,仍是繼承自prototype
對象的屬性。
Car.prototype.color = "white"; alert(car1.hasOwnProperty("name")) //true alert(car1.hasOwnProperty("color")) //false
in運算符
格式:"屬性"in對象
功能:in
運算符能夠用來判斷,某個實例是否含有某個屬性,不論是不是本地屬性。
alert("type" in car1); // true alert("length" in car1); // false
in運算符還能夠用來遍歷某個對象的全部屬性,包括添加在它身上的方法。
for(var prop in car1) { alert("car1["+prop+"]="+car1[prop]); }
ES6提供了簡單的定義類的語法糖class
構造函數的寫法:
function Iphone(size, color){ this.size = size; this.color = color; } Iphone.prototype.show = function(){ alert(`您選擇了一部${this.color}顏色的,內存大小是${this.size}GB的手機`); } var iphone1 = new Iphone(64, "玫瑰金"); iphone1.show();//您選擇了一部玫瑰金顏色的,內存大小是64GB的手機
類的寫法:
class Iphone{ constructor(size, color){ this.size = size; this.color = color; } show(){ alert(`您選擇了一部${this.color}顏色的,內存大小是${this.size}GB的手機`); } } var iphone2 = new Iphone(256, "黑色"); iphone2.show();//您選擇了一部黑色顏色的,內存大小是256GB的手機
因爲全部的實例對象共享同一個prototype對象,那麼從外界看起來,prototype對象就好像是實例對象的原型,而實例對象則好像"繼承"了prototype對象同樣。這就是Javascript繼承機制的設計思想。
繼承一方面是爲了實現面向對象,另外一方面爲了幫助你們更高效的編寫代碼,可讓一個構造函數繼承另外一個構造函數中的屬性和方法。
首先咱們定義一個People類
function Person(name, sex){ this.name = name; this.sex = sex; } Person.prototype.showName = function(){ alert("我叫" + this.name); } Person.prototype.showSex = function(){ alert("我是" + this.sex); }
如今咱們要在Person類的基礎上建立一個Worker類,擁有Person類的所有屬性和方法,同時添加它本身的屬性job,咱們能夠經過如下幾種方法實現繼承。
function Person(name, sex){ this.name = name; this.sex = sex; } Person.prototype.showName = function(){ alert("我叫" + this.name); } Person.prototype.showSex = function(){ alert("我是" + this.sex); }
第一種方法也是最簡單的方法,使用call或apply方法,將父對象的構造函數綁定在子對象上,這種繼承Person的方式叫作構造函數的假裝,由於Person對象本來應該只爲new的Person對象服務,可是它如今還能夠被Worker對象使用。
function Worker(name, sex, job){ //繼承Person的屬性 Person.call(this, name, sex); this.job = job;//添加本身的屬性 } var w1 = new Worker("小明", "男", "程序員");
call和apply的區別在於參數形式不一樣,call(obj, pra, pra)後面是單個參數。apply(obj, [args])後面是數組,做用都是強制改變this的指向。
function Worker(name, sex, job){ //繼承Person的屬性 //構造函數的假裝 Person.apply(this, arguments); this.job = job; }
如今Worker想繼承Person上的方法,Person方法都放在prototype中,prototype本質是對象,存儲的是引用數據類型,因此咱們不能直接將父級的方法賦值給子,繼承只能是單向的,子繼承父,可是不能影響父。
每個構造函數身上都會有一個prototype原型,咱們能夠將Person身上的prototype遍歷,在遍歷的過程當中將Person身上的函數取出,放入Worker的prototype中,這樣它們就不會互相影響了。
for(var i in Person.prototype){ Worker.prototype[i] = Person.prototype[i]; }
同時經過prototype來拓展本身的方法
Worker.prototype.showJob = function(){ alert("我是幹" + this.job + "工做的"); }
第二種方法更常見,使用prototype屬性。若是"Worker"的prototype對象,指向一個Person的實例,那麼全部"Worker"的實例,就能繼承Person了。
Worker.prototype = new Person();
每個prototype對象都有一個constructor屬性,指向它的構造函數。若是沒有"Worker.prototype = new Person();"這一行,Worker.prototype.constructor是指向Worker的;加了這一行之後,Worker.prototype.constructor指向Person。
alert(Worker.prototype.constructor == Person) //true
更重要的是,每個實例也有一個constructor屬性,默認調用prototype對象的constructor屬性。所以,在運行"Worker.prototype = new Person();"這一行以後,w1.constructor也指向Person。
var w1 = new Worker("小明", "男", "程序員"); alert(w1.constructor == Worker.prototype.constructor) //true alert(w1.constructor == Person.prototype.constructor) //true alert(w1.constructor == Person) //true
這顯然會致使繼承鏈的紊亂(w1明明是用構造函數Worker生成的),所以咱們必須手動糾正,將Worker.prototype對象的constructor值改成Worker。這是很重要的一點,編程時務必要遵照。
Worker.prototype.constructor = Worker alert(w1.constructor == Worker.prototype.constructor) //true alert(w1.constructor == Person.prototype.constructor) //false alert(w1.constructor == Person) //false
上面是採用prototype對象,實現繼承。咱們也能夠換一種思路,純粹採用"拷貝"方法實現繼承。簡單說,若是把父對象的全部屬性和方法,拷貝進子對象。Object.create()相似於數組中的concat方法,能夠建立一個新對象。這樣就能夠將父對象的prototype對象中的屬性,一一拷貝給Child對象的prototype對象。
//拷貝原有對象,建立新對象 Worker.prototype = Object.create(Person.prototype);
建立一個父親的構造函數
function Father(name, sex, age){ this.name = name; this.sex = sex; this.age = age; } Father.prototype.sing = function(){ alert(`父親的民歌唱得很是好`); }
再建立一個兒子構造函數,繼承父親的屬性和方法
function Son(name, sex, age, degree){ //構造函數的假裝 Father.call(this,name, sex, age); //拓展本身的屬性 this.degree = degree; } //繼承方法 //原型鏈 for(var i in Father.prototype[i]){ Son.prototype[i] = Father.prototype[i]; }
經過new建立一個兒子對象,執行son1.sing()調用父親原型上的方法。
var son1 = new Son("小明", "男", 20, "本科"); son1.sing();//父親的民歌唱得很是好
如今咱們重寫父級繼承的函數
Son.prototype.sing = function(){ alert(`唱搖滾`); }
再次調用son1.sing(),執行的是son1本身添加的sing方法。
var son1 = new Son("小明", "男", 20, "本科"); son1.sing();/唱搖滾
對於父親自身的sing方法沒有影響,在子級重寫的方法只在子級生效。
var f2 = new Father("大明", "男", 40); f2.sing();//父親的民歌唱得很是好
如今咱們再來看繼承和多態的概念,其實它們都是繼承某一部分,是同一件事情的兩個側重。繼承側重於從父級繼承到屬性和方法,而多態側重於本身拓展的屬性和方法,也就是重寫的內容。簡單來講凡是跟父級同樣的部分叫繼承,不同的部分叫多態。
注意:雖然Son繼承了Father的屬性和方法,可是經過Son構造函數new出來的對象不屬於Father。
alert(son1 instanceof Son) //true alert(f2 instanceof Father) //true alert(son1 instanceof Father) //false alert(son1 instanceof Object) //true
一樣的案例使用ECMA6 class類的方法來實現繼承和多態,對比原來的方法更簡單,更形象。
class Father{ constructor(name, sex, age){ this.name = name; this.sex = sex; this.age = age; } //聲明方法 sing(){ alert("會唱民歌"); } } /* 繼承Father建立一個子類Son 經過extends繼承 */ class Son extends Father{ constructor(name, sex, age, degree){ super(name, sex, age); } sing(){ alert("唱搖滾"); } } var son1 = new Son("小明", "男", 30, "本科"); alert(son1.name);//小明 son1.sing();//唱搖滾 var f1 = new Father("大明", "男", 40); alert(f1.name);//大明 f1.sing();//會唱民歌
在javascript中,每一個對象都有一個指向它的 原型(prototype)對象的內部連接。每一個原型對象又有本身的原型,直到某個對象的原型爲null爲止,組成這條鏈的最後一環。
在構造函數有一個prototype的屬性,經過構造函數構造出來的對象有_ _proto _ _屬性,指向構造該對象的構造函數的原型,它還有一個名字叫魔術變量。
也就是說經過Son構造函數構造出來的對象son1的_ _proto _ _徹底等於Son的原型prototype。
alert(son1.__proto__ === Son.prototype);//true alert(f2.__proto__ === Father.prototype);//true
這也就解釋了爲何經過同一個構造函數構造出來的對象,使用的都是同一套函數,由於經過該構造函數構造出來全部對象的_ _proto _ _就指向該構造函數的原型。