1.對象的定義
在ECMAScript-262中八回,對象被定義為“無序?qū)傩缘募希鋵傩钥梢园局捣园樱瑢ο蠡蛘吆瘮?shù)”毒姨。也就是說,在JavaScript中钾恢,對象無非就是由一些列無序的key-value對組成手素。其中value可以是基本值,對象或者函數(shù)瘩蚪。
// 這里的person就是一個對象
var person = {
name: 'Tom',
age: 18,
getName: function() {},
parent: {}
}
創(chuàng)建對象
我們可以通過new的方式創(chuàng)建一個對象泉懦。
var obj = new Object();
也可以通過對象字面量的形式創(chuàng)建一個簡單的對象。
var obj = {};
當(dāng)我們想要給我們創(chuàng)建的簡單對象添加方法時疹瘦,可以這樣表示崩哩。
// 可以這樣
var person = {};
person.name = "TOM";
person.getName = function() {
return this.name;
}
// 也可以這樣
var person = {
name: "TOM",
getName: function() {
return this.name;
}
}
訪問對象的屬性和方法
假如我們有一個簡單的對象如下:
var person = {
name: 'TOM',
age: '20',
getName: function() {
return this.name
}
}
當(dāng)我們想要訪問他的name屬性時,可以用如下兩種方式訪問言沐。
person.name
// 或
person['name']
如果我們想要訪問的屬性名是一個變量時邓嘹,常常會使用第二種方式。例如我們要同時訪問person的name與age险胰,可以這樣寫:
['name', 'age'].forEach(function(item) {
console.log(person[item]);
})
2.工廠模式
使用上面的方式創(chuàng)建對象很簡單汹押,但是在很多時候并不能滿足我們的需求。就以person對象為例起便。假如我們在實際開發(fā)中棚贾,不僅僅需要一個名字叫做TOM的person對象窖维,同時還需要另外一個名為Jake的person對象,雖然他們有很多相似之處妙痹,但是我們不得不重復(fù)寫兩次铸史。
var perTom = {
name: 'TOM',
age: 20,
getName: function() {
return this.name
}
};
var perJake = {
name: 'Jake',
age: 22,
getName: function() {
return this.name
}
}
我們可以使用工廠模式的方式解決這個問題。顧名思義怯伊,工廠模式就是我們提供一個模子琳轿,然后通過這個模子復(fù)制出我們需要的對象。我們需要多少個耿芹,就復(fù)制多少個崭篡。
var createPerson = function (name, age) {
// 聲明一個對象,該對象就是工廠模式的模子
var o = new Object();
// 依次添加我們需要的屬性和方法
o.name = name;
o.age = age;
o.getName = function () {
return this.name;
}
return o;
}
// 創(chuàng)建兩個實例
var perTom = createPerson('TOM', 20);
var perJake = createPerson('Jake', 20);
相信上面的代碼并不難理解猩系,也不用把工廠模式看得太過高大上媚送。很顯然,工廠模式幫助我們解決了重復(fù)代碼上的麻煩寇甸,讓我們可以寫很少的代碼塘偎,就能夠創(chuàng)建很多個person對象。但是這里還有兩個麻煩拿霉,需要我們注意吟秩。 這種方法的問題是,perTom和perJake之間沒有內(nèi)在的聯(lián)系绽淘,不能反映出它們是同一個原型對象的實例涵防。 第一個麻煩就是這樣處理,我們沒有辦法識別對象實例的類型沪铭。使用instanceof可以識別對象的類型壮池,如下例子:
var obj = {};
var foo = function() {}
console.log(obj instanceof Object); // true
console.log(foo instanceof Function); // true
因此在工廠模式的基礎(chǔ)上,我們需要使用構(gòu)造函數(shù)的方式來解決這個麻煩杀怠。
3.構(gòu)造函數(shù)
所謂”構(gòu)造函數(shù)”椰憋,其實就是一個普通函數(shù),但是內(nèi)部使用了this變量赔退。對構(gòu)造函數(shù)使用new運(yùn)算符橙依,就能生成實例,并且this變量會綁定在實例對象上硕旗。
比如窗骑,貓的原型對象現(xiàn)在可以這樣寫,
function Cat(name,color){
this.name=name;
this.color=color;
}
我們現(xiàn)在就可以生成實例對象了漆枚。
var cat1 = new Cat("大毛","黃色");
var cat2 = new Cat("二毛","黑色");
alert(cat1.name); // 大毛
alert(cat1.color); // 黃色
這時cat1和cat2會自動含有一個constructor屬性创译,指向它們的構(gòu)造函數(shù)。
alert(cat1.constructor == Cat); //true
alert(cat2.constructor == Cat); //true
Javascript還提供了一個instanceof運(yùn)算符墙基,驗證原型對象與實例對象之間的關(guān)系软族。
alert(cat1 instanceof Cat); //true
alert(cat2 instanceof Cat); //true
構(gòu)造函數(shù)方法很好用辛藻,但是存在一個浪費(fèi)內(nèi)存的問題。
請看互订,我們現(xiàn)在為Cat對象添加一個不變的屬性type(種類),再添加一個方法eat(吃)痘拆。那么仰禽,原型對象Cat就變成了下面這樣:
function Cat(name,color){
this.name = name;
this.color = color;
this.type = "貓科動物";
this.eat = function(){
alert("吃老鼠");
};
}
還是采用同樣的方法,生成實例:
var cat1 = new Cat("大毛","黃色");
var cat2 = new Cat ("二毛","黑色");
alert(cat1.type); // 貓科動物
cat1.eat(); // 吃老鼠
表面上好像沒什么問題纺蛆,但是實際上這樣做吐葵,有一個很大的弊端。那就是對于每一個實例對象桥氏,type屬性和eat()方法都是一模一樣的內(nèi)容温峭,每一次生成一個實例,都必須為重復(fù)的內(nèi)容字支,多占用一些內(nèi)存凤藏。這樣既不環(huán)保,也缺乏效率堕伪。 能不能讓type屬性和eat()方法在內(nèi)存中只生成一次揖庄,然后所有實例都指向那個內(nèi)存地址呢?回答是可以的欠雌。
4.Prototype模式(原型模式)
Javascript規(guī)定蹄梢,每一個構(gòu)造函數(shù)都有一個prototype屬性,指向另一個對象富俄。這個對象的所有屬性和方法禁炒,都會被構(gòu)造函數(shù)的實例繼承。
這意味著霍比,我們可以把那些不變的屬性和方法幕袱,直接定義在prototype對象上。
function Cat(name, color) {
this.name = name;
this.color = color;
}
Cat.prototype.type = "貓科動物";
Cat.prototype.eat = function () {
alert("吃老鼠");
}
然后桂塞,生成實例凹蜂。
var cat1 = new Cat("大黑", "黑色");
var cat1 = new Cat("大黃", "黃色");
這是所有實例的type屬性和eat()方法,其實都是同一個內(nèi)存地址阁危,指向prototype對象玛痊,因此提高了運(yùn)行效率。
alert(cat1.eat == cat2.eat); //true
5.構(gòu)造函數(shù)的繼承
現(xiàn)在有一個“動物”對象的構(gòu)造函數(shù)狂打。
function Animal (){
this.species = "動物";
}
還有一個“貓”對象的構(gòu)造函數(shù)擂煞。
function Cat(name, color) {
this.name = name;
this.color = color;
}
怎樣使“貓”繼承“動物”呢?
(1)構(gòu)造函數(shù)綁定
第一種方法就是使用call或apply方法趴乡,將父對象的構(gòu)造函數(shù)綁定在子對象上对省,即在子對象構(gòu)造函數(shù)中加一行:
function Cat(name,color){
Animal.apply(this, arguments); // arguments指參數(shù)數(shù)組[name,color]
this.name = name;
this.color = color;
}
var cat1 = new Cat("大黑", "黑色")蝗拿;
console.log(cat1.species); //動物
(2)prototype模式
第二種方法更常見,使用prototype屬性蒿涎。
Javascript規(guī)定哀托,每一個構(gòu)造函數(shù)都有一個prototype屬性,指向另一個對象劳秋。這個對象的所有屬性和方法仓手,都會被構(gòu)造函數(shù)的實例繼承。
如果“貓”的prototype對象玻淑,指向一個Animal的實例嗽冒,那么所有“貓”的實例,就能繼承Animal了补履。
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大黑", "黑色");
console.log(cat1.species); //動物
代碼的第一行添坊,我們將Cat的prototype對象指向一個Animal的實例。
Cat.prototype = new Animal();
相當(dāng)于完全刪除了prototype對象原先的值箫锤,然后賦予一個新值贬蛙。 但是在這一行之前Cat.prototype.constructor是指向Cat的,加了這一行后谚攒,Cat.prototype.constructor指向了Animal速客。 由于每一個實例也有一個constructor屬性,默認(rèn)調(diào)用prototype對象的constructor屬性五鲫。 這會導(dǎo)致構(gòu)造函數(shù)Cat生成實例的constructor卻指向了Animal,從而造成繼承鏈的紊亂溺职。
所以第二行就是將Cat.prototype.constructor改回指向Cat。
Cat.prototype.constructor = Cat;
所以在編程中位喂,如果替換了prototype對象浪耘,必須為新的prototype對象加上constructor屬性,并將這個屬性指回原來的構(gòu)造函數(shù)塑崖。
(3)直接繼承prototype
第三種方法是對第二種方法的改進(jìn)七冲。由于Animal對象中,不變的屬性都可以直接寫入Animal.protype规婆。所以澜躺,我們也可以讓Cat()跳過Animal(),直接繼承Animal.prototype抒蚜。
現(xiàn)在掘鄙,我們將Animal對象改寫:
function Animal (){ }
Animal.prototype.species = "動物";
然后嗡髓,將Cat的prototype對象操漠,然后指向Animal的prototype對象,這樣就完成了繼承饿这。
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大黃","黃色");
alert(cat1.species); // 動物
與前一種方法相比浊伙,這樣做的優(yōu)點是效率比較高(不用執(zhí)行和建立Animal的實例了)撞秋,比較省內(nèi)存。缺點是Cat.prototype和Animal.prototype現(xiàn)在指向了同一個對象嚣鄙,那么任何對Cat.prototype的修改吻贿,都會影響到Animal.prototype。
所以哑子,上面的代碼是有問題的廓八。第二行
Cat.prototype.constructor = Cat;
這一句實際上把Animal.prototype對象的屬性也改掉了
alert(Animal.prototype.constructor); // Cat
(4)利用空對象作為中介
由于直接繼承prototype存在上述的缺點,所以就有第四種方法赵抢,利用一個空對象作為中介。
var F = function (){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
F是空對象声功,所以幾乎不占內(nèi)存烦却。這時,修改Cat的prototype對象先巴,就不會影響到Animal的prototype對象其爵。
alert(Animal.prototype.constructor); //Animal
我們將上面的方法,封裝成一個函數(shù)伸蚯,便于使用摩渺。
function extend(Child, Parent) {
var F = function() {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Clild;
Child.uber = Parent.prototype;
//這里的意思是為子對象設(shè)置一個uber屬性,直接指向父對象的prototype屬性剂邮。
這等于在子對象上打開一條通道摇幻,可以直接調(diào)用父對象的方法。
這一行放在這里挥萌,只是為了實現(xiàn)繼承的完備性绰姻,純屬備用性質(zhì)
}
使用的時候
extend(Cat, Animal);
var cat1 = new Cat("大黑", "黑色");
console.log(cat1.species); // 動物
(5)拷貝繼承
上面采用prototype對象,實現(xiàn)繼承引瀑。我們換一種思路狂芋,純粹采用“拷貝”方法實現(xiàn)繼承。簡單說憨栽,如果把父對象的所有屬性和方法帜矾,拷進(jìn)子對象,不也能實現(xiàn)繼承嗎屑柔?這樣我們就有了第五種方法屡萤。
首先,還是把Animal的所有不變屬性掸宛,都放到它的prototype對象上灭衷。
function Animal(){}
Animal.prototype.species = "動物";
然后旁涤,再寫一個函數(shù)翔曲,實現(xiàn)屬性拷貝的目的迫像。
function extend2(Child, Parent){
var p = Parent.prototype;
var c = Child.prototype;
for(var i in p) {
c[i] = p[i];
}
c.uber = p;
}
這個函數(shù)的作用,就是將父對象的prototype對象中的屬性瞳遍,一一拷貝給Child對象的prototype對象闻妓。 使用的時候
extend2(Cat, Animal);
var cat1 = new Cat("大黑", "黑色");
console.log(cat1.species); // 動物
6.非構(gòu)造函數(shù)的繼承
一、什么是”非構(gòu)造函數(shù)”的繼承掠械? 比如下面有兩個普通對象由缆,不是構(gòu)造函數(shù)
var Chinese = {
nation: '中國'
};
var Doctor = {
career: '醫(yī)生'
};
怎么讓“醫(yī)生”去繼承“中國人”,然后生成一個“中國醫(yī)生”的對象猾蒂?
1.object()方法
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
這個object()函數(shù)均唉,其實只做一件事,就是把子對象的prototype屬性肚菠,指向父對象舔箭,從而使得子對象與父對象連在一起。
使用的時候蚊逢,第一步先在父對象的基礎(chǔ)上层扶,生成子對象:
var Doctor = object(Chinese);
然后,再加上子對象的屬性:
Doctor.career = '醫(yī)生';
這時烙荷,子對象已經(jīng)繼承了父對象的屬性了镜会。
2.淺拷貝 除了使用“prototype鏈”以外,還有另一招那個思路:把父對象的屬性全部拷貝給子對象终抽,也能實現(xiàn)繼承戳表。
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
var Doctor = extendCopy(Chinese);
Doctor.career = '醫(yī)生';
alert(Doctor.nation); // 中國
但是,這樣的拷貝有一個問題昼伴。那就是扒袖,如果父對象的屬性等于數(shù)組或另一個對象,那么實際上亩码,子對象獲得的只是一個內(nèi)存地址季率,而不是真正拷貝,因此存在父對象被篡改的可能描沟。
3.深拷貝 所謂”深拷貝”飒泻,就是能夠?qū)崿F(xiàn)真正意義上的數(shù)組和對象的拷貝。它的實現(xiàn)并不難吏廉,只要遞歸調(diào)用”淺拷貝”就行了泞遗。
var Chinese = {
nation: '中國',
birthPlaces : ['北京','上海','香港'],
};
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
var Doctor = deepCopy(Chinese);
Doctor.birthPlaces.push('廈門');
console.log(Doctor.birthPlaces, Chinese.birthPlaces);
目前,jQuery庫使用的就是這種繼承方法席覆。