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拷貝的實例。因為這個實例的類型並不是我們關心的。
JavaScript | 對象和繼承