1. 程式人生 > >JavaScript學習日誌(二):面向對象的程序設計

JavaScript學習日誌(二):面向對象的程序設計

循環 理想 soft 日誌 傳遞 數組 用例 定義 基本類型

1,ECMAScript不像其他面向對象的語言那樣有類的概念,它的對象與其他不同。

2,ECMAScript有兩種屬性:數據屬性和訪問器屬性。([[]]這種雙中括號表示屬性為內部屬性,外部不可直接訪問)
1.數據屬性:[[ Configurable ]]:表示能否通過delete刪除屬性,能否修改屬性的特性,能否將屬性修改為訪問器屬性,默認為true。
[[ Enumerable ]]:表示能否通過for-in循環返回屬性,默認為true。
[[ Writable ]]:表示能否修改屬性的值,默認為true。
[[ Value ]]:包含這個屬性的數據值,默認undefined。


要修改上述屬性,必須用Object.defineProperty()方法,接收三個參數:屬性所在的對象,屬性的名字和描述符對象(就是上述4個屬性名)

2.訪問器屬性:[[ Configurable ]]:表示能否通過delete刪除屬性,能否修改屬性的特性,能否將屬性修改為數據屬性,默認為true。
[[ Enumerable ]]:表示能否通過for-in循環返回屬性,默認為true。
[[ Get ]]:在讀取屬性時調用的函數,默認為undefined。
[[ Set ]]:再寫入屬性時調用的函數,默認為undefined。


也是必須用Object.defineProperty()方法來定義

3,創建對象的幾種模式:
1.工廠模式:
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(name)
};
return o
}

var person = createPerson(“Nical”, 24, “Engineer”);

缺點:1,無法確定對象的類型(都是Object);
2,創建的多個對象之間沒有聯系

2,構造函數模式:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(name)
}
}

var person = new Person(“Nical”, 24, “Engineer”);

缺點:①多個實例重復創建方法,無法共享。
   ②多個實例都有sayName方法,但均不是同一個Function的實例。

3,原型模式:
function Person(){}

Person.prototype.name = ‘Nical‘;
Person.prototype.age = 24;
Person.prototype.job = ‘Engineer‘;
Person.prototype.sayName = function(){
alert(this.name);
}

var person = new Person();

缺點:①無法傳入參數,不能初始化屬性值。
   ②如果包含引用類型的值時,改變其中一個實例的值,則會在所有實例中體現。

===========================================================================

(理解原型對象:

只要創建了一個函數(Person),就會為這個函數(Person)自動創建一個prototype屬性,這個屬性指向該函數的原型對象。這個原型對象會自動獲得一個constructor(構造函數)屬性,指向該函數(Person)。(初來理解:這裏我們可以猜想函數的原型對象構造了函數,但是下面寫了一個函數的實例也有一個構造函數屬性指向了函數,那這個說法就不通了)(第二次理解:再讀的時候發現可以理解通,其實總結起來很簡單,一句話,被構造的東西.constructor === 構造它的東西,比如說下面的第一行代碼,Person.prototype哪裏來的,就是Person在創建的時候自動構造出來的,也就是說Person.prototype是Person這個函數構造出來的,所以第一行成立!第二三行,person1是怎麽出來的,是Person這個函數構造出來的,所以person1.constractor===Person)

Person.prototype.constructor === Person; //true
var person1 = new Person();
person1.constructor === Person; //true
(這裏我不太理解第一次為什麽理解錯了,person1這個實例就是由Person這個構造函數構造出來的,那麽person1的constructor屬性就是指向的Person這個構造函數啊,沒毛病,跟繼承沒什麽關系啊)

constructor屬性指向的是Person構造函數,不管是它的原型對象還是它的創建的實例,它們的constructor屬性都會指向Person。

字符表示:
Person.prototype ——> Person Prototype(原型對象),Person Prototype(原型對象).constructor ——>Person

這裏我的理解就是,‘指向’的意思其實就是代表‘是’,Person.prototype就是Person的原型對象,所以原型對象的constructor屬性指向函數,就是
Person.prototype.constructor === Person; //true


構造函數創建之後,它的原型對象只有一個constructor屬性,其他屬性都是從Object那裏繼承來的。此時,用構造函數創建一個實例,比如
var person1 = new Person();
此時實例的內部包含一個指針(內部屬性),指向構造函數的原型對象,註意,不是構造函數Person,而是Person的原型對象(Person Prototype),在FireFox,Safari,chrome瀏覽器裏,這個指針叫__proto__,再三註意,這個連接是實例與構造函數的原型對象之間的,不是實例與構造函數之間的。所以當我們想為實例添加屬性的時候,不用在構造函數裏添加,而是直接在原型對象裏添加,就是 Person.prototype.name = ‘Nical‘;
Person.prototype.age = 24;
Person.prototype.job = ‘Engineer‘;
此時person1.name = “Nical”

判斷一個原型對象是否是某個實例的原型,用isPrototypeOf(),括號裏面傳實例,原型寫在方法前面,這個方法可以確定該實例是否存在內部屬性,指向該原型
Person.prototype.isPrototypeOf(person1); // true

一個實例的原型,如何知道,用Object.getPrototypeOf(),返回值就是原型對象,括號裏面傳實例。
Object.getPrototypeOf(person1); //Person.prototype

以上兩個方法傳的參數都是實例。

代碼在讀取某個對象的屬性值的時候,先查找該實例對象是否包含該屬性,如果有就返回該屬性值,如果沒有,直接查找該實例所對應的構造函數的原型對象是否包含該屬性,如果有就返回該屬性值。如果實例中存在和原型對象中同名屬性,則會自動屏蔽原型中的屬性,但不會修改原型中的屬性。

如何判斷對象的某個屬性是否是對象自身添加的還是原型中的?用hasOwnProperty():
person1.hasOwnProperty(“name”); //false

註意:如果在實例中新定義了一個和原型中同名的屬性,那麽hasOwnProperty會返回true。

如何判斷通過對象能夠訪問某個屬性?用in方法:
“name” in person1; //true

如何判斷某個對象的屬性是否只是他的原型中的,而不是實例中的?

function hasPrototypeProperty( object , name ){
return (!object.hasOwnProperty(name))&& (name in object)
}
註意,如果新定義了一個和原型中同名的屬性,則改值會返回false

幾種訪問對象屬性的方法,註意他們的區別:
1,for-in循環返回的是能通過對象訪問的、可枚舉的屬性。
2,Object.keys()返回的是僅僅是當前對象的自身屬性、並且是可枚舉的所有屬性的字符串數組。
3,Object.getOwnPropertyNames()返回的是僅僅是當前對象的自身屬性、並且是可枚舉和不可枚舉的所有屬性的字符串數組。
這個一定要用例子來說明:

function Person(){};
Person.prototype.name = ‘Nical’;
var person1 = new Person();
person1.age = 12;

for(var a in person){
console.log(a)
};
// age
// name

for(var b in Person){
console.log(b)
};
//

for(var c in Person.prototype){
console.log(c)
}
// name

Object.keys(person1);
// [‘age’];

Object.keys(Person);
// [];

Object.keys(Person.prototype)
// [“name"]

Object.getOwnPropertyNames(person1);
// [‘age’]

Object.getOwnPropertyNames(Person);
// ["length", "name", "arguments", "caller", "prototype"]

Object.getOwnPropertyNames(Person.prototype)
// [“constructor", "name"]

關於原型對象的重寫問題,下面我用一個較長的例子來解釋一下,這裏面有些坑要註意:

function Person(){};
Person.prototype = {
name: ‘Nical’
};
//這裏重寫了Person的原型對象,此時,Person原型對象的構造函數屬性已經不再指向Person了,同樣,由Person新建的實例(註意,這裏是新建的,也就是說是在重寫之後新建的,如果是在重寫之前就已經建立了新的實例,那麽構造函數屬性依然指向Person)的構造函數屬性也不再指向Person了,二者的constructor屬性都指向Object了。
Person.prototype.constructor === Person; //false
Person.prototype.constructor === Object; //true
var person1 = new Person();
person1.constructor === Person; //false
person1.constructor === Object; //true

那麽如果我要讓constructor屬性指向Person呢,很簡單,在原型對象裏面強行添加constructor屬性,並賦給Person,接著上面的例子:
Person.prototype = {
constructor: Person,
name: ‘Tom’
}
var person2 = new Person();
Person.prototype.constructor === Person; //true
person2.constructor === Person;//true

但是此時的person1和person2的原型已經不是同一個了,所以
person1.name; // ‘Nical’

上面直接在原型中添加constructor屬性會導致他的[[Enumerable]]特性為true,但是原生的constructor屬性是不可枚舉的,所以我們要用Object.defineProperty()方法來修改constructor屬性,具體的傳參在上面第6章第2節裏面有:
Object.defineProperty( Person.prototype , ’constructor’, {
enumerable: false,
value: Person
});

如果實例新建後,再給實例的構造函數的原型對象添加屬性或方法,實例還是可以訪問到新增的屬性或方法,原因是實例與原型之間的松散連接關系,他們之間的連接是一個指針(__proto__),而不是副本。但是如果原型對象是重寫的,那麽這個指針就指不到新的原型對象了,所以那些新增的屬性或方法在實例中就不可訪問了(這裏的實例是指重寫之前建立的實例,他們訪問的還是最初的原型,如果實例是在重寫之後,那麽還是可以訪問到的)。


===========================================================================


回到原型模式的問題,他的缺點我用一個實例來說明:

function Person(){};
Person.prototype = {
name: ’Nical’,
friends: [‘a’ , ‘b’, ‘c’]
}
var person1 = new Person();
var person2 = new Person();
person1.name = ‘Sam’;
person1.friends.push(‘d’);

person2.name; //’Nical’;
person2.friends; //[‘a’ ,’b’, ‘c’, ‘d’];

如果一個實例修改了屬性是基本類型的,那麽不會影響到其他實例,但是如果一個實例修改了屬性是引用類型的,那麽其他的也會受影響。這裏的屬性都是指的是原型中的屬性,不是構造函數中的屬性(補充一下,如果是構造函數中的屬性,不管修改的是什麽類型,其他實例都不會受影響,具體案例看下面那個),實例中沒有的。如果是實例本身的屬性或者是與原型對象中同名的屬性,那麽無論修改的屬性是什麽類型,都不會受影響。


4,組合使用構造函數模式和原型模式

function Person(name , age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = [‘a’, ‘b’, ‘c’];
}

Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name)
}
};

var person1 = new Person(‘Nical’, 29, ‘Engineer’);
var person2 = new Person(‘Greg’, 27, ‘Doctor’);

person1.friends.push(“d”);
person1.friends; // [‘a’, ‘b’, ‘c’, ‘d’];
person2.friends; // [‘a’, ‘b’, ‘c’];

因為這裏friends的屬性是在構造函數裏定義的,所以某個實例改變它,其他實例不改變。


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)
}
}
}

可以通過檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。

6,寄生構造函數模式


function Person(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name)
};
return o
}

var person = new Person(“Nical”, 24, “Engineer”);

//不推薦使用


7,穩妥構造函數模式

function Person(name, age, job){
var o = new Object();

//在這邊定義私有變量和函數

o.sayName = function(){
alert(name)
};

return o;
}

這種方法能保證除了sayName方法外,沒有別的方式可以訪問到其他數據成員。

4,繼承

ECMAScript只支持實現繼承,主要是依靠原型鏈實現繼承。

=============

1,原型鏈

每一個構造函數都有一個prototype屬性,指向他的原型對象,原型對象又有一個constructor屬性,指向構造函數,構造函數生成的實例,又有一個內部屬性(__proto__),指向它的構造函數的原型對象。如果這時候將原型對象作為另一個構造函數的實例,則這個實例(原型對象)又會有一個指針(__proto__)指向該實例(原型對象)的原型對象,而這個實例(原型對象)的原型對象又會有一個constructor屬性,指向該實例(原型對象)的構造函數,該實例(原型對象)又會有一個prototype屬性指向實例(原型對象)的原型對象。原型鏈就是實例和原型的鏈條。

function Person1(){};
var person1 = new Person1();
function Person2(){};
Person1.prototype = new Person2();

Person1.prototype.__proto__ === Person2.prototype; // true 等同於person1.__proto__.__proto__ === Person2.prototype; //true
Person1.prototype.constructor === Person1; //false,因為Person1的prototype重寫了,Person1的原型對象被當作了Person2的實例,所以應該指向Person2,即Person1.prototype.constructor === Person2
person1.constructor === Person1; // true,因為此時Person1的原型對象已經不是最初的了,所以person1不會繼承新的原型對象,所以它的constructor還是指向Person1;如果這個person1實例是在原型對象重寫之後新建的話,那麽constructor就不會指向Person1了。

這是跟寫法有關的,如果我們換一種寫法:

function A(){};
function B(){};
A.prototype = new B();
var a = new A();

A.prototype.constructor === B; //true
a.constructor === B; //true
a.__proto__ === A.prototype; //true
A.prototype.__proto__ === B.prototype; //true
a.__proto__.__proto__ === B.prototype; // true

此時的A.prototype已經被重寫了,所以a和A原型對象的constructor屬性都指向B,而不是A;所謂的繼承就是A的原型對象會繼承構造函數B的原型對象的所有方法和屬性。所以搜索一個實例的屬性,三步:1,先在該實例中尋找該屬性,2,如果沒有則會在該實例的構造函數的原型對象中搜索,3,如果沒有則會在該實例的原型對象的構造函數的原型對象中搜索。繼承都是原型對象繼承原型對象。

默認的原型:
所有引用類型都繼承了Object的方法,這也是通過原型鏈來實現的,所有函數的原型對象默認都是Object的實例,當然也會繼承,所以上面的代碼可以接著寫:

B.prototype.__proto__ === Object.prototype; // true;
var b = new B();
b.__proto__.__proto__ === Object.prototype; //true;
a.__proto__.__proto__.__proto__ === Object.prototype; // true

b.constructor === B; // true
B.prototype = new Object();
b.constructor === B; // true
B.prototype.constructor === B; //false
B.prototype.constructor === Object; //true

var c = new B();
c.constructor === Object; //true
這裏的結果解釋同上,如果重寫了prototype,則直接指向上一層原型

原型鏈的兩個問題:
1,之前我們提到了如果原型屬性中有引用類型的值被實例修改了,那麽所有的實例都會被改變,但是如果是構造函數裏面定義的屬性,不管是引用類型還是基本類型,怎麽修改都不會改變。但是如果是通過原型來繼承,那麽構造函數裏的屬性就會變成下一個原型的屬性,所以如果修改它,還是會影響到下一個原型的實例。一個例子說明:
function SuperType(){
this.colors = [‘red’,’blue’,’yellow’];
}
function SubType(){
}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push(‘black’);
instance1.colors; //‘red’,’blue’,’yellow’,’black’
var instance2 = new SubType();
instance2.colors; //‘red’,’blue’,’yellow’,’black’

上面的instance1修改的屬性雖然是SuperType的構造函數裏的屬性,但是此時已經繼承給了SubType的原型了,所以相當於修改了原型的引用類型的屬性,因此所有的實例都會被修改

2,在創建子類型的實例的時候,無法向超類型的構造函數傳遞參數,因為會影響到所有的實例。主要是引用類型的屬性的修改

=============


2,借用構造函數
針對上面的兩個問題,用一種借用構造函數的方法可以暫時避免:

function SuperType(){
this.colors = [“red”,”blue”,”green”]
}
function SubType(){
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push(‘black’);
instance1.colors; //‘red’,’blue’,’green’,’black’
var instance2 = new SubType();
instance2.colors; //‘red’,’blue’,’green’

這裏的instance2沒有收到影響,因為這裏相當於把SuperType裏的屬性繼承過來(復制),每一個新生成的實例都有一個colors的副本,所以互不影響。

但是也有一個問題,所有定義的屬性都要在構造函數SubType裏定義,這樣就沒有函數的可復用性了,看下一個例子:

function SuperType(name){
this.name = name
}
function SubType(){
SuperType.call(this, ‘Nical’);
this.age = ’29’
}
var instance = new SubType();
instance.name; //Nical
instance.age; //29

在這裏我無法給SubType傳參,返回的name和age都是統一的,如果要修改只能在構造函數裏修改,這樣太麻煩了,所以我們很少用這個方法


==============

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(‘Nical’, 29);
instance1.colors.push(‘black’);
instance1.colors; //‘red’,’blue’,’green’,’black’
instance1.sayName; // ‘Nical’
instance1.sayAge; // 29
var instance2 = new SubType(‘Greg’, 21);
instance2.colors; //‘red’,’blue’,’green’
instance2.sayName; // ‘Greg’
instance2.sayAge; // 21


這個方法是最常用的????????????????

==========

4,原型式繼承
function object(o){
function F(){};
F.prototype = o;
return new F();
}
ES5有一個方法:Object.create()方法,等同於上面這個,這裏的o是一個對象實例

==========

5,寄生式繼承
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
alert(“hi”)
};
return clone
}

==========

6,寄生組合式繼承
組合繼承的缺點是,有兩次調用超類型構造函數:第一次,SubType.prototype = new SuperType()會調用一次;第二次,在子類型的構造函數裏寫SuperType.call(this, name)。調用兩次的後果是每一次調用都會重新創建實例屬性,自動屏蔽原來的屬性,所以用寄生組合式繼承會避免這種情況:
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)
}

寄生組合式繼承是引用類型最理想的繼承範式。??????????

JavaScript學習日誌(二):面向對象的程序設計