姓名:房小慧
學號:17101223361
專業(yè):軟件工程
【嵌牛導讀】:學Web前端的人都會用到JavaScript缎玫,本文是對js原型的一個簡單總結(jié)民轴,希望能夠幫到大家
【嵌牛鼻子】:js原型
【嵌牛提問】:js原型是什么?
【嵌牛正文】:
對象
JavaScript 是一種基于對象的編程語言踊谋,但它與一般面向?qū)ο蟮木幊陶Z言不同蝉仇,因為他沒有類(class)的概念。
對象是什么?ECMA-262 把對象定義為:「無序?qū)傩缘募辖蜗危鋵傩钥梢园局党良!ο蠡蛘吆瘮?shù)『裕」簡單來說鞭呕,對象就是一系列的鍵值對(key-value),我習慣把鍵值對分為兩種宛官,屬性(property)和方法(method)葫松。
面向?qū)ο缶幊蹋谖业睦斫饫锸且环N編程思想底洗。這種思想的核心就是把萬物都抽象成一個個對象进宝,它并不在乎數(shù)據(jù)的類型以及內(nèi)容,它在乎的是某個或者某種數(shù)據(jù)能夠做什么枷恕,并且把數(shù)據(jù)和數(shù)據(jù)的行為封裝在一起党晋,構(gòu)建出一個對象,而程序世界就是由這樣的一個個對象構(gòu)成徐块。而類是一種設計模式未玻,用來更好地創(chuàng)建對象。
舉個例子胡控,把我自己封裝成一個簡單的對象扳剿,這個對象擁有我的一些屬性和方法。
//構(gòu)造函數(shù)創(chuàng)建
varklaus =newObject();
klaus.name ='Klaus';
klaus.age =22;
klaus.job ='developer';
klaus.introduce =function(){
console.log('My name is '+this.name +', I\'m '+this.age +' years old.');
};
//字面量語法創(chuàng)建昼激,與上面效果相同
varklaus = {
name:'Klaus',
age:22,
job:'developer',
introduce:function(){
console.log('My name is '+this.name +', I\'m '+this.age +' years old.');
}
};
這個對象中庇绽,name、age 和 job 是數(shù)據(jù)部分橙困,introduce 是數(shù)據(jù)行為部分瞧掺,把這些東西都封裝在一起就構(gòu)成了一個完整的對象。這種思想不在乎數(shù)據(jù)(name凡傅、age 和 job)是什么辟狈,它只在乎這些數(shù)據(jù)能做什么(introduce),并且把它們封裝在了一起(klaus 對象)夏跷。
跑一下題哼转,與面向?qū)ο缶幊滔鄬木幊趟枷胧敲嫦蜻^程編程,它把數(shù)據(jù)和數(shù)據(jù)行為分離槽华,分別封裝成數(shù)據(jù)庫和方法庫壹蔓。方法用來操作數(shù)據(jù),根據(jù)輸入的不同返回不同的結(jié)果猫态,并且不會對輸入數(shù)據(jù)之外的內(nèi)容產(chǎn)生影響佣蓉。與之相對應的設計模式就是函數(shù)式編程披摄。
工廠模式創(chuàng)建對象
如果創(chuàng)建一個簡單的對象,像上面用到的兩種方法就已經(jīng)夠了偏螺。但是如果想要創(chuàng)建一系列相似的對象行疏,這種方法就太過麻煩了。所以套像,就順勢產(chǎn)生了工廠模式酿联。
functioncreatePerson(name, age, job){
varo =newObject();
o.name = name;
o.age = age;
o.job = job;
o.introduce =function(){
console.log('My name is '+this.name +', I\'m '+this.age +' years old.');
};
returno;
}
varklaus = createPerson('Klaus',22,'developer');
隨著 JavaScript 的發(fā)展,這種模式漸漸被更簡潔的構(gòu)造函數(shù)模式取代了夺巩。(高程三中提到工廠模式無法解決對象識別問題贞让,我覺得完全可以加一個_type 屬性來標記對象類型)
構(gòu)造函數(shù)模式創(chuàng)建對象
我們可以通過創(chuàng)建自定義的構(gòu)造函數(shù),然后利用構(gòu)造函數(shù)來創(chuàng)建相似的對象柳譬。
functionPerson(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.introduce =function(){
console.log('My name is '+this.name +', I\'m '+this.age +' years old.');
};
}
varklaus =newPerson('Klaus',22,'developer');
console.log(klausinstanceofPerson);//true
console.log(klausinstanceofObject);//true
現(xiàn)在我們來看一下構(gòu)造函數(shù)模式與工廠模式對比有什么不同:
函數(shù)名首字母大寫:這只是一種約定喳张,寫小寫也完全沒問題,但是為了區(qū)別構(gòu)造函數(shù)和一般函數(shù)美澳,默認構(gòu)造函數(shù)首字母都是大寫销部。
不需要創(chuàng)建對象,函數(shù)最后也不需要返回創(chuàng)建的對象:new 操作符幫你創(chuàng)建對象并返回制跟。
添加屬性和方法的時候用 this:new 操作符幫你把 this 指向創(chuàng)建的對象舅桩。
創(chuàng)建的時候需要用 new 操作符來調(diào)用構(gòu)造函數(shù)。
可以獲取原型上的屬性和方法雨膨。(下面會說)
可以用 instanceof 判斷創(chuàng)建出的對象的類型擂涛。
new
這么看來,構(gòu)造函數(shù)模式的精髓就在于這個 new 操作符上聊记,所以這個 new 到底做了些什么呢撒妈?
創(chuàng)建一個空對象。
在這個空對象上調(diào)用構(gòu)造函數(shù)排监。(所以 this 指向這個空對象)
將創(chuàng)建對象的內(nèi)部屬性__proto__指向構(gòu)造函數(shù)的原型(原型狰右,后面講到原型會解釋)。
檢測調(diào)用構(gòu)造函數(shù)后的返回值社露,如果返回值為對象(不包括 null)則 new 返回該對象挟阻,否則返回這個新創(chuàng)建的對象。
用代碼來模仿大概是這樣的:
function_new(fn){
returnfunction(){
varo =newObject();
varresult = fn.apply(o,arguments);
o.__proto__ = fn.prototype;
if(result && (typeofresult ==='object'||typeofresult ==='function')){
returnresult;
}else{
returno;
}
}
}
varklaus = _new(Person)('Klaus',22,'developer');
組合使用構(gòu)造函數(shù)模式和原型模式
構(gòu)造函數(shù)雖然很好峭弟,但是他有一個問題,那就是創(chuàng)建出的每個實例對象里的方法都是一個獨立的函數(shù)脱拼,哪怕他們的內(nèi)容完全相同瞒瘸,這就違背了函數(shù)的復用原則,而且不能統(tǒng)一修改已創(chuàng)建實例對象里的方法熄浓,所以情臭,原型模式應運而生省撑。
functionPerson(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.introduce =function(){
console.log('My name is '+this.name +', I\'m '+this.age +' years old.');
};
}
varklaus1 =newPerson('Klaus',22,'developer');
varklaus2 =newPerson('Klaus',22,'developer');
console.log(klaus1.introduce === klaus2.introduce);//false
什么是原型?我們每創(chuàng)建一個函數(shù)俯在,他就會自帶一個原型對象竟秫,這個原型對象你可以理解為函數(shù)的一個屬性(函數(shù)也是對象),這個屬性的 key 為 prototype跷乐,所以你可以通過 fn.prototype 來訪問它肥败。這個原型對象除了自帶一個不可枚舉的指向函數(shù)本身的 constructor 屬性外,和其他空對象并無不同愕提。
那這個原型對象到底有什么用呢馒稍?我們知道構(gòu)造函數(shù)也是一個函數(shù),既然是函數(shù)那它也就有自己的原型對象浅侨,既然是對象你也就可以給它添加一些屬性和方法纽谒,而這個原型對象是被該構(gòu)造函數(shù)所有實例所共享的,所以你就可以把這個原型對象當做一個共享倉庫如输。下面來說說他具體是如何共享的鼓黔。
上面講 new 操作符的時候講過有一步,將創(chuàng)建對象的內(nèi)部屬性__proto__指向構(gòu)造函數(shù)的原型不见,這一步才是原型共享的關(guān)鍵澳化。這樣你就可以在新建的實例對象里訪問構(gòu)造函數(shù)原型對象里的數(shù)據(jù)。
functionPerson(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.introduce =this.__proto__.introduce;//這句可以省略脖祈,后面會介紹
}
Person.prototype.introduce =function(){
console.log('My name is '+this.name +', I\'m '+this.age +' years old.');
};
varklaus1 =newPerson('Klaus',22,'developer');
varklaus2 =newPerson('Klaus',22,'developer');
console.log(klaus1.introduce === klaus2.introduce);//true
這樣肆捕,我們就達到了函數(shù)復用的目的,而且如果你修改了原型對象里的 introduce 函數(shù)后盖高,所有實例的 introduce 方法都會同時更新慎陵,是不是很方便呢?但是原型絕對不止是為了這么簡單的目的所創(chuàng)建的喻奥。
我們首先明確一點席纽,當創(chuàng)建一個最簡單的對象的時候,其實默認用 new 調(diào)用了 JavaScript 內(nèi)置的 Objcet 構(gòu)造函數(shù)撞蚕,所以每個對象都是 Object 的一個實例(用 Object.create(null) 等特殊方法創(chuàng)建的暫不討論)润梯。所以根據(jù)上面的介紹,每個對象都有一個__proto__的屬性指向 Object.prototype甥厦。這是理解下面屬性查找機制的前提纺铭。
varklaus = {
name:'Klaus',
age:22,
job:'developer',
introduce:function(){
console.log('My name is '+this.name +', I\'m '+this.age +' years old.');
}
};
console.log(klaus.friend);//undefined
console.log(klaus.toString);//? toString() { [native code] }
上面代碼可以看出,如果我們訪問 klaus 對象上沒有定義的屬性 friend刀疙,結(jié)果返回 undefined舶赔,這個可以理解。但是同樣訪問沒定義的 toString 方法卻返回了一個函數(shù)谦秧,這是不是很奇怪呢竟纳?其實一點不奇怪撵溃,這就是 JavaScript 對象的屬性查找機制。
屬性查找機制:當訪問某對象的某個屬性的時候锥累,如果存在該屬性缘挑,則返回該屬性的值,如果該對象不存在該屬性桶略,則自動查找該對象的__proto__指向的對象的此屬性语淘。如果在這個對象上找到此屬性,則返回此屬性的值删性,如果__proto__指向的對象也不存在此屬性亏娜,則繼續(xù)尋找__proto__指向的對象的__proto__指向的對象的此屬性。這樣一直查下去蹬挺,直到找到 Object.prototype 對象维贺,如果還沒找到此屬性,則返回 undefined巴帮。(原型鏈查找溯泣,講繼承時會詳細講)
理解了上面的查找機制以后,也就不難理解 klaus.toString 其實也就是 klaus.__proto__.toString榕茧,也就是 Object.prototype.toString垃沦,所以就算你沒有定義依然也可以拿到一個函數(shù)。
理解了這一點以后用押,也就理解了上面 Person 構(gòu)造函數(shù)里的那一句我為什么注釋了可以省略肢簿,因為訪問實例的 introduce 找不到時會自動找到實例__proto__指向的對象的 introduce,也就是 Person.prototype.introduce蜻拨。
這也就是原型模式的強大之處池充,因為你可以在每個實例上訪問到構(gòu)造函數(shù)的原型對象上的屬性和方法,而且可以實時修改缎讼,是不是很方便呢收夸。
除了給原型對象添加屬性和方法之外,也可以直接重寫原型對象(因為原型對象本質(zhì)也是一個對象)血崭,只是別忘記添加 constructor 屬性卧惜。
還需要注意一點,如果原型對象共享的某屬性是個引用類型值夹纫,一個實例修改該屬性后咽瓷,其他實例也會因此受到影響。
以及舰讹,如果用 for-in 循環(huán)來遍歷屬性的 key 的時候忱详,會遍歷到原型對象里的可枚舉屬性。
functionPerson(name, age, job){
this.name = name;
this.age = age;
this.job = job;
}
Person.prototype = {
introduce:function(){
console.log('My name is '+this.name +', I\'m '+this.age +' years old.');
},
friends: ['person0','person1','person2']
};
Object.defineProperty(Person.prototype,'constructor', {
enumerable:false,
value: Person
});
varklaus1 =newPerson('Klaus',22,'developer');
varklaus2 =newPerson('Klaus',22,'developer');
console.log(klaus1.friends);//['person0', 'person1', 'person2']
klaus1.friends.push('person3');
console.log(klaus1.friends);//['person0', 'person1', 'person2', 'person3']
console.log(klaus2.friends);//['person0', 'person1', 'person2', 'person3']
for(varkeyinklaus1){
console.log(key);//name, age, job, introduce, friends
}
ES6 class
如果你有關(guān)注最新的 ES6 的話跺涤,你會發(fā)現(xiàn)里面提出了一個關(guān)鍵字 class 的用法匈睁,難道 JavaScript 要有自己類的概念了嗎?
tan90°桶错,不存在的航唆,這只是一個語法糖而已,上面定義的 Person 構(gòu)造函數(shù)可以用 class 來改寫院刁。
classPerson{
constructor(name, age, job){
this.name = name;
this.age = age;
this.job = job;
}
introduce(){
console.log('My name is '+this.name +', I\'m '+this.age +' years old.');
}
}
Person.prototype.friends = ['person0','person1','person2'];
varklaus =newPerson('Klaus',22,'developer');
很遺憾糯钙,ES6 明確規(guī)定 class 里只能有方法而不能有屬性,所以像 friends 這樣的屬性可能只能在外面單獨定義了退腥。
下面簡單舉幾個差異點任岸,如果想詳細了解可以去看阮一峰的《ECMAScript 6 入門》或者 Nicholas C. Zakas 的《Understanding ECMAScript 6》。
class 里的靜態(tài)方法(類似于 introduce)是不可枚舉的狡刘,而用 prototype 定義的是可枚舉的享潜。
class 里面默認使用嚴格模式。
class 已經(jīng)不屬于普通的函數(shù)了嗅蔬,所以不使用 new 調(diào)用會報錯剑按。
class 不存在變量提升。
class 里的方法可以加 static 關(guān)鍵字定義靜態(tài)方法澜术,這種靜態(tài)方法就不是定義在 Person.prototype 上而是直接定義在 Person 上了艺蝴,只能通過 Person.method() 調(diào)用而不會被實例共享。
作用域安全的構(gòu)造函數(shù)
不管是高程還是其他的一些資料都提到過作用域安全的構(gòu)造函數(shù)這個概念鸟废,因為構(gòu)造函數(shù)如果不用 new 來調(diào)用就只是一個普通的函數(shù)而已猜敢,這樣在函數(shù)調(diào)用的時候 this 會指向全局(嚴格模式為 undefined),這樣如果錯誤調(diào)用構(gòu)造函數(shù)就會把屬性和方法定義在 window 上盒延。為了避免這種情況缩擂,可以將構(gòu)造函數(shù)稍加改造,先用 instanceof 檢測 this 然后決定調(diào)用方法兰英。
functionPerson(name, age, job){
if(thisinstanceofPerson){
this.name = name;
this.age = age;
this.job = job;
}else{
returnnewPerson(name, age, job);
}
}
varklaus1 = Person('Klaus',22,'developer');
varklaus2 =newPerson('Klaus',22,'developer');//兩種方法結(jié)果一樣
不過個人認為這種沒什么必要撇叁,構(gòu)造函數(shù)已經(jīng)首字母大寫來加以區(qū)分了,如果還錯誤調(diào)用的話那也沒啥好說的了畦贸。陨闹。。