- 看透JavaScript:原理、方法與實踐
- 韓路彪
- 4133字
- 2020-11-28 15:50:45
4.4 創(chuàng)建對象
JS中的function除了前面介紹的兩種用法之外,還有一種非常重要的用法,那就是創(chuàng)建object實例對象。關于object類型對象的具體內容,本書將在下一章詳細講解,本節(jié)主要介紹如何使用function來創(chuàng)建object對象以及創(chuàng)建時的一些細節(jié)問題。
4.4.1 創(chuàng)建方式
使用function對象創(chuàng)建object類型對象的方法非常簡單,直接在function對象前面使用new關鍵字就可以了,例如下面的例子。
function F(){ this.v = 1; } var obj = new F(); //創(chuàng)建F類型對象obj console.log(obj.v); //1
這個例子中,首先定義了一個function類型的對象F,然后使用F創(chuàng)建了object類型的對象obj,最后在控制臺打印出obj對象的v屬性的值。
使用function(例如F)創(chuàng)建object類型的對象(例如obj),只需要在function對象(F)前加new關鍵字就可以了。也就是說,對于一個function類型的對象,如果調用時前面沒有new關鍵字,那么調用方法處理業(yè)務,如果前面有new關鍵字,那么用來創(chuàng)建對象。當然,創(chuàng)建對象時函數(shù)體也會被執(zhí)行,對于具體創(chuàng)建的步驟下一節(jié)將詳細介紹。
其實,經常使用的Array、Date等對象也都是function類型,可以使用new關鍵字來創(chuàng)建相應的object類型的對象實例。
為了區(qū)分主要用于處理業(yè)務的function和主要用于創(chuàng)建對象的function,一般會將主要用于創(chuàng)建對象的function的首字母大寫而將主要用于處理業(yè)務的function的首字母小寫。但這只是人為區(qū)分,實際使用時并沒有什么影響。
4.4.2 創(chuàng)建過程
使用function創(chuàng)建object類型對象的過程可以簡單地分為以下兩步(可以這么理解,實際創(chuàng)建過程要復雜一些)。
1)創(chuàng)建function對應類型的空object類型對象。
2)將function的函數(shù)體作為新創(chuàng)建的object類型對象的方法來執(zhí)行(主要目的是初始化object對象)。
例如下面的例子。
function Car(color, displacement){ this.color = color;
this.displacement = displacement; } var car = new Car("black", "2.4T"); console.log(car.color+", "+car.displacement); //black, 2.4T
這個例子中,首先創(chuàng)建了function類型的Car,然后使用它新建object類型的car實例對象。在新建car對象時首先會新建Car類型的空對象car,然后再將Car函數(shù)作為新建對象的方法來調用,從而初始化新建的car實例對象,相當于下面的過程。
function Car(){} var car = new Car(); car.init = function (color, displacement){ this.color = color; this.displacement = displacement; }; car.init("black", "2.4T"); console.log(car.color+", "+car.displacement); //black, 2.4T
上述示例將原來Car對象中的函數(shù)體的內容放到新建的car的init方法中,在使用Car創(chuàng)建完car實例對象后,再調用init方法初始化,這種方式和前面例子中將初始化內容放到Car的函數(shù)體內的效果是完全相同的。
需要特別注意的是,創(chuàng)建過程的第二步,也就是說,在使用function對象新建object對象時依然會執(zhí)行function的函數(shù)體。通過下面的例子可以更加直觀地看到這一點。
var name="和珅"; function Sikuquanshu(){ name="紀曉嵐"; } console.log(name); //和珅 var skqs = new Sikuquanshu(); console.log(name); //紀曉嵐
這個例子中存在一個全局變量name,原值為“和珅”,在函數(shù)Sikuquanshu內部將其改為“紀曉嵐”,在上述代碼中并沒有直接執(zhí)行此函數(shù),但在使用它創(chuàng)建skqs對象時其函數(shù)體得到了執(zhí)行,這從創(chuàng)建skqs對象前后打印出的內容就能看出來,全局變量name被修改了。在使用function對象創(chuàng)建實例對象時一定要注意這一點。
理解了對象創(chuàng)建的過程就可以理解為什么在構造函數(shù)(例如Car)中使用this可以將屬性添加到新創(chuàng)建的對象上。因為這時的函數(shù)體就相當于新創(chuàng)建的對象的一個方法,方法中的this指的就是新創(chuàng)建的對象自身,給this賦值就是給新創(chuàng)建的對象賦值,因此在function對象中使用this就可以給新創(chuàng)建出來的對象添加屬性,就像本書第一個例子中的Car方法的“this.color = color; ”語句,這條語句會給新創(chuàng)建的car對象添加color屬性并將color參數(shù)的值賦給它。對于這一點,在后面講到object的屬性時還會做進一步介紹。
如果覺得這里不容易理解也沒關系,因為還沒有學習object類型對象和this關鍵字的具體含義,可以先繼續(xù)往下看,等學習完相關內容之后再返回來理解就容易了。
多知道點
對象在內存中是怎么保存的
前面主要介紹了對象屬性、函數(shù)參數(shù)以及變量在內存中的保存方法,那么對象自身在內存中怎么保存呢?一般來說主要分為兩大類方法。一類方法是直接將名稱(或者名稱的哈希值)和值(可能是實際內容也可能是地址)全部保存進去,這時可以使用類似JSON的格式。這種方式使用起來會比較靈活,但是在執(zhí)行效率上會存在些問題,因為查找屬性的過程會比較費時間。另一類方法是使用類似C語言中結構體的方式來保存,即只保存值而不保存變量,變量通過偏移量來查找。例如,{width:10, length:15}這個對象可以直接用8個字節(jié)來表示,width的偏移量是0, length的偏移量是4。這樣在使用時不需要查找,直接按偏移量調用,效率就比第一類方法高,C++中的類就采用這種方法。但是,JS中的對象有些特殊,因為它的對象的屬性是不確定的,而且可以隨時修改(例如添加新的屬性),這對于編程來說會很方便,但是對于按照第二類方法來保存對象數(shù)據(jù)來說就有點麻煩了。具體使用哪種方法來保存對象數(shù)據(jù)是由具體引擎的設計者來定的,不同的引擎可能會采用不同的處理方法。早期的JS引擎以第一類方法為主,而新的引擎為了提高效率也有采用第二類方法來保存的。
對于第二類方法來說,用同一個function創(chuàng)建的實例對象具有相同的結構,這時就可以將其看作同一類型(類似于C語言中的同一個結構體)來處理,但所創(chuàng)建的對象自身的屬性也是可以修改的,在修改之后就成了新的類型。
在使用function創(chuàng)建對象時需要注意一種特殊情況,當function的函數(shù)體返回一個對象類型時,使用new關鍵字創(chuàng)建的對象就是返回的對象而不是function所對應的對象,例如下面的例子。
function F(){} function Car(color, displacement){ this.color = color; this.displacement = displacement; return new F(); } var car = new Car("black", "2.4T"); console.log(car.color+", "+car.displacement); //undefined, undefined console.log(car instanceof Car); //false console.log(car instanceof F); //true
這個例子中存在兩個function對象:F和Car。在Car的函數(shù)體中返回了新建的F類型實例對象,這時使用Car新建出來的car對象就成了F類型的實例對象,而不是Car類型的實例對象。
4.4.3 prototype屬性與繼承
1.繼承方法
繼承是Java、C++等基于類的語言中的一個術語。它的含義是子類的對象可以調用父類的屬性和方法。基于對象的ES語言根本沒有類的概念,當然也就不存在基于類的那種繼承方式,但是,它可以通過prototype屬性來達到類似于繼承的效果。
prototype是ES中function類型對象的一個特殊屬性。每個function類型的對象都有prototype屬性,prototype屬性的值是object類型的對象。在FireBug中可以看到Object(Object本身是function類型)的prototype屬性類型如圖4-8所示。

圖4-8 FireBug中的prototype屬性類型
function對象中prototype屬性對象的作用是這樣的:在function對象創(chuàng)建出的object類型對象實例中可以直接調用function對象的prototype屬性對象中的屬性(包括方法屬性),例如下面的例子。
function Car(color, displacement){ this.color = color; this.displacement = displacement; } Car.prototype.logMessage = function(){ console.log(this.color+", "+this.displacement); } var car = new Car("black", "2.4T"); car.logMessage(); //black, 2.4T
這個例子中,給Car的prototype屬性對象添加了logMessage方法,這樣使用Car創(chuàng)建的car對象就可以直接調用logMessage方法。雖然這里可以使用car調用logMessage方法,但是car對象本身并不會添加這個方法,只是可以調用而已。
function創(chuàng)建的實例對象在調用屬性時會首先在自己的屬性中查找,如果找不到就會去function的prototype屬性對象中查找。但是,創(chuàng)建的對象只是可以調用prototype中的屬性。但是并不會實際擁有那些屬性,也不可以對它們進行修改(修改操作會在實例對象中添加一個同名屬性)。當創(chuàng)建的實例對象定義了同名的屬性后就會覆蓋prototype中的屬性,但是原來prototype中的屬性并不會發(fā)生變化,而且當創(chuàng)建出來的對象刪除了添加的屬性后,原來prototype中的屬性還可以繼續(xù)調用,請看下面的例子。
function Car(color, displacement){ this.color = color;
this.displacement = displacement; } Car.prototype.logMessage = function(){ console.log(this.color+", "+this.displacement); } var car = new Car("black", "2.4T"); car.logMessage(); //black,2.4T car.logMessage = function() { console.log(this.color); } car.logMessage(); //black delete car.logMessage; car.logMessage(); //black,2.4T
在這個例子中,使用Car直接創(chuàng)建的car對象并沒有l(wèi)ogMessage方法,所以第一次調用logMessage方法時會調用Car的prototype屬性對象中的logMessage方法,然后給car定義了logMessage方法,這時再調用logMessage方法就會調用car自己的logMessage方法了,最后又刪除了car的logMessage方法,此時調用logMessage方法就會再次調用Car的prototype屬性對象中的logMessage方法,而且Car的prototype屬性對象中的logMessage方法的內容也沒有發(fā)生變化。代碼執(zhí)行后的輸出結果如下。
black,2.4T black black,2.4T
我們可以通過FireBug將整個過程看得更加清楚,在增加斷點后可以看到圖4-9所示的結構。

圖4-9 在增加斷點后可以看到的結構
從圖4-9中可以看出,Car的prototype屬性并沒有發(fā)生變化,而car對象自己的屬性中先被添加,然后又被刪除了logMessage屬性方法。
2.多層繼承
function的prototype屬性是object類型的屬性對象,其本身可能也使用function創(chuàng)建的對象,通過這種方法就可以實現(xiàn)多層繼承,例如下面的例子。
function log(msg){ console.log(msg); } function Person(){} Person.prototype.logPerson = function () { log("person"); } function Teacher(){ this.logTeacher = function () { log("teacher"); } } Teacher.prototype = new Person(); Teacher.prototype.logPrototype = function () { log("prototype"); } var teacher = new Teacher(); teacher.logTeacher(); teacher.logPrototype(); teacher.logPerson();
這個例子中,因為Teacher的prototype屬性是Person創(chuàng)建的實例對象,而使用Teacher創(chuàng)建出來的teacher對象可以調用Teacher的prototype屬性對象的屬性,所以teacher對象可以調用Person創(chuàng)建的實例對象的屬性。又因為Person創(chuàng)建的實例對象可以調用Person的prototype屬性對象中的屬性,所以teacher對象也可以調用Person的prototype屬性對象中的屬性方法logPerson。另外,因為此程序給Teacher的prototype屬性對象添加了logPrototype方法,所以teacher也可以調用logPrototype方法。最后的輸出結果如下。
teacher prototype person
這種調用方法相當于基于類語言中的多層繼承,它的結構如圖4-10所示。

圖4-10 基于類語言中的多層繼承
Teacher創(chuàng)建出來的teacher對象在調用屬性時會首先在自己的屬性中查找,如果找不到就會到Teacher的prototype屬性對象的屬性中查找,如果還找不到就會到Person的prototype屬性對象的屬性中查找,而Teacher的prototype又由兩部分組成,一部分是用Person創(chuàng)建的person對象,另一部分是直接定義的logPrototype方法。
3.使用prototype時的注意事項
在function的prototype屬性對象中默認存在一個名為constructor的屬性。這個屬性默認指向function方法自身,例如上節(jié)例子中Person的prototype屬性對象的constructor屬性就指向了Person。但是,Teacher的prototype由于被賦予了新的值,因此它的constructor屬性就不存在(使用Person創(chuàng)建的person對象自身并沒有constructor屬性)。這時,如果調用teacher.constructor就會返回Person函數(shù)(因為最后會沿著prototype找到person的prototype屬性對象的constructor屬性)。為了可以使用constructor屬性得到正確的構造函數(shù),可以手動給Teacher的prototype屬性對象的constructor屬性賦值為Teacher,代碼如下所示。
Teacher.prototype = new Person(); Teacher.prototype.logPrototype = function () {
console.log("prototype"); } Teacher.prototype.constructor = Teacher;
使用prototype時應注意以下三點。
一是,prototype是屬于function類型對象的屬性,prototype自身的屬性可以被function創(chuàng)建的object類型的實例對象使用,但是object類型的實例對象自身并沒有prototype屬性。
二是,如果要給function對象的prototype屬性賦予新的值并且又要添加新的屬性,則需要先賦予新值,然后再添加新的屬性,否則在賦值時,會將原先添加的屬性覆蓋掉,例如下面的代碼。
function log(msg){ console.log(msg); } function Person(){} function Teacher(){} Teacher.prototype.logPrototype = function () { log("prototype"); } Teacher.prototype = new Person(); var teacher = new Teacher(); teacher.logPrototype();
在上述代碼中,在執(zhí)行最后一行代碼teacher.logPrototype()的時候會報錯,這是因為給Teacher的prototype屬性對象添加了logPrototype屬性方法后,又將prototype賦值為new Person(),而新的prototype中并沒有l(wèi)ogPrototype方法,所以調用就會出錯,也就是說logPrototype被新的對象覆蓋。
function創(chuàng)建的對象在調用屬性時是實時按prototype鏈依次查找的,而不是將prototype中的屬性關聯(lián)到創(chuàng)建的對象本身,因此創(chuàng)建完對象后,再修改function的prototype也會影響到創(chuàng)建的對象的調用,例如下面的例子。
function Teacher(){} var teacher = new Teacher(); Teacher.prototype.log = function (msg) { console.log(msg); } teacher.log("hello"); //hello
這里的log方法是在teacher對象已經創(chuàng)建完成后添加的,但是在teacher對象中仍然可以使用,也就是說prototype中的屬性是動態(tài)查詢的。
另外,使用prototype除了可以實現(xiàn)繼承之外,還可以節(jié)約內存,因為無論使用function創(chuàng)建多少對象,它們所指向的prototype對象在內存中都只有一份。但是,使用prototype中的屬性比直接使用對象中定義的屬性在執(zhí)行效率上理論來說會低一些。
- 深入理解Android(卷I)
- Designing Machine Learning Systems with Python
- 青少年美育趣味課堂:XMind思維導圖制作
- jQuery從入門到精通 (軟件開發(fā)視頻大講堂)
- MATLAB實用教程
- 深入淺出DPDK
- Go并發(fā)編程實戰(zhàn)
- Domain-Driven Design in PHP
- Rust游戲開發(fā)實戰(zhàn)
- Creating Data Stories with Tableau Public
- Raspberry Pi Robotic Projects(Third Edition)
- Natural Language Processing with Python Quick Start Guide
- Spring Boot從入門到實戰(zhàn)
- Elastix Unified Communications Server Cookbook
- Python Penetration Testing Essentials