面向對象 面向你(一)

關於 JS 中的對象,咱們能夠理解爲是一組名值對,其中值能夠是數據或函數的散列表。

每一個對象都是基於一個引用類型建立, 這個引用類型能夠是原生類型,如 Object 類型Array 類型Date 類型RegExp 類型基本包裝類型,也能夠是 自定義類型javascript

理解對象java

ok,首先咱們來看下用 對象字面量 的方式建立自定義對象:數組

var person = {
 name: 'Fly_001',
 age: 22,
 sayName: function() {
     alert(this.name);
 }
};
複製代碼

其中這些屬性在建立的時候都帶有一些 特徵值 ,JavaScript 經過這些 特徵值 來定義它們的行爲。函數

ECMAScript 定義只有內部才使用的特性,描述了屬性的各類特徵,爲了表示特性是內部值,該規範把它們放在了兩對方括號裏,例如 [[Enumerable]],而且在 JavaScript 中不能直接訪問它們。ui

ECMAScript 中有兩種屬性,數據屬性訪問器屬性this

  • 數據屬性spa

    數據屬性有 4 個描述其行爲的特性:prototype

    名稱 描述
    [[Configurable]] 表示可否經過 delete 刪除屬性從而從新定義屬性,默認爲 true。
    [[Enumerable]] 表示可否經過 for-in 循環 ♻️ 返回屬性,默認爲 true。
    [[Writable]] 表示可否修改屬性的值,默認爲 true。
    [[Value]] 包含這個屬性的數據值。讀取和寫入屬性值都是在這個位置,默認值爲 undefined。

    要修改屬性的特性,可以使用 Object.defineProperty() 方法 ( 話說 Vue 就是經過這個方法實現雙向數據綁定的 ),該方法接受三個參數:屬性所在的對象、屬性的名字和一個描述符對象,其中描述符對象必須是 configurableenumerablewritablevalue 的一個或多個:3d

    var person = {};
    Object.defineProperty(person, 'name', {
        writable: false,
        value: 'Fly_001'
    });
    
    alert(person.name); // 'Fly_001';
    
    person.name = 'juejin'; // 修改 person 的 name 屬性;
    alert(person.name); // 'Fly_001';
    複製代碼

    上面代碼建立了一個 name 屬性,它的值是隻讀、不可修改的,若是嘗試爲它指定新值將會被忽略。( 在嚴格模式下會拋出 Cannot assign to read only property 'name' of object 的錯誤 )指針

    另外要注意的是若是把 configurable 設置爲 false,就不能從對象中刪除屬性,同時也不能再把它變回可配置了( 此時調用 Object.defineProperty() 方法只能修改 writable 特性 )

    多數狀況下可能用不到 Object.defineProperty() 方法提供的這些高級功能,不過理解這些概念對咱們理解 JavaScript 對象很是有用。

  • 訪問器屬性

    訪問器屬性包含一對 getter 和 setter 函數,有以下 4 個特性:

    特性名 描述
    [[Configurable]] 表示可否經過 delete 刪除屬性從而從新定義屬性、可否修改屬性的特性。
    [[Enumerable]] 表示可否經過 for-in 循環 ♻️ 返回屬性。
    [[Get]] 在讀取屬性時調用的函數,默認值爲 undefined。
    [[Set]] 在寫入屬性時調用的函數,默認值爲 undefined。

    一樣滴~ 訪問器屬性不能直接定義,必須使用 Object.defineProperty() 來定義:

    var book = {
        _year = 2018,
        edition: 1
    }
    
    Object.defineProperty(book, 'year', {
        get: function() {
            return this._year;
        },
        set: function(newYear) {
            if (newYear > 2018) {
                this._year = newYear;
                this.edition += newYear - 2018;
            }
        }
    });
    
    book.year = 2020;
    alert(book.edition); // 3;
    複製代碼

    _year 前面的下劃線是一種經常使用的記號,表示只能經過對象方法訪問的屬性。

    另外,不必定要同時指定 getter 和 setter。

    只指定 getter 意味着屬性是不可寫🙅,而只指定 setter 意味着不可讀 🙅~

定義多個屬性

因爲爲對象定義多個屬性的可能性很大,ECMAScript 又定義了一個 Object.defineProperties() 方法,能夠一次定義多個屬性,這個方法接受兩個對象參數:目標對象要添加或修改的屬性

var book = {};

Object.defineProerties(book, {
    _year: {
        value: 2018
    },
    edition: {
        value: 1
    },
    year: {
        get: function() {
            return this._year;
        },
        set: function(newYear) {
            if (newYear > 2018) {
                this._year = newYear;
                this.edition += newYear - 2018;
            }
        }
    }
});
複製代碼

上述代碼在 book 上定義了兩個數據屬性 ( _year 和 edition ) 和一個訪問其屬性 ( year ),值得一提的是這裏的屬性都是在同一時間建立的。

讀取屬性的特性

既然能修改屬性的特性,那就應該能獲取屬性的特性,因此 ECMAScript 又給咱們提供了 Object.getOwnPropertyDescriptior() 方法,該方法接受兩個參數:屬性所在的對象要讀取的屬性名稱

它的返回值是一個對象。

若是是數據屬性,則對象的屬性有 configurableenumerablewritablevalue

若是是訪問器屬性,這個對象的屬性有 configurableenumerablegetset

舉個栗子 🌰 :

var descriptior = Object.getOwnPropertyDescriptior(book, '_year');
alert(descriptior.value); // 2018;
alert(descriptior.configurable); // false;
複製代碼

Tips: 在 JavaScript 中,能夠針對任何對象 —— 包括 DOMBOM 對象,使用 Object.getOwnPropertyDescriptior() 方法。

建立對象

雖然使用 Object 構造函數或對象字面量均可以建立單個對象,但若是使用同一接口建立不少對象就會產生大量的重複代碼。

爲解決這個問題,咱們可使用工廠模式的一種變體。

  • 工廠模式

    工廠模式是用函數來封裝 📦 接口建立對象的細節:

    function createPerson(name, age) {
        var o = new Object();
        o.name = name;
        o.age = age;
        o.sayName = function() {
            alert(this.name);
        };
        return o;
    }
    
    var person1 = createPerson('Fly_001', 22);
    var person1 = createPerson('juejin', 24);
    複製代碼

    工廠模式雖然解決了建立多個類似對象的問題,但卻沒有解決 對象識別 的問題 ( 即怎樣知道一個對象的類型 ),因此又出現了另外一個模式 ~ 構造函數模式

  • 構造函數模式

    ECMAScript 的構造函數可用來建立特定類型的對象,從而定義對象的屬性和方法:

    function Person(name, age) {
        this.name = name;
        this.age = age;
        this.sayName = function() {
            alert(this.name);
        };
    }
    
    var person1 = new Person('Fly_001', 22);
    var person1 = new Person('juejin', 24);
    複製代碼

    咱們注意到,Person() 中的代碼與前面 createPerson() 不一樣之處在於:

    1. 沒有顯示地建立對象
    2. 直接將屬性和方法賦給了 this 對象
    3. 沒有 return 語句

    Tips: 按照慣例,構造函數始終都應該以一個大寫字母開頭。

    要建立 Person 的新實例,必須使用 new 操做符,以這種方式調用構造函數會經歷如下 4 個步驟:

    1. 建立一個新對象;
    2. 將構造函數的做用域賦給新對象( 所以 this 就指向這個新對象 );
    3. 執行構造函數中的代碼(爲新對象添加屬性);
    4. 返回新對象。

    這樣,person1 和 person2 分別保存着 Perosn 的一個不一樣實例:

    alert(person1.constructor == Person); // true;
    alert(person2.constructor == Person); // true;
    
    alert(person1 instanceof Person); // true;
    alert(person2 instanceof Person); // true;
    複製代碼

    使用構造函數模式能夠將它的實例標識爲一種特定的類型,這也正是構造函數模式賽過工廠模式的地方。

    構造函數模式雖然好用,但它的缺點是每一個方法在每一個實例上都要從新建立一遍,從邏輯角度來說,此時的構造函數也能夠這樣定義:

    function Person(name, age) {
        this.name = name;
        this.age = age;
        this.sayName = new function('alert(this.name)'); } 複製代碼

    同時不一樣實例上的同名函數是 不相等 的:

    alert(person1.sayName == person2.sayName); // false;
    複製代碼

    因此建立兩個完成一樣的任務的 Function 實例確實沒有必要,好在這些問題又能夠經過 原型模式 來解決。

  • 原型模式

    咱們建立的每一個函數都有一個 prototype ( 原型 ) 屬性,這個屬性是一個指針,指向一個對象,這個對象的用途是包含能夠由特定類型的全部實例共享的屬性和方法。

    是否是很繞,若是按照字面意思來理解,那麼 prototype 就是經過調用構造函數而建立的那個對象實例的原型對象。(好吧,仍是很迷 🤕),那就放碼出來吧~

    function Person() {}
    
    Person.prototype.name = 'Fly_001';
    Person.prototype.age = 22;
    Person.prototype.sex = 'male';
    Person.property.sayName = function() {
        alert(this.name);
    };
    
    var person1 = new Person();
    person1.sayName(); // 'Fly_001';
    
    var person2 = new Person();
    alert(person1.sayName == person2.sayName); // true;
    複製代碼

    使用原型對象的好處是可讓全部對象實例共享它所包含的屬性和方法。

    與構造函數模式不一樣的是,新對象的屬性和方法是由全部實例共享的,換句話說,person1 和 person2 訪問的都是同一組屬性和同一個 sayName() 方法。

    要理解原型模式的工做原理,就必須先理解 ECMAScript 中 原型對象 的性質。

  • 理解原型對象

    不管何時,只要建立了一個新函數,就會根據一組特定的規則爲該函數建立一個 prototype 屬性,這個屬性指向函數的原型對象。在默認狀況下,全部原型對象都會自動得到一個 constructor(構造函數)屬性,這個屬性包含一個指向 prototype 屬性所在函數的指針。

    就拿前面的栗子 🌰 來講,Person.prototype.constructor 指向 Person,而經過這個構造函數,咱們還可繼續爲原型對象添加其它屬性和方法。

    下圖展現了各個對象之間的關係:

在此,Person.prototype 指向了原型對象,而 Person.prototype.constructor 又指回了 Person。

原型對象中除了包含 constructor 屬性外,還包括後來添加的其它屬性。

Person 的每個實例 —— person1 和 person2 都包含一個內部屬性,該屬性僅僅指向 Person.prototype, 換句話說,它們與構造函數沒有直接的關係。此外,要格外注意的是,雖然這兩個實例都不包含屬性和方法,但卻能夠調用 sayName() 方法,這是經過 查找對象屬性的過程 來實現的。

同時咱們能夠經過 isPrototypeOf() 方法來肯定對象之間是否存在這種關係:

alert(Person.prototype.isPrototypeOf(person1)); // true;
alert(Person.prototype.isPrototypeOf(person2)); // true;
複製代碼

這裏由於 person1 和 person2 內部都有一個指向 Person.prototype 的指針,所以都返回了 true。

每當代碼讀取某個對象的某個屬性時,都會執行一次搜索,目標是具備給定的屬性名。搜索首先從對象實例自己開始,若是找到對應的屬性,則返回屬性值並中止搜索;若是沒找到,則繼續搜索指針指向的原型對象。

也就是說,在咱們調用 person1.sayName() 的時候,會前後執行兩次搜索,先從 person1 實例自己上找,沒找到後再從 person1 的原型上尋找,最後發現了 sayName() 方法定義並返回。

另外可使用 hasOwnProperty() 方法來檢測一個屬性是存在於實例中,仍是存在於原型中,只有當屬性存在於對象實例中才會返回 true

alert(person1.hasOwnProperty('name')); // false;
複製代碼
  • 更簡單的原型語法

    在前面的例子裏,每添加一個屬性和方法就要敲一遍 Person.prototype,顯得有些繁瑣,因此更常見的作法是使用對象字面量來進行封裝 📦:

    function Person() {}
    
    Person.prototype = {
        name: 'Fly_001',
        age: 22,
        sex: 'male',
        sayName: function() {
            alert(this.name);
        }
    };
    複製代碼

    在上面的代碼中,咱們將 Person.prototype 設置爲等於一個以對象字面量形式建立的新對象,最終結果相同,可是有一個例外:constructor 屬性再也不指向 Person 了。

    前面介紹過,每建立一個函數,就會同時建立它的 prototype 對象,這個對象也會自動得到 constructor 屬性。

    而咱們剛纔的代碼,本質上徹底重寫了默認的 prototype 對象,所以 constructor 屬性也就變成了新對象的 constructor 屬性 ( 指向 Object 構造函數 ),再也不指向 Person 函數。

    此時,儘管 instanceof 操做符還能返回正確的結果,但經過 constructor 已經沒法肯定對象的類型了:

    var friend = new Person();
    
    alert(friend instanceof Person); // true;
    alert(friend.constructor == Person); //false;
    alert(friend.constructor == Object); // true;
    複製代碼

    若是 constructor 的值真的很重要,能夠像下面這樣特地將它設置回適當的值:

    function Person () {};
    
    Person.prototype = {
        constructor: Person,
        // 設置其它屬性和方法;
    }
    複製代碼

    但要注意一點,以這種方式重設 constructor 屬性會致使它的 [[enumerable]] 特性被設置爲 true。而默認狀況下,原生的 constructor 屬性是不可枚舉的,所以我們可使用 Object.defineProperty() 方法:

    // 重設構造函數;
    Object.defineProperty(Person.prototype, 'constructor', {
        enumerable: false,
        value: Person
    });
    複製代碼
  • 原型的動態性

    因爲在原型中查找值的過程是一次搜索,所以咱們對原型對象所作的任何修改都可以當即從實例上反映出來 —— 即便是先建立了實例後修改原型也照樣如此:

    var friend = new Person();
    
    Person.prototype.sayName = function() {
        alert('hi');
    };
    
    friend.sayName(); // 'hi', 木有問題~
    複製代碼

    其緣由能夠歸結爲實例與原型之間的鬆散鏈接關係。

    當咱們調用 friend.sayName() 時,首先會在實例中搜索名爲 sayName 的方法,在沒找到的狀況下會繼續搜索原型。由於實例與原型之間的鏈接是一個指針而非副本,所以能夠在原型中找到並返回保存在那裏的函數。

    儘管能夠隨時爲原型添加屬性和方法,而且可以當即在全部對象實例中反映出來,但若是是重寫整個對象,那麼狀況就不同了:

    function Person() {}
    
    var friend = new Person();
    
    Person.prototype = {
        constructor: Person,
        name: 'Fly_001',
        age: 22,
        sex: 'male',
        sayName: function() {
            alert(this.name);
        }
    };
    
    friend.sayName(); // error!
    複製代碼

    由於 friend 指向的原型中不包含以該名字命名的屬性,下圖展現了這個過程的內幕:

從圖中能夠看出,重寫原型對象切斷了現有原型與任何以前已經存在的對象實例之間的聯繫,它們引用的仍然是最初的原型。

  • 原生對象的原型

    原型模式的重要性不只體如今建立自定義類型方面,就連全部原生的引用類型,都是採用這種模式建立的。全部原生引用類型 ( Object、Array、String,等等 )都在其構造函數的原型上定義了方法。

    例如,在 Array.prototype 中能夠找到 sort() 方法,而在 String.prototype 中能夠找到 substring() 方法:

    alert(typeof Array.prototype.sort); // 'function';
    alert(typeof String.prototype.substring); // 'function';
    複製代碼

    經過原生對象的原型,不只能夠取得全部默認方法的引用,還能夠定義新方法:

    String.prototype.startWith = function(text) {
        return this.indexOf(text) == 0;
    };
    
    var msg = 'Hello World';
    alert(msg.startWith('Hello')); // true;
    複製代碼

    上述代碼 👆 就給基本包裝類型 String 添加了一個 startWith() 方法。既然方法被添加給了 String.prototype,那麼當前環境中的全部字符串均可以調用這個方法。

  • 原型對象的問題

    原型模式也不是沒有缺點,它的最大問題是由其 共享性 的本性致使的。

    原型中全部屬性是被不少實例共享的,這種共享對於函數很是合適,對於那些包含基本值的屬性也說得過去,畢竟經過在實例上添加一個同名屬性,能夠隱藏原型中的對應屬性。

    然鵝,對於包含引用類型值的屬性來講,問題就比較突出了:

    function Person() {}
    
    Person.prototype = {
        constructor: Person,
        name: 'Fly_001',
        age: 22,
        friends: ['Jack', 'Tom'],
        sayName: function() {
            alert(this.name);
        }
    };
    
    var person1 = new Person();
    var person2 = new Person();
    
    person1.friends.push('Daniel');
    
    alert(person1.friends); // 'Jack, Tom, Daniel';
    alert(person2.friends); // 'Jack, Tom, Daniel';
    alert(person1.friends === person2.friends); // true;
    複製代碼

    因爲 friends 數組存在於 Person.prototype 而非 person1 中,因此剛剛的修改也會經過 person2.friends 反映出來。

    因此正是這個問題,咱們不能單獨使用原型模式~

  • 組合使用構造函數模式和原型模式 ( 閃亮登場 ✨ )

    建立自定義類型的最多見方式,就是組合使用構造函數模式與原型模式。

    構造函數模式用於定義實例屬性,而原型模式用於定義方法和共享的屬性。

    這樣,每一個實例都會有本身的一份實例屬性的副本,但同時又共享着對方法的引用,最大限度地節省了內存。另外,這種混合模式還支持向構造函數傳遞參數;可謂是集兩種模式之長呀:

    funciton Person(name, age, sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.friends = ['Jack', 'Tom'];
    }
    
    Person.prototype = {
        constructor: Person,
        sayName: function() {
            alert(this.name);
        }
    }
    
    var person1 = new Perosn('Fly_001', 22, 'male');
    var person2 = new Person('juejin', 24, 'unknown');
    
    person1.friends.push('Daniel');
    
    alert(person1.friends); // 'Jack, Tom, Daniel';
    alert(person2.friends); // 'Jack, Tom';
    
    alert(person1.friends === person2.friends); // false;
    alert(person1.sayName === person2.sayName); // true;
    複製代碼

    這裏修改了 person1.friends 不會影響到 person2.friends,由於它們分別引用了不一樣的數組。

    這種構造函數與原型混成的模式,是使用最普遍、認同度最高的一種建立自定義類型的方式,能夠說,這是用來定義引用類型的一種默認模式。

關於 JS 中對象的一些淺薄知識,就先講到這裏,下一篇會談談 JS 中幾種繼承方式,敬請期待~ ❤️

相關文章
相關標籤/搜索