面向對象的理解
前段時間來到深圳,一時不習慣這邊的生活習慣什麽的,沒有心情去寫,差點把習慣給丟了,也好久沒去打過球了,胖了許多!但不管怎樣,路還得走,技術還得學
面向對象
(Object Oriented,OO)是語言的一個標誌,那就是它們都有類的概念,而通過類可以創建任意多個具有相同屬性和方法的對象,因為js中沒有類的概念,所以js的對象和基於類的語言中的對象有所不同,(也就是說,js中的對象是沒有類的概念)
簡單理解對象
var person = new Object();//創建一個空對象賦值給person
person.name = "如花";/給對象設置屬性
person.age = 29;
person.job = "演員";
person.sayName = function(){//給對象設置方法
alert(this.name);
}
以下是字面量方式寫法 和上面的例子是一樣的
var person = {
name : "似玉",
age : 29,
job : "歌手",
sayName : function(){
alert(this.name);
}
}
屬性類型(對屬性的控制)
js中有兩種屬性:數據屬性和訪問器屬性
數據屬性:
數據屬性包含一個數據值的位置,在這個位置的讀取和寫入值,
##數據屬性有四個行為特性
1.[[Configurable]]:表示能否通過delete刪除屬性從而重新定義屬性,能否修改,或者修改成訪問器屬性, 默認為true
2.[[Enumerable]]:表示能否通過for-in循環返回屬性,默認為true
3.[[Writable]]:表示能否修改屬性的值,默認為true
4.[[Value]]:包含這個屬性的數據值,讀取屬性的時候從這個位置讀取,寫入的時候也是從這個位置寫入*/
// 例如:
/*var person = {
name:‘大頭‘
};*/
//創建一個名為name的屬性,指定它的值[[Value]]為"大頭",對這個值的任何修改都會在這裏體現
如何控制?
Object.definePropenty()【修改屬性默認特性】來控制 接受三個值:1.對象名,2.和對象的屬性相同名字的值,3.描述符(就是json格式,寫入四個行為屬性,並且修改)
例子:
var Person = {};//創建一個空屬性
Object.definePropenty(Person,"name",{//調用方法
Writable:false,//禁止修改屬性
value:"小頭"//初始化修改name對應的value值
});
alert(person.name);//小頭
Person.name = ‘大頭‘;
alert(person.name);//小頭*/
// 在非嚴格模式下被忽略,在嚴格模式下會拋出錯誤
但是:把Configurable設置成false,表示不能從對象中刪除,如果對這個屬性調用delete,則在非嚴格模式下沒有什麽發生,但在嚴格模式會導致錯誤,而且,一但把屬性定義為不可配置的,就不能再把它變回可配置的(不可逆的修改),此時,再調用Object.definePropenty()方法修改,除了writable之外的特點特性,都會報錯
var Person = {};//創建一個空屬性
Object.definePropenty(Person,"name",{//調用方法
Configurable:false,//禁止刪除屬性
value:"小頭"//初始化修改name對應的value值
});
alert(person.name);//小頭
delete Person.name;
alert(person.name);//小頭
工廠模式
工廠模式是軟件工程領域的一種廣為人知的設計模式,這種模式抽象了創建具體對象的過程,考慮到js中無法創建類,開發人員發明了一種函數,用函數來封裝以特定接口創建對象的細節。
function createPerson(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
}
return o;
}
var person1 = createPerson(‘大頭‘,18,‘doctor‘);
var person2 = createPerson(‘小頭‘,28,‘message‘);
工廠模式雖然解決了創建多個相似對象的問題,但卻沒有解決對象識別問題(即怎樣知道一個對象的類型),因此,js又出現了新的模式
構造函數,(工廠模式,構造函數,區別)
構造函數用來創建特定類型的對象,像object 和array這樣的原生構造函數,在運行時會自動出現在執行環節中,此外,也可以創建自定義構造函數,從而定義對象的類型和屬性方法,例如:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 =new Person(‘大頭‘,18,‘doctor‘);
var person2 =new Person(‘小頭‘,28,‘message‘);
/*創建構造函數要經歷一下四個步驟
1.創建一個新對象
2.將構造函數的作用域賦給新對象(因此this就指向這個新對象)
3.執行構造函數中的代碼(給新對象添加屬性方法)
4.返回新對象
註意:與工廠模式不同之處
1.沒有明顯的創建對象
2.直接將屬性和方法賦給了this對象
3.沒有return語句
註釋:按照慣例:構造函數始終應該以一個大寫的字母開頭,所以Person的p用大寫,而非構造函數的函數表達式,建議使用小寫開頭,以示區分!
constructor屬性
對象都有一個constructor的屬性,用來標識對象的類型,也能指定對象的類型*/
alert(person1.constructor == Person);//true
alert(person2.constructor == Person);//true
默認情況下,原型對象會自動獲取一個constructor(構造函數)屬性,這個屬性包含一個指向Person屬性所在函數的指針。就拿前面的例子說,Person1.prototype.constructor指向Person,而通過這個構造函數,我們還可以繼續為原型對象添加其他屬性和方法
instanceof屬性
用於檢測對象類型*/
alert(person1 instanceof Person);//true
alert(person1 instanceof Object);//true
alert(person2 instanceof Person);//true
alert(person2 instanceof Object);//true
創建自定義的構造函數意味著將來可以將它的實例標識為一種特定類型,而person1和person2之所以都是object,是因為所有對象均繼承自object
構造函數和普通函數的區別:
- 構造函數和其他函數的唯一區別,就在於調用方式不一樣,但,構造函數也是函數,不存在定義構造函數的特殊語法。
- 任何函數,通過new操作符調用的,都可以作為構造函數,而任何函數,不用new操作符來調用,和普通函數也沒什麽區別。
構造函數的問題:
構造函數雖然很好用,但並不是沒有缺點。主要問題是每個方法都要在每個實例上重新創建一遍,在前面例子中sayName方法,雖然都是同一個名字,但兩個方法不是同一個function實例(每一個實例化的對象,都是一個新的函數 this.sayName = function(){})
alert(person1.sayName == person2.sayName);//false 使用的不是同一個sayName方法
// 可以使用一下方式解決問題
function Person (name,age,job){
this.name = name,
this.age = age,
this.job = job,
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 =new Person(‘大頭‘,18,‘doctor‘);
var person2 =new Person(‘小頭‘,28,‘message‘);
person1.sayName();//大頭
person2.sayName();//小頭
alert(person1.sayName == person2.sayName);//true
在以上例子中,把sayName函數定義在構造函數外,當做全局變量,這樣一來,調用時,每次創建的實例都只是得到一個指針,指向全局變量的sayName函數,這樣就能解決了兩個實例用一個方法的問題
原型模式
在全局作用域中定義的函數實際上只能被某個對象調用,這讓全局作用域有點名不副實,而更讓人無法接受的是,如果對象需要定義很多方法時,那麽就要定義很多個全局函數,於是,我們這個自定義的引用類就絲毫沒有封裝性可言,好在,這些問題可以通過使用原型模式解決。
//我們創建的每個函數都有一個prototype(原型)屬性,這個屬性是一個(指針),指向一個對象,而這個對象的用途是包含了由特定類型的所有實例共享的屬性和方法。
//(通過調用構造函數而創建的那個對象實例的原型對象)(很難理解?)
//使用原型對象的好處是可以讓所有的對象實例共享它所包含的屬性和方法
function Person(){}
Person.prototype.name = ‘大頭‘;
Person.prototype.age = 17;
Person.prototype.job = ‘message‘;
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.sayName();//大頭
person2.sayName();//大頭
alert(person1.sayName == person2.sayName);//true
// ===========更簡單的原型語法================
function Person(){}
Person.prototype = {
name : "大頭",
age : 19,
job : ‘message‘,
sayName : function(){
alert(this.name);
}
}
但 constructor屬性不再指向Person,在本質上完全重寫了默認的prototype對象,因此constructor屬性也就變成了新對象的constructor屬性,不再指向Person函數,所以需要特意的將constructor指定一下Person(如果認為很重要的話)
function Person(){}
Person.prototype = {
constructor : Person,
name : "大頭",
age : 19,
job : ‘message‘,
sayName : function(){
alert(this.name);
}
}
在讀取代碼時,解析器首先會去搜索實例本身,在實例上查找所調用的方法和屬性,如果找到了特定名字的屬性或方法,則返回該屬性或方法,如果沒有找到,則繼續搜索指針指向的原型對象,在原型對象裏面查找給定的屬性和方法。
function Person(){}
Person.prototype.name = ‘大頭‘;
Person.prototype.age = 17;
Person.prototype.job = ‘message‘;
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = ‘大傻‘;
alert(person1.name);//大傻 --來自實例的name值
alert(person2.name);//大頭 --來自原型的name值
// 原型和in操作符
有兩種方式使用in操作符
1.for-in循環中使用
2.單獨使用:in操作符會通過對象能夠訪問給定屬性時返回true,無論該屬性存在於實例中還是原型中
不可枚舉的屬性,constructor
// Object.keys()方法 接受一個對象,返回一個包含所有可枚舉屬性的字符串數組
function Person(){}
Person.prototype.name = ‘大頭‘;
Person.prototype.age = 17;
Person.prototype.job = ‘message‘;
Person.prototype.sayName = function(){
alert(this.name);
};
var keys = Object.keys(Person.prototype);
console.log(keys);//"name","age","job","sayName"
原型對象的缺點
- 首先,它省略了為構造函數傳遞初始化參數的環節,在默認情況下都會取相同的屬性值,雖然這會在某種程度上帶來一些不方便
- 而最大的問題是,由其共享的本性所導致的。原型中所有屬性是被很多實例共享的,這種共享對於函數非常合適,對於那些包含基本值的屬性倒也說得過去,然而,對於包含引用類型值的屬性來說,問題就比較突出了
function Person(){} Person.prototype = { constructor : Person, name : "大頭", age : 19, job : ‘message‘, friends : ["傻臉娜","呆蒙"], sayName : function(){ alert(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push(‘vue‘); alert(person1.friends);//傻臉娜,呆蒙,vue alert(person2.friends);//傻臉娜,呆蒙,vue console.log(person1.friends === person2.friends);//true*/ // 就是在原型當中給到一個引用類型的屬性時,每個實例都會使用共享裏面的方法,同時,friend是引用類型,因此每個實例訪問或修改了friends,其他實例引用時都會發生改變
組合使用構造函數和原型模式
function Person(name,age,job){//構造函數創建實例的內容是單獨的,不能共享
this.name = name;
this.age = age;
this.job = job;
this.friends = ["zhangsan","lisi"];//引用類型
}
Person.prototype ={//原型裏面的方法是共享的
constructor:Person,
sayName:function(){
console.log(this.name);
}
}
var person1 = new Person("大頭",28,"doctor");
var person2 = new Person("小頭",18,"message");
person1.friends.push("wangwu");
console.log(person1.friends);//["zhangsan", "lisi", "wangwu"]
console.log(person2.friends);//["zhangsan", "lisi"]
console.log(person1.friends === person2.friends);//false
console.log(person1.sayName === person2.sayName);//true*/
這種構造函數與原型混成的模式,是目前在 JavaScript 中使用最廣泛、認同度最高的一種創建自定義類型的方法。可以說,這是用來定義引用類型的一種默認模式。
構造函數模式
// 動態原型模式:
//它把所有信息都封裝到了構造函數裏面,而通過在構造函數中初始化原型,又保持了同時使用構造函數和原型的優點。
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
//方法:只有sayName方法不存在的情況下,才執行
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friends = new Person("小頭",18,"message");
friends.sayName();
這段代碼只會在初次調用構造函數的時候才會執行,此後,原型已經完成了初始化,不需要做什麽修改,不過要記住,這裏對原型所在的修改,能夠立即在所有實例中得到反映,因此這種方法確實可以說是非常完美了。if語句檢測的可以在初始化之後應該存在的任何屬性和方法。
註意:在動態原型模式下,不能使用對象字面量重寫原型,重寫會切斷現有實例與新原型之間的聯系
寄生構造函數模式:不推薦使用
// 這種模式的基本思想是創建一個函數,該函數的作用僅僅是封裝創建對象的代碼,然後再返回新創建的對象.看起來像一個典型的構造函數。(函數內容有個對象,又通過new操作符創建了構造函數的實例)
function Person(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var friend = new Person("小頭",18,"message");
friend.sayName();*/
// 除了使用new操作符並把使用的包裝函數叫做構造函數之外,這個模式和工廠模式其實是一模一樣的,構造函數在不返回值的情況下,默認會返回新對象實例,而通過在構造函數的末尾添加一個return語句,可以重寫調用構造函數時返回的值
穩妥構造函數模式
//所謂的穩妥對象,指的是沒有公共屬性,而且其他方法也不引用this的對象,穩妥對象最適合在一些安全的環境中(禁止使用this和new的環境),或者在防止數據被其他應用程序改動時使用,。
//穩妥構造函數遵循與寄生構造函數類似的模式,但有兩點不同:
// 1.一是新創建的對象實例方法不引用this,
// 2.二是不適用new操作符調用構造函數
function Person(name,age,job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(name);
};
return o;
}
var friend = Person("小頭",18,"message");
friend.sayName();*/
// 在這種模式情況對象中,除了使用sayName()方法外,沒有其他辦法訪問name的值
繼承
- 繼承是OO(面向對象)語言中的一個最為人津津樂道的概念,也許OO語言都支持兩種繼承方式:接口繼承和實現繼承
- 接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。如前所說,由於函數沒有簽名,在ECMAScript中無法實現接口繼承,js只支持實現繼承,而且其實現繼承主要依靠原型鏈來實現的
繼承方式
主要思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
回顧構造函數,原型和實例的關系
每個構造函數都有一個原型對象,
原型對象都包含一個指向構造函數的指針,
而實例都包含一個指向原型對象內部指針
// 如果我們讓原型對象等於另一個類型的實例,結果會怎樣呢
// 此時的原型對象將包含一個指向另一個原型的指針,相應的,另一個原型中也會包含著一個指向另一個構造函數的指針,
// 假如另一個原型由是另一個類型的實例,那麽上述關系依然成立,如此層層遞進,就形成了實例與原型的鏈條,就是所謂的原型鏈的基本概念
function SuperType(){//創建一個函數
this.property = true;//屬性
}
SuperType.prototype.getSuperValue = function(){//對象原型裏面的內容
return this.property;//方法
}
function SubType(){//創建另一個函數
this.subproperty = false;//屬性
}
// 繼承 SuperType 通過創建原型對象的實例,重寫了SubType的原型對象
SubType.prototype = new SuperType();
console.log(SubType.prototype);
SubType.prototype.getSuperValue = function(){//重寫了getSuperValue方法的return值
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue());//false
*/
/*原理:定義了兩個類型SuperType和SubType 每個類型分別有一個屬性和一個方法。它們的主要區別是SubType 繼承了 SuperType ,
而繼承是通過SuperType的實例,並將該實例賦給SubType.prototype實現的:而實現的本質就是重寫原型對象。
換句話說就是原來存在於SuperType的實例中的所有屬性和方法,現在也存在於SubType.prototype中,在確立了繼承關系之後,我們給SubType.prototype添加一個方法,就是在繼承了SuperType的屬性和方法的基礎上添加一個新的方法 (SubType.prototype.getSuperValue)。
*/
// 以上代碼,我們沒有使用SubType默認提供的原型,而是把它換成了新原型(即SuperType的實例)。於是,新原型不僅具有了SuperType的實例所擁有的全部屬性和方法,,而且其內部還有一個指針,指向了SuperType的原型。
//(模擬典型的原型鏈的結構) 結果就是 :instance指向SubType的原型,而SubType的原型又指向了SuperType的原型
// getSuperValue方法依然存在於SuperType.prototype中,但property則位於SubType.prototype中,這是因為property是一個實例實行,而getSuperValue則是一個原型方法。
// 既然SubType.prototype現在是SuperType的實例,那麽property當然就位於該實例中
// 但是,要註意的是,instance.constructor現在指向的是SuperType,這是由於SubType.prototype被重寫了的緣故(嚴格來說,是說SubType.prototype指向了SuperType.prototype,所以constructor自然是SubType.prototype裏面的,也就指向了SuperType)
原型的搜索方式和原型鏈中的搜索方式
記得在原型模式當中說過,搜索會先中實例的屬性中查找,如果找到,就會往原型裏面找
而在原型鏈繼承的情況下,搜索過程會沿著原型鏈繼續往上查找,
1.搜索實例(如果沒有)
2.搜索SubType.prototype(如果沒有)
3.搜索SuperType.prototype
4.搜索如果找不到,會一層一層沿著原型鏈找到末端才會停止(可查看完成的原型鏈.jpg)
/*原型鏈雖然強大,但也是存在問題的,就是我們的引用數據類型,
1.引用數據類型的數據寫到原型裏面,會被所有實例所共享
2.引用類型在繼承的情況下,同樣會被所有實例所共享
借用構造函數
/*為了解決引用類型值所帶來的問題,開發人員使用了一種“借用構造函數(constructor stealing)”的技術(有時候也叫做偽造對象或經典繼承)。
基本思想:在子類型構造函數的內部調用超類型構造函數。使用apply()和call()
也就是說,通過使用apply()和call()方法也可以在新創建的對象上執行構造函數。*/
function SuperType(){
this.colors = ["red","blue","yellow"];
}
function SubType(){
// 繼承了SuperType
SuperType.apply(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);//"red","blue","yellow","black"
var instance2 = new SubType();
alert(instance2.colors);//"red","blue","yellow" 解決了引用類型是問題*/
// 我們實際上是在(未來將要)新創建的SubType實例環境下調用SuperType構造函數。
// 這樣一來,就是在新的SubType對象執行SuperType()對象中的所有對象初始化代碼。所以,SubType的每一個實例都會有自己的colors屬性的副本
// 借用構造函數的優勢:在子類型的構造函數中向超類型構造函數中傳遞參數
function SuperType(name){
this.name = name
}
function SubType(){
// 繼承了SuperType
// SuperType.apply(this,["hah"]);
SuperType.call(this,"hah");
this.age = 22;
}
var instance = new SubType();
alert(instance.name);//hah
/*借用構造函數的問題
方法都是在構造函數中定義,因此函數的復用無從談起,而且在超類型的原型定義的方法,對子類型是不可見的,結構所有類型都只能使用構造函數模式。因此,借用構造函數往往很少單獨使用。*/
組合繼承
組合繼承也叫偽典型繼承,指的是講原型鏈和借用構造函數的技術組合成一塊,從而發揮二者之長的一種繼承模式 思路:使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。 這樣,既通過在原型上定義方法實現了函數的復用,又能保證每個實例都有它自己的屬性 function SuperType (name){ this.name = name; this.colors = ["red","blue","yellow"]; } SuperType.prototype.sayName = function(){ alert(this.name); } function SubType(name,age){ // 繼承屬性 SuperType.call(this,name); this.age = age; } // 繼承方法 SubType.prototype = new SuperType(); SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ alert(this.age); }; var instance1 = new SubType(‘大頭‘,21); instance1.colors.push("black"); console.log(instance1.colors); instance1.sayName(); instance1.sayAge(); var instance2 = new SubType(‘小頭‘,1); // instance2.colors.push("pink"); console.log(instance2.colors); instance2.sayName(); instance2.sayAge();
(完!)
面向對象的理解