【讀】JavaScript之面向對象

本篇是JavaScript高級程序設計第三版第六章《面向對象的程序設計》閱讀記錄。若有疑問能夠聯繫我html

理解對象

根據ECMA-262,對象爲無序屬性的集合,其屬性能夠包含基本值、對象、或函數。下面是個例子:數組

var person = new Object();
person.name = 'GY';
person.age = 18;
person.sayHi = function() {
    alert('Hi!')
};

// 或者能夠這樣

var person = {
    name: 'GY',
    age: 18,
    sayHi: function() {
        alert('Hi!');
    }
};
複製代碼

這裏完美的展示了JavaScript對象無序屬性的集合的定義。瀏覽器

屬性類型

ECMAScript中有兩種屬性:數據屬性 和 訪問器屬性。bash

用來描述屬性(property)各類特徵的,稱之爲特性(attribute)。對比屬性描述對象。這些特性不能直接訪問,經常使用[[name]]來描述,好比[[configurable]]閉包

  • 數據屬性app

    能夠寫入和讀取值。該種屬性有4個特性:函數

    • [[value]],寫入和讀取時,都操做的是這裏。默認值undefined
    • [[configurable]],表示是否能經過delete從對象中刪除該屬性、可否修改該屬性的特性、可否把屬性修改成訪問器屬性。默認值true
    • [[enumerable]],表示是否能經過for-in遍歷該屬性。默認值true
    • [[writable]],表示是否能夠修改該屬性的值。默認值true

    好比上面定義的person,其中的屬性值,都存儲在其[[value]]特性中。ui

    那麼如何操做這些特徵值呢?先來看看如何修改,使用Object.defineProperty方法。this

    /// 參數依次爲:
    /// 須要操做的對象(這裏爲person),
    /// 須要操做的屬性(這裏爲name),
    /// 特徵值(類型爲對象)
    Object.defineProperty(object, 'property', attributes);
    複製代碼

    好比對對於上面的person對象,咱們寫出以下代碼spa

    Object.defineProperty(person, 'name', {
        writable: false,
        value: 'OtherName'
    });
    
    console.log(person.name); // OtherName
    person.name = '任意值';
    console.log(person.name); // OtherName
    複製代碼

    這裏經過Object.definePropertyname屬性的writable特性定義爲false,那麼name屬性將爲只讀屬性。沒法再次賦值。對writable特性爲false的屬性賦值,非嚴格模式下會忽略,嚴格模式下會拋出錯誤Cannot assign to read only property 'name' of object

    相應的,能夠經過該方法修改configurableenumerable特性。值得注意的是,對configurable設置爲false後,將致使configurableenumerable不能再次更改。

    Object.defineProperty(person, 'name', {
        configurable: false
    });
    
    console.log(person.name); // GY
    // delete person.name; // 嚴格模式會報錯
    
    // 在configurable: false這裏將不能再次修改
    // Cannot redefine property: name at Function.defineProperty
    Object.defineProperty(person, 'name', {
        configurable: true,
        enumerable: true,
        
    });
    複製代碼
  • 訪問器屬性

    該類型屬性不存儲值,沒有[[value]]。包含getset函數(這二者都是非必須的)。在讀取和寫入時將調用對應函數。訪問器屬性有4個特性:[[configurable]][[enumerable]][[get]][[set]].

    訪問器屬性不能直接定義,須要使用Object.defineProperty,好比:

    var person = {
        // 下劃線一般表示須要經過對象方法訪問。規範!
        _age: 0
    };
    
    Object.defineProperty(person, 'age', {
        get: function() {
            return this._age;
        },
        set: function(v) {
            this._age = v >= 0 ? v : 0;
        }
    });
    複製代碼

    如只提供get,意味着該屬性只讀;只提供set,讀取會返回undefined

    若你想一次定義多個屬性及其特性,可使用Object.defineProperties,向下面這樣:

    var person = {
        // 下劃線一般表示須要經過對象方法訪問。規範!
        _age: 10
    };
    
    Object.defineProperties(person, {
        age: {
            get: function() {
                return this._age;
            },
            set: function(v) {
                this._age = v >= 0 ? v : 0;
            }
        },
        name: {
            value: 'unnamed',
            writable: true,
            enumerable: true
        }
    });
    複製代碼
  • 如何獲取屬性特徵

    使用Object.getOwnPropertyDescriptor方法

    var descriptor = Object.getOwnPropertyDescriptor(person, 'name');
    console.log(descriptor.value + ' ' + descriptor.writable + ' ' + descriptor.enumerable);
    複製代碼

建立對象

這一節,將會介紹多種建立對象的方法。

工廠模式

/// 工廠模式
/// 這種模式減小了建立多個類似對象的重複代碼,
/// 但沒法解決對象識別問題(即怎樣知道對象的類型)
function createPerson(name, age) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayHi = function() {
        alert('Hi! I\'m ' + this.name); } return o; } var instance = createPerson('GY', 18); person.sayHi(); 複製代碼

構造函數模式

/// 構造函數模式
/// 這種方式須要顯示的使用 new 關鍵字
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayHi = function() {
        alert('Hi! I\'m ' + this.name); }; } var instance = new Person('GY1', 18); person.sayHi(); 複製代碼

這裏不像工廠模式那樣直接建立對象,但最終結果相同。主要由於使用new關鍵字會經歷下面幾個步驟:

  • 建立對象
  • 將構造函數的做用域賦值給新對象(所以this就指向了該新對象)
  • 執行構造函數中的代碼
  • 返回新對象

這裏能夠經過person.constructor === Person明確知道其類型。使用instanceof檢測也是經過的。

alert(instance.constructor === Person); // true
alert(instance instanceof Person); // true
複製代碼
  • 構造函數也是函數,能夠不經過new直接使用

    任何函數經過new來調用,均可以做爲構造函數;任何函數,不經過new調用,那它跟普通函數也沒什麼兩樣。

    因爲構造函數中使用了this,不經過new來使用,this將指向global對象(瀏覽器中就是window)。

    // 這樣直接調用會在window對象上定義name和age屬性
    Person('Temp', 10);
    複製代碼
  • 構造函數也存在問題

    使用構造函數的主要問題是,每個方法都會在每一個對象上從新定義一遍。每一個對象上的方法都是不相等的。這樣會形成內存浪費以及不一樣的做用域鏈和標識符解析。很明顯,這樣是不必的。

    咱們可使用下面的方法來避免:

    function sayHi() {
        alert(this.name);
    }
    
    function Person(name, age) {
        this.name = name;
        this.age = age;
        this.sayHi = sayHi;
    }
    
    var instance1 = new Person('instance1', 18);
    var instance2 = new Person('instance2', 20);
    複製代碼

    就是把每一個方法都單獨定義,在構造函數內部引用。這樣又引起了新的問題:全局域上定義的函數實際爲某些對象而服務,這樣全局域有點名存實亡。其次,若是對象上須要有不少方法,那麼這些方法都須要在全局域上定義。

    再來看看下面生成對象的方法。

原型模式

每一個函數都有一個prototype(原型)屬性,這是一個指針,指向一個對象,該對象是用來包含特定類型全部實例共享的屬性和方法的。 這樣,以前在構造函數中定義的實例信息就能夠寫在原型對象中了。以下:

function Person() {
}

Person.prototype.name = 'unnamed';
Person.prototype.age = 18;
Person.prototype.sayHi = function() {
    alert(this.name);
}

var instance1 = new Person();
alert(instance1.name);
var instance2 = new Person();
alert(instance2.name);
複製代碼

繼續往下以前,先來了解下原型對象:

不管何時,只要建立了一個新函數,就會根據特定規則爲該函數建立prototype屬性,這個屬性指向函數的原型對象。默認狀況下,該原型對象還會擁有constructor(構造函數)屬性,指向該函數。固然,也包含從Object對象繼承來的屬性(這個咱們後面再講)。

在經過構造函數建立新實例對象後,每一個實例對象能夠經過__proto__來訪問構造函數的原型對象。

下面是他們的關係圖:

咱們可使用Person.prototype.isPrototypeOf(instance1)來檢測一個對象(這裏爲Person的prototype對象)是否爲指定對象(這裏爲instance1)的原型。

推薦使用Object.getPrototypeOf(instance1)獲取指定對象的原型對象,而不是使用__proto__

上面的例子中,實例對象中均沒有name屬性,卻可以訪問到。也正是由於原型對象的緣由:當代碼獲取某個對象屬性時,都會執行一次搜索,目標是具備給定名字的屬性。搜索首先從對象實例自己開始,若是找到對應屬性,返回該值;若是沒有找到,繼續搜索原型對象。

從搜索過程能夠發現,實例對象和原型對象都有的屬性,實例中的會覆蓋原型中的。使用delete刪除時,只是刪除了實例中的屬性。

使用hasOwnProperty方法能夠檢測一個屬性是否在實例中。只有給定屬性存在於實例中,纔會返回true。

使用in操做符時(property in object),無論是在實例中,仍是原型中都會返回true。

使用for-in操做時,返回的是全部可以經過對象訪問的、可枚舉的屬性,無論是在實例中,仍是原型中;

既然原型對象也是對象,那咱們能夠手動賦值原型對象,從而減小沒必要要的輸入。向下面這樣:

function Person() {
}

Person.prototype = {
    constructor: Person, // constructor記得要聲明
    name: 'unnamed',
    age: 0,
    sayHi: function() {
        alert('Hi! this is ' + this.name);
    }
};
複製代碼

注意:原生的constructor是不可枚舉的,這樣定義後,致使constructor也能夠枚舉。

因爲在原型中查找值是一次搜索過程,這就致使了原型的動態性。也就是說咱們對原型對象的操做都會馬上反應在實例對象上。可是,若是咱們從新賦值了構造函數的原型對象,那在賦值以前建立的對象將不受影響,緣由是以前的實例對象指向的原型和如今的原型是徹底不一樣的兩個對象了。

到這裏,你也許就能理解原生對象的方法都存儲在其原型對象上了吧。

那麼使用原型建立對象的方法是否是沒有問題了呢?答案是否認的。首先,其省略了構造函數傳參的環節,結果就是全部實例默認會有相同的屬性值;其次,因爲共享屬性,在屬性值爲引用類型時,一個實例修改屬性會影響另外一個實例。

function Person() {
}

Person.prototype = {
    constructor: Person, // constructor記得要聲明
    name: 'unnamed',
    age: 0,
    sayHi: function() {
        alert('Hi! this is ' + this.name);
    },
    friends: ['A', 'B'] // 增長了friends屬性
};

var instance1 = new Person();
var instance2 = new Person();

instance1.friends.push('C'); // 修改instance1的friends屬性
alert(instance2.friends); // 但instance2的friends屬性一樣也改變成了A, B, C
複製代碼

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

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

/// 定義實例屬性
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.friends = [];
}

/// 定義公共方法
Person.prototype = {
    constructor: Person, // constructor記得要聲明
    sayHi: function() {
        alert('Hi! this is ' + this.name);
    }
};

var instance1 = new Person('instance1', 18);
var instance2 = new Person('instance2', 20);
複製代碼

這種使用構造函數與原型混合的模式,是目前ECMAScript使用最普遍、認同度最高的建立自定義類型的方法。

動態原型

也許你對上面的組合模式將屬性和方法分開來寫的形式感到彆扭。那麼動態原型模式能夠來拯救你。

動態原型模式,將組合模式中分開的代碼合併在一塊兒,經過判斷動態的添加共享的方法或屬性。

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

    // 這裏就是動態原型模式和混合模式的區別
    // 對於共享的屬性或方法,你沒必要每個都去判斷
    // 找到一個標準就行
    if (typeof this.sayHi != 'function') {
        Person.prototype.sayHi = function() {
            alert('Hi! I\'m ' + this.name); }; } } var instance1 = new Person('instance1', 18); instance1.sayHi(); 複製代碼

注意:這裏不能使用字面量語法重寫原型屬性,這樣會切斷以前建立的實例和現有原型的聯繫。

寄生構造函數模式

寄生構造函數模式提供一個函數用於封裝建立對象的代碼.這和工廠模式極爲類似。

function Person(name, age) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayHi = function() {
        alert('Hi! I\'m ' + this.name); }; return o; } var instance = new Person('instance', 18); instance.sayHi(); 複製代碼

既然函數內部並無使用new操做生成的實例對象,爲啥還要生成?這個點暫時沒搞懂。

能夠看到,和工廠模式相比,除了使用new操做符覺得,其餘的沒啥兩樣。可是在一些特定場合,仍是有它的用武之地的。好比ES6以前原生類型是沒法繼承的,可使用這種方法生成繼承原生類型的實例

看下這個例子:

function SpecialArray() {
    var values = new Array();
    values.push.apply(values, arguments);
    // 提供新方法
    values.toPipedString = function() {
        return this.join('|');
    };
    
    return values;
}

var colors = new SpecialArray('red', 'blue', 'green');
alert(colors.toPipedString());
複製代碼

這裏提供了構造函數,內部使用Array對象,並添加了特有方法。

之因此叫作寄生,緣由大概由於新的功能依託於原有對象吧。

穩妥構造函數模式

所謂穩妥,指沒有公共屬性,並且其方法不引用this。在該模式中不適用new來調用構造函數。下面是個例子:

function Person(name, age) {
    var o = new Object();
    o.sayHi = function() {
        alert('Hi! I\'m ' + name); } return o; } var instance = Person('instance', 18); instance.sayHi(); 複製代碼

也許你會納悶,這裏的name沒有顯示的存儲,到底如何能訪問到?請看下面的斷點截圖。說明了其存儲在閉包中,或者說被閉包捕獲了。(若理解有誤請告知。謝謝!)

繼承

ECMAScript中依靠原型鏈來實現繼承。

原型鏈

簡單回顧下構造函數、原型、和實例之間的關係:構造函數也是對象,該對象擁有prototype屬性,指向了其原型對象,原型對象存在一個constructor屬性,指向了該構造函數;實例對象擁有__proto__屬性,也指向了構造函數的原型對象(再次說明下,__proto__不推薦使用哈,)。

如今,若是讓構造函數的prototype屬性指向另外一個類型的實例對象呢?上面的狀況會層層遞進。讓咱們看下面的例子:

// 父類型
function SuperType() {
    this.property = true;
}

SuperType.prototype.getSuperValue = function() {
    return this.property;
}

// 子類型
function SubType() {
    this.subproperty = false;
}

// 子類型的prototype指向父類型的實例對象
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

var instance = new SubType();
alert(instance.getSuperValue()); // true
複製代碼

對應關係圖:

值得注意的是,instance.constructor將獲得的是SuperType。

  • 默認的原型

    全部引用類型都繼承了Object,也就是說全部函數的prototype指向了Object實例。讓咱們更新下上面的關係圖:

  • 肯定原型和實例的關係 可使用instanceofisPrototypeOf方法

    alert(instance instanceof Object);
    alert(instance instanceof SuperType);
    alert(instance instanceof SubType);
    alert(Object.prototype.isPrototypeOf(instance));
    alert(SuperType.prototype.isPrototypeOf(instance));
    alert(SubType.prototype.isPrototypeOf(instance));
    複製代碼

    這裏都會返回true。判斷規則爲:實例的原型在原型鏈中。咱們可使用下面的方法進行模擬。

    /// 對象是不是指定類型的實例,會查找原型鏈
    Object.prototype.isKindsOf = function(func) {
        for (
            let proto = Object.getPrototypeOf(this); 
            proto !== null; 
            proto = Object.getPrototypeOf(proto)
        ) {
            if (proto === func.prototype) {
                return true
            }
        }
        return false
    }
    
    /// 對象是不是指定類型的實例,不進行原型鏈判斷
    Object.prototype.isMemberOf = function(func) {
        return Object.getPrototypeOf(this) === func.prototype
    }
    複製代碼
  • 原型鏈存在的問題

    經過原型實現繼承,是將子類的原型對象賦值爲父類(這裏暫時使用子類和父類來表述)的實例,這樣,原先父類的實例屬性成了子類原型屬性,會被子類的全部實例共享,這也包含引用類型的屬性。

    再者,建立子類類型實例時,沒法向父類的構造函數中傳遞參數。

    下面咱們一塊兒看看如何解決這些問題。

借用構造函數

// 父類型
function SuperType() {
    this.colors = ['red', 'blue', 'green'];
}

// 子類型
function SubType() {
    // 使父類的構造函數在子類實例對象上初始化
    SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push('gray');
var instance2 = new SubType();
alert(instance2.colors); // red, blue, green
複製代碼

這裏在構造子類實例時,調用父類構造函數,完成父類特定的初始化。 像下面這樣,還能夠完成參數的傳遞。

// 父類型
function SuperType(name) {
    this.name = name;
}

// 子類型
function SubType() {
    // 使父類的構造函數在子類實例對象上初始化
    // 這樣子類的屬性就會覆蓋其原型屬性
    SuperType.call(this, 'unnamed');
}

var instance = new SubType();
alert(instance.name); // unnamed
複製代碼

借用構造函數也存在一些問題。好比,方法沒法實現複用;父類原型中定義的方法對子類不可見(由於這種情形子類和父類並無原型鏈上的關係,只是子類在構造過程當中借用了父類的構造過程)。

組合繼承

將原型鏈和借用構造函數組合一塊兒,使用原型鏈實現對原型屬性和方法的繼承,借用構造函數實現實例屬性的繼承。這成爲最經常使用的繼承模式。下面是一個例子:

// 父類型
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayHi = function() {
    alert('Hi! I\'m ' + this.name);
};

// 子類型
function SubType(name, age) {
    // 使父類的構造函數在子類實例對象上初始化, 完成實例屬性的繼承
    SuperType.call(this, name);
    // 子類特有的屬性
    this.age = age;
}

// 子類型的prototype指向父類型的實例對象,完成繼承
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){ 
    alert(this.name + ' is ' + this.age + ' years old!');
}

var instance1 = new SubType('instance1', 18);
instance1.colors.push('black');
alert(instance1.colors); // red,blue,green,black
instance1.sayHi(); // Hi! I'm instance1
instance1.sayAge(); // instance1 is 18 years old!

var instance2 = new SubType('instance2', 20);
alert(instance2.colors); // red,blue,green
instance2.sayHi(); // Hi! I'm instance2
instance2.sayAge(); // instance2 is 20 years old!
複製代碼

原型式繼承

這是一種藉助已有對象建立新對象,同時沒必要建立自定義類型的方法。先看下面的例子:

function object(o) {
    /// 建立臨時構造函數
    function F(){}
    /// 將構造函數的原型賦值爲傳入的對象
    F.prototype = o;

    /// 返回實例
    return new F();
}

var person = {
    name: 'instance1',
    friends: ['gouzi', 'maozi']
};

var anotherPerson = object(person);
anotherPerson.name = 'cuihua';
anotherPerson.friends.push('xiaofeng');

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'daha';
yetAnotherPerson.friends.push('bob');

alert(person.friends);
複製代碼

能夠看到,這裏至關於複製了person的兩個副本。在ECMAScript5中,新增了Object.create方法來規範了原型繼承模式。

/// 能夠只傳入一個參數
var anotherPerson = Object.create(person);

// 也可多傳入屬性及其特性
var yetAnotherPerson = Object.create(person, {
    name: {
        value: 'dab'
    }
});
複製代碼

寄生式繼承

相比原型繼承,寄生式繼承僅是提供一個函數,用來封裝對象的繼承過程。以下:

function createAnother(original) {
    /// 向原型繼承同樣,建立新對象
    var clone = Object.create(original)
    /// 自定義的加強過程
    clone.sayHi = function() {
        alert('Hi!');
    }
    return clone;
}
複製代碼

能夠發現,該模式,沒法對函數進行復用。

寄生組合模式

回顧下以前的組合繼承方式,這會致使兩次父類構造函數調用:一次在建立子類原型,另外一次在建立子類實例。這將致使,子類的實例和原型中都存在父類的屬性。以下:

// 父類型
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayHi = function() {
    alert('Hi! I\'m ' + this.name);
};

// 子類型
function SubType(name, age) {
    // 使父類的構造函數在子類實例對象上初始化, 完成實例屬性的繼承
    SuperType.call(this, name); // 又一次父類構造函數調用
    // 子類特有的屬性
    this.age = age;
}

// 子類型的prototype指向父類型的實例對象,完成繼承
SubType.prototype = new SuperType(); // 一次父類構造函數調用
SubType.prototype.sayAge = function(){ 
    alert(this.name + ' is ' + this.age + ' years old!');
}

var instance = new SubType();
instance.colors.push('gray');
alert(instance.colors); // red,blue,green,gray
alert(instance.__proto__.colors); // red,blue,green
複製代碼

爲了解決這個問題,能夠考慮使用構造函數繼承屬性,使用原型鏈來繼承方法;沒必要爲了指定子類的原型而調用父類的構造函數(這樣就避免了生成父類的實例,由於父類的原型對象已經存在了),咱們須要的就是原型對象的一個副本而已。看看下面的例子:

// 父類型
function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayHi = function() {
    alert('Hi! I\'m ' + this.name);
};

// 子類型
function SubType(name, age) {
    // 使父類的構造函數在子類實例對象上初始化, 完成實例屬性的繼承
    SuperType.call(this, name); // 又一次父類構造函數調用
    // 子類特有的屬性
    this.age = age;
}

/// 使用寄生的方式完成繼承
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function(){ 
    alert(this.name + ' is ' + this.age + ' years old!');
}

function inheritPrototype(subType, superType) {
    // 得到父類原型副本,做爲子類的原型
    var prototype = Object.create(superType.prototype);
    // 配置子類原型
    prototype.constructor = subType;
    // 指定子類原型
    subType.prototype = prototype;
}

var instance = new SubType();
instance.colors.push('gray');
alert(instance.colors); // red,blue,green,gray
alert(instance.__proto__.colors); // undefined
複製代碼

至此,咱們找到了一種最理想的繼承模式。

總結

該篇從對象的含義,到對象的建立方式,再到繼承的多種實現。由淺入深的介紹了ECMAScript中面向對象相關知識。也許你在閱讀過程當中感到疑惑,那麼就動手實現一遍...那時,就沒必要多說什麼了!

參考

相關文章
相關標籤/搜索