Javascript面向對象全面剖析 —創建對象


先介紹目前在ECMAScript中使用最廣泛,認同度最高的默認模式。

1.組合使用構造函數及原型

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby","Court"];
}
Person.prototype = {
    constructor : Person,
    sayName : function(){
        alert(this.name);
    }
}

var person1 = new Person('Nocholas',29,'Software Engineer');
alert(person1.friends);  //"Shelby,Count,Van"
person1.sayName();  //"Nocholas"


 其中實例屬性都是在構造函數中定義的,而由所有實例共享的屬性 constructor 和方法 sayName() 則是在原型中定義的。

constructor屬性始終指向創建當前對象的構造函數,不用刻意牢記。constructor屬性

1.在構造函數中用this.創建屬性 而不是在原型上。

這么做的本質是因為:對於一個屬性而言,在類的對象中的值要求是獨立的,對象獨自保存自己屬性的值,修改的同時不能影響到其他實例化對象。在構造函數中使用this創建的屬性,在每個實例上重新創建一遍,保證了此特性。 

原型創建的內容是這個類所有對象所共享的,如果使用原型對象來創建對象屬性,那么任何一個類的對象修改了自己的某個屬性(即原型屬性),其他對象的相同屬性也會被修改。 

所以一般情況我們在函數內部創建屬性。


2.用原型來保存方法 而不在構造函數中(之后我們會講到什么情況下我們才需要在構造函數中定義方法)

方法對所有類對象來說都應該是一樣的,沒有必要每個對象都保存一個方法,只要由類的原型保存一份,每個對象需要使用方法的時候就調用原型對象中保存的方法。節省了資源。

傳統的方式在構造函數定義方法,每個類的被實例化的時候,都會重復創建這個方法,這樣會耗 
費很多資源。

下面我們來系統地認識構造函數模式和原型模式

 2.構造函數模式

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName : function(){
        alert(this.name);
    }
}

var person1 = new Person('Nocholas',29,'Software Engineer');
var person2 = new Person('Greg',27,'Doctor');

person1.sayName();  //"Nocholas"
person1.sayName();  //"Greg"

按照慣例構造函數始終都應該以一個大寫字母開頭

特點:這種方法沒有顯式地創建對象;直接將屬性和方法賦給了 this 對象;沒有返回值。

要創建Person的新實例時,必須使用 new 操作符。以這種方式調用構造函數實際上會經歷以下4個步驟:

  (1) 創建一個新對象

  (2) 將構造函數的作用域賦給對象(因此 this 就指向了這個新對象)

  (3) 執行構造函數中的代碼(為這個新對象添加屬性)

  (4) 返回新對象

 我們來檢測一下對象類型

alert( person1 instanceof Object); //true
alert( person1 instanceof Person);  //true
alert( person2 instanceof Object);  //true
alert( person2 instanceof Person);  //true

 這個例子中,person1 和 person2 都是Person的實例,同時所有對象均繼承自Object。

  構造函數模式的缺點:

  使用構造函數的主要問題就是每個方法都要在每個實例上重新創建一遍。在前面的例子中,person1 和 person2 都有一個名為 sayName 的方法,但那兩個方法不是同一個 Function 的實例。 ECMAScript中的函數是對象,因此每定義一個函數就是實例化了一個對象,從邏輯角度講,此時的構造函數也可以這樣定義:

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName  = new  Function(){
        alert(this.name);
    }
}

  從這個角度看構造函數更容易明白每個Person 實例都包含一個不同的 Function 實例的本質,如前所述這兩個函數是不相等的,

alert(person1.sayName() == person2.sayName()) //false

  我們沒有理由對實現同一功能的方法多次創建,特別是在方法數量較多的情況,即便是可以通過下面的方法來避免多次創建:

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName  = sayName;
}

function sayName(){
    alert(this.name);
}

  我們創建全局函數 sayName,將構造函數內部的屬性設置成等於全局的 sayName 函數;由於sayName 包含的是一個指向函數的指針,person1 和 person2 對象就共享了全局作用域中的函數,這樣確實解決了兩個函數共做一件事的問題,可是這樣在全局作用域中的函數 sayName 只是為了Person 實例化的對象調用,讓全局作用域有點名不副實。

  而更讓人無法接受的是要定義很多方法的時候,就要定義很多函數,於是我們自定義的類就變得絲毫沒有封裝性可言了。還好這些問題可以通過原型模式來解決。

3.原型模式

  簡單理解:我們創建的每個函數都有一個prototype(原型)屬性,這個屬性是一個對象,它的用途就是可以讓所有實例共享它包含的的屬性和方法。 換句話說,不必在構造函數中定義對象信息,而是可以將這些信息直接添加到原型對象中,如下所示:

function Person(){}

Person.prototype.name = "Nicholas";
proson.prototype.age = 29;
Person.prototype.sayName = function(){
   alert(this.name);      
}

var person1 = new Person();
person1.sayName();                  //"Nicholas"

var person2 = new Person();
person2.sayName();                  //"Nicholas"

alert(person1.sayName == person2.sayName);   //true

  我們將sayName() 方法和所有屬性直接添加到了Person 的 prototype 屬性中,構造函數變成了空函數,即便如此也仍可以通過調用構造函數來創建一個新對象,而且新對象還會具有相同的實行和方法,新對象中的屬性和方法是由所有實例共享的。

 下面我們來理解原型模式的工作原理,有點抽象,不過卻是js面向對象編程的最核心部分,理解他很重要,多看幾遍就是:

 理解原型(prototype)

   每一個JavaScript對象(null除外)都和另一個對象相關聯,“另一個”對象就是我們熟知的原型,每個對象都從原型繼承屬性。

  無論什么時候,只要創建了一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性,我們可以為 prototype 添加屬性和方法。默認情況下,prototype 屬性都會自動獲得一個 constructor 屬性,constructor屬性始終指向創建當前對象的構造函數,默認情況下指向函數自己,我們不用深究constructor。

function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby","Court"];
}
Person.prototype.sayName =function(){
    alert(this.name);
}
console.log(Person.prototype.constructor === Person); //true 

  另外每個對象都會在其內部初始化一個屬性,就是__proto__,__proto__指向當前對象父對象的pertotype 當我們訪問一個對象的屬性時,如果這個對象內部不存在這個屬性,那么他就會去__proto__里找這個屬性,這個__proto__又會有自己的__proto__,於是就這樣一直找下去,也就是我們平時所說的原型鏈的概念。

 

function Person( name, age, job ){
    this.namee = name;
    this.age = age;
    this.job = job;
}
Person.prototype.sayName = function(){
    alert(this.name);
}
console.log(Person.__proto__ === Function.prototype); //true from Function;
console.log("******************");
console.log(Function.prototype.__proto__ === Object.prototype);
console.log(Object.prototype.__proto__ === null); //true

 

  原型鏈的起點是Object.prototype Object.prototype中包含着toString()、valueOf() 等內置方法,這也是各種數據類型的同名方法,其實都是繼承於此。

  看下面,注意區分 prototype 和 __proto__ ,通俗的來理解:

  一個普通的函數 function Person(){} 同時擁有 prototype 和 __proto__。 Person.prototype 包含着Person擁有的一切以后要傳給兒子的屬性和方法 ,一開始只包含一個constructor屬性 可以自由增加 Person.prototype.familyName = "陳";Person.prototype.skill = “泡妞”;

  Person.__proto__ 則指向 Person的老爸的原型 Function.prototype,顯然默認也只包含一個constructor屬性 如果曾經發生過 Function.prototype.car = "勞斯萊斯",老爸有輛勞斯萊斯的車,那么  console.log(Person.car)  //勞斯萊斯Person也繼承了。

  實例化的對象 person1 = new Person(); 是沒有prototype的  console.log(person.prototype); //undefined。其他的對象類型也一樣。

var arr = new Array();
var fun = new Function();
var obj= new Object();

console.log(arr.prototype) //undefined 
console.log(fun .prototype) //undefined 
console.log(obj.prototype) //undefined 

 更簡單的原型語法

 前面的例子中沒添加一個屬性和方法就要敲一遍 Person.prototype。為減少不需要的輸入,也從視覺上更好地封裝原型的功能,常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象,如下

function Person(){}

Person.prototype = {
constructor : Person, name :
"Nicholas", age : 29, job : "Software Engineer", sayName : function(){ alert(this.name); } }

原型對象的問題:

  首先,它省略了為構造函數傳遞初始化參數這一環節,結果所有實力在默認情況下都取得相同的屬性值,這會在某種程度上帶來一些不便,但這還不是原型的最大問題,原型的最大問題是由其共享的本性所導致的。

  原型中的所有屬性是被很多實例共享的,這種共享對於函數非常合適。對於那些包含基本值的屬性倒也說得過去(通過在實例上添加一個同名屬性,可以隱藏原型中的對應屬性),然而對於包含引用類型值得屬性來說,問題就比較突出了。

  如果對於值類型,引用類型不太清楚的同學,請參閱 Javascript傳值方式

function Person(){}
Psrson.prototype = {
     constructor : Person,
     name : "Nicholas",
     age : "29",
     friends : ["Shelby","Court"],
     sayName : function(){
        alert(this.name);
    }
}

var person1 = new Person();
var Person2 = new persin();

person1.friends.push("Van"); // 向friends屬性添加一個元素

alert(person1.friends);  //  ["Shelby","Court","Van"]
alert(person2.friends);  //  ["Shelby","Court","Van"]
alert(person1.friends === person2.friends);  //  true 

  由於Person的friends屬性是一個數組,是引用類型(對象),我們修改了person1.friends 引用的數組,向數組中添加了一個字符串。由於friends數組存在於Person.prototype 而非 person1 中,所以我們的修改會影響到到所有的實例,假如我們的初衷就是這樣在所有實例中共享一個數組,那么這個結果倒也可以接受,可是實例一般都是要有屬於自己的全部屬性的,而這個問題正是我們很少看到有人單獨使用原型模式的原因所在。

  我們最常見的方式,就是在開篇中介紹的組合使用構造函數模式與原型模式,構造函數用於定義實例屬性,原型模式用於定義方法和共享屬性。結果,每個實例都會有自己的一份實例屬性的副本,但同時又共享着對方法的引用,最大限度地節省了內存。另外這種混成模式還支持向構造函數傳遞參數,可謂是集兩種模式之長。

  下面貼出Javascript中最常用的繼承模式:

  

/*=========== 父類 ============*/
function SuperType(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
    alert(this.name);
}

/*=========== 子類 ============*/
function SubType(name, age){
    this.age = age;    
    SuperType.call(this,name);
}
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
    alert(this.age);
}

 

 

 

 

  注:本文知識點源自《javascript高級程序設計》,想要對javascript面向對象了解更多的園友,可以自行查閱。

  如果感覺本文對您有所助益,勞駕您推薦下,在此謝過。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM