鉴别一个人是否 js 入门的标准竟然是?!
不知不覺(jué)跳入前端「大坑」也已經(jīng)有大半年了,學(xué)到了很多知識(shí)。為了讓知識(shí)更好地沉淀,我打算寫(xiě)一系列的知識(shí)總結(jié),希望能在回顧知識(shí)的同時(shí)也能幫到別的同學(xué)。
忘記在哪里看到過(guò),有人說(shuō)鑒別一個(gè)人是否 js 入門(mén)的標(biāo)準(zhǔn)就是看他有沒(méi)有理解 js 原型,所以第一篇總結(jié)就從這里出發(fā)。
對(duì)象
JavaScript 是一種基于對(duì)象的編程語(yǔ)言,但它與一般面向?qū)ο蟮木幊陶Z(yǔ)言不同,因?yàn)樗麤](méi)有類(lèi)(class)的概念。
對(duì)象是什么?ECMA-262 把對(duì)象定義為:「無(wú)序?qū)傩缘募?#xff0c;其屬性可以包含基本值、對(duì)象或者函數(shù)。」簡(jiǎn)單來(lái)說(shuō),對(duì)象就是一系列的鍵值對(duì)(key-value),我習(xí)慣把鍵值對(duì)分為兩種,屬性(property)和方法(method)。
面向?qū)ο缶幊?#xff0c;在我的理解里是一種編程思想。這種思想的核心就是把萬(wàn)物都抽象成一個(gè)個(gè)對(duì)象,它并不在乎數(shù)據(jù)的類(lèi)型以及內(nèi)容,它在乎的是某個(gè)或者某種數(shù)據(jù)能夠做什么,并且把數(shù)據(jù)和數(shù)據(jù)的行為封裝在一起,構(gòu)建出一個(gè)對(duì)象,而程序世界就是由這樣的一個(gè)個(gè)對(duì)象構(gòu)成。而類(lèi)是一種設(shè)計(jì)模式,用來(lái)更好地創(chuàng)建對(duì)象。
舉個(gè)例子,把我自己封裝成一個(gè)簡(jiǎn)單的對(duì)象,這個(gè)對(duì)象擁有我的一些屬性和方法。
//構(gòu)造函數(shù)創(chuàng)建
var?klaus =?new?Object();
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.');
};
//字面量語(yǔ)法創(chuàng)建,與上面效果相同
var?klaus = {
? ?name:?'Klaus',
? ?age:?22,
? ?job:?'developer',
? ?introduce:?function(){
? ? ? ?console.log('My name is '?+?this.name +?', I\'m '?+?this.age +?' years old.');
? ?}
};
這個(gè)對(duì)象中,name、age 和 job 是數(shù)據(jù)部分,introduce 是數(shù)據(jù)行為部分,把這些東西都封裝在一起就構(gòu)成了一個(gè)完整的對(duì)象。這種思想不在乎數(shù)據(jù)(name、age 和 job)是什么,它只在乎這些數(shù)據(jù)能做什么(introduce),并且把它們封裝在了一起(klaus 對(duì)象)。
跑一下題,與面向?qū)ο缶幊滔鄬?duì)應(yīng)的編程思想是面向過(guò)程編程,它把數(shù)據(jù)和數(shù)據(jù)行為分離,分別封裝成數(shù)據(jù)庫(kù)和方法庫(kù)。方法用來(lái)操作數(shù)據(jù),根據(jù)輸入的不同返回不同的結(jié)果,并且不會(huì)對(duì)輸入數(shù)據(jù)之外的內(nèi)容產(chǎn)生影響。與之相對(duì)應(yīng)的設(shè)計(jì)模式就是函數(shù)式編程。
工廠模式創(chuàng)建對(duì)象
如果創(chuàng)建一個(gè)簡(jiǎn)單的對(duì)象,像上面用到的兩種方法就已經(jīng)夠了。但是如果想要?jiǎng)?chuàng)建一系列相似的對(duì)象,這種方法就太過(guò)麻煩了。所以,就順勢(shì)產(chǎn)生了工廠模式。
? ?var?o =?new?Object();
? ?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.');
? ?};
? ?return?o;
}
var?klaus = createPerson('Klaus',?22,?'developer');
隨著 JavaScript 的發(fā)展,這種模式漸漸被更簡(jiǎn)潔的構(gòu)造函數(shù)模式取代了。(高程三中提到工廠模式無(wú)法解決對(duì)象識(shí)別問(wèn)題,我覺(jué)得完全可以加一個(gè)_type 屬性來(lái)標(biāo)記對(duì)象類(lèi)型)
構(gòu)造函數(shù)模式創(chuàng)建對(duì)象
我們可以通過(guò)創(chuàng)建自定義的構(gòu)造函數(shù),然后利用構(gòu)造函數(shù)來(lái)創(chuàng)建相似的對(duì)象。
? ?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.');
? ?};
}
var?klaus =?new?Person('Klaus',?22,?'developer');
console.log(klaus?instanceof?Person); ?//true
console.log(klaus?instanceof?Object); ?//true
現(xiàn)在我們來(lái)看一下構(gòu)造函數(shù)模式與工廠模式對(duì)比有什么不同:
函數(shù)名首字母大寫(xiě):這只是一種約定,寫(xiě)小寫(xiě)也完全沒(méi)問(wèn)題,但是為了區(qū)別構(gòu)造函數(shù)和一般函數(shù),默認(rèn)構(gòu)造函數(shù)首字母都是大寫(xiě)。
不需要?jiǎng)?chuàng)建對(duì)象,函數(shù)最后也不需要返回創(chuàng)建的對(duì)象:new 操作符幫你創(chuàng)建對(duì)象并返回。
添加屬性和方法的時(shí)候用 this:new 操作符幫你把 this 指向創(chuàng)建的對(duì)象。
創(chuàng)建的時(shí)候需要用 new 操作符來(lái)調(diào)用構(gòu)造函數(shù)。
可以獲取原型上的屬性和方法。(下面會(huì)說(shuō))
可以用 instanceof 判斷創(chuàng)建出的對(duì)象的類(lèi)型。
new
這么看來(lái),構(gòu)造函數(shù)模式的精髓就在于這個(gè) new 操作符上,所以這個(gè) new 到底做了些什么呢?
創(chuàng)建一個(gè)空對(duì)象。
在這個(gè)空對(duì)象上調(diào)用構(gòu)造函數(shù)。(所以 this 指向這個(gè)空對(duì)象)
將創(chuàng)建對(duì)象的內(nèi)部屬性__proto__指向構(gòu)造函數(shù)的原型(原型,后面講到原型會(huì)解釋)。
檢測(cè)調(diào)用構(gòu)造函數(shù)后的返回值,如果返回值為對(duì)象(不包括 null)則 new 返回該對(duì)象,否則返回這個(gè)新創(chuàng)建的對(duì)象。
用代碼來(lái)模仿大概是這樣的:
? ?return?function(){
? ? ? ?var?o =?new?Object();
? ? ? ?var?result = fn.apply(o,?arguments);
? ? ? ?o.__proto__ = fn.prototype;
? ? ? ?if(result && (typeof?result ===?'object'?||?typeof?result ===?'function')){
? ? ? ? ? ?return?result;
? ? ? ?}else{
? ? ? ? ? ?return?o;
? ? ? ?}
? ?}
}
var?klaus = _new(Person)('Klaus',?22,?'developer');
組合使用構(gòu)造函數(shù)模式和原型模式
構(gòu)造函數(shù)雖然很好,但是他有一個(gè)問(wèn)題,那就是創(chuàng)建出的每個(gè)實(shí)例對(duì)象里的方法都是一個(gè)獨(dú)立的函數(shù),哪怕他們的內(nèi)容完全相同,這就違背了函數(shù)的復(fù)用原則,而且不能統(tǒng)一修改已創(chuàng)建實(shí)例對(duì)象里的方法,所以,原型模式應(yīng)運(yùn)而生。
? ?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.');
? ?};
}
var?klaus1 =?new?Person('Klaus',?22,?'developer');
var?klaus2 =?new?Person('Klaus',?22,?'developer');
console.log(klaus1.introduce === klaus2.introduce); ?//false
什么是原型?我們每創(chuàng)建一個(gè)函數(shù),他就會(huì)自帶一個(gè)原型對(duì)象,這個(gè)原型對(duì)象你可以理解為函數(shù)的一個(gè)屬性(函數(shù)也是對(duì)象),這個(gè)屬性的 key 為 prototype,所以你可以通過(guò) fn.prototype 來(lái)訪問(wèn)它。這個(gè)原型對(duì)象除了自帶一個(gè)不可枚舉的指向函數(shù)本身的 constructor 屬性外,和其他空對(duì)象并無(wú)不同。
那這個(gè)原型對(duì)象到底有什么用呢?我們知道構(gòu)造函數(shù)也是一個(gè)函數(shù),既然是函數(shù)那它也就有自己的原型對(duì)象,既然是對(duì)象你也就可以給它添加一些屬性和方法,而這個(gè)原型對(duì)象是被該構(gòu)造函數(shù)所有實(shí)例所共享的,所以你就可以把這個(gè)原型對(duì)象當(dāng)做一個(gè)共享倉(cāng)庫(kù)。下面來(lái)說(shuō)說(shuō)他具體是如何共享的。
上面講 new 操作符的時(shí)候講過(guò)有一步,將創(chuàng)建對(duì)象的內(nèi)部屬性__proto__指向構(gòu)造函數(shù)的原型,這一步才是原型共享的關(guān)鍵。這樣你就可以在新建的實(shí)例對(duì)象里訪問(wèn)構(gòu)造函數(shù)原型對(duì)象里的數(shù)據(jù)。
? ?this.name = name;
? ?this.age = age;
? ?this.job = job;
? ?this.introduce =?this.__proto__.introduce; ?//這句可以省略,后面會(huì)介紹
}
Person.prototype.introduce =?function(){
? ?console.log('My name is '?+?this.name +?', I\'m '?+?this.age +?' years old.');
};
var?klaus1 =?new?Person('Klaus',?22,?'developer');
var?klaus2 =?new?Person('Klaus',?22,?'developer');
console.log(klaus1.introduce === klaus2.introduce); ?//true
這樣,我們就達(dá)到了函數(shù)復(fù)用的目的,而且如果你修改了原型對(duì)象里的 introduce 函數(shù)后,所有實(shí)例的 introduce 方法都會(huì)同時(shí)更新,是不是很方便呢?但是原型絕對(duì)不止是為了這么簡(jiǎn)單的目的所創(chuàng)建的。
我們首先明確一點(diǎn),當(dāng)創(chuàng)建一個(gè)最簡(jiǎn)單的對(duì)象的時(shí)候,其實(shí)默認(rèn)用 new 調(diào)用了 JavaScript 內(nèi)置的 Objcet 構(gòu)造函數(shù),所以每個(gè)對(duì)象都是 Object 的一個(gè)實(shí)例(用 Object.create(null) 等特殊方法創(chuàng)建的暫不討論)。所以根據(jù)上面的介紹,每個(gè)對(duì)象都有一個(gè)__proto__的屬性指向 Object.prototype。這是理解下面屬性查找機(jī)制的前提。
? ?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] }
上面代碼可以看出,如果我們?cè)L問(wèn) klaus 對(duì)象上沒(méi)有定義的屬性 friend,結(jié)果返回 undefined,這個(gè)可以理解。但是同樣訪問(wèn)沒(méi)定義的 toString 方法卻返回了一個(gè)函數(shù),這是不是很奇怪呢?其實(shí)一點(diǎn)不奇怪,這就是 JavaScript 對(duì)象的屬性查找機(jī)制。
屬性查找機(jī)制:當(dāng)訪問(wèn)某對(duì)象的某個(gè)屬性的時(shí)候,如果存在該屬性,則返回該屬性的值,如果該對(duì)象不存在該屬性,則自動(dòng)查找該對(duì)象的__proto__指向的對(duì)象的此屬性。如果在這個(gè)對(duì)象上找到此屬性,則返回此屬性的值,如果__proto__指向的對(duì)象也不存在此屬性,則繼續(xù)尋找__proto__指向的對(duì)象的__proto__指向的對(duì)象的此屬性。這樣一直查下去,直到找到 Object.prototype 對(duì)象,如果還沒(méi)找到此屬性,則返回 undefined。(原型鏈查找,講繼承時(shí)會(huì)詳細(xì)講)
理解了上面的查找機(jī)制以后,也就不難理解 klaus.toString 其實(shí)也就是 klaus.__proto__.toString,也就是 Object.prototype.toString,所以就算你沒(méi)有定義依然也可以拿到一個(gè)函數(shù)。
理解了這一點(diǎn)以后,也就理解了上面 Person 構(gòu)造函數(shù)里的那一句我為什么注釋了可以省略,因?yàn)樵L問(wèn)實(shí)例的 introduce 找不到時(shí)會(huì)自動(dòng)找到實(shí)例__proto__指向的對(duì)象的 introduce,也就是 Person.prototype.introduce。
這也就是原型模式的強(qiáng)大之處,因?yàn)槟憧梢栽诿總€(gè)實(shí)例上訪問(wèn)到構(gòu)造函數(shù)的原型對(duì)象上的屬性和方法,而且可以實(shí)時(shí)修改,是不是很方便呢。
除了給原型對(duì)象添加屬性和方法之外,也可以直接重寫(xiě)原型對(duì)象(因?yàn)樵蛯?duì)象本質(zhì)也是一個(gè)對(duì)象),只是別忘記添加 constructor 屬性。
還需要注意一點(diǎn),如果原型對(duì)象共享的某屬性是個(gè)引用類(lèi)型值,一個(gè)實(shí)例修改該屬性后,其他實(shí)例也會(huì)因此受到影響。
以及,如果用 for-in 循環(huán)來(lái)遍歷屬性的 key 的時(shí)候,會(huì)遍歷到原型對(duì)象里的可枚舉屬性。
? ?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
});
var?klaus1 =?new?Person('Klaus',?22,?'developer');
var?klaus2 =?new?Person('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(var?key?in?klaus1){
? ?console.log(key); ?//name, age, job, introduce, friends
}
ES6 class
如果你有關(guān)注最新的 ES6 的話,你會(huì)發(fā)現(xiàn)里面提出了一個(gè)關(guān)鍵字 class 的用法,難道 JavaScript 要有自己類(lèi)的概念了嗎?
tan90°,不存在的,這只是一個(gè)語(yǔ)法糖而已,上面定義的 Person 構(gòu)造函數(shù)可以用 class 來(lái)改寫(xiě)。
? ?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'];
var?klaus =?new?Person('Klaus',?22,?'developer');
很遺憾,ES6 明確規(guī)定 class 里只能有方法而不能有屬性,所以像 friends 這樣的屬性可能只能在外面單獨(dú)定義了。
下面簡(jiǎn)單舉幾個(gè)差異點(diǎn),如果想詳細(xì)了解可以去看阮一峰的《ECMAScript 6 入門(mén)》或者 Nicholas C. Zakas 的《Understanding ECMAScript 6》。
class 里的靜態(tài)方法(類(lèi)似于 introduce)是不可枚舉的,而用 prototype 定義的是可枚舉的。
class 里面默認(rèn)使用嚴(yán)格模式。
class 已經(jīng)不屬于普通的函數(shù)了,所以不使用 new 調(diào)用會(huì)報(bào)錯(cuò)。
class 不存在變量提升。
class 里的方法可以加 static 關(guān)鍵字定義靜態(tài)方法,這種靜態(tài)方法就不是定義在 Person.prototype 上而是直接定義在 Person 上了,只能通過(guò) Person.method() 調(diào)用而不會(huì)被實(shí)例共享。
作用域安全的構(gòu)造函數(shù)
不管是高程還是其他的一些資料都提到過(guò)作用域安全的構(gòu)造函數(shù)這個(gè)概念,因?yàn)闃?gòu)造函數(shù)如果不用 new 來(lái)調(diào)用就只是一個(gè)普通的函數(shù)而已,這樣在函數(shù)調(diào)用的時(shí)候 this 會(huì)指向全局(嚴(yán)格模式為 undefined),這樣如果錯(cuò)誤調(diào)用構(gòu)造函數(shù)就會(huì)把屬性和方法定義在 window 上。為了避免這種情況,可以將構(gòu)造函數(shù)稍加改造,先用 instanceof 檢測(cè) this 然后決定調(diào)用方法。
? ?if(this?instanceof?Person){
? ? ? ?this.name = name;
? ? ? ?this.age = age;
? ? ? ?this.job = job;
? ?}else{
? ? ? ?return?new?Person(name, age, job);
? ?}
}
var?klaus1 = Person('Klaus',?22,?'developer');
var?klaus2 =?new?Person('Klaus',?22,?'developer'); ?//兩種方法結(jié)果一樣
不過(guò)個(gè)人認(rèn)為這種沒(méi)什么必要,構(gòu)造函數(shù)已經(jīng)首字母大寫(xiě)來(lái)加以區(qū)分了,如果還錯(cuò)誤調(diào)用的話那也沒(méi)啥好說(shuō)的了。。。
結(jié)語(yǔ)
以上就是我眼中的 JavaScript 原型,可能解釋的不夠清楚,大家如果還想看更詳細(xì)的內(nèi)容可以去看高程三的第六章或者你不知道的 JavaScript(上卷)的第二部分關(guān)于原型的內(nèi)容,下一次我可能會(huì)寫(xiě)一些關(guān)于 JavaScript 繼承的內(nèi)容。
總結(jié)
以上是生活随笔為你收集整理的鉴别一个人是否 js 入门的标准竟然是?!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 女神一秒变路人!腾讯研究AI卸妆效果算法
- 下一篇: 【干货】通俗理解神经网络中激活函数作用