JavaScript -- 面向?qū)ο?/h2>

1. 創(chuàng)建對(duì)象

創(chuàng)建定義對(duì)象的最簡(jiǎn)單方式就是創(chuàng)建一個(gè) Object 實(shí)例,然后為其添加屬性和方法。

        var person = new Object();
        person.name = "Hwaphon";
        person.age = 19;
        person.sayName = function() {
            console.log(this.name);
        }

當(dāng)然,我們還可以通過一個(gè)對(duì)象字面量創(chuàng)建對(duì)象,事實(shí)上這種方式也是使用最多的。

        var person = {
            name: "Hwaphon",
            age: 19,
            sayName: function() {
                console.log(this.name);
            }
        };

雖然 Object 構(gòu)造函數(shù)或?qū)ο笞置媪慷伎梢詣?chuàng)建單個(gè)對(duì)象,但是這兩種方式存在明顯的缺點(diǎn): 使用同一接口創(chuàng)建很多對(duì)象,會(huì)產(chǎn)生大量重復(fù)的代碼。為了解決這個(gè)問題,人們開始用工廠模式的一種變體。下面就用類工廠模式解決這種問題。

        function createPerson(name, age) {
            var o = new Object();
            o.name = name;
            o.age = age;
            o.sayName = function() {
                console.log(this.name);
            };

            return o;
        }

可以頻繁的調(diào)用上面這個(gè)函數(shù)以創(chuàng)造不同的對(duì)象,但是這種方法仍然存在一個(gè)問題,就是對(duì)象識(shí)別問題,即我們無法判斷創(chuàng)建出來的對(duì)象是 Person, 我們能判斷的就是它屬于 Object 類型而已。所以這個(gè)時(shí)候又出現(xiàn)了一種創(chuàng)建對(duì)象的方式,就是構(gòu)造函數(shù)模式。

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

        var person = new Person("Hwaphon", 21);
        person.sayName();

這個(gè)時(shí)候我們就可以利用 instanceof 運(yùn)算符檢測(cè)我們創(chuàng)建的對(duì)象類型。

        console.log(person instanceof Object);  // true
        console.log(person instanceof Person);  // true

我們通過 new 操作符去創(chuàng)建一個(gè)對(duì)象,這也就意味著每個(gè)對(duì)象是相互獨(dú)立的,當(dāng)然也包括 sayName 方法, 但是這個(gè)方法在每個(gè)實(shí)例中實(shí)現(xiàn)的功能是相同的,實(shí)在沒必要每創(chuàng)建一個(gè)對(duì)象實(shí)例就創(chuàng)建一個(gè) Function 實(shí)例。我們通過以下代碼可以看出這種問題。

        var person1 = new Person("Hwaphon", 21);
        var person2 = new Person("Hello", 22);

        console.log(person1.sayName == person2.sayName);    // false

如果是這樣的話,我們只能將函數(shù)放置到構(gòu)造函數(shù)外去解決這個(gè)問題。

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

        function sayName() {
            console.log(this.name);
        }

        var person1 = new Person("Hwaphon", 21);
        person1.sayName();

雖說這種方法能解決上述問題,但是又引入了新的問題,因?yàn)檫@種方式引入了全局的 sayName 函數(shù)。更讓人無法接受的是,如果對(duì)象需要定義很多方法,那么就要定義多個(gè)全局函數(shù),這樣我們自定義的引用類型就絲毫沒有封裝性可言了。所以不得不引入原型模式來解決這個(gè)問題。

2. 原型模式

我們創(chuàng)建的每個(gè)函數(shù)都有一個(gè) prototype 屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,而這個(gè)對(duì)象的用途是包含可以由特定類型的所有實(shí)例共享的屬性和方法。使用原型的好處是可以讓所有的對(duì)象實(shí)例共享它保函的屬性和方法。所以為了解決上面的問題,我們可以像下面這樣定義。

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

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

        console.log(person1.sayName == person2.sayName);    // true

通過 hasOwnProperty() 方法可以檢測(cè)實(shí)例屬性,而非原型屬性,通過 in 不僅可以檢測(cè)實(shí)例屬性也可以檢測(cè)原型屬性。

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

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

        var person = new Person("Hwaphon", 21);
        if (person.hasOwnProperty("sayName")) {
            console.log("true");
        }

        if ("sayName" in person) {
            console.log("true");
        }

當(dāng)然,如果你想在原型添加多個(gè)函數(shù),那么可以將這些方法組成一個(gè)對(duì)象字面量。

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

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

這個(gè)時(shí)候存在一個(gè)問題,因?yàn)檫@個(gè)時(shí)候 constructor 不再指向 Person 了,所以我們可以手動(dòng)設(shè)置 constructor。

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

上面我們將原型和構(gòu)造函數(shù)分開了,這可能會(huì)讓人感到困惑,所以可以使用動(dòng)態(tài)原型模式,將所有信息封裝在構(gòu)造函數(shù)中。

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

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

    var person = new Person("Hwaphon", 21);
    person.sayName();

只有在 sayName() 方法不存在的情況下才會(huì)將它添加進(jìn)原型中,所以 if 判斷語句只有在初次調(diào)用構(gòu)造函數(shù)時(shí)才會(huì)執(zhí)行。

除了上面的方法以外,還有一種寄生構(gòu)造函數(shù)模式,這種模式的基本思想是創(chuàng)建一個(gè)函數(shù),該函數(shù)的作用僅僅是封裝創(chuàng)建對(duì)象的過程,然后將新創(chuàng)建的對(duì)象返回。比如,這種模式創(chuàng)建一個(gè)對(duì)象的過程可能如下所示:

        function Person(name, age) {
            var o = new Object();
            o.name = name;
            o.age = age;
            o.sayName = function() {
                console.log(this.name);
            };

            return o;
        }

        var person = new Person("hwaphon", 21);
        person.sayName();

那么這種方式的優(yōu)點(diǎn)在哪里呢?正如它名稱中的 “寄生” 所說的,這種模式可以寄生在一個(gè)指定的對(duì)象中,也就是說它可以為一個(gè)已經(jīng)存在的對(duì)象增加功能。比如說,對(duì)于自帶的 Array 對(duì)象,如果不是迫不得已,一般是不應(yīng)該直接使用 Array.prototype 擴(kuò)充原生數(shù)組對(duì)象的,所以這時(shí)候寄生構(gòu)造模式可以發(fā)揮它的作用。

        function SpecialArray() {
            var array = new Array();

            array.push.apply(array, arguments);
            array.formatArray = function() {
                return this.join("->");
            }

            return array;
        }

        var array = new SpecialArray(1,2,3,4),
            result = array.formatArray();

        console.log(result);

可見,當(dāng)你想要擴(kuò)充一個(gè)對(duì)象的功能而又不想直接修改原本的對(duì)象時(shí),可以使用寄生構(gòu)造函數(shù)模式,這和設(shè)計(jì)模式中的裝飾者模式倒是很像。

還有一個(gè)稱為穩(wěn)妥構(gòu)造方式的創(chuàng)建方法,當(dāng)你不想共享任何變量而且環(huán)境不允許使用 this, new 的情況下,可以選擇這種方式。

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

            o.sayName = function() {
                console.log(name);
            };

            o.sayAge = function() {
                console.log(age);
            };

            return o;
        }

        var person = Person("Hwaphon", 33);
        person.sayName();
        person.sayAge();

值得注意的是,這個(gè)時(shí)候只有通過 sayName() 和 sayAge() 才可以訪問到 nameage 屬性,而之前介紹的方法都在要返回的對(duì)象中設(shè)置了屬性,而這種方式依賴于直接傳入的參數(shù),所以返回的對(duì)象中是不包含 nameage 屬性的,這倒是滿足了封裝的特性。


3. 復(fù)制對(duì)象

還有一個(gè)常見的問題就是如何復(fù)制一個(gè)對(duì)象,看下面這個(gè)例子:

        function anotherFunction() {}

        var anotherObject = {
            c: true
        };

        var anotherArray = [];

        var myObject = {
            a: 2,
            b: anotherObject,
            c: anotherFunction,
            d: anotherArray
        };

如果我現(xiàn)在想復(fù)制 myObject 對(duì)象,可以看出只有 a 是一個(gè)基礎(chǔ)類型的值,其它 b, c, d 全是對(duì)象類型的值,根據(jù)從其它高級(jí)語言中的到的經(jīng)驗(yàn),對(duì)于基礎(chǔ)類型的值默認(rèn)是復(fù)制其值,而對(duì)于對(duì)象類型,則復(fù)制的是引用。

JavaScript 中自然也是這樣的,涉及淺復(fù)制和深復(fù)制。首先我們先介紹淺復(fù)制,因?yàn)?ES6 中定義了一個(gè) Object.assign() 方法,我們可以通過這個(gè)方法輕松地實(shí)現(xiàn)淺復(fù)制。

Object.assign() 第一個(gè)參數(shù)是目標(biāo)對(duì)象,之后可以跟多個(gè)源對(duì)象,它會(huì)遍歷一個(gè)或者多個(gè)源對(duì)象所有可枚舉的自有鍵并把他們復(fù)制到目標(biāo)對(duì)象,最后返回目標(biāo)對(duì)象。

        var newObj = Object.assign({}, myObject);
        console.log(newObj.a);
        console.log(newObj.b === myObject.b);   // true
        console.log(newObj.c === myObject.c);   // true
        console.log(newObj.d === myObject.d);   // true

可見,newObjmyObj 的對(duì)象比較都返回了 true, 這說明其實(shí)二者都是一個(gè)引用,指向同一個(gè)函數(shù)對(duì)象。

下面介紹的深復(fù)制只適用于 JSON 安全的對(duì)象,因?yàn)槲覀兊纳顝?fù)制是借助 JSON 來實(shí)現(xiàn)的。

        var newObj = JSON.parse(JSON.stringify(myObject));
        console.log(newObj.a);
        console.log(newObj.b === myObject.b);   // false
        console.log(newObj.c === myObject.c);   // false
        console.log(newObj.d === myObject.d);   // false

4. 屬性描述符

自從 ES5 開始,所有的屬性都具備了屬性描述符,下面讓我們來一一介紹。

  • Writable - 決定是否可以修改屬性的值。

          var myObjet = {};
    
          Object.defineProperty(myObjet, "a", {
              value: 2,
              writable: false,
              configurable: true,
              enumerable: true
          });
    
          console.log(myObjet.a); // 2
    
          myObjet.a = 4;
          console.log(myObjet.a); // 2
    

可見,如果將 writable 的值為 false 的時(shí)候,那么修改對(duì)象屬性的值將會(huì)失效(在嚴(yán)格模式下會(huì) TypeError)。

  • Configurable - 控制是否可以修改屬性描述符

          var myObjet = {};
    
          Object.defineProperty(myObjet, "a", {
              value: 2,
              writable: true,
              configurable: false,
              enumerable: true
          });
    
          console.log(myObjet.a); // 2
    
          delete myObjet.a;
          console.log(myObjet.a); // 2
    

從上面的例子可以看出,我們對(duì)屬性的刪除已經(jīng)失效了,另外值得注意的是,一旦將 configurable 設(shè)置為 false, 我們就不可以再利用 defineProperty() 再去修改屬性的 writable, configurable, enumerable 描述符。好吧,我承認(rèn)還有一個(gè)例外,這時(shí)候還是可以將 writrabletrue 設(shè)置為 false,但是不可以在改為 true 。

  • Enumerable - 設(shè)置屬性是否支持枚舉

一旦將 Enumerable 設(shè)置為 false, 雖然它還是可以正常訪問,但是它將不會(huì)出現(xiàn)在對(duì)象的屬性枚舉中。更具體的說,當(dāng)你使用 for...in 遍歷對(duì)象的屬性時(shí),設(shè)置為 false 的屬性會(huì)被直接跳過。

看到這里,你可能會(huì)問了,在對(duì)象中設(shè)置一個(gè)屬性有這么麻煩嗎?我平常的時(shí)候好像根本沒接觸過上面提到的這幾個(gè)屬性,那是因?yàn)樗鼈兌加幸粋€(gè)默認(rèn)的設(shè)置,怎么查看呢?看下面這個(gè)例子:

        var myObjet = {
            a: 2
        };

        var despritor = Object.getOwnPropertyDescriptor(myObjet, "a");
        console.log(despritor);

它的返回值如下所示

        configurable: true
        enumerable:true
        value:2
        writable:true

有的時(shí)候,我們希望自己定義的屬性或者對(duì)象是不可改變的,那么改怎么做呢?比如說我想設(shè)置定義一個(gè)常量?

我們可以將 writableconfigurable 都設(shè)置為 false, 這樣我們就可以輕松地創(chuàng)建一個(gè)真正意義上的常量。

        var myObjet = {};

        Object.defineProperty(myObjet, "PI", {
            value: 3.1415926,
            writable: false,
            configurable: false
        });

下面來看幾個(gè)實(shí)用的函數(shù)。

  • 禁止拓展 - 如果你想禁止一個(gè)對(duì)象添加屬性并且保留已有屬性,可以使用 Object.preventExtensions()

          var myObject = {
              a: 2
          };
    
          Object.preventExtensions(myObject);
          console.log(myObject.a);    // 2
    
          myObject.b = 10;
          console.log(myObject.b);    // undefined
    
  • 密封 - 密封也就是說在禁止拓展的基礎(chǔ)上, 將 configurable 也設(shè)置為 false,將對(duì)象密封的方法為 Object.seal()。

  • 凍結(jié) - 凍結(jié)就是在密封的基礎(chǔ)上將 writable 也設(shè)置為 false, 這意味著連對(duì)象中屬性的值也無法修改了,這個(gè)方法用于定義那種一旦定義了就不用在改動(dòng)的對(duì)象。將對(duì)象凍結(jié)的方法為 Object.freeze()。


5. 實(shí)例

構(gòu)造函數(shù),實(shí)例以及原型的關(guān)系基本如下所示:

  1. 對(duì)于構(gòu)造函數(shù)而言,它內(nèi)部有一個(gè) prototype 屬性,這個(gè)屬性直接指向原型,而且通過此構(gòu)造函數(shù)創(chuàng)建的實(shí)例,將默認(rèn)指向該原型,這也就意味著原型中的方法在實(shí)例中也是可用的。

  2. 在實(shí)例中,我們不可以通過 prototype 訪問原型,不過可以通過 __proto__ 訪問該原型。

  3. 在原型中,有一個(gè) constructor 屬性,這個(gè)屬性默認(rèn)指向構(gòu)造函數(shù),而且原型中也有一個(gè) __proto__ 屬性,這個(gè)屬性指向 Object,所以我們可以說,所有的對(duì)象都是繼承自 Object 的。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容