JavaScript的OO思想(一)

類class是Object-Oriented面向對象的語言有一個標誌,通過類我們可以創建任意多個具有相同屬性和方法的對象。JavaScript中沒有類的概念,但它也是面向對象的,只是實現方法會有所不同。

創建單個對象有兩種基本方法:

1.使用Object的構造函數創建實例然後在實例上添加屬性和方法;
2.使用對象字面量的方法。

在簡單的場景這兩種方法是很實用的,但如果遇到有很多對象有相同的屬性,相同的方法的時候,這兩種方法的弊端就暴露出來了,會產生大量的重複代碼。

爲了應對不同的場景和方法,創建對象有以下幾種方法。

1、工廠模式。所謂的工廠模式指的是使用函數封裝利用Object的構造函數創建實例再添加屬性和方法的步驟,然後返回該實例。這樣每次運行這個方法我們都會得到一個結構相同的對象。

function createPerson(name, age){
    var person = new Object();
    person.name = name;
    person.age = age;
    person.sayHi = function () {
        alert("My name is " + person.name + ", I'm " + person.age);
    }

    return person;
}

var liLei = createPerson("LiLei", 12);
liLei.sayHi();

這個模式雖然解決了重複代碼的問題,但是創建出的對象都是Object類型,沒有解決對象識別問題

2、構造函數模式。利用構造函數模式可以解決對象是別問題。改造上面的代碼可得:

function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayHi = function () {
        alert("My name is " + this.name + ", I'm " + this.age);
    }
}

var liLei = new Person("LiLei", 12);
liLei.sayHi();

對比工程模式我們可以看到以下3處不同:

1、沒有顯示創建對象
2、直接將屬性和方法賦值給了this對象
3、沒有return

使用構造函數會經歷一下4個步驟:

1、創建一個新的對象
2、將構造函數的作用域賦值給新的對象,這樣this就指向了新的對象
3、執行構造函數中的代碼(爲這個新對象添加屬性或方法)
4、返回新對象

所以上面的的形式就如同以下所示一般:

var hanMeimei = new Object();
Person.call(hanMeimei, "Han Meimei", 12);
hanMeimei.sayHi();

然而構造函數模式並非是完美的,就比如Person的sayHi方法,我們每次創建一個新的實例的時候,都會創建一個新的sayHi併爲之開闢新空間,儘管這個sayHi方法做的事是一模一樣的

var liLei = new Person("LiLei", 12);
var hanMeimei = new Person("Han MeiMei", 12);
alert(liLei.sayHi == hanMeimei.sayHi); //false

創建多個相同的方法確實沒有必要。雖然我們可以像下面的例子一樣定義一個全局的函數來解決這個問題,但是由此引發的問題就是,如果我們有多個方法,就需要定義多個全局函數了,這不僅會破壞自定義引用類型的封裝性,還會使全局作用域混亂,是一種很不安全的做法。

function Person(name, age){
    this.name = name;
    this.age = age;
    this.sayHi = sayHi;
}

function sayHi () {
    alert("My name is " + this.name + ", I'm " + this.age);
}

3、原型模式。爲了解決構造函數的重複定義相同的方法的問題,原型模式便出現了。

首先我們得理解什麼是原型,原型即prototype,它是一個指針,指向一個對象,這個對象的用途就是包含特定類型的所有示例共享的屬性和方法,我們創建的每一個函數都會有原型(prototype)屬性。使用原型的好處是,我們將共享的屬性或方法賦值給原型對象,該類型的所有實例都可以訪問該屬性或方法。

function Person(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.sayHi = function () {
    alert("My name is " + this.name + ", I'm " + this.age);
}

var liLei = new Person("Li Lei", 12);
liLei.sayHi();//My name is Li Lei, I'm 12
var hanMeimei = new Person("Han MeiMei", 12);
hanMeimei.sayHi();//My name is Han MeiMei, I'm 12
alert(liLei.sayHi == hanMeimei.sayHi); //true

關於原型對象有以下3點需要注意:

(1)、如果我們在實例中添加一個屬性,該屬性與原型對象中的屬性同名的話,處理的結果是原型中的屬性被屏蔽了,記住是被屏蔽了而不是重寫了,因爲如果這個時候再刪除實例中的這個屬性的話,再訪問這個值就會訪問原型中的同名屬性,並且該值爲原型初始化時的值,一直未改過。

function Dog(age){
    this.age = age;
}

Dog.prototype.name = "Little White";

var littleWhite = new Dog();
alert(littleWhite.name); // Little White;
littleWhite.name = "Xiao Bai"; 
alert(littleWhite.name); //Xiao Bai
delete littleWhite.name;
alert(littleWhite.name); // Little White;

(2)、原型對象中的引用屬性在其中一個實例裏被改變,則所有的實例獲取該屬性都將得到新的值。

function MyToy(){

}

MyToy.prototype.group = ["Dingding", "Dingxi", "Lala"];

var toy1 = new MyToy();
var toy2 = new MyToy();
toy1.group.push("Po");
alert(toy2.group); // Dingding,Dingxi,Lala,Po

如上上述例子所示,我們通過實例toy1修改了原型對象的屬性group,toy2去獲取的時候值已經發生了變化。

(3)、初始化一個實例後,再給函數的原型對象重新賦值,已創建的實例與新的原型對象不會有聯繫,也就是說已創建的實例不能訪問新原型對象的方法和屬性。如下所示:

var toy = new MyToy();

MyToy.prototype = {
    group: ["Dingding", "Dingxi", "Lala"]
};

alert(toy.group); // undefined

以上三點也可以很好的幫助我們理解原型prototype,它是一枚指針,默認指向一個Object對象,在創建函數時會相應爲其初始化一枚這樣的指針,當使用該函數new出一個新的實例時,該指針又會賦值給new出的實例。所以在new出一個新實例後,再給函數的原型對象賦值個新的對象是,函數的原型指針指向了新的對象,但是此時的實例的原型的指針指向的還是舊的對象。
這裏寫圖片描述

如果單單只是用原型模式的話,事實上並不能滿足所有的需求。原型模式也是沒有缺點,主要體現在以下兩點:

1、省去了構造函數傳參
2、所有的實例共享原型的屬性,這個是非常可怕的

4、組合使用構造函數和原型模式。單獨使用原型模式會有弊端,但和其他模式組合起來使用的話,有些問題就迎刃而解了。典型的就是組合使用構造函數和原型模式,它是目前ECMAScript中,認同度最高,使用最廣泛的一種創建自定義類型的方法。

function MyToy(owner){
    this.owner = owner;
    this.group = ["Dingding", "Dingxi", "Lala"];
}

MyToy.prototype.sayOwner = function(){
    alert(this.owner);
};


var lileis = new MyToy("Li Lei");
var hanMeimeis = new MyToy("Han Meimei");
lileis.group.push("Po");
alert(lileis.group);//Dingding,Dingxi,Lala,Po
alert(hanMeimeis.group);//Dingding,Dingxi,Lala
alert(lileis.group == hanMeimeis.group); // false
alert(lileis.sayOwner == hanMeimeis.sayOwner); // true

5、動態原型模式。有其它OO語言開發經驗的朋友看到獨立的函數和獨立的原型的時候會有點困惑,甚至難以理解。例如在C#類中,方法和屬性是一個整體,是一起定義的

public class MyToy
{
    public string owner { get; set;}
    public string[] group { get; set; }

    public void sayOwner()
    {
        //...
    }
}

當然JS裏也可以變成這樣,只是方法有所不同。

function MyToy(owner){
    this.owner = owner;
    this.group = ["Dingding", "Dingxi", "Lala"];

    if(typeof this.sayOwner != "function") {
        MyToy.prototype.sayOwner = function(){
            alert(this.owner);
        };
    }
}

var liLei = new MyToy("Li Lei");
liLei.sayOwner();//Li Lei

這便是動態原型模式,當你需要同時定義很多個原型函數的時候,不需要爲每一個函數都做一次if判斷,僅需要判斷一個就可以了,然後把所有的定義都放在一個if語句中。

6、寄生構造函數模式。當我們想封裝JS中的引用對象時,可能以上的方式都不適用,這個時候我們就可以使用這個所謂的寄生構造函數模式了。

function MyToy(){
    // Create Array
    var arr = new Array();

    // Add values
    arr.push.apply(arr, arguments);

    // Add method
    arr.toPipedString = function(){
        return this.join("-");
    }

    return arr;
}

var toys = new MyToy("Dingding", "Dingxi", "Lala");
alert(toys.toPipedString()); // Dingding-Dingxi-Lala
alert(toys instanceof MyToy); // false
alert(toys instanceof Array); // true

我們可以注意到此時toys instanceof MyToy的值是false,這主要是因爲構造函數返回的對象不是MyToy類型的實例,而由於在構造函數中出現了return arr,所以得到的arr,是一個Array類型的對象。

7、穩妥構造函數模式。所謂的穩妥模式,即使在函數體內不訪問this,沒有公共屬性,變量私有化,這種模式比較適合在安全的環境中或者不想數據被第三方的應用程序改動時使用。

function MyToy (name) {
    var obj = new Object();
    obj.sayName = function(){
        alert(name);
    }
    return obj;
}

var dingding = MyToy("Ding ding");
dingding.sayName();

在這個創建的對象中除了調用sayName方法,是沒有其它辦法可以訪問變量name的。

看起來它和寄生構造函數很相似,都不能使用instanceof來判斷創建的對象的類型,不過它們有以下兩點是不同的。

1、不使用new操作符來調用構造函數
2、在方法體內不引用this

以上便是在JavaScript中創建對象7種常見方法,每個方法都有利有弊,最重要的還是在合適的場景中使用合適的方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章