大家好,我是IT修真院深圳分院第01期學員,一枚正直純潔善良的web程序員。
今天給大家分享一下,修真院官網JS(職業)任務4,深度思考中的知識點——JS面向對象編程
1.介紹
“面向對象編程”(Object-Oriented Programming,縮寫為OOP)是目前主流的編程范式。它的核心思想是將真實世界中各種復雜的關系,抽象為一個個對象,然后由對象之間的分工與合作,完成對真實世界的模擬。
面向對象的語言有一個標志,就是類的概念,通過類可以創建任意多個具有相同屬性和方法的對象。ECMAScript中沒有類的概念,它的對象與基于類的語言中的對象有所不同。
2.涉及
2.1對象
ECMA-262 把對象定義為:無序屬性的集合,其屬性可以包含基本值、對象或者函數。嚴格來講,這就相當于說對象是一組沒有特定順序的值。對象的每個屬性或方法都有一個名字,而每個名字都映射到一個值。正因為這樣(以及其他將要討論的原因).我們可以把 ECMAScript 的對象想象成散列表:無非就是一組名值對,其中值可以是數據或函數。
2.1.1Object構造對象
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer ";
person.sayName = function() {
alert (this.name) ;
};
2.1.2對象字面量創建對象
var person = {
name: "Nicholas",
age : 29 ,
job: "Software Engineer",
sayName: function () {
alert(this.name) ;
}
};
2.2對象屬性類型
ECMA-262第5版定義了JS對象屬性的特征(用于JS引擎,外部無法直接訪問)。ECMAScript中有兩種屬性:數據屬性和訪問器屬性。
2.2.1數據屬性
數據屬性指包含一個數據值的位置,可在該位置讀取或寫入值,有4個供述其行為的特性:
[[configurable]]:表示能否通過 delete 刪除屬性從而重新定義屬性.能否修改屬性的特性,或者能否把屬性修改為訪問器屬性。默認為true;
[[Enumerable]]:表示能否通過 for-in 循環返回屬性。默認為true;
[[Writable]]:表示能否修改屬性的值。默認true;
[[Value]]:包含該屬性的數據值。讀取/寫入都是該值。默認為undefined;
如上面實例對象person中定義了name屬性,其值為’Nicholas’,對該值的修改都反映在這個位置,要修改對象屬性的默認特征(默認都為true),必須使用用Object.defineProperty()方法,它接收三個參數:屬性所在對象,屬性名和一個描述符對象(必須是:configurable、enumberable、writable和value,可設置一個或多個值)。
var person = {};
Object.defineProperty(person, 'name', {
configurable: false,
writable: false,
value: 'Nicholas'
});
alert(person.name);//"Nicholas"
delete person.name;
person.name = 'aaa';
alert(person.name);//"Nicholas"
以上,delete及重置person.name的值都沒有生效,這就是因為configurable: false和writable: false;值得注意的是一旦將configurable設置為false,則無法再使用defineProperty將其修改為true(執行會報錯:can't redefine non-configurable property);
2.2.2訪問器屬性
訪問器屬性不包含數據值。它包含一對 getter 和 setter 函數(這兩個函數都不是必需的)。讀取訪問器屬性時,調用 getter 函數,返回有效的值;寫入訪問器屬性時,調用 setter 函數并傳入新值并設置。該屬性有以下4個特征:
[[Configurable]]:是否可通過delete刪除屬性從而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為數據屬性,默認值為true。
[[Enumerable]]:是否可通過for-in循環屬性;
[[Get]]:讀取屬性時調用,默認:undefined;
[[Set]]:寫入屬性時調用,默認:undefined;
訪問器屬性不能直接定義,必須使用defineProperty()來定義.如:
var book = {
_year: 2004,
edition: 1
};
Object.defineProperty(book, 'year', {
get: function () {
return this._year;
},
set: function (newValue) {
if (newValue > 2004) {
this._year=newValue;
this.edition += newValue-2004;
}
}
});
book.year=2005;
alert(book.edition);//2
不一定非要同時指定 getter 和 setter,只指定 getter 意味著屬性是不能寫,嘗試寫入屬性會被忽略。沒有指定getter函數的屬性也不能讀,會返回undefined。
此外,ECMA-262(5)還提供了一個Object.defineProperties()方法,可以用來一次性定義多個屬性的特性:
var book = {};
Object.defineProperties(book,{
_year:{
value:2004
},
edition:{
value:1
},
year:{
get: function () {
return this._year;
},
set: function (newValue) {
if (newValue > 2004) {
this._year=newValue;
this.edition += newValue-2004;
}}}
});
使用ECMAScript 5的Object.getOwnPropertyDescriptor()方法,可以取得給定屬性的描述符。這個方法接收兩個參數: 屬性所在的對象和要讀取其描述符的屬性名稱。返回值是一個對象,如果是訪問器屬性,這個對象的屬性有configurable、enumerable、get和set; 如果是數據屬性,這個對象的屬性有configurable、enumerable、writable和value。
var descriptor = Object.getOwnPropertyDescriptor(book ,"_year" ) ;
alert(descriptor.value); //2004
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //undefined
var descriptor = Object.getOwnPropertyDescriptor(book. "year");
alert(descriptor.value); //undefined
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //function
2.3 創建對象
使用Object構造函數或對象字面量都可以創建對象,缺點是創建多個對象時,會產生大量的重復代碼。因此使用用工廠模式的變體來解決問題。
2.3.1工廠模式:用函數來封裝以特定接口創建對象的細節
function createPerson(name, age, job) {
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.getName = function () {
return this.name;
}
return o;//使用return返回生成的對象實例
}
var person = createPerson('Nicholas',29,'Software Engineer');
var person = createPerson('Greg',27,'Doctor');
創建對象交給一個工廠方法來實現,可以傳遞參數。缺點是無法識別對象類型,因為創建對象都是使用Object的原生構造函數來完成的。
2.3.2構造函數模式:創建特定類型的對象
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.getName = function () {
return this.name;
}
}
var person1 = new Person('Nicholas',29,'Software Engineer');
var person2 = new Person('Greg',27,'Doctor');
使用自定義的構造函數來創建對象,它與工廠方法區別在于:
1.沒有顯式地創建對象
2.直接將屬性和方法賦值給this對象;
3.沒有return語句;
此外,要創建Person的實例,必須使用new關鍵字,以Person函數為構造函數,傳遞參數完成對象創建;實際創建經過以下4個過程:
1.創建一個對象
2.將函數的作用域賦給新對象(因此this指向這個新對象,如:person1)
3.執行構造函數的代碼
4.返回該對象
上面person1與person2都是Person的實例,可以使用instanceof判斷,且都繼承了Object。
alert(person1 instanceof Person);//true;
alert(person2 instanceof Person);//true;
alert(person1 instanceof Object);//true;
alert(person1.constructor === person2.constructor);//ture;
構造函數方式也存在缺點,那就是在創建對象時,特別針對對象的屬性指向函數時,會重復的創建函數實例,以上述代碼為基礎,可以改寫為:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function ("alert(this.name)");//與聲明函數在邏輯上是等價的
}
alert(person1.sayName == person2.sayName); //false
上述代碼,創建多個實例時,會重復調用new Function(),創建多個函數實例,這些函數實例不在一個作用域中,造成內存浪費。
可以在函數中定義一個this.sayName = sayName的引用,而sayName函數在Person外定義,這樣可以解決重復創建函數實例問題,但在效果上并沒有起到封裝的效果,如下所示:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
alert(this.name);
}
var person1 = new Person('Nicholas',29,'Software Engineer');
var person2 = new Person('Greg',27,'Doctor');
2.3.3原型模式
JS每個函數都有一個prototype(原型)屬性,這個屬性是一個指針,指向一個對象,它是所有通過new操作符使用函數創建的實例的原型對象。原型對象最大特點是,所有對象實例共享它所包含的屬性和方法,也就是說,所有在原型對象中創建的屬性或方法都直接被所有對象實例共享。
function Person(){
}
Person.prototype.name = 'Nicholas'; //使用原型來添加屬性
Person.prototype.age = 29;
person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){
alert(this.name);
}
var person1 = new Person();
person1.sayName(); //Nicholas
var person2 = new Person();
person2.sayName(); //Nicholas
alert(person1.sayName === person2.sayName); //true;
原型模式的缺點,它省略了為構造函數傳遞初始化參數,結果所有實例在默認情況下都將取得相同的屬性值。最主要是當對象的屬性是引用類型時,它的值是不變的,總是引用同一個外部對象,所有實例對該對象的操作都會影響其它實例:
function Person() {
}
Person.prototype ={
name:'Nicholas',
lessons = ['Math','Physics'];
}
var person1 = new Person();
var person2 = new Person();
person1.lessons.push('Biology');
alert(person2.lessons);//Math,Physics,Biology,修改person1影響了person2
2.3.4組合構造函數及原型模式
目前最為常用的定義類型方式,是組合使用構造函數模式與原型模式。構造函數模式用于定義實例的屬性,而原型模式用于定義方法和共享的屬性。結果,每個實例都會有自己的一份實例屬性的副本,但同時又共享著對方方法的引用,最大限度的節約內存。
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ['Shelby','Court'];
}
Person.prototype ={
constructor: Person,
this.sayName: function() {
alert(this.name);
}
}
var person1 = new Person('Nicholas',29,'Software Engineer');
var person2 = new Person('Greg',27,'Doctor');
person1.friends.push('Van');
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court"
alert(parson1.friends === parson2.friends); //false
alert(parson1.sayName === parson2.sayName); //true
2.3.5動態原型模式
將所有信息封裝在構造函數中,而通過在構造函數中初始化原型(僅在必要的情況下),又保持了同時使用構造函數和原型的優點。換句話說,可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
//方法
if (typeof this.sayName != 'function'){
Person.prototype.sayName = function() {
alert(this.name);
};
}
}
var person1 = new Person('Nicholas',29,'Software Engineer');
person1.sayName();
方法代碼:if語句在sayName()方法不存在的情況下,將它添加到原型中,只在初次調用構造函數時執行。對于采用這種模式創建的對象,可以使用instanceof操作符確定它的類型。
2.4 繼承
ECMAScript 無法實現接口繼承,只支持實現繼承。
2.4.1原型鏈
上一期講過
2.4.2借用構造函數
使用apply()和call()方法在子類型構造函數的內部調用超類型構造函數。
function SuperType() {
this.colors = ['red', 'blue','green'];
}
function SubType() (
// 繼承了SuperType
SuperType.call(this);
}
var instance1 = new SubType() ;
instance1.colors.push("black");
alert (instance1.colors); / /"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors); //"red,blue,green"
2.4.3組合繼承
使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。
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;
}
/ /繼承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor=SubType;
SubType.prototype.sayAge = function() (
alert(this.age);
};
var instance1 = new SubType('Nicholas',29);
instance1.colors.push('black');
alert(instance1.colors); // "red,blue,green,black"
instance1.sayName(); //'Nicholas';
instance1.sayAge(); //29
var instance2 = new SubType('Greg',27);
alert(instance2.colors); //'red, blue, green'
instance2.sayName(); //'Greg';
instance2.sayAge(); //27
讓兩個不同的 SubType 實例既分別擁有自己屬性————包括colors屬性,又可以使用相同的方法。
此外,還存在下列可供選擇的繼承模式。
1).原型式繼承. 可以在不必預先定義構造函數的情況下實現繼承,其本質是執行對給定對象的淺復制。而復制得到的副本還可以得到進一步改造。
2).寄生式繼承. 與原型式繼承非常相似.也是基于某個對象或某些信息創建一個對象,然后增強對象,最后返回對象。為了解決組合繼承模式由于多次調用超類型構造函數而導致的低效率問題,可以將這個模式與組合繼承一起使用。
3).寄生組合式繼承. 集寄生式繼承和組合繼承的優點與一身,是實現基于類型繼承的最有效方式。
3.常見問題
面向對象編程
4.解決方案
以上
5.編碼實戰
6.擴展思考
面向對象與面向過程的區別?
傳統的過程式編程(procedural programming)由一系列函數或一系列指令組成,使用時一步步調用;而面向對象編程的程序由一系列對象組成。
每一個對象都是功能中心,具有明確分工,可以完成接受信息、處理數據、發出信息等任務。因此,面向對象編程具有靈活性、代碼的可重用性、模塊性等特點,容易維護和開發,非常適合多人合作的大型應用型軟件項目。
7.參考文獻
《Javascript高級程序設計》chapter 6
8.更多討論
狀態機也是用對象實現的。
鳴謝
感謝大家觀看
------------------------------------------------------------------------------------------------------------------------
今天的分享就到這里啦,歡迎大家點贊、轉發、留言、拍磚~
下期預告:cookies,sessionStorage和localStorage的區別?不見不散~