JavaScript面向物件的程式設計(一)——物件的創建
目錄- JavaScript面向物件的程式設計(一)——物件的創建
- 一、Object建構式
- 二、物件字面量
- 三、 工廠模式
- 四、 建構式模式
- 4.1 建構式模式
- 4.2 建構式模式的問題
- 五、原型模式,
- 5.1 三個屬性(三個指標)
- 5.1.1 prototype
- 5.1.2 [[prototype]]
- 5.1.3 constructor
- 5.2 理解原型物件
- 5.3 讀取物件的屬性
- 5.4 重寫原型
- 5.5 原型模式的問題
- 5.1 三個屬性(三個指標)
- 六、組合使用建構式模式和原型模式
- 七、動態原型模式
- 八、其他模式
- 十、下一篇
- 參考資料
一、Object建構式
類似Java等面向物件語言中創建物件的語法,在 JavaScript中可以通過執行 new運算子后跟要創建的物件型別的名稱來創建,JavaScript中通過如下方式可以創建一個Object物件:
var obj = new Object()
// 可以像這樣添加屬性
obj.name = 'AAA'
obj.age = 26
在 ECMAScript中,如果不給建構式傳遞引數,則可以省略 Object后的括號,寫為:
var obj = new Object
但是這種寫法并不推薦,
二、物件字面量
創建物件的第二種方法為:物件字面量(或物件直接量)
創建物件最簡單的一種方式就是在 JavaScript代碼中使用物件字面量,這在我們平時的專案中使用的最多,物件字面量是由若干名/值對組成的映射表,名/值對之間用逗號分隔,整個映射表用花括號括起來,示例如下:
var empty = {} // 沒有任何屬性的物件
var point = {
x: 0,
y: 0
} // 兩個屬性
var book = {
"main book": "JavaScript", // 屬性名字有空格,則必須用字串表示
'sub-title': "物件的創建", // 屬性名字有連字符,則必須用字串表示
author: { // 屬性可以是一個物件
firstname: "David",
surname: "Flanagan"
}
}
使用Object建構式和物件字面量 都可以用來創建單個物件,但在碰到使用同一個介面創建很多同型別物件時,就會產生大量重復的代碼,如下示例:
var book1 = {
"main book": "JavaScript", // 屬性名字有空格,則必須用字串表示
'sub-title': "物件的創建", // 屬性名字有連字符,則必須用字串表示
author: { // 屬性可以是一個物件
firstname: "David",
surname: "Flanagan"
}
}
var book2 = {
"main book": "Java",
'sub-title': "面向物件",
author: {
firstname: "愛誰誰",
surname: "Flanagan"
}
}
var book3 = {
"main book": "C語言",
'sub-title': "鼻祖",
author: {
firstname: "愛誰誰",
surname: "Flanagan"
}
}
三、 工廠模式
使用工廠模式,可以抽象創建具體物件的程序,如下示例所示:
function createPerson (name, age, job) {
var obj = new Object()
obj.name = name
obj.age = age
obj.job = job
obj.sayName = function () {
alert(this.name)
}
return obj
}
var person1 = createPerson('Bob', 20, 'Student')
var person2 = createPerson('Jack', 26, 'Software Engineer')
函式 createPerson()能夠根據接受的引數來構建一個包含所有必要資訊的Person物件,可以無數次的呼叫這個函式,每次都會回傳一個包含三個屬性和一個方法的物件,
工廠模式雖然解決了創建多個相似物件的問題,但卻沒有解決物件識別的問題(怎么知道該物件表示的是一個Person的型別)
四、 建構式模式
4.1 建構式模式
ECMAScript中的建構式可以用創建特定型別的物件,比如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('Bob', 20, 'Student')
var person2 = new Person('Jack', 26, 'Software Engineer')
重寫之后,Perosn()函式取代了 createPerson() 函式,它們之間存在以下不同:
- 沒有顯式的創建物件;
- 直接將屬性和方法賦給this物件
- 沒有return陳述句
要創建Person的新實體,必須使用new運算子,以這種方式呼叫建構式,實際上會經過4個步驟:
- 創建一個新物件;
- 將建構式的作用域賦給新物件(因此,this就指向了這個變數);
- 執行建構式中的代碼,為這個新物件添加屬性;
- 回傳新物件;
上面例子中,person1和person2中都保存著Person型別的一個不同的實體,這兩個物件都有一個constructor(建構式)屬性,該屬性指向Person
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function () {
alert(this.name)
}
}
var person1 = new Person('Bob', 20, 'Student')
var person2 = new Person('Jack', 26, 'Software Engineer')
console.log( person1.constructor == Person) // true
console.log( person2.constructor == Person) // true
constructor 保存著用于創建當前物件的建構式,是每個Object物件中都存在的屬性,以上實體中,創建當前物件的建構式就是 Person,constructor屬性是來自實體的原型,這在后面會講到,
創建自定義的建構式意味著將來可以將它的實體標識為一種特定的實體,這正是建構式模式勝過工廠模式的地方,
4.2 建構式模式的問題
使用建構式的問題就在于,每個方法都要在每個實體中重新創建一遍
以上的person1和person2都含有一個sayName()方法,但這兩個方法并非相同的 Function實體,因此,每定義一個實體,都要在其內部重新創建一個新的sayName()方法,
// 以上的實體,從邏輯的角度講,也可以這樣定義
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = new Function("alert(this.name)")
}
// person1和person2的sayName()方法是不相等的
console.log( person1.sayName == person2.sayName) // false
以上代碼可證明person1和person2的sayName()方法是不相等的,
然而,每個新的Person實體中的sayName()方法所完成的事情實際上是相同的,創建兩個完成相同任務的的Function實體的確沒有必要,
我們不必在執行代碼前就將函式系結在特定實體上,大可像下面這樣,通過把函式定義轉移到建構式外部來解決這個問題,
function Person (name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = sayName
}
function sayName () {
console.log(this.name)
}
var person1 = new Person('Bob', 20, 'Student')
var person2 = new Person('Jack', 26, 'Software Engineer')
console.log(person1.sayName())
在以上的例子中,把 sayName()函式的定義轉移到了建構式外部,而在建構式內部,將sayName屬性設定為全域的sayName函式,這樣一來,由于sayName包含的是一個指向全域函式的指標,因此,person1和person2物件就共享了全域作用域中定義的同一個sayName()函式,以下代碼可以證明,兩個實體的sayName函式是相同的函式:
console.log( person1.sayName == person2.sayName) // true
這種處理方式的確解決了多次創建一個實作相同功能的函式的問題,但新的問題出現了,
- 我們定義的這個全域函式,實際上只能被Person型別的實體呼叫,這讓全域函式的全域作用域顯得有點名不副實,
- 如果物件需要定義很多方法,那么就要定義很多個全域函式,這讓我們自定義的這個Person參考型別的封裝性極差了,
這些問題可以通過使用原型模式來解決
五、原型模式,
5.1 三個屬性(三個指標)
5.1.1 prototype
prototype屬性是每個函式都包含的一個屬性,是一個指標,指向一個物件,這個物件的作用是:包含可被特定的型別的所有實體共享的屬性和方法,
如,
Person()函式的的prototype屬性,指向的就是特定型別 Person 的所有實體person1、person2所共享的屬性和方法,
這里可以這樣來講,Person()函式的prototype屬性就是 perosn1和person2的原型物件,
需要注意的是,prototype屬性是只屬于函式的,實體和實體的原型都沒有這個屬性
// 控制臺下
Person instanceof Function // true
Person.prototype // 它的原型物件
person1 instanceof Function // false
person1.prototype // undefined
使用原型的好處是,可以讓所有的實體共享它所包含的屬性和方法,不必在建構式中定義特定物件實體的資訊,而是可以將這些資訊直接添加到原型物件中
function Person () {
}
Person.prototype.name = "Jack"
Person.prototype.age = 26
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
console.log( person1.sayName == person2.sayName) // true
5.1.2 [[prototype]]
當呼叫建構式創建一個新實體后,該實體的內部將包含一個指標(內部屬性)[[protoytype]],指向建構式的原型物件,在主流的瀏覽器中,這個屬性就是 _prpto_屬性,需要明確的是,_proto_屬性連接的是實體和建構式的原型物件,而不是存在于 實體和建構式之間,
_proto_是任何物件的實體都擁有的一個屬性:
person1、person2是Person建構式的一個實體,它們的_proto_指向起原型實體;Person本質是一個Function的實體,所以它的_proto_指向的是Function.prototype,Function.prototype的_proto_指向Object.prototype;Object的原型也有_proto_屬性,但其指向為null;
// 控制臺下
Object.prototype._proto_ // null
5.1.3 constructor
默認情況下,所有的原型物件都會自動獲取一個 constructor屬性,這個屬性指向 prototype所在的函式(或者說我們自定義的特定的類)
constructor屬性是實體從原型中繼承過來的屬性,雖然從Person、person1和原型中,都可以獲取其 constructor的值,但是,constructor不是實體自有的屬性,
person1.hasOwnProperty('name') // true
Person.hasOwnProperty('constructor') // true
Person.prototype.hasOwnProperty('constructor') // true
Function.hasOwnProperty('constructor') // false
Function.prototype.hasOwnProperty('constructor') //true
5.2 理解原型物件
下圖展示了建構式、原型物件和實體之間的關系

Person建構式的prototype指向了Person的原型物件,原型物件的constructor屬性指向Person建構式,
Person的實體person1和person2都包含一個_proto_屬性,這個屬性指向的是Person的原型物件,物件實體和建構式之間并沒有直接的關系,

5.3 讀取物件的屬性
每當代碼讀取某個物件的屬性的時候,都會執行一次搜索,搜索首先從物件實體本身開始,如果在實體中找到了具有指定名字的屬性,則回傳該屬性的值,并終斷搜索;如果在實體中沒有找到該屬性,則繼續搜索實體的_proto_屬性所指向的原型物件,如果找到對應的屬性,則回傳該屬性的值,這種搜索方式,是多個物件實體共享原型所保存的屬性和方法的基本原理,
需要注意的是,雖然可以通過實體訪問原型中的值,但卻不能通過物件實體重寫原型中的值,
當向實體中添加和原型中同名的屬性時,原型中的屬性會被屏蔽,但不會修改原型中的屬性,
function Person () {
}
Person.prototype.name = "Jack"
Person.prototype.age = 26
Person.prototype.job = "Software Engineer"
Person.prototype.sayName = function () {
console.log(this.name)
}
var person1 = new Person()
var person2 = new Person()
person1.name = "Greg"
console.log(person1.name) // Greg
console.log(person2.name) // Jack
以上示例中,在person1中添加了同名的 name屬性,分別列印 person1 和 person2中的 name屬性,發現 person1中輸出的是在實體中添加的屬性,而 person2中輸出的則依然是原型中的屬性,
使用delete運算子可以完全洗掉實體屬性,從而讓原型中的屬性重新被訪問到,如下示例:
function Person () {
}
Person.prototype.name = "Jack"
Person.prototype.age = 26
Person.prototype.job = "Software Engineer"
Person.prototype.sayName = function () {
console.log(this.name)
}
var person1 = new Person()
var person2 = new Person()
person1.name = "Greg"
console.log(person1.name) // Greg
console.log(person2.name) // Jack
// delete person1中name屬性
delete person1.name
console.log(person1.name) // Jack
hasOwnProperty()方法可以檢測一個屬性是存在于實體中,還是存在于原型中,該方法只在屬性存在于物件實體中時,才回傳 true,以下是一個示例:
function Person () {
}
Person.prototype.name = "Jack"
Person.prototype.age = 26
Person.prototype.job = "Software Engineer"
Person.prototype.sayName = function () {
console.log(this.name)
}
var person1 = new Person()
var person2 = new Person()
console.log(person1.hasOwnProperty('name')) // false
person1.name = "Greg"
console.log(person1.name) // Greg
console.log(person1.hasOwnProperty('name')) // true
delete person1.name
console.log(person1.hasOwnProperty('name')) // false
下圖展示了以上示例在不同情況下 實體與原型的關系:

5.4 重寫原型
物件的原型是可以重寫的,當重寫原型后,實體和默認原型之間的聯系被切斷了,最直觀的影響是:重寫后的原型的 constructor不再指向原來的建構式,即:Person.prototype.constructor != Person.
在給原型添加屬性的時候,可以使用物件字面量的寫法來重寫整個原型物件,示例如下:
function Person () {
}
Person.prototype = {
name: "Nicholas",
age: 29,
job: 'Software Engineer',
sayName: function () {
console.log(this.name)
}
}
以上的寫法中,將對原型添加屬性的方式改為使用物件字面量的方法,最終得到的基本相同,但有一點例外:
這樣寫,其原型的 constructor屬性不再指向 Person,而是指向Object().
實體的原型的constructor的指向也可以通過如下方式進行指定:
function Person () {
}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: 'Software Engineer',
sayName: function () {
console.log(this.name)
}
}
這樣,可以讓實體的原型的 constructor屬性指向實體的建構式,
原型的重寫是 JavaScript中使用
prototype實作面向物件中繼承的思想的重要依據,
5.5 原型模式的問題
原型模式省略了為建構式傳遞初始化引數的這一環節,所有的實體在默認情況下都將取得相同的屬性值,這在某種程度上會帶來一些不便,
原型模式的屬性是被很多實體所共享的,這種共享對于函式非常合適,但對于屬性來說,問題就比較突出了,看如下示例:
function Person () {
}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: 'Software Engineer',
friends: ['Shelby','Court'],
sayName: function () {
console.log(this.name)
}
}
var person1 = new Person()
var person2 = new Person()
person1.friends.push('Van')
person1.age = 20
console.log(person1.age) // 20
console.log(person2.age) // 29
console.log(person2.friends) // ['Shelby','Court','Van']
在對值是基本型別的屬性來說,問題還不那么大,對單獨某個實體的屬性進行更改并不會影響到其他的實體(相當于添加同名屬性,屏蔽了原型中的屬性),但對于哪些參考型別的屬性來說,就會比較麻煩,
如以上示例中,當 向person1的friends屬性中添加一個成員 Van時,其變化也反應在實體 person2中,這是因為這兩個實體共享了原型中的 friends 屬性,
如果希望每個實體擁有自己的 friends屬性,則需要對每個實體分別添加自己的 friends屬性,對于添加參考型別的屬性,這種操作是比較麻煩的(參考型別的資料基本都是擁有很多內部成員,寫起來比較麻煩),
function Person () {
}
Person.prototype = {
constructor: Person,
name: "Nicholas",
age: 29,
job: 'Software Engineer',
friends: ['Shelby','Court'],
sayName: function () {
console.log(this.name)
}
}
var person1 = new Person()
var person2 = new Person()
person1.friends = ['A','B','C']
person2.friends = [
{ name:'X', age: 25},
{ name:'Y', age: 20},
{ name:'Z', age: 29},
{ name:'W', age: 15}
]
實體一般都應該具有自己的屬性,因此這種純粹的原型模式在實際應用中仍不完美,使用建構式模式和原型模式結合,能更好的解決這個問題,
六、組合使用建構式模式和原型模式
在設計一個自定義型別的時候,我們可以預先將 實體的自定義屬性和方法和型別的公共屬性和方法分離開來,組合使用建構式模式和原型模式,將公共的屬性和方法添加到原型中,將自定義的屬性和方法定義在建構式中,
以下是一個示例:
function Person (name, age, job, friends) {
this.name = name
this.age = age
this.job = job
this.friends = ['Shelby','Court']
}
Person.prototype = {
constructor: Person,
sayName: function () {
console.log(this.name)
}
}
var person1 = new Person('Nicholas', 26, 'Software Engineer')
var person2 = new Person('Greg', 33, 'Doctor')
person1.friends.push('Van')
console.log(person1.friends) // Shelby,Court,Van
console.log(person2.friends) // Shelby,Court
person1.sayName() // Nicholas
person2.sayName() // Greg
console.log(person1.sayName === person2.sayName) //true
這種建構式模式和原型模式組合使用的模式,是JavaScript中使用最廣泛,認同度最高的一種創建自定義型別的模式,是用來定義自定義型別的一種默認的模式,
七、動態原型模式
動態原型模式是對組合模式的一種改進,組合模式將建構式的定義和原型的定義分成兩部分來寫,這對熟悉面向物件語言的人開發人員來說,似乎有點怪怪的,
動態原型模式將原型的初始化程序封裝到建構式中
function Person (name, age, job, friends) {
this.name = name
this.age = age
this.job = job
this.friends = ['Shelby','Court']
if( typeof this.sayName != 'function') {
Person.prototype.sayName = function () {
console.log(this.name)
}
Person.prototype.others = {} // 其他屬性
}
}
以上這種方法,可以在創建一個實體時,動態的初始化原型,在第一次呼叫建構式時,判斷某個公共的方法是否存在,如果不存在,則初始化原型,添加所有公共屬性和方法,等以后再呼叫實體時,這個條件不會觸發,因此,原型只會被初始化一次,
八、其他模式
除了以上7中創建物件的模式外,還有 寄生建構式模式 和 穩妥建構式模式
十、下一篇
JavaScript面向物件程式設計(二)——繼承
參考資料
《JavaScript高級程式設計》
強烈推薦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/202187.html
標籤:其他
