JavaScript面向對象的程序設計——「建立對象」的注意要點

建立對象

Object 構造函數或對象字面量均可以用來建立單個對象。但這個方法的缺點很是明顯:同一個接口建立很可耐多對象會產生大量的重複代碼。爲了解決這個問題,人們開始使用工廠模式的一種變體。html

工廠模式(摒棄,不推薦)

這個模式沒有解決對象識別的問題(即怎樣知道一個對象的類型)。如:數組

具體的建立單個對象:函數

var person = {};
person.name = "Oliver";
person.age = 18;
person.sayName = function(){
    return this.Name;
};

改變成工廠模式:this

function createPerson(name,age){
    var obj = {};
    obj.name = name;
    obj.age = age;
    obj.sayName = function(){
        return this.name
    };
    return obj; //注意這裏要返回obj 對象,這樣才能把obj 對象傳給createPerson 變量。
}

var newPerson = createPerson("Oliver",18);

構造函數模式

構造函數能夠建立特定類型的對象。因此,能夠建立自定義的構造函數,從而定義自定義對象類型的屬性和方法。如:prototype

function Person(name,age){ //注意大小寫,構造函數應該把第一個字幕大寫化
    this.name = name;
    this.age = age;
    this.sayName = function (){
        return this.name;
    };
}

var newPerson = new Person("Oliver",18);

document.write(newPerson.name); //Oliver
document.write(newPerson.age); //18
document.write(newPerson.sayName()); //Oliver

確實至關方便設計

這裏必定要記住,構造函數都應該以一個大寫字母開頭,用來區分其餘的函數指針

又如:code

function Person(name,age){ //注意大小寫,構造函數應該把第一個字幕大寫化
    this.name = name;
    this.age = age;
    this.sayName = function (){
        return this.name;
    };
}

var person1 = new Person("Oliver",18);
var person2 = new Person("Troy",24);

document.write(person1.constructor == Person); //true
document.write(person2.constructor == Person); //true

這裏的person1 和person2 分別保存着Person 的一個不一樣的實例。兩個對象都有一個constructor(構造函數)屬性,該屬性指向Person。htm

在上面這個例子中,person1 和person2 便是Object 的實例,同時也是Person 的實例。能夠經過instanceof 操做符來驗證:對象

console.log((person1 instanceof Object) && (person2 instanceof Person)); //true

以這種方式建立構造函數是定義在Global 中的(window 對象)

將構造函數當作函數

任何函數,只要經過new 操做符來調用,那它就能夠座位構造函數;而任何函數,若是不經過new 操做符來調用,那它就跟普通函數沒區別。以下面這個構造函數:

function Car(name,color,sound){
    this.name = name;
    this.color = color;
    this.sound = function(){
        return sound;
    };
    console.log(this.name + " " + this.color + " " + this.sound());
}

若是當作構造函數來使用:

var benz = new Car("C200","White","Boom Boom"); //C200 White Boom Boom

若是做爲普通函數來調用:

Car("Benz","White","Boom!"); //Benz White Boom!
console.log(window.name + window.color + window.sound()); //BenzWhiteBoom!

若是在另外一個對象的做用域中調用:

var cars = {};
Car.call(cars,"Benz","White","Boom Boom!");
document.write(cars.sound()); //Boom Boom!

構造函數的問題

問題是每一個方法都要在每一個實例是從新建立一遍。可用經過把內部的函數轉移到外部來解決這些問題。如:

function Car(name,color){
    this.name = name;
    this.color = color;
    this.show = show;
}
function show(){
    console.log(this.name + this.color);
}
var benz = new Car("Benz","white");
benz.show(); //Benzwhite

但這個問題是徹底沒有了封裝性可言。不過能夠經過原型模式來解決。

原型模式

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};
var person1 = new Person();
person1.sayName(); //Oliver
var person2 = new Person();
person2.sayName(); //Oliver;
console.log(person1.sayName == person2.sayName); //true

與構造函數不一樣的是,新對象的這些屬性和方法是由全部實例共享的。這裏兩個新的person 訪問的都是同一組屬性和同一個sayName() 函數。

理解原型對象

以上面的Person 爲例,Person 構造函數裏面存在一個prototype 屬性,這個屬性指向原型對象Person Prototype,該Person Prototype 裏面包含了constructor 屬性,該屬性又指向構造函數Person。構造函數的實例包含了一個[[Prototype]]的內部屬性,該內部屬性則指向Person Prototype。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.sayName(); //Oliver

var person2 = new Person();
person2.sayName(); //Oliver;

console.log(Person.prototype);
/*
age: 18
constructor: function Person() {}
name: "Oliver"
sayName: function () {
__proto__: Object
*/
console.log(Person.prototype.constructor);
//function Person() {}
console.log(Object.getPrototypeOf(person1));
/*
age: 18
constructor: function Person() {}
name: "Oliver"
sayName: function () {
__proto__: Object
*/

對於構造函數、原型屬性以及實例之間的關係,參見《js高級程序設計》一書中第6.2.3 章節。

兩個方法:isPrototypeOf()Object.getProtytypeOf()(ECMAScript 5)。前者是用來肯定[[Prototype]];後者是用來返回[[Prototype]]值。如:

console.log(Person.prototype.isPrototypeOf(person1)); //true
console.log(Object.getPrototypeOf(person1).name); //Oliver

console.log(Object.getPrototypeOf(person1));
/*
age: 18
constructor: function Person() {}
name: "Oliver"
sayName: function () {
__proto__: Object
*/

爲對象添加一個屬性時,這個屬性會屏蔽原型對象中的同名屬性,可是並不會修改那個屬性。若是使用delete 刪除這個屬性,就能夠從新訪問原型中的屬性。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.sayName(); //Oliver 原型中的Name

person1.name = "Troy";
person1.sayName(); //Troy 實例中的Name

delete person1.name;
person1.sayName(); //Oliver 原型中的Name

每次讀取某個對象的某個屬性,都會從實例自己開始搜索,若是沒有找到給定名字的屬性,則會在原型對象中再次搜索。

方法hasOwnProperty()檢測屬性若是在對象實例中時,返回true。如:

console.log(person1.hasOwnProperty("age")); //false age屬性來自於原型
console.log(person1.hasOwnProperty("name")); //true name屬性來自於實例

原型與in 操做符

兩種方法使用in 操做符:單獨使用和for-in 循環中使用。

單獨使用時,in 返回true 說明該屬性存在於實例或原型中。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.name = "Troy";
person1.sayName(); //Troy 實例中的Name

console.log("name" in person1); //true name屬性在實例或原型中
console.log(person1.hasOwnProperty("name")); //true name屬性在實例中
//上面兩個就說明name屬性必定在實例中

又如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.name = "Troy";
person1.sayName(); //Troy 實例中的Name

var person2 = new Person();

console.log("name" in person1); //true name屬性在實例或原型中
console.log(person1.hasOwnProperty("name")); //true name屬性在實例中
//上面兩個就說明name屬性必定在實例中

console.log("name" in person2); //true
console.log(person2.hasOwnProperty("name")); //false
//上面兩個說明name屬性必定在原型中

自定義一個函數hasPrototypeProperty(object,name);即同時使用上面兩個方法來肯定屬性究竟是不是存在於實例中。如:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.age = 18;
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
person1.name = "Troy";
person1.sayName(); //Troy 實例中的Name

var person2 = new Person();

function hasPrototypeProperty(object,name){
    console.log((name in object) && !(object.hasOwnProperty(name)))
}

hasPrototypeProperty(person2,"name"); //true name屬性是在原型中
hasPrototypeProperty(person1,"name"); //false name屬性是在實例中

Object.defineProperty()方法定義的屬性:

function Person(){};
Person.prototype.name = "Oliver";
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
Object.defineProperty(person1, "age", {
    value: 18
})

console.log(person1.hasOwnProperty("age")); //true age屬性是實例屬性

關於for-in、[[enumerable]]、defineProperty、hasOwnProperty 的例子:

var person1 = {
    age: 18
};
Object.defineProperty(person1, "name", {
    value: "Oliver",
    enumerable: true
})
for(var x in person1){
    console.log(x);
}
console.log(person1.hasOwnProperty("name")); //true

又如:

function Person(age){
    this.age = age;
}
var person1 = new Person(18);
Object.defineProperty(person1, "name", {
    value: "Oliver",
    enumerable: false
})
for(var x in person1){
    console.log(x); //用defineProperty 定義的name 屬性是實例屬性,這裏不會枚舉到
}
console.log(person1.hasOwnProperty("name")); //true

又如:

function Person(){};
Person.prototype.age = 18;
var person1 = new Person();
Object.defineProperty(person1, "name", {
    value: "Oliver",
    enumerable: false
})
for(x in person1){
    console.log(x); //這裏仍然不回枚舉到自定義的name 實例屬性
}

可是:

function Person(){};
Person.prototype.age = 18;
Person.prototype.name = "Oliver";
var person1 = new Person();
Object.defineProperty(person1, "name", {
    enumerable: false
})
for(x in person1){
    console.log(x); //這裏則返回枚舉到自定義的name 原型屬性
}

原型屬性的[[enumerable]]設置爲false,調用for-in 仍然能夠被枚舉到。

另外,Object.keys()方法能夠返回全部可枚舉屬性的字符串數組:

function Person(){};
Person.prototype.age = 18;
Person.prototype.name = "Oliver";

var person1 = new Person();
Object.defineProperty(person1, "sound", {
    value: "miao~",
    enumerable: true //可枚舉
});
Object.defineProperty(person1, "sound2", {
    value: "wang~",
    enumerable: false //不可枚舉
});

console.log(Object.keys(Person.prototype)); //["age", "name"]
console.log(Object.keys(person1)); //["sound"]

Object.getOwnPropertyName()方法,則能夠返回不管能否枚舉的全部實例屬性:

function Person(){};
Person.prototype.age = 18;
Person.prototype.name = "Oliver";

var person1 = new Person();
Object.defineProperty(person1, "sound", {
    value: "miao~",
    enumerable: true //可枚舉
});
Object.defineProperty(person1, "sound2", {
    value: "wang~",
    enumerable: false //不可枚舉
});

console.log(Object.keys(Person.prototype)); //["age", "name"]
console.log(Object.keys(person1)); //["sound"]
console.log(Object.getOwnPropertyNames(Person.prototype)); //["constructor", "age", "name"]
console.log(Object.getOwnPropertyNames(person1)); //["sound","sound2"]

更簡單的原型語法

即字面量方法:

function Person(){};
Person.prototype = {
    name: "Oliver",
    age: 18,
    sayName: function(){
        console.log(this.name);
    }
};

var person1 = new Person();
console.log(Person.prototype.constructor); //再也不指向Person()構造函數

function People(){};
People.prototype.name = "Troy";
People.prototype.age = 26;
People.prototype.sayName = function(){
    console.log(this.name);
};

var people1 = new People();
console.log(People.prototype.constructor); //這裏則指向People()構造函數

上面第一種就是字面量方法。可是由此帶來的問題是,他的原型對象中的constructor 屬性將再也不指向上個例子中的Person() 構造函數了。(其實咱們本質上是重寫了prototype對象)

若是constructor 值真的很是重要,則只須要把它設置回適當的值就能夠了:

function Person(){};
Person.prototype = {
    constructor: Person,
    name: "Oliver",
    age: 18,
    sayName: function(){
        console.log(this.name);
    }
};

var person1 = new Person();
console.log(Person.prototype.constructor); //從新指向Person()構造函數

function People(){};
People.prototype.name = "Troy";
People.prototype.age = 26;
People.prototype.sayName = function(){
    console.log(this.name);
};

var people1 = new People();
console.log(People.prototype.constructor); //這裏則指向People()構造函數

然而用字面量的方法致使的問題仍然沒有結束,以上面這種方式重設constructor 屬性會致使[[Enumerable]]特性被設置爲true。所以在支持ECMAScript 5 的js 引擎中能夠用Object.defineProperty()方法把它修改成false:

function Person(){};
Person.prototype = {
    constructor: Person,
    name: "Oliver",
    age: 18,
    sayName: function(){
        console.log(this.name);
    }
};

var person1 = new Person();
console.log(Person.prototype.constructor);
for (var x in person1){
    console.log(x); //這裏會出現constructor,可是咱們實際上不該該讓他可以被枚舉出
}

Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false
});

for (var x in person1){
    console.log(x); //這裏就不會出現constructor 了,可是這種方法只支持ECMAScript 5的js 引擎
}

/*
[Log] function Person() {} (repetition.html, line 130)
[Log] constructor (repetition.html, line 132)
[Log] name (repetition.html, line 132)
[Log] age (repetition.html, line 132)
[Log] sayName (repetition.html, line 132)
[Log] name (repetition.html, line 140)
[Log] age (repetition.html, line 140)
[Log] sayName (repetition.html, line 140)
*/

原型的動態性

咱們對原型對象所作的任何修改都能當即從實例上反應出來。由於實例與原型之間的連接只不過是一個指針而不是副本:

function Person(){};
var person = new Person(); //person在Person()構造函數修改以前建立的
Person.prototype.name = "Oliver";
console.log(person.name); //仍然會出現實時的變化

可是若是重寫了prototype 則就不一樣了,由於實例的[[Prototype]]會指向原型對象,若是修改了原來的原型對象,則就是切斷了構造函數與最初原型之間的聯繫:

function Person(){};
var person = new Person();

Person.prototype = { //這裏重寫了Person.prototype,屬於新的Person.prototype
    constructor: Person,
    name: "Oliver"
}

console.log(person.name); //原型對象被修改了,指針仍然指向舊的Person.prototype

從這裏就能夠很明顯看出,Person.prototype={},實際上字面量方法就是重寫了原型對象。若是是Person.prototype.name="Oliver",則並非重寫而是修改,不會建立「新的原型對象。」

《js高級程序設計》一書中6.2.3 中的圖6-3 很清楚的描述了該原理

原生對象的原型

全部原生的引用類型(Object、Array、String等等)都在其構造函數的原型上定義了方法。同時,咱們也能夠給原生對象自定義方法:

var array = new Array();
Array.prototype.name = function(){
    console.log("Array")
};

array.push("hello ","there");
console.log(array);

array.name();

也能夠修改:

var array = new Array();
Array.prototype.toString = function(){
    return("Array")
};

array.push("hello ","there");
console.log(array.toString());
//這樣就抹去了toString()方法

強烈不推薦修改和重寫原生對象的原型

原型對象的問題

就是包含引用類型值的屬性來講,問題比較嚴重。具體體如今原型中的屬性被實例共享:

function Person(){};
Person.prototype = {
    constructor: Person,
    name: "Oliver",
    age: 18,
    friends: ["Troy","Alice"]
}

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Ellen");
console.log(person1.friends); //["Troy", "Alice", "Ellen"]
console.log(person2.friends); //["Troy", "Alice", "Ellen"]
//二者徹底相同,由於原型中的該屬性被實例所共享,push()方法只是修改了person1.friends,沒有重寫

person1.friends = ["Troy", "Alice"];
console.log(person1.friends); //["Troy", "Alice"]
console.log(person2.friends); //["Troy", "Alice", "Ellen"]
//雖然能夠經過重寫和覆蓋來解決該問題,可是仍然很是麻煩

console.log(person1.hasOwnProperty("friends")); //true;
console.log(person2.hasOwnProperty("friends")); //false;
//這裏就能夠看到,重寫能解決問題是由於重寫致使它建立了實例屬性"friends"

這裏能夠看出,若是咱們的初衷像這樣只是想建立一個共享的數組,那麼固然不會有什麼問題;可是實例通常都會有本身的屬性,因此不該該單獨使用原型模式。而是組合使用構造函數模式和原型模式。

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

這是一種用來定義引用類型的一種默認模式:

function Person(name,age){
    this.name = name;
    this.age = age;
    this.friends = [];
} //獨享的部分
Person.prototype = {
    constructor: Person,
    sayName: function(){
        return this.name;
    }
} //共享的部分
var person1 = new Person("Oliver",18);
var person2 = new Person("Troy",24);
person1.friends.push("Alice","Mark");
person2.friends.push("Mac");
console.log(person1.friends.toString());
console.log(person2.friends.toString());
/*
[Log] Alice,Mark (repetition.html, line 228)
[Log] Mac (repetition.html, line 229)
*/

動態原型模式

能夠經過檢查某個應該存在的方法是否有效,來決定是否須要初始化原型:

function Person(name,age){
    this.name = name;
    this.age = age;
    if (typeof this.sayName != "function"){
        Person.prototype.sayName = function(){
            return (this.name);
        };
    }
}
var person = new Person("Oliver",18);
console.log(person.sayName()); //Oliver

實際上就是把下面代碼封裝在了構造函數中:

function Person(name,age){
    this.name = name;
    this.age = age;
}
Person.prototype.sayName = function(){
    return(this.name);
};
var person = new Person("Troy",24);
console.log(person.sayName()); //Troy

寄生構造函數模式

世紀撒好難過跟工廠模式同樣。建議在可使用其餘模式的狀況下,不要使用該模式。

穩妥構造函數模式

穩妥對象,指的是沒有公共屬性,且其方法也不引用this 的對象如:

function Person(name,age){
    var obj = new Object();
    obj.sayName = function(){
        console.log(name);
    };
    return obj;
}
var person1 = Person("Oliver",18);
person1.sayName(); //Oliver
相關文章
相關標籤/搜索