JavaScript | 物件和繼承
一、 建立物件的幾種方式
- 工廠模式
工廠模式就是:定義一個“工廠函式”,每次呼叫這個函式就會得到一個物件。工廠模式建立的物件,是一個函式結果而不能確定型別。
function createPerson(name,age){
var o = new Object()
o.name=name
o.age=age
return o
}
var p = createPerson('hhh',12)
//這時候p就相當於
{
name:'hhh',
age:12
}
//但是P是 沒有一個型別可言的。如果用typeof,只能是Object型別、
2.建構函式模式
建構函式建構函式,顧名思義,如果你還記得原型鏈的話,應該知道,每個物件例項都有一個__proto__屬性,指向的是它們的原型。而原型裡有一個屬性是 constructor ,這個屬性其實就是這個建構函式。也就是說:
通過建構函式生成物件,其實就是先寫一個建構函式,這個建構函式的裡面的this,指的其實是這個函式的prototype,即原型。建構函式定義好,那通過new這個建構函式,就可以建立這個建構函式的原型的孩子,即物件例項。
//定義建構函式 function Person(namem,age){ this.name= name; this.age= age; //想一下這裡面的this指向的是誰? //原型鏈:每個函式都有一個prototype屬性,每個物件都有一個__proto__屬性 //指向的 便是 `原型` //所以,這裡的this,其實指向的就是Person這個函式的原型:Person.prototype this.sayHello=function(){ console.log("hello") } } //生成物件例項 var p = new Person('hhh',12) //這裡,p.__proto__=== Person.prototype //那你想一下,例項物件p的原型的建構函式(constructor)又是誰呢? //就是Person()
這時候例項物件p是有型別可言的。它就是Person型別。
這裡就出現了一個新規則:凡是通過new來呼叫的函式,會被當作建構函式。否則就是普通函式。
建構函式的缺點就是:每次生成 一個物件例項,相當於呼叫一次建構函式,每次呼叫建構函式,裡面的方法都會被重新建立一遍,造成資源浪費。(怎麼證明每個建立的物件例項裡面的方法都是不等價的?)
//1.首先,this.sayHello=function(){}這句話其實是: var tmp = new Function() this.sayHello= tmp //所以,每次呼叫建構函式,都會建立一個函式例項(即Function型別的物件例項) //2.其次,通過測試 p1 = new Person('1',12) p2 = new Person('2',21) console.log(p1.sayHello===p2.sayHello)//結果是false
這個問題可以通過把函式定義到外面來解決。在建構函式外面只宣告一次函式。
但是新問題又來了,這個宣告的函式就和物件、型別沒什麼關係了。
3.原型模式
因為每個建立的函式都有一個prototype屬性。這個屬性是一個指標,指向的是一個物件。也就是父親。
這個父親所擁有的所有的屬性和方法,都可以被孩子繼承。那麼,給這個父親新增屬性和方法,其實也在給孩子新增屬性和方法。
function Person(){
//這是構造孩子的建構函式
}
Person.prototype.name = 'tmpname'
Person.prototype.age = 0
Person.protytype.sayHello = function(){
console.log('hello')
}
var p1 = new Person()
var p2 = new Person()
//看這兩個物件例項,就是通過建構函式建立的孩子,他們其實都有name和age屬性
//p1和p2都可以訪問name 和 age,但是都是原型中的屬性值
//如果我們給屬性值重新賦值,其實不是改變值,而是覆蓋掉孩子繼承的這個屬性
p1.name='ware'
//這句話的意思是,給p1一個name屬性,值為ware,然而這個屬性由於和原型中的屬性同名
//則會覆蓋掉原型中的這個屬性,而不是修改掉原型中的這個屬性值
//如果我們想重新訪問原型中的屬性值,只需要把這個屬性delete掉就可以了
delete p1.name
hasOwnProperty() 方法,可以檢測物件的屬性是自己的還是繼承自原型的
3.1 in操作符
in 操作符 在 通過物件能夠訪問到給定屬性時 返回true
console.log('name' in p1) //true
同時使用hasOwnProperty()和In操作符能夠確定屬性是存在於物件中還是原型中:
function whereProperty(obj,pro){
console.log(!obj.hasOwnProperty(pro)&&(pro in obj)?'在原型裡':'在物件裡')
}
in 操作符可以和for聯合使用,用來遍歷物件所有能訪問到的(包括原型中的) 可列舉(enumerated)屬性。
通過keys()方法,也可以達到類似效果。這個方法返回一個數組。
3.2 原型模式的簡寫
前面的例子可以簡寫:
function Person(){
}
Person.prototype = {
name:'tmpName',
age:0,
sayHello:function(){
console.log('hello')
}
}
但是這樣的寫法,相當於給Person的原型賦值了,而原來的寫法只是給Person的原型新增屬性。這是兩種概念。
預設的,我們建立一個函式,同時會建立它的prototype物件。而這個函式本身,就是原型物件的construtor。
但是這樣的簡寫方式,相當於覆蓋掉了預設的prototype物件。所以,既然覆蓋掉了,而我們重寫的時候,這個原型物件就沒有construtor屬性,那就會從Object類裡面繼承,因為
{
name:'tmpName',
age:0,
sayHello:function(){
console.log('hello')
}
本身是一個Object型別的物件。
如果我們希望以後通過這個建構函式建立的物件例項,可以訪問construtor,並且指向的是Person,那我們就應該在重新給原型賦值的時候,帶上constructor屬性。
Person.prototype = {
constructor:Person,
name:'tmpName',
age:0,
sayHello:function(){
console.log('hello')
}
}
不過直接寫明,會讓constructor屬性變為可列舉的。如果想要原來不可列舉的效果,用Object.defineProperty() 這個方法。
Object.defineProperty(Person.prototype,'constructor',{
enumerable:false,
value:Person
})
對原型的操作(比如新增屬性、方法)是動態的,不管孩子是什麼時候建立的,只要父親變了,孩子就會跟著變。
原型模式的缺點就是:所有的孩子在建立時,會有統一的屬性及屬性值。也就是說,沒有定製性了。
- 混合模式
所謂混合就是:建構函式定義和原型自定義兩種模式的混合。
建構函式定義,定義的是什麼?是當前建構函式可生成的例項的屬性和方法。
原型定義,定義的是什麼? 是原型的屬性和方法,共享於每個例項。
構造+原型、動態原型、寄生構造、穩妥構造 四種方式。寄生構造模式只需要瞭解,用處不大。穩妥構造方式其實就是封裝物件屬性。
二、繼承
如果原型鏈沒有任何問題的話,繼承其實就是:所有的例項繼承其原型,或其原型鏈上面的所有父原型。
但是,不湊巧,原型鏈有個問題。
原型中定義的屬性,會被所有例項共享,除非例項物件裡覆蓋掉這個屬性。——這是對於基本資料型別而言。
原型中定義的“引用型別值”的屬性,會被所有例項共享。
那什麼是“引用型別值” 呢?
ECMAScript 變數可能包含兩種不同資料型別的值:基本型別值和引用型別值。基本型別值指的是簡單的資料段,而引用型別值指那些可能由多個值構成的物件。引用型別的值是儲存在記憶體中的物件。與其他語言不同,JavaScript 不允許直接訪問記憶體中的位置,也就是說不能直接操作物件的記憶體空間。在操作物件時,實際上是在操作物件的引用而不是實際的物件。為此,引用型別的值是按引用訪問的。
也就是說,我們如果在原型裡定義一個屬性——陣列型別的。那孩子繼承的這個屬性其實是這個屬性的引用。更改孩子中這個陣列,意味著更改引用。
2.1 借用建構函式
借用建構函式,其實就是在 孩子的建構函式中,呼叫父親(原型)的建構函式。這樣,就把父親建構函式中定義的所有程式碼都在子類的建構函式中執行了一遍。
function Father(){
this.color=[1,2,3]
}
function Child(){
Father.call(this)
}
//這時候用Child new一個物件例項,那物件例項就擁有了color這個屬性,而且是獨自擁有color的拷貝。
call 和 apply 講解 改變當前作用域中this物件。
這種繼承方式有建構函式模式的問題:方法都定義在建構函式裡,不可複用且資源浪費。
2.2 組合繼承
組合繼承 其實就是應用個 混合模式中的原型+建構函式模式。
function Father(name){
this.name = name
this.color = ['red','yellow','blue']
}
function Child(name,age){
Father.call(this,name)
this.age=age
}
Father.prototype.sayHello=function(){
console.log(this.name)
}
//1.建立父建構函式
//2.建立子建構函式並繼承父類中的name屬性
//3.給父型別的原型新增一個方法sayHello
Child.prototype = new Father()//給子型別新增一個原型,這個原型就是父型別的例項
Child.prototype.constructor = Child//確定通過子型別生成的例項物件 是Child型別
//到這裡,所有通過new Child()建立的物件例項,都擁有了sayHello方法,各自擁有color/name/age屬性
2.3 原型式繼承/寄生式繼承
很有意思的一個想法,道格拉斯·克羅克福德在2006年提出來的。我們不急去了解它,先整理一下思路:按前面那些方式,到底建立一個繼承於父類的物件例項的本質是什麼?
本質很簡單:按照父親創建出孩子。不僅要保證每個孩子有自己的個性,還要保證每個孩子一樣的地方不需要重複創造,而且單個孩子的某個動作,不會影響到父親以至於波及到其他孩子。
逐條分析:
- 保證每個孩子有自己的個性:孩子的建構函式就是幹這個事的。每個孩子有自己的獨有屬性,那這些獨有屬性就在建構函式裡寫。其他的都在父親(原型)裡繼承。
- 保證孩子一樣的地方不需要重複創造:每個孩子都會說話、吃飯、睡覺,這些不必要在孩子的建構函式裡寫,只需要在父親(原型)裡寫就可以了。
- 不會影響到父親波及其他孩子:引用型別值的屬性。這些屬性如果是繼承的,那一個孩子更改了這個屬性,這個父親的所有孩子都會改變了。因為所有的孩子裡的這個屬性,都是引用,而不是值。
所以前面才會有這些繼承方式,這些建立物件的方式。
道格拉斯這位兄弟有一天突發奇想,這世界上某個物件了,那通過現有的這個物件,是不是可以直接建立新物件?
function child(FatherIns){
function F(){}
F.prototype = FaherIns
return new F()
}
//本質是建立一個把FahterIns當作原型的 建構函式
//然後通過這個建構函式建立一個孩子
其實這種繼承方式的本質是:物件的深拷貝。而並非嚴格的繼承。所以,這種繼承方式的前提是:1.有現成繼承的物件,2.不需要考慮型別 3.現有物件中如果存在引用型別值屬性,將會被所有孩子繼承。
於是,ES5為此給Object增添了一個新方法:Object.create()用來建立新物件,接收兩個引數:1.用作新物件原型的物件,2.一個為新物件定義額外屬性的物件。
然後,道哥又想,能不能給生成的物件新增方法呢?
然後就:
function child(fatherObj){
var tmp = Object.create(fatherObj,{
childPro:{
value:'xxxxx'
}
})
tmp.childMethod = function(){
...
}
return tmp;
}
這特麼就是寄生式繼承。
2.4 寄生組合式繼承
你以為道哥思想的影響真的就這麼簡單麼?然而並不是。回看一下組合式繼承。
組合式繼承的思路:
建立子型別的建構函式。
在建構函式中,呼叫父類的建構函式。
- 定義完建構函式之後,外面還要給子型別指定原型:Child.prototype = new Father()
- 我們都知道指定原型造成的弊端就是失去constructor。所以再指定一下constructor. Child.prototype.constructor = Child
這時候繼承定義完成。
這時候我們發現,Father()這個建構函式呼叫了兩次啊。而且,Child的prototype我們其實是不關心它的型別的。並且,Child.prototype可不可以從一個現有的物件建立呢?完全可以啊。那這個現有的物件就是Father.prototype啊。
所以我們就可以把3、4步寫成:
var prototype = Object.create(Father.prototype)
Child.prototype = prototype
prototype.constructor = Child
看,這裡並沒有給Child一個通過Father()新建的例項,而是通過Father.prototype拷貝的例項。因為這個例項的型別並不是我們關心的。