你對JavaScript面向對象瞭解多少?
前言
前兩天看到一個有意思的觀點:工具的進步,不代表你能力的進步。前端框架風起雲涌,我們用得得心應手,回過頭來,脫離框架我們還剩下什麼?我覺得這是個值得深思的問題。
扯遠了,本文主要是想把JavaScript
中面向對象的知識做一個整理和回顧,加深印象。
怎怎怎麼找對象?
new 一個對象
沒有對象怎麼辦?
new 一個!
let obj = new Object();
obj.name = 'object';
obj.value = 11;
obj.methods = function() {
console.log('this is a object')
};
這便是創建一個對象最簡單的方式。但是,每次都要 new
一個,複雜又麻煩,有沒有更簡單的方式呢?
往下看
使用字面量創建
what?啥是字面量?
字面量:literals
,有些書上叫做直接量。看見什麼,它就是什麼
舉個栗子:
let obj = {
name : 'object';
value : 11;
methods : function() {
console.log('this is a object')
};
};
簡單粗暴?!事實上,如果是簡單的創建幾個對象,使用字面量創建對象無可厚非,但若有很多相似對象需要創建,這種方式便會產生大量的重複代碼,顯然這是很不友好的。
於是工廠模式應運而生。
工廠模式
function createFactory(name, value) {
let obj = new Object();
obj.name = name;
obj.value = value;
obj.methods = function() {
console.log('this is a object, my name is ' + this.name)
};
return obj;
}
let createFactory1 = factory('saints', 12)
let createFactory2 = factory('Google', 50)
工廠模式雖然解決了創建 多個相似對象的問題,但卻沒有解決對象識別的問題,也就是說,無法區分它們的對象類型。
這該怎麼辦呢?
構造函數模式
先用構造函數模式重寫上面的例子:
function Factory(name, value) {
this.name = name;
this.value = value;
this.methods = function() {
console.log('this is a object, my name is ' + this.name)
};
}
let factory1 = new Factory('saints', 12);
let factory2 = new Factory('Google', 50);
這裏我們使用一個大寫字母F開頭的構造函數替代了上例中的createFactory
,注意按照約定構造函數的首字母要大寫。
它和工廠模式有什麼區別?
- 沒有顯示的創建對象
- 直接將屬性和方法賦值給了
this
對象 - 沒有
return
語句 - 創建
Factory
實例時,必須使用new
操作符
構造函數大法好啊,只不過它也不是萬能的,最大的問題是,它的每個方法都要在每個實例上重新創建一次。
換句話說,兩個實例中調用的構造函數中的method
方法不是同一個Function
實例:
console.log(factory1.method === factory2.method) // false
爲啥會這樣呢?
不要忘了,ECMAScript
中的函數是對象,因此每定義一個函數,也就是實例化了一個對象。
我們可以把
this.methods = function() {
console.log('this is a object, my name is ' + this.name)
};
看成:
this.methods = new Function() {
console.log('this is a object, my name is ' + this.name)
};
這樣看是不是更加清楚了呢?
調用同一個方法,卻聲明瞭不同的實例,實在浪費資源。大可像下面這樣,通過把函數定義轉移到構造函數外部來解決這個問題。
function Factory(name, value) {
this.name = name;
this.value = value;
this.methods = methods
}
function methods() {
console.log('this is a object, my name is ' + this.name)
}
let factory1 = new Factory('saints', 12);
let factory2 = new Factory('Google', 50);
堪稱完美。
But!!!
- 我爲要要在全局作用域中定義一個,只能被某個對象調用的函數呢?
- 如果,這個對象有多個方法,那我得在全局作用域中定於多個函數。。。這讓我們如何去優(zhuang)雅(bi)的封裝一個對象呢?
好在, 這些問題可以通過使用原型模式來解決。
原型模式
我們每創建一個函數,都有一個prototype
(原型)屬性,這個屬性是一個指針,指向一個對象。也就是說,prototype
就是,通過調用構造函數創建的那個對象實例的原型對象。看到這我已經暈了。
使用原型對象的好處是:可以讓所有對象實例共享它所包含的屬性和方法。
上代碼!
function Factory() { }
Factory.prototype.name = 'saints';
Factory.prototype.value = 12;
Factory.prototype.methods = function() {
console.log('this is a object, my name is ' + this.name)
}
let factory1 = new Factory();
factory1.methods(); // this is a object, my name is saints
let factory2 = new Factory();
factory2.methods(); // this is a object, my name is saints
console.log(factory1.methods === factory2.methods) // true
這樣就完美的解決了屬性和方法共享的問題,所有的實例共享同一組屬性和方法。
我們要知其然,還要知其所以然,原型模式的原理是什麼呢?
通過下面的原型鏈,一目瞭然:
在默認情況下,所有原型對象都會自動獲得一個 constructor
(構造函數)屬性,這個屬性包含一個指向 prototype
屬性所在函數的指針,圖中,Factory.prototype
指向了原型對象,而 Factory.prototype.constructor
又指回了 Factory
每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,首先會詢問實例對象中有沒有該屬性,如果沒有則繼續查找原型對象(這就是執行期上下文)
let factory1 = new Factory();
factory1.name = 'google'
let factory2 = new Factory();
console.log(factory1.name); // google
console.log(factory2.name); // saints
當爲對象實例添加一個屬性時, 這個屬性屏蔽原型對象中的同名屬性,注意是屏蔽,這隻會阻止我們去訪問這個同名屬性,而不會對它做修改。即使將該屬性修改爲null
,也不會恢復我們對原型對象中同名屬性的訪問,除非使用delete
徹底刪除該屬性。
大家可以看到,每次新增一個屬性,都要輸入一次Factory.prototype
,爲了減少不必要的輸入,同時更加直觀的封裝原型對象的功能,我們使用字面量來重寫整個原型對象:
function Factory() {}
Factory.prototype = {
name : 'saints';
value : 12;
methods : function() {
console.log('this is a object, my name is ' + this.name)
}
}
有個地方需要注意的是,以對象字面量形式創建的新對象,本質上完全重寫了默認的 prototype
對象,因此,此時的Factory.prototype.constructor
已不再指向Factory
,而是指向了Object
。
let factory1 = new Factory();
console.log(factory1.constructor == Factory); //false
console.log(factory1.constructor == Object); //true
一般情況下,這種改變不會對我們造成困擾,如果 constructor
的值真的很重要,可以像下面這樣特意將它設置回適當的值:
function Factory() {}
Factory.prototype = {
constructor: Factory,
name : 'saints';
value : 12;
methods : function() {
console.log('this is a object, my name is ' + this.name)
}
}
let factory1 = new Factory();
console.log(factory1.constructor == Factory); //true
你以爲這樣就完了麼?too young too simple!
來談談這種方式有哪些問題:
- 不能給構造函數傳遞初始化參數,因此,所有實例在默認情況下都將取得相同的屬性值。
- 共享問題
假如原型的屬性中包含引用類型,在實例中修改該屬性的值,那麼,其他實例中對應的屬性的值,也會被修改。
因此開發者很少單獨使用這種方式來創建對象。
組合使用構造函數模式和原型模式
在實際開發過程中,我們使用構造函數模式來定義實例屬性,而原型模式用於定義方法和共享的屬性:
function Factory(name, value) {
this.name = name;
this.value = value;
}
Factory.prototype = {
constructor: Factory,
methods : function() {
console.log('this is a object, my name is ' + this.name)
}
}
let factory1 = new Factory('saints', 22);
console.log(factory1.constructor == Factory); //true
每個實例都會有自己的一份實例屬性,但同時又共享着方法,最大限度的節省了內存,還支持傳遞初始參數,優點甚多。在ECMAScript
中是使用最廣泛、認同度最高的一種創建自定義對象的方法。
動態原型模式
動態原型模式,把所有信息都封裝在了構造函數中,在構造函數中初始化原型(僅在必要的情況下),可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。
function Factory(name, value) {
this.name = name;
this.value = value;
if (typeof this.methods == 'function') {
Factory.prototype.methods = function() {
console.log('this is a object, my name is ' + this.name)
}
}
}
let factory1 = new Factory('saints', 22);
Factory
是一個構造函數,通過new Factory(...)
來生成實例對象。每當一個Factory的對象生成時,Factory內部的代碼都會被調用一次。
如果去掉if的話,每new
一次(即每當一個實例對象生產時),都會重新定義一個新的函數,然後掛到Factory.prototype.methods
屬性上。而實際上,你只需要定義一次就夠了,因爲所有實例都會共享此屬性的。所以如果去掉if
的話,會造成沒必要的時間和空間浪費;而加上if後,只在new
第一個實例時纔會定義methods
方法,之後就不會了。
假設除了methods
方法外,你還定義了很多其他方法,比如sayBye、cry、smile
等等。此時你只需要把它們都放到對methods
判斷的`if塊裏面就可以了。
if (typeof this.methods != "function") {
Factory.prototype.methods = function() {...};
Factory.prototype.sayBye = function() {...};
Factory.prototype.cry = function() {...};
...
}
萬惡的面試題
使用 new 操作符,經歷了哪些步驟
- 創建一個新的對象;
- 將構造函數的作用域賦給新的對象(因此,
this
就指向了新的對象); - 執行構造函數中的代碼(爲這個新對象添加屬性);
- 返回新的對象。
構造函數和普通函數的區別
構造函數和其他函數的唯一區別,就在於調用他們的方式不同。
任何函數,只要是 通過 new
操作符來調用,那它就可以作爲構造函數;
任何函數,如果不通過 new
操作符來調用,那它和普通的函數沒什麼兩樣。
原型對象的問題
- 不能給構造函數傳遞初始化參數,因此,所有實例在默認情況下都將取得相同的屬性值。
- 共享問題
假如原型的屬性中包含引用類型,在實例中修改該屬性的值,那麼,其他實例中對應的屬性的值,也會被修改。
結束
終終於整理完畢,感覺每次更新都像是難產。不過感覺自己又回到了兩年前,初識javaScript,拿着紅寶書迷茫的啃。現在依舊迷茫,只是在迷茫的路上,堅定了一點。
本文也收錄在個人博客上lostimever.github.io。
參考
- 《JavaScript高級程序設計》第3版