《JavaScript高級程序設(shè)計》之筆記四

第六章 面向?qū)ο蟪绦蛟O(shè)計

1. 理解對象 :

//創(chuàng)建對象的第一種方法
var person = new Object();
person.name = "Jack";
person.age = 29;
person.job = "Software Enjineer";
person.sayName = function(){
  alert(this.name);
};
//創(chuàng)建對象的第二種方法(常用的方法)
var person = {
  name: "Jack",
  age: 29,
  job: "Software Enjineer",
  sayName: function(){
alert(this.name);
  }
}

2. 屬性類型 :

數(shù)據(jù)屬性 :
數(shù)據(jù)屬性包含一個數(shù)據(jù)值的位置,在這個位置可以讀取和寫入值,該屬性有4個描述其行為的 特性。

  • configurable : 表示能否通過 delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,能否把屬性修改成為訪問器屬性,默認(rèn)值為 true。

  • enumerable : 表示能否通過 for-in 循環(huán)返回屬性,默認(rèn)值為 true。

  • writable : 表示能否通過 for-in 循環(huán)返回屬性,默認(rèn)值為 true。

  • value : 表示能否通過 for-in 循環(huán)返回屬性,默認(rèn)值為 true。

    PS:向前面那個對象定義的屬性,他們的 configurable,enumerable,writable 的特性都被設(shè)置為 true,而 value 屬性被設(shè)置為特定的值 Jack。

    ——————————接下來看一個可以修改默認(rèn)屬性特性的方法————————————

Object.defineProperty() : 用于修改屬性默認(rèn)的特性,該方法接受三個參數(shù)(屬性所在的對象,屬性的名字,一個描述符對象),描述符對象指的就是(configurable,enumerable,writable,value)其中的一個或多個。

var person = {};            //聲明一個對象
Object.defineProperty(person,"name",{
  writable: false,       //不可修改屬性的值
  value: "Sam"          //給name賦值
});
alert(person.name);         //Sam
person.name = "Jack";
alert(person.name);         //Jack
//類似的規(guī)則也可適用于不可配置的屬性
var person = {};
Object.defineProperty(person,"name",{
  configurable: false,   //不可配置
  value: "Jack"
});
alert(person.name);         //Jack
delete person.name;
alert(person.name);         //Jack
//需要說明的是一旦把屬性改為不可配置就不能再把它變回可配置了,此時,再調(diào)用Object.defineProperty()方法修改除writable之外的特性,都會導(dǎo)致錯誤

訪問器屬性(類似于C#中的訪問器):
訪問器屬性不包含數(shù)據(jù)值,它們包含一對 gettersetter 函數(shù)(這兩個都不是必須的),讀取訪問器屬性時,調(diào)用 getter 函數(shù);寫入訪問器屬性時,調(diào)用 setter 函數(shù)并傳入新值,這個函數(shù)決定如何讓處理數(shù)據(jù),訪問器屬性有如下4大特性。

  • configurable : 表示能否通過 delete 刪除屬性從而重新定義屬性,能否修改屬性的特性,能否把屬性修改成為訪問器屬性,默認(rèn)值為 true。

  • enumerable : 表示能否通過 for-in 循環(huán)返回屬性,默認(rèn)值為 true

  • get : 表示能否通過 for-in 循環(huán)返回屬性,默認(rèn)值為 true。

  • set : 表示能否通過 for-in 循環(huán)返回屬性,默認(rèn)值為 true

//訪問器屬性不能直接被定義,必須通過Object.defineProperty()來定義
var book = {
  _year: 2004;     //下劃線表示只能通過對象方法訪問的屬性
  edition: 1;
};
Object.definedProperty(book,"year",{
  get: function(){
    return this._year;
  }
  set: function(newValue){
    if(newValue > 2004){
      this._year = newValue;
      edition = newValue - 2004;
    }
  }
});

3. 定義多個屬性 :

Object.defineProperties() : 該方法可以通過描述符一次性定義多個屬性,接受兩個 對象 參數(shù),第一個對象是要添加和修改其屬性的對象第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應(yīng)。

var book = {};
Object.defineProperties(book,{
  _year: {
    value: 2004
  },
  edition: {
    value: 1
  },
  year: {
    get: function(){
      return this._year;
    },
    set: function(newValue){
      if(newValue > 2004){
        this._year = newValue;
        this.edition += newValue - 2004;
      }
    }       
  }         
});

4. 創(chuàng)建對象 :

————————————接下來讓我們進(jìn)入重頭戲,創(chuàng)建對象———————————————

目前在JavaScript中最常用的創(chuàng)建對象的模式是 原型模式,其他的一些模式如工廠模式,構(gòu)造函數(shù)模式由于各自的缺點已經(jīng)很少被使用,(我沒說原型模式?jīng)]有缺點),那么咱們就重點來介紹 原型模式。

在開始之前,先來說說什么是JavaScript中的構(gòu)造函數(shù)(為什么要這樣說,是因為JavaScript中的構(gòu)造函數(shù)和其他語言中的不同),JavaScript的構(gòu)造函數(shù)并不是作為類的一個特定方法存在的;當(dāng)任意一個普通函數(shù)用于創(chuàng)建一類對象時,它就被稱作構(gòu)造函數(shù),或構(gòu)造器。構(gòu)造函數(shù)本身也是一個函數(shù),只不過可以用它來 創(chuàng)建對象 而已。

//先來看一個構(gòu)造函數(shù)
function Person(name,age,job){   //構(gòu)造函數(shù)命名首字母大寫
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function(){
    alert(this.name);
  };
};
var person1 = new Person("Jack",29,"Software Enginner");
var person2 = new Person("Sam",27,"Doctor");
//要想創(chuàng)建一個新實例,必須使用new操作符,因為你創(chuàng)建的是一個對象
//person1,person2分別保存著Person的一個不同的實例。這兩個對象都有一個constructor(構(gòu)造函數(shù))屬性,該屬性指向Person,這點要記著...
alert(person1.constructor == Person);   //true
alert(person2.constructor == Person);   //true

好,下面我們正式進(jìn)入原型模式
我們創(chuàng)建的每個函數(shù)都有一個 prototype(原型) 屬性,這個屬性是一個 指針,指向一個對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法(看到這里你肯定會懵,繼續(xù)往下看)。

使用原型對象的好處 :
可以讓所有實例對象共享它所包含的屬性和方法,換句話說,不必在構(gòu)造函數(shù)中定義對象實例的信息,而是可以將這些信息直接添加到原型對象中。

//下面來看個例子
function Person(){
  Person.prototype.name = "Jack";
  Person.prototype.age = 29;
  Person.prototype.job = "Software Engineer";
  Person.prototype.sayName = function(){
    alert(this.name);
  };
};
var person1 = new Person();
person1.sayName();    //Jack
var person2 = new Person();
person2.sayName();    //Jack
alert(person1.sayName == person2.sayName);    //true

下面我用一張圖來讓大家理解原型對象 :

原型對象
原型對象

這張圖展示了Person構(gòu)造函數(shù),Person的原型屬性以及Person現(xiàn)有的兩個實例之間的關(guān)系。

另外,雖然這兩個實例都不包含自己的屬性和方法,但我們卻可以調(diào)用原型中的屬性和方法,這是通過查找對象屬性的過程來實現(xiàn)的。

那么查找對象屬性的過程又是怎樣的呢?

當(dāng)代碼讀取某個對象的某個屬性時,都會執(zhí)行一次搜索,目標(biāo)是具有給定名字的屬性。搜索會從對象實例本身開始。如果在實例中找到了具有給定名子的屬性,則返回該屬性的值;如果沒有找到,則繼續(xù)搜索指針指向的原型對象。

那么根據(jù)這個過程我們就可以很好地將原型對象屬性的值給屏蔽掉,換上我們希望的值。

function Person(){
  Person.prototype.name = "Jack";
  Person.prototype.age = 29;
  Person.prototype.job = "Software Engineer";
  Person.prototype.sayName = function(){
    alert(this.name);
  };
};
var person1 = new Person();
person1.name = "Sam";
alert(person.name);      //Sam,來自實例
var person2 = new Person();
alert(person2.name);     //Jack,來自原型

當(dāng)為對象實例添加一個對象時,這個屬性會屏蔽原型對象中保存的屬性,也就是不影響原型對象中保存的屬性。

function Person(){
  Person.prototype.name = "Jack";
  Person.prototype.age = 29;
  Person.prototype.job = "Software Engineer";
  Person.prototype.sayName = function(){
    alert(this.name);
  };
};
var person1 = new Person();
person1.name = "Sam";
alert(person1.name);     //Sam
delete person1.name;
alert(person1.name);     //Jack

hasOwnProperty() : 該方法可以檢測一個屬性是存在于實例中還是存在于原型中,因為是從 Object 繼承來的,所以給定屬性在 對象實例 中時,返回 true

function Person(){
  Person.prototype.name = "Jack";
  Person.prototype.age = 29;
  Person.prototype.job = "Software Engineer";
  Person.prototype.sayName = function(){
    alert(this.name);
  };
};
var person1 = new Person();     
alert(person1.hasOwnProperty("name"));    //false
person1.name = "Sam";
alert(person1.hasOwnProperty("name"));    //true
delete person1.name;
alert(person1.hasOwnProperty("name"));     //false

通過使用 hasOwnProperty() 方法,什么時候訪問的是實例屬性,什么時候訪問的是原型屬性就一清二楚了,下圖展示了上面例子在不同情況下的實現(xiàn)與原型的關(guān)系。

hasOwnProperty
hasOwnProperty

原型與in操作符 :
有兩種方式使用 in 操作符,單獨使用和在 for-in 里面使用,在單獨使用時,in 操作符會在通過對象能夠訪問屬性時返回 true,無論屬性在實例還是原型中。

function Person(){
  Person.prototype.name = "Jack";
  Person.prototype.age = 29;
  Person.prototype.job = "Software Engineer";
  Person.prototype.sayName = function(){
    alert(this.name);
  };
};
var person1 = new Person();
alert(person1.hasOwnProperty("name"));    //false
alert("name" in person1);   //true
person1.name = "Sam";
alert(person1.hasOwnProperty("name"));     //true
alert("name" in person1);   //true

上述代碼執(zhí)行的整個過程中,調(diào)用 ”name” in person 返回的值總是 true,我們可以同時使用 hasOwnProperty()in 操作符,來確定該屬性到底是存在于隊對象中,還是存在于原型中。

function hasPrototypePerperty(object,name){
  return !hasOwnProperty(name) && (name in object);
}
function Person(){
  Person.prototype.name = "Jack";
  Person.prototype.age = 29;
  Person.prototype.job = "Software Engineer";
  Person.prototype.sayName = function(){
    alert(this.name);
  };
};
var person = new person();
alert(hasPrototypeProperty(person,"name"));   //true
person.name = "Sam";
alert(hasPrototypeProperty(person,"name"));   //false

Object.keys() :
取得對象上所有可枚舉的 實例 屬性,接受 一個對象 作為參數(shù),返回 一個包含所有可枚舉屬性的數(shù)組。

function Person(){
  Person.prototype.name = "Jack";
  Person.prototype.age = 29;
  Person.prototype.job = "Software Engineer";
  Person.prototype.sayName = function(){
    alert(this.name);
  };
};
var keys = Object.keys(Person.prototype);
alert(keys);  //name,age,job,sayName    
var person1 = new Person();
person1.name = "Sam";
person1.age = 27;
alert(Object.keys(person1));    //name.age
//如果你想要得到所有屬性,不管能不能被枚舉,可以使用Object.getOwnPropertyNames()方法
alert(Object.getOwnPropertyNames(Person.prototype));
//constructor,name,age,job,sayName

更簡單的原型方法 :

function Person{
};
Person.prototype = {
  name: "Jack",
  age: 29,
  job: "Software Enjineer",
  sayName: function(){
    alert(this.name);
  };
};
//需要注意的是如果這樣寫,constructor屬性不在指向Person了,如果想讓他指向Person,我們可以手動添加
function Person{
};
Person.prototype = {
  constructor: Person,
  name: "Jack",
  age: 29,
  job: "Software Enjineer",
  sayName: function(){
    alert(this.name);
  };
};
//但是還跟原來的有點不一樣,哪點不一樣呢?我們發(fā)現(xiàn)現(xiàn)在constructor變成可枚舉的了,要想把它變回來,可以使用我們前面介紹的enumerable特性,將它設(shè)置為false
function Person{
}
Person.prototype = {
  name: "Jack";
  age: 29;
  job: "Software Enjineer";
  sayName: function(){
    alert(this.name);
  }
};
Object.defineProperty(Person.prototype,"constructor",{
  enumerable: false,
  value: Person
});

原型的動態(tài)性 :
由于在原型中查找值的過程是一次搜索,因此我們對原型對象作出的任何修改都能夠在實例上反映出來————即使先創(chuàng)建了實例也是如此(可以按照指針來理解)。

var friend = new Person();
Person.prototype.sayHi = function(){
  alert("Hi");
};
friend.sayHi();    //Hi
//但是我們不能重寫原型對象,那樣會導(dǎo)致實例指不到新定義的原型對象,仍然指向你先前定義的原型對象
var friend = new Person();
Person.prototype = {
  constructor: Person,
  name: "Sam",
  age: 27,
  job: "Doctor",
  sayHi: function(){
    alert("Hi");
  };            
};
friend.sayHi();    //error

下圖演示了上面代碼的過程:

重寫原型對象
重寫原型對象

最后說兩句 :
原型模式的重要性不僅體現(xiàn)在創(chuàng)建自定義類方面,就連原聲的引用類型,都是采用這種模式創(chuàng)建的。所有原聲引用類型(Object,Array,String等等)都在其構(gòu)造函數(shù)的原型上定義了方法。下面舉兩個例子。

alert(typeof Array.prototype.sort);    //function       
alert(typeof String.prototype.substring);   //function

前面花了大量的篇幅來介紹原型模式,但是原型模式還不是目前使用最為廣泛的,目前使用最為廣泛的是構(gòu)造函數(shù)模式加原型模式

function Person(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ["Tom","Marry"];
}
Person.prototype = {
  constructor: Person,
  sayName: function(){
    alert(this.name);
  };
};
var person1 = new Person("Jack",29,"Software Engineer");
var person2 = new Person("Sam",27,"Doctor");
person1.friends.push("Frank");
alert(person1.friends);  //Tom,Marry,Frank
alert(person2.friends);  //Tom,Marry
alert(person1.friends == person2.friends);   //false
alert(person1.sayName == person2.sayName);   //true

動態(tài)原型模式 :
有些人可能對這種定義對象的方式覺得麻煩,他可能會說能不能把構(gòu)造函數(shù)和原型封裝在一個函數(shù)里?答案是肯定的。

function Person(name,age,job){
  //屬性
  this.name = name;
  this.age = age;
  this.job = job;
  //方法
  if(typeof this.sayName != "function"){
    Person.prototype.sayName = function(){
    alert(this.name);
    };
  }
}
var friend = new Person("Jack",29,"Software Engineer");
friend.sayName();     //Jack

5. 繼承 :

JavaScript中敘述了 原型鏈 的概念,并將其作為實現(xiàn)繼承的主要方法。其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。

咱們來簡單回顧一下構(gòu)造函數(shù),原型,和實例的關(guān)系。

每個 構(gòu)造函數(shù) 都有一個 原型對象,原型對象里有一個指向 構(gòu)造函數(shù) 的指針,而 實例 都包含一個指向 原型對象 的內(nèi)部指針。

那么假如我們讓 原型對象 作為另一個引用類型的 實例,結(jié)果會怎樣呢?顯然,此時的 原型對象 會包含一個指向 最開始那個原型對象 的指針,相應(yīng)的,最開始的原型對象 中也包含著一個指向 最開始的構(gòu)造函數(shù) 的指針。

這就是原型鏈的基本概念...

function SuperType(){
  this.property = true;
};
SuperType.prototype.getSuperValue = function(){
  return this.property;
};
function Subtype(){
  this.subproperty = false;
};
//繼承了SuperType
SubType.prototype = new SuperType();
SubType.protptype.getSubValue = function(){
  return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue());      //true
原型鏈
原型鏈

在上面的代碼中,我們沒有使用 SubType 默認(rèn)提供的原型,而是給它替換了一個新原型;這個新原型就是 SuperType 的實例。于是,新原型不僅具有作為一個 SuperType 的實例擁有的所有屬性和方法,而且其內(nèi)部還有一個指針,指向 SuperType 的原型。

最終的結(jié)果就是這樣,instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型。getSuperValue() 方法仍然還在 SuperType.prototype 中,但 property 則位于 SubType.prototype 中。這是因為 property 是一個實例屬性,而 getSuperValue() 則是一個原型方法。既然 SubType.prototype 現(xiàn)在是 SuperType 的實例,那么 property 當(dāng)然也就位于該實例中了。

別忘記了默認(rèn)的原型。

事實上,前面的例子展示的原型鏈少一環(huán)。因為所有的引用類型都是 Object,而這個繼承也是通過原型鏈實現(xiàn)的。所有的函數(shù)的默認(rèn)原型都是 Object 的實例。

完整原型鏈
完整原型鏈

原型鏈的問題 :
如果有包含引用類型的原型(比如數(shù)組),那么數(shù)組一旦改動就將一起整條鏈上的數(shù)組發(fā)生變化,而這一點我們有時候是不希望看到的。

解決方法 :
借用構(gòu)造函數(shù)
通過使用 apply()call() 方法在新創(chuàng)建的對象上執(zhí)行構(gòu)造函數(shù)。

function SuperType(){
  this.colors = ["red","blue","green"];
};
function SubType(){
  //繼承了SuperType
  SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);  //red,blue,green,black        
var instance2 = new SubType();
alert(instance2.colors);  //red,blue,green

相對于原型鏈而言,借用構(gòu)造函數(shù)還有一個很大的優(yōu)勢,那就是可以傳遞參數(shù)。

function SuperType(name){
  this.name = name;
};
function SubType(){
  //繼承了SuperType,同時還傳遞了參數(shù)
  SuperType.call(this,"Jack");
  //實例屬性
  this.age = 29;
}        
var instance = new SubType();
alert(instance.name);    //Jack
alert(instance.age);     //29

但是這種方法還是很少人用。

那么最常用的繼承方法是什么呢?

答案是 組合繼承

組合繼承有時候也成為偽經(jīng)典繼承,指的是將原型鏈和借用構(gòu)造函數(shù)技術(shù)組合到一塊,從而發(fā)揮二者之長的繼承模式。

主要的思路是:使用原型鏈實現(xiàn)對原型 屬性和方法 的繼承,而通過借用構(gòu)造函數(shù)來實現(xiàn)對 實例屬性 的繼承。這樣一來,既通過在原型上定義方法實現(xiàn)了函數(shù)復(fù)用,又能保證每個實例都有它自己的屬性。

function SuperType(name){
  this.name = name;
  this.colors = ["red","blue","green"];
};
SuperType.prototype.sayName = function(){
  alert(this.name);
};
function SubType(name,age){
  //繼承屬性
  SuperType.call(this,name);
  this.age = age;
};
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
  alert(this.age);
};
var instance1 = new SubType("Jack",29);
instance1.colors.push("Black");
alert(instance1.colors);     //red,blue,green,black
instance1.sayName();         //Jack
instance1.sayAge();          //29
var instance2 = new SubType("Sam",27);
alert(instance2.colors);    //red,blue,green
instance2.sayName();        //Sam
instance2.sayAge();         //27

組合繼承避免了原型鏈和借用構(gòu)造函數(shù)的缺陷,融合了他們的優(yōu)點,成為了JavaScript中最受歡迎的繼承模式。

小結(jié) :

  • 工廠模式 : 使用簡單的函數(shù)創(chuàng)建對象,為對象添加屬性和方法,然后返回對象,這個模式后來被構(gòu)造函數(shù)模式所取代。

  • 構(gòu)造函數(shù)模式 : 可以創(chuàng)建自定義引用類型,可以像創(chuàng)建內(nèi)置對象實例一樣使用 new 操作符,不過,構(gòu)造函數(shù)的缺點就在于他的每個成員都無法得到復(fù)用,包括函數(shù)。

  • 原型模式 : 使用構(gòu)造函數(shù)的 prototype 屬性來指定那些應(yīng)該共享的屬性和方法。

  • 組合使用構(gòu)造函數(shù)和原型模式 : 使用構(gòu)造函數(shù)定義實例屬性,使用原型定義共享的屬性和方法。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容