javascript創建對象
創建一個對象,然后給這個對象新建屬性和方法。
var box = new Object();//創建一個 Object 對象 box.name = 'Lee';//創建一個 name 屬性並賦值 box.age = 100;//創建一個 age 屬性並賦值 box.run = function () {//創建一個 run()方法並返回值 return this.name + this.age + '運行中...'; }; alert(box.run());//輸出屬性和方法的值
上面創建了一個對象,並且創建屬性和方法,在 run()方法里的 this,就是代表 box 對象本身。這種是 JavaScript 創建對象最基本的方法,但有個缺點,想創建一個類似的對象,就會產生大量的代碼。
var box2 = box;//得到 box 的引用 box2.name = 'Jack';//直接改變了 name 屬性 alert(box2.run()); //用 box.run() 發現 name 也改變了 var box2 = new Object(); box2.name = 'Jack'; box2.age = 200; box2.run = function () { return this.name + this.age + '運行中...'; }; alert(box2.run());//這樣才避免和 box 混淆,從而保持獨立 為了解決多個類似對象聲明的問題,我們可以使用一種叫做工廠模式的方法,這種方法就是為了解決實例化對象產生大量重復的問題。 function createObject(name, age) {//集中實例化的函數 var obj = new Object(); obj.name = name; obj.age = age; obj.run = function () { return this.name + this.age + '運行中...'; }; return obj; } var box1 = createObject('Lee', 100); var box2 = createObject('Jack', 200); alert(box1.run()); alert(box2.run()); //第一個實例 //第二個實例 //保持獨立
工廠模式解決了重復實例化的問題,但還有一個問題,那就是識別問題,因為根本無法搞清楚他們到底是哪個對象的實例。
alert(typeof box1);//Object alert(box1 instanceof Object);//true
ECMAScript 中可以采用構造函數(構造方法)可用來創建特定的對象。類型於 Object 對象。
function Box(name, age) {//構造函數模式 this.name = name; this.age = age; this.run = function () { return this.name + this.age + '運行中...'; }; } var box1 = new Box('Lee', 100); var box2 = new Box('Jack', 200); //new Box() alert(box1.run()); alert(box1 instanceof Box); //很清晰的識別他從屬於 Box
使用構造函數的方法,即解決了重復實例化的問題,又解決了對象識別的問題,但問題是,這里並沒有 new Object(),為什么可以實例化 Box(),這個是哪里來的呢?使用了構造函數的方法,和使用工廠模式的方法他們不同之處如下:
1.構造函數方法沒有顯示的創建對象(new Object());
2.直接將屬性和方法賦值給 this 對象;
3.沒有 renturn 語句。
構造函數的方法有一些規范:
1.函數名和實例化構造名相同且大寫,(PS:非強制,但這么寫有助於區分構造函數和 普通函數);
2.通過構造函數創建對象,必須使用 new 運算符。 既然通過構造函數可以創建對象,那么這個對象是哪里來的,new Object()在什么地方執行了?執行的過程如下:
1.當使用了構造函數,並且 new 構造函數(),那么就后台執行了 new Object();
2.將構造函數的作用域給新對象,(即 new Object()創建出的對象),而函數體內的 this 就代表 new Object()出來的對象。
3.執行構造函數內的代碼;
4.返回新對象(后台直接返回)。
關於 this 的使用,this 其實就是代表當前作用域對象的引用。如果在全局范圍 this 就代表 window 對象,如果在構造函數體內,就代表當前的構造函數所聲明的對象。
var box = 2; alert(this.box);//全局代表 window構造函數和普通函數的唯一區別,就是他們調用的方式不同。只不過,構造函數也是函數,必須用 new 運算符來調用,否則就是普通函數。 var box = new Box('Lee', 100);//構造模式調用 alert(box.run()); Box('Lee', 20); var o = new Object(); Box.call(o, 'Jack', 200) alert(o.run()); //普通模式調用,無效 //對象冒充調用
探討構造函數內部的方法(或函數)的問題,首先看下兩個實例化后的屬性或方法是否相等。
var box1 = new Box('Lee', 100);//傳遞一致 var box2 = new Box('Lee', 100);//同上 alert(box1.name == box2.name);//true,屬性的值相等 alert(box1.run == box2.run);//false,方法其實也是一種引用地址 alert(box1.run() == box2.run());//true,方法的值相等,因為傳參一致
可以把構造函數里的方法(或函數)用 new Function()方法來代替,得到一樣的效果,更加證明,他們最終判斷的是引用地址,唯一性。
function Box(name, age) {//new Function()唯一性 this.name = name; this.age = age; this.run = new Function("return this.name + this.age + '運行中...'"); }
我們可以通過構造函數外面綁定同一個函數的方法來保證引用地址的一致性,但這種做法沒什么必要,只是加深學習了解:
function Box(name, age) { this.name = name; this.age = age; this.run = run; } function run() {//通過外面調用,保證引用地址一致 return this.name + this.age + '運行中...'; }
雖然使用了全局的函數 run()來解決了保證引用地址一致的問題,但這種方式又帶來了一個新的問題,全局中的 this 在對象調用的時候是 Box 本身,而當作普通函數調用的時候,this 又代表 window。
原型 我們創建的每個函數都有一個 prototype(原型)屬性,這個屬性是一個對象,它的用途是包含可以由特定類型的所有實例共享的屬性和方法。
邏輯上可以這么理解:prototype 通過調用構造函數而創建的那個對象的原型對象。使用原型的好處可以讓所有對象實例共享它所包含的屬性和方法。也就是說,不必在構造函數中定義對象信息,而是可以直接將這些信息添加到原型中。
function Box() {}//聲明一個構造函數 Box.prototype.name = 'Lee'; Box.prototype.age = 100; Box.prototype.run = function () { return this.name + this.age + '運行中...'; };
比較一下原型內的方法地址是否一致:
var box1 = new Box(); //在原型里添加屬性 //在原型里添加方法 var box2 = new Box(); alert(box1.run == box2.run); //true,方法的引用地址保持一致
在原型模式聲明中,多了兩個屬性,這兩個屬性都是創建對象時自動生成的。__proto__屬性是實例指向原型對象的一個指針,它的作用就是指向構造函數的原型屬性 constructor。通過這兩個屬性,就可以訪問到原型里的屬性和方法了。PS:IE 瀏覽器在腳本訪問__proto__會不能識別,火狐和谷歌瀏覽器及其他某些瀏覽器均能識別。雖然可以輸出,但無法獲取內部信息。
alert(box1.__proto__);//[object Object]
判斷一個對象是否指向了該構造函數的原型對象,可以使用 isPrototypeOf()方法來測試。
alert(Box.prototype.isPrototypeOf(box));
只要實例化對象,即都會指向 原型模式的執行流程:
1.先查找構造函數實例里的屬性或方法,如果有,立刻返回;
2.如果構造函數實例里沒有,則去它的原型對象里找,如果有,就返回; 雖然我們可以通過對象實例訪問保存在原型中的值,但卻不能訪問通過對象實例重寫原型中的值。
var box1 = new Box(); alert(box1.name);//Lee,原型里的值 box1.name = 'Jack'; alert(box.1name);//Jack,就近原則, var box2 = new Box(); alert(box2.name); //Lee,原型里的值,沒有被 box1 修改
如果想要 box1 也能在后面繼續訪問到原型里的值,可以把構造函數里的屬性刪除即可,具體如下:
delete box1.name;//刪除屬性 alert(box1.name);
如何判斷屬性是在構造函數的實例里,還是在原型里?可以使用 hasOwnProperty()函數來驗證:
alert(box.hasOwnProperty('name'));//實例里有返回 true,否則返回 false
構造函數實例屬性和原型屬性,in 操作符會在通過對象能夠訪問給定屬性時返回 true,無論該屬性存在於實例中還是原型中。
alert('name' in box);//true,存在實例中或原型中
我們可以通過 hasOwnProperty()hasOwnProperty()方法檢測屬性是否存在實例中,也可以通過 in 來判斷實例或原型中是否存在屬性。那么結合這兩種方法,可以判斷原型中是否存在屬性。
function isProperty(object, property) {//判斷原型中是否存在屬性 return !object.hasOwnProperty(property) && (property in object); } var box = new Box(); alert(isProperty(box, 'name')) //true,如果原型有 [/task] 為了讓屬性和方法更好的體現封裝的效果,並且減少不必要的輸入,原型的創建可以使用字面量的方式: function Box() {}; Box.prototype = {//使用字面量的方式 name : 'Lee', age : 100, run : function () { return this.name + this.age + '運行中...'; } };
使用構造函數創建原型對象和使用字面量創建對象在使用上基本相同,但還是有一些區別,字面量創建的方式使用 constructor 屬性不會指向實例,而會指向 Object,構造函數創建的方式則相反。
var box = new Box(); alert(box instanceof Box); alert(box instanceof Object); alert(box.constructor == Box);//字面量方式,返回 false,否則,true alert(box.constructor == Object);//字面量方式,返回 true,否則,false
如果想讓字面量方式的 constructor 指向實例對象,那么可以這么做:
Box.prototype = { constructor : Box,//直接強制指向即可 };
PS:字面量方式為什么 constructor 會指向 Object?因為 Box.prototype={};這種寫法其實就是創建了一個新對象。而每創建一個函數,就會同時創建它 prototype,這個對象也會自動獲取 constructor 屬性。所以,新對象的 constructor 重寫了 Box 原來的 constructor,因此會指向新對象,那個新對象沒有指定構造函數,那么就默認為 Object。原型的聲明是有先后順序的,所以,重寫的原型會切斷之前的原型。
function Box() {}; Box.prototype = { constructor : Box, name : 'Lee', age : 100, //原型被重寫了 run : function () { return this.name + this.age + '運行中...'; } }; Box.prototype = { age = 200 }; var box = new Box(); alert(box.run()); //在這里聲明 //box 只是最初聲明的原型
原型對象不僅僅可以在自定義對象的情況下使用, ECMAScript 內置的引用類型都可而以使用這種方式,並且內置的引用類型本身也使用了原型。
alert(Array.prototype.sort);//sort 就是 Array 類型的原型方法 alert(String.prototype.substring);//substring 就是 String 類型的原型方法 String.prototype.addstring = function () { return this + ',被添加了!'; }; alert('Lee'.addstring()); //給 String 類型添加一個方法 //this 代表調用的字符串 //使用這個方法
PS:盡管給原生的內置引用類型添加方法使用起來特別方便,但我們不推薦使用這種方法。因為它可能會導致命名沖突,不利於代碼維護。 原型模式創建對象也有自己的缺點,它省略了構造函數傳參初始化這一過程,帶來的缺點就是初始化的值都是一致的。而原型最大的缺點就是它最大的優點,那就是共享。原型中所有屬性是被很多實例共享的,共享對於函數非常合適,對於包含基本值的屬性也還可以。但如果屬性包含引用類型,就存在一定的問題:
function Box() {}; Box.prototype = { constructor : Box, name : 'Lee', age : 100, family : ['父親', '母親', '妹妹'],//添加了一個數組屬性 run : function () { return this.name + this.age + this.family; } }; var box1 = new Box(); box1.family.push('哥哥'); alert(box1.run()); var box2 = new Box(); alert(box2.run()); //在實例中添加'哥哥' //共享帶來的麻煩,也有'哥哥'了
PS:數據共享的緣故,導致很多開發者放棄使用原型,因為每次實例化出的數據需要保留自己的特性,而不能共享。為了解決構造傳參和共享問題,可以組合構造函數+原型模式:
function Box(name, age) {//不共享的使用構造函數 this.name = name; this.age = age; this. family = ['父親', '母親', '妹妹']; }; Box.prototype = {//共享的使用原型模式 constructor : Box, run : function () { return this.name + this.age + this.family; } };
PS:這種混合模式很好的解決了傳參和引用共享的大難題。是創建對象比較好的方法。原型模式,不管你是否調用了原型中的共享方法,它都會初始化原型中的方法,並且在聲明一個對象時,構造函數+原型部分讓人感覺又很怪異,最好就是把構造函數和原型封裝到一起。為了解決這個問題,我們可以使用動態原型模式。
function Box(name ,age) {//將所有信息封裝到函數體內 this.name = name; this.age = age; if (typeof this.run != 'function') {//僅在第一次調用的初始化 Box.prototype.run = function () { return this.name + this.age + '運行中...'; }; } } var box = new Box('Lee', 100); alert(box.run());
當第一次調用構造函數時,run()方法發現不存在,然后初始化原型。當第二次調用,就不會初始化,並且第二次創建新對象,原型也不會再初始化了。這樣及得到了封裝,又實現了原型方法共享,並且屬性都保持獨立。
if (typeof this.run != 'function') { alert('第一次初始化');//測試用 Box.prototype.run = function () { return this.name + this.age + '運行中...'; }; } var box = new Box('Lee', 100); alert(box.run()); alert(box.run()); var box2 = new Box('Jack', 200); alert(box2.run()); alert(box2.run()); //第一次創建對象 //第一次調用 //第二次調用 //第二次創建對象
PS:使用動態原型模式,要注意一點,不可以再使用字面量的方式重寫原型,因為會切斷實例和新原型之間的聯系。以上講解了各種方式對象創建的方法,如果這幾種方式都不能滿足需求,可以使用一開始那種模式:寄生構造函數。
function Box(name, age) { var obj = new Object(); obj.name = name; obj.age = age; obj.run = function () { return this.name + this.age + '運行中...'; }; return obj; }
寄生構造函數,其實就是工廠模式+構造函數模式。這種模式比較通用,但不能確定對象關系,所以,在可以使用之前所說的模式時,不建議使用此模式。在什么情況下使用寄生構造函數比較合適呢?假設要創建一個具有額外方法的引用類型。由於之前說明不建議直接 String.prototype.addstring,可以通過寄生構造的方式添加。
function myString(string) { var str = new String(string); str.addstring = function () { return this + ',被添加了!'; }; return str; } var box = new myString('Lee'); alert(box.addstring()); //比直接在引用原型添加要繁瑣好多
在一些安全的環境中,比如禁止使用 this 和 new,這里的 this 是構造函數里不使用 this,這里的 new 是在外部實例化構造函數時不使用 new。這種創建方式叫做穩妥構造函數。
function Box(name , age) { var obj = new Object(); obj.run = function () { return name + age + '運行中...';//直接打印參數即可 }; return obj; } var box = Box('Lee', 100); alert(box.run());
PS:穩妥構造函數和寄生類似。 繼承 繼承是面向對象中一個比較核心的概念。其他正統面向對象語言都會用兩種方式實現繼承:一個是接口實現,一個是繼承。而 ECMAScript 只支持繼承,不支持接口實現,而實現繼承的方式依靠原型鏈完成。
function Box() { this.name = 'Lee'; } function Desk() { this.age = 100; } Desk.prototype = new Box(); var desk = new Desk(); alert(desk.age); alert(desk.name); function Table(){ this.level = 'AAAAA'; } Table.prototype = new Desk(); var table = new Table(); alert(table.name); //Box 構造 //直接調用函數 //Desk 構造 //Desc 繼承了 Box,通過原型,形成鏈條 //得到被繼承的屬性 //Table 構造 //繼續原型鏈繼承 //繼承了 Box 和 Desk
原型鏈繼承流程圖

如果要實例化 table,那么 Desk 實例中有 age=100,原型中增加相同的屬性 age=200, 最后結果是多少呢?
Desk.prototype.age = 200;//實例和原型中均包含 age
PS:以上原型鏈繼承還缺少一環,那就是 Obejct,所有的構造函數都繼承自 Obejct。而繼承 Object 是自動完成的,並不需要程序員手動繼承。經過繼承后的實例,他們的從屬關系會怎樣呢?
alert(table instanceof Object);//true alert(desk instanceof Table);//false,desk 是 table 的超類 alert(table instanceof Desk);//true alert(table instanceof Box);//true
在 JavaScript 里,被繼承的函數稱為超類型(父類,基類也行,其他語言叫法),繼承的函數稱為子類型(子類,派生類)。繼承也有之前問題,比如字面量重寫原型會中斷關系,使用引用類型的原型,並且子類型還無法給超類型傳遞參數。為了解決引用共享和超類型無法傳參的問題,我們采用一種叫借用構造函數的技術,或者成為對象冒充(偽造對象、經典繼承)的技術來解決這兩種問題。
function Box(age) { this.name = ['Lee', 'Jack', 'Hello'] this.age = age; } function Desk(age) { Box.call(this, age); //對象冒充,給超類型傳參 } var desk = new Desk(200); alert(desk.age); alert(desk.name); desk.name.push('AAA'); alert(desk.name); //添加的新數據,只給 desk
借用構造函數雖然解決了剛才兩種問題,但沒有原型,復用則無從談起。所以,我們需要原型鏈+借用構造函數的模式,這種模式成為組合繼承。
function Box(age) { this.name = ['Lee', 'Jack', 'Hello'] this.age = age; } Box.prototype.run = function () { return this.name + this.age; }; function Desk(age) { Box.call(this, age); } Desk.prototype = new Box(); var desk = new Desk(100); alert(desk.run()); //對象冒充 //原型鏈繼承
還有一種繼承模式叫做:原型式繼承;這種繼承借助原型並基於已有的對象創建新對象,同時還不必因此創建自定義類型。
function obj(o) {//傳遞一個字面量函數 function F() {}//創建一個構造函數 F.prototype = o;//把字面量函數賦值給構造函數的原型 return new F();//最終返回出實例化的構造函數 } var box = { name : 'Lee', arr : ['哥哥','妹妹','姐姐'] }; var box1 = obj(box); alert(box1.name); //字面量對象 //傳遞 box1.name = 'Jack'; alert(box1.name); alert(box1.arr); box1.arr.push('父母'); alert(box1.arr); var box2 = obj(box); alert(box2.name); alert(box2.arr); //傳遞 //引用類型共享了
寄生式繼承把原型式+工廠模式結合而來,目的是為了封裝創建對象的過程。
function create(o) {//封裝創建過程 var f= obj(o); f.run = function () { return this.arr;//同樣,會共享引用 }; return f; }
組合式繼承是 JavaScript 最常用的繼承模式;但,組合式繼承也有一點小問題,就是超類型在使用過程中會被調用兩次:一次是創建子類型的時候,另一次是在子類型構造函數的內部。
function Box(name) { this.name = name; this.arr = ['哥哥','妹妹','父母']; } Box.prototype.run = function () { return this.name; }; function Desk(name, age) { Box.call(this, name); this.age = age; } Desk.prototype = new Box(); //第二次調用 Box //第一次調用 Box
以上代碼是之前的組合繼承,那么寄生組合繼承,解決了兩次調用的問題。
function obj(o) { function F() {} F.prototype = o; return new F(); } function create(box, desk) { var f = obj(box.prototype); f.constructor = desk; desk.prototype = f; } function Box(name) { this.name = name; this.arr = ['哥哥','妹妹','父母']; } Box.prototype.run = function () { return this.name; }; function Desk(name, age) { Box.call(this, name); this.age = age; } inPrototype(Box, Desk); var desk = new Desk('Lee',100); desk.arr.push('姐姐'); alert(desk.arr); alert(desk.run()); var desk2 = new Desk('Jack', 200); alert(desk2.arr); //通過這里實現繼承 //只共享了方法 //引用問題解決
