摘要:前言說到面向對象,可能第一想到的是或者這樣的語言。默認情況下,所有原型對象都會自動獲得一個構造函數屬性會執行屬性所在函數。相對于原型鏈而言,借用構造函數有一個很大的優勢,即
前言
說到面向對象,可能第一想到的是C++或者Java這樣的語言。這些語言有都一個標志,那就是引入了類的概念。我們可以通過類創建任意數量的具有相同屬性和方法的對象。ECMAScript(JavaScript分為ECMAScript、DOM和BOM)中沒有類的概念,所以它的對象相比較基于類的語言還是有所不同的。
說說對象的屬性var person={ name:"張三", age:23, sex:"男", sayName:function () { alert(this.name); } }
上面我們用了對象字面量的方式建了一個非常簡單的person對象,他擁有name、age、sex、sayName這些屬性,而這些屬性在創建時都帶有一些特征值,JavaScript通過這些特征值就可以定義這些屬性的行為。在ECMAScript中,屬性分為兩種:數據屬性和訪問器屬性。下面我們一一學習下。
數據屬性數據屬性有四個特征值,分別為如下四個:
configurable
表示能否delete刪除屬性,能否修改屬性的特性,默認值是false
enumerable
表示能否通過for-in循環返回屬性,默認值是false
writable
表示能否修改屬性的值,默認值是false
value
表示該屬性的值,我們讀取和修改都是在這個位置,默認值是undefined
接下來我們一一理解這四個特征值。要修改屬性默認的特性,必須使用ECMAScript 5的Object.defineProperty()方法
configurablevar person={}; Object.defineProperty(person,"name",{ configurable:false, value:"張三" }); console.log(person.name);//張三 delete person.name; console.log(person.name);//張三
var person={}; Object.defineProperty(person,"name",{ configurable:true, value:"張三" }); Object.defineProperty(person,"name",{ value:"李四" }); console.log(person.name);//李四
var person={}; Object.defineProperty(person,"name",{ configurable:false, value:"張三" }); Object.defineProperty(person,"name",{ value:"李四" }); console.log(person.name); //控制臺報錯 Uncaught TypeError: Cannot redefine property: nameenumerable
var person={}; Object.defineProperty(person,"name",{ enumerable:true, value:"張三" }); Object.defineProperty(person,"age",{ value:"23" }); Object.defineProperty(person,"sayName",{ enumerable:true, value:function () { alert(this.name); } }); for( var prop in person){ console.log(prop); } //控制臺輸出 name和sayNamewritable
var person={}; Object.defineProperty(person,"name",{ writable:false, value:"張三" }); console.log(person.name);//張三 person.name="李四"; console.log(person.name);//張三value
var person={}; Object.defineProperty(person,"name",{ writable:true, value:"張三" }); console.log(person.name);//張三 person.name="李四"; console.log(person.name);//李四訪問器屬性
訪問器屬性有四個特征值,分別為如下四個:
configurable
表示能否delete刪除屬性,能否修改屬性的特性,默認值是false
enumerable
表示能否通過for-in循環返回屬性,默認值是false
get
在讀取屬性調用的函數,默認值是undefined
set
在設置屬性調用的函數,默認值是undefined
下面我們一一了解一下訪問器屬性的特征值,其中configurable和enumerable與上面數據類型一樣,這里我們就不多做介紹,主要我們說一下get和set。
var person={ name:"張三", age:32 }; Object.defineProperty(person,"sayAge",{ get:function () { return this.name+":"+this.age+"歲"; }, set:function (newAge) { console.log("想要重返"+newAge+"歲?不存在的!"); } }); console.log(person.sayAge);//張三:32歲 person.sayAge=18;//想要重返18歲?不存在的! console.log(person.sayAge);//張三:32歲
get和set并非需要同時都要指定。如果只指定get,那么這個屬性就是不可寫的;如果只指定set,那么這個屬性就是不可讀的。
var person1={ name:"張三", age:32 }; Object.defineProperty(person1,"sayAge",{ get:function () { return this.name+":"+this.age+"歲"; } }); console.log(person1.sayAge);//張三:32歲 person1.sayAge=18; console.log(person1.sayAge);//張三:32歲 var person2={ name:"李四", age:46 }; Object.defineProperty(person2,"sayAge",{ set:function () { console.log("想要重返18歲?不存在的!"); } }); console.log(person2.sayAge);//undefined person2.sayAge=18;//想要重返18歲?不存在的! console.log(person2.sayAge);//undefined定義多個屬性
這個里我們就要說一個Object.defineProperties()方法,具體用下看如下示例:
var person = {}; Object.defineProperties(person, { name: { writable: true, value: "張三" }, age: { enumerable: true, value: 23, }, sayName: { get: function () { return this.name; }, set: function (newName) { console.log("名字修改完成"); this.name=newName+"(修改)"; } } });讀取屬性的特性
這里我們可以正好驗證我們前面所有默認值。
var person={}; Object.defineProperty(person,"name",{ value:"張三" }); var descriptor=Object.getOwnPropertyDescriptor(person,"name"); console.log("configurable:"+descriptor.configurable); //configurable:false console.log("enumerable:"+descriptor.enumerable); //enumerable:false console.log("writable:"+descriptor.writable); //writable:false console.log("value:"+descriptor.value); //張三創建對象 字面量模式
var person={}; person.name="張三"; person.age=22; person.sex="男"; person.sayName=function () { alert(this.name); }
優點:創建單個對象簡單方便
缺點:創建多個相似對象會產生大量代碼
function createPerson(name, age, sex) { var person = new Object(); person.name = name; person.age = age; person.sex = sex; person.sayName = function () { alert(this.name); }; return person; } var person=createPerson("張三",22,"男");
優點:可以快速創建多個相似對象
缺點:無法進行對象的識別
function Person(name, age, sex) { this.name = name; this.age = age; this.sex = sex; this.sayName = function () { alert(this.name); }; } var person=new Person("張三",22,"男");
以構造函數的方式創建對象要經歷下面四個步驟:
創建一個新對象
將構造函數的作用域賦給新對象(因此this指向這個新對象)
執行構造函數中的代碼,為這個新對象添加屬性
返回新對象
這里person有一個constructor(構造函數)屬性指向Person,我們可以驗證一下。
alert(person.constructor===Person);//true
鑒于這個特性我們可以用constructor來驗證對象的類型。除了這個,我們還可以利用instanceof。
alert(person instanceof Person);//true
雖然我們使用構造函數模式可以進行對象的識別,但是構造函數模式卻有一個缺點,就是每個方法都要在每個實例上重新創建一遍。下面我們舉個例子說明一下。
function Person(name, age, sex) { this.name = name; this.age = age; this.sex = sex; this.sayName = function () { alert(this.name); }; } var person1=new Person("張三",22,"男"); var person2=new Person("李四",25,"男"); alert(person1.sayName===person2.sayName);//false
從上面的例子我們看出來,person1和person2的sayName函數并非共用同一個。
優點:可以進行對象的識別
缺點:構造函數里面的函數在實例化的時候都需要每次都創建一遍,導致不同作用域鏈和標識符解析。
function Person() { } Person.prototype.name="張三"; Person.prototype.age=22; Person.prototype.sex="男"; Person.prototype.sayName=function () { alert(this.name); }; var person=new Person();
任何時候我們只要創建一個新函數,就會根據一組特定的規則為該函數創建一個prototype屬性這個屬性指向函數的原型對象。默認情況下,所有原型對象都會自動獲得一個constructor(構造函數)屬性會執行prototype屬性所在函數。以上面這個例子為例,即:
Person.prototype.constructor===Person//true
另外我們的實例對象都會有一個__proto__屬性指向構造函數的原型對象。即:
person.__proto__===Person.prototype //true
接下來我們要說的一點是我們可以通過對象實例訪問保存在原型中的值,但是不能通過對象實例重寫原型中的值。下面我們看個例子:
function Person() { } Person.prototype.name="張三"; Person.prototype.age=22; Person.prototype.sex="男"; Person.prototype.sayName=function () { alert(this.name); }; var person1=new Person(); var person2=new Person(); person1.name="李四"; alert(person1.name);//李四 alert(person2.name);//張三
從上面的例子我們可以看出,我們修改了person1的name屬性實際是實例對象person1中的屬性,而不是Person.prototype原型對象。如果我們想要person1.name指向Person.prototype.name則需要刪除實例對象person1中name屬性,如下所示:
delete person1.name; alert(person1.name);//張三
說到這里我們遇到一個一個問題,就是如何判斷一個屬性在原型上還是在實例對象上?這個是有方法可以做到的,那就是hasOwnProperty()方法,接著上面的代碼,我們可以用這個hasOwnProperty()方法去驗證一下。
alert(person1.hasOwnProperty("name"));//false
上面我們刪除實例對象person1中name屬性之后,name應該不屬于實例對象person1的屬性,所以hasOwnProperty()返回false.
如果只是想知道person1能否訪問name屬性,不論在實例對象上還是原型上的話,我們可以用in操作符。如下所示:
alert("name" in person1);//true
相對上面的原型語法,我們有一個相對簡單的原型語法。
function Person() { } Person.prototype = { constructor:Person, name: "張三", age: 22, sex:"男", sayName: function () { alert(this.name); } };
這里注意的是,需要重新設置constructor為Person,否則constructor指向Object而不是Person。但是這樣有一個缺點,就是constructor的enumerable特性被設為true。導致constructor屬性由原本不可枚舉變成可枚舉。如果想解決這個問題可以嘗試這種寫法:
function Person() { } Person.prototype = { name: "張三", age: 22, sex:"男", sayName: function () { alert(this.name); } }; Object.defineProperty(Person.prototype,"constructor",{ enumerable:false, value:Person });
說完這個之后,我們來說一下原型的動態性。由于在原型中查找值的過程是一次搜索,因此我們對原型對象所做的任何修改都能夠立即從實例上反映出來。我們看一下下面的例子:
function Person() { } var person=new Person(); person.name="張三"; person.sayName=function () { alert(this.name); }; person.sayName();//張三
但是在重寫整個原型對象的時候情況就不一樣了,我們看一下下面這個例子:
function Person() { } var person = new Person(); Person.prototype = { constructor:Person, name: "張三", age: 22, sayName: function () { alert(this.name); } }; person.sayName(); //Uncaught TypeError: person.sayName is not a function
重寫原型對象會切斷現有原型與任何之前已經存在的對象實例之間的聯系,引用的仍然是最初的原型,上面的例子由于最初的原型的沒有sayName()方法,所以會報錯。
優點:可以進行對象的識別,以及實例對象的函數不會被重復創建,從而不會導致不同的作用域鏈。
缺點:省略了為構造函數傳遞初始化參數這一環節,所有實例在默認情況都取相同的值。
function Person(name, age) { this.name = name; this.age = age; } Person.prototype = { constructor:Person, sayName: function () { alert(this.name); } }; var person1 = new Person("張三", 22); var person2 = new Person("李四", 23); alert(person1.sayName===person2.sayName);//true
優點:結合構造函數模式和原型模式的優點,是目前使用最廣泛、認同度最高的一種創建自定義類型的方法。
動態原型模式function Person(name,age,sex) { this.name=name; this.age=age; this.sex=sex; if(typeof this.sayName!="function"){ Person.prototype.sayName=function () { alert(this.name); }; } } var person=new Person("張三",22,"男"); person.sayName();//張三
上面代碼中if語句只有在初次調用構造函數時才會執行。此后,原型已經初始化,不需要再做什么修改了。
優點:保留了構造函數模式和原型模式的優點,又將所有信息封裝信息封裝在了構造函數中。
function Person(name,age,sex) { var object=new Object(); object.name=name; object.age=age; object.sex=sex; object.sayName=function () { alert(this.name); }; return object; } var person=new Person("張三",22,"男"); person.sayName();//張三
由于我們可以重寫調用構造函數時的返回值,所以我們可以在特殊情況下為對象創建構造函數。例如我們想創建一個具有特殊方法的數組,由于我們不能修改Array構造函數,因此可以使用這種方式。
function SpecialArray() { var values=new Array(); values.push.apply(values,arguments); values.toPipedString=function () { return this.join("|"); }; return values; } var colors=new SpecialArray("red","yellow","white"); alert(colors.toPipedString());//"red|yellow|white"
但這種方式缺點也是很明顯,由于構造函數返回的對象與構造函數外部創建的對象沒有什么不同。所以,instanceof操作符不能確定對象類型。因此這種模式優先級很低,不推薦優先使用。
優點:可以重寫構造函數函數的返回值,特殊情況下比較好用。
缺點:instanceof操作符不能確定對象類型。
function Person(name) { var object=new Object(); var name=name; object.sayName=function () { alert(name); }; return object; } var person=new Person("張三",22,"男"); person.sayName();//張三 alert(person.name);//undefined
在這種模式下,想訪問name這個數據成員時,除了調用sayName()方法,沒有其他方法可以訪問傳入構造函數中的原始數據。這種模式的安全性就很高。
優點:安全性高。
缺點:instanceof操作符不能確定對象類型。
function SuperType() { this.property=true; } SuperType.prototype.getSuperValue=function () { return this.property; }; function SubType() { this.subproperty=false; } //繼承SuperType SubType.prototype=new SuperType(); SubType.prototype.getSubValue=function () { return this.subproperty; }; var instance=new SubType(); alert(instance.getSuperValue());
上面代碼中,我們沒有使用SubType默認提供的原型,而是給它換了一個新原型(SuperType實例)。于是,新原型不僅具有作為SuperType的實例所擁有的全部屬性和方法,而且其內部還擁有一個指針,指向了SuperType的原型。最終結果如下:
根據上面,需要說明一點的是所有函數的默認原型都是Object的實例,因此默認原型都會包含一個內部指針指向Object.prototype。
在我們使用原型鏈的過程會有一個問題就是確定原型和實例之間的關系。這里我們有兩種方式,我們接著上面代碼繼續看。
第一種:instanceof操作符,測試實例與原型中出現過的構造函數
alert(instance instanceof Object);//true alert(instance instanceof SuperType);//true alert(instance instanceof SubType);//true
第二種:方法isPrototypeOf(),測試原型鏈中出現過的原型
alert(Object.prototype.isPrototypeOf(instance));//true alert(SuperType.prototype.isPrototypeOf(instance));//true alert(SubType.prototype.isPrototypeOf(instance));//true
如果子類型需要覆蓋超類型中的某個方法,或者需要添加超類型中不存在的某個方法,我們要遵循一個原則,給原型添加方法的代碼一定要放在替換原型的語句之后。如下所示:
function SuperType() { this.property=true; } SuperType.prototype.getSuperValue=function () { return this.property; }; function SubType() { this.subproperty=false; } //繼承SuperType SubType.prototype=new SuperType(); //添加新方法 SubType.prototype.getSubValue=function () { return this.subproperty; }; //重寫超類型中的方法 SubType.prototype.getSuperValue=function () { return false; }; var instance=new SubType(); alert(instance.getSuperValue());
另外我們還需要注意在添加方法時候,不能使用對象字面量創建原型方法。如下所示:
function SuperType() { this.property=true; } SuperType.prototype.getSuperValue=function () { return this.property; }; function SubType() { this.subproperty=false; } //繼承SuperType SubType.prototype=new SuperType(); //使用字面量添加新方法,會導致上一行代碼無效 SubType.prototype={ getSubValue:function () { return this.subproperty; } }; var instance=new SubType(); alert(instance.getSuperValue()); //Uncaught TypeError: instance.getSuperValue is not a function
說到這里我們,我來總結一下原型鏈的優缺點:
優點:功能很強大,可以連續繼承多個原型的全部屬性和方法。
缺點:
1.原型的通用問題就是屬性被共用,修改原型的屬性將會動態映射到所有指向該原型的實例。 2.鑒于屬性是共用的,我們無法給超類型的構造函數傳遞參數。借用構造函數
function SuperType() { this.colors=["red","blue","green"]; } function SubType() { //繼承了SuperType SuperType.call(this); } var instance1=new SubType(); instance1.colors.push("black"); console.log(instance1.colors);//["red", "blue", "green", "black"] var instance2=new SubType(); console.log(instance2.colors);//["red", "blue", "green"]
通過使用call()方法(或apply()方法),在新建的SubType實例的環境下條用了SuperType構造函數。這樣一來,就會在新的SubType對象上執行SuperType()函數中定義的所有對象初始化代碼。結果,SubType的每個實例就都會具有自己的colors屬性的副本了。
相對于原型鏈而言,借用構造函數有一個很大的優勢,即可以在子類型構造函數中向超類型構造函數傳遞參數。如下所示:
function SuperType(name) { this.name=name; } function SubType() { //繼承了SuperType,同時還傳遞了參數 SuperType.call(this,"張三"); //實例屬性 this.age=22; } var instance=new SubType(); alert(instance.name);//張三 alert(instance.age);//22
優點:彌補原型鏈的共用屬性和不能傳遞參數的缺點。
缺點:函數不能復用,超類型的原型中定義的方法在子類型中是不可見的。
function SuperType(name) { this.name=name; this.colors=["red","blue","green"]; } SuperType.prototype.sayName=function () { alert(this.name); }; function SubType(name,age) { SuperType.call(this,name);//第二次調用SuperType() this.age=age; } //繼承方法 SubType.prototype=new SuperType(); SubType.prototype.constructor=SubType; SubType.prototype.sayAge=function () { alert(this.age); }; var instance1=new SubType("張三",22);//第一次調用SuperType() instance1.colors.push("black"); console.log(instance1.colors);//["red", "blue", "green", "black"] instance1.sayName();//張三 instance1.sayAge();//22 var instance2=new SubType("李四",25); console.log(instance2.colors);//["red", "blue", "green"] instance2.sayName();//李四 instance2.sayAge();//25
缺點:創建對象時都會調用兩次超類型構造函數。
優點:融合了原型鏈和借助構造函數的優點,避免了他們的缺陷。Javascript中最常用的繼承模式。
var person={ name:"張三", friends:["李四","王五"] }; var person1=Object(person);//或者Object.create(person) person1.name="趙六"; person1.friends.push("孫七"); var person2=Object.create(person); person2.name="周八"; person2.friends.push("吳九"); console.log(person.friends);//["李四", "王五", "孫七", "吳九"]
原型式繼承實際上是把實例的__proto__屬性指向了person。
優點:只想讓一個對象跟另一個對象保持相似的情況下,代碼變得很簡單。
缺點:共享了相應的值,原型的通病。
function createPerson(obj) { var clone=Object(obj); clone.sayMyfriends=function () { console.log(this.friends); }; return clone; } var person={ name:"張三", friends:["李四","王五","趙六"] }; var anotherPerson= createPerson(person); anotherPerson.sayMyfriends();//["李四", "王五", "趙六"]
優點:可以為任意對象添加指定屬性,代碼量很少。
缺點: 在為對象添加函數,由于函數不能復用。每次添加都會新建一個函數對象,降低了效率。這一點與構造函數模式類似。
function inheritPrototype(subType,superType) { var prototype=Object(superType.prototype);//創建對象 prototype.constructor=subType;//增強對象 subType.prototype=prototype;//指定對象 } function SuperType(name) { this.name=name; this.colors=["red","blue","green"]; } SuperType.prototype.sayName=function () { alert(this.name); }; function SubType(name,age) { SuperType.call(this,name); this.age=age; } inheritPrototype(SubType,SuperType); SubType.prototype.sayAge=function () { alert(this.age); }; var instance1=new SubType("張三",22); instance1.colors.push("yellow"); instance1.sayName();//張三 instance1.sayAge();//22 var instance2=new SubType("李四",25); console.log(instance2.colors);// ["red", "blue", "green"] instance2.sayName();//李四 instance2.sayAge();//25
上面的inheritPrototype()函數接收兩個參數:子類型構造函數和超類型構造函數。在函數內部,第一部是創建超類型原型的一個副本。第二步是為創建的副本添加constructor屬性,從而彌補因重寫原型而失去的默認的constructor屬性。最后一步,將新創建的對象(即副本)賦值給子類型的原型。這樣,我們就可以用調用inheritPrototype()函數的語句,去替換前面例子中為子類型原型賦值的語句。
優點:集寄生式繼承和組合繼承的優點于一身,是實現基于類型繼承的最有效方式。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/83374.html
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
摘要:是完全的面向對象語言,它們通過類的形式組織函數和變量,使之不能脫離對象存在。而在基于原型的面向對象方式中,對象則是依靠構造器利用原型構造出來的。 JavaScript 函數式腳本語言特性以及其看似隨意的編寫風格,導致長期以來人們對這一門語言的誤解,即認為 JavaScript 不是一門面向對象的語言,或者只是部分具備一些面向對象的特征。本文將回歸面向對象本意,從對語言感悟的角度闡述為什...
摘要:用代碼可以這樣描述安全到達國外面向過程既然說了面向對象,那么與之對應的就是面向過程。小結在這篇文章中,介紹了什么是面向對象和面向過程,以及中對象的含義。 這是 javascript 面向對象版塊的第一篇文章,主要講解對面向對象思想的一個理解。先說說什么是對象,其實這個還真的不好說。我們可以把自己當成一個對象,或者過年的時候相親,找對象,那么你未來的老婆也是一個對象。我們就要一些屬性,比...
摘要:對象重新認識面向對象面向對象從設計模式上看,對象是計算機抽象現實世界的一種方式。除了字面式聲明方式之外,允許通過構造器創建對象。每個構造器實際上是一個函數對象該函數對象含有一個屬性用于實現基于原型的繼承和共享屬性。 title: JS對象(1)重新認識面向對象 date: 2016-10-05 tags: JavaScript 0x00 面向對象 從設計模式上看,對象是...
閱讀 2111·2021-11-24 10:28
閱讀 1117·2021-10-12 10:12
閱讀 3337·2021-09-22 15:21
閱讀 679·2021-08-30 09:44
閱讀 1895·2021-07-23 11:20
閱讀 1147·2019-08-30 15:56
閱讀 1751·2019-08-30 15:44
閱讀 1483·2019-08-30 13:55