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