JavaScript中Object的總結

基於原型繼承,動態對象擴展,閉包,JavaScript已經成爲當今世界上最靈活和富有表現力的編程語言之一。javascript

這裏有一個很重要的概念須要特別指出:在JavaScript中,包括全部的函數,數組,鍵值對和數據結構都是對象。 舉個簡單的例子:css

var testFunc = function testFunc() {

};
testFunc.customP = "James";
console.log(testFunc.customP);

上邊的代碼中,testFunc能夠添加customP這個屬性,說明testFunc自己就是一個對象。在JavaScript中,函數名是一個指向函數對象的指針,咱們看下邊的代碼:html

var testFunc = function testFunc() {
    console.log("Hello world");
};
var anotherTestFunc = testFunc;
testFunc = null;
anotherTestFunc();

即便把testFunc置爲空,上邊的程序仍然打印了Hello world。經過上邊的例子,咱們演示了函數做爲對象的證據,然而JavaScript中的基本數據類型,在特定的狀況下也會體現出對象的特性:java

'tonya@example.com'.split('@')[1]; // => example.com

當咱們可使用屬性獲取符.來操做基本類型的時候,它就表現的很像對象,但咱們不能給它賦值。緣由是:基本類型會被臨時包裝成object,以後會馬上拋棄這個包裝,表面上看像是賦值成功了,但下次是沒法訪問以前的賦值的。git

接下來,咱們會探討JavaScript中Object的一些問題,這和其餘面向對象的語言有很大不一樣,咱們會解釋爲何JavaScripts不是面向對象類型的語言,而是原型語言。github

類型繼承是否應該被淘汰

Design Patterns: Elements of Reusable Object Oriented Software這本書中有兩個關於面向對象設計程序的原則:web

  • Program to an interface, not an implementation 面向接口編程
  • Favor object composition over class inheritance 優先使用組合,而非繼承

在某種意義上,上邊的第二個原則一樣遵循了第一個原則,由於繼承把父類暴露給了子類,這樣的話,子類就被設計成了一個實現,而不是接口了。所以咱們得出一個結論,類型繼承破壞了封裝原則,而且把子類同它的祖先緊密聯繫在一塊兒。編程

舉一個更生動的例子,咱們能夠把類型繼承比做是傢俱,咱們用不少設計好的零部件來拼裝一個傢俱,若是這些零部件都符合設計,那麼咱們有很高的機會組裝成功,若是有一些不符合要求,那麼就會組裝失敗。數組

在程序設計中,組合就像樂高積木,大部分的零件被設計成可以和其餘零件拼接到一塊兒,這樣,咱們就可以很靈活的進行組裝了。數據結構

如何按照組件化的思想來設計程序,不是本篇文章的內容,如今咱們來看看反對繼承的理由是什麼:

  • 高耦合 繼承在面向對象的設計中的耦合性最高,子類與其祖先類緊密相連
  • 層次劃分不靈活 在真實的開發中,每每不會出現單層的繼承,若是使用了多層次的繼承,那麼極可能有不少繼承過來的屬性是不須要的,這樣就形成了代碼的過分重複
  • 多繼承難以理解 有時候頗有必要會繼承多個父類,對於這種狀況的處理跟單一繼承是不同的,會更復雜,須要處理衝突和不一致的狀況,而且代碼也變得難以閱讀和理解
  • 脆弱的架構 高耦合的程序,很難對某個類進行代碼重構,這就體現了架構的脆弱性
  • 大猩猩/香蕉問題 父類中的某些屬性可能不是咱們須要的,子類能夠重寫父類的屬性,但不能選擇繼承那些屬性,就像,我只須要香蕉,繼承卻給了我一個拿着香蕉的大猩猩和整片叢林

JavaScript的繼承和其餘面嚮對象語言的繼承有很大不一樣,只有咱們瞭解了繼承的缺點,才能更好的使用好這些特性。

Prototypes

prototype是JavaScript中很重要的一個概念,他能夠說是對其餘對象的一個模仿。又很像是一個類,你能夠用他來構建不少實例對象。但他的本質就是一個對象。 咱們能夠經過prototype作兩件事情:

  • 訪問一個共享的原型對象,也叫代理原型
  • 克隆一個原型

代理原型

咱們先把原型編程這一律念弄明白,在大多數的面向對象的語言中,對象就跟模具同樣,咱們根據模具來製造對象,在JavaScript中,不是這樣的,經過prototype給object賦值一個原型或對象,而後在新產生的對象身上作修改,也就是說新對象獲取了原型的數據。這就是原型編程思想。

在JavaScript中,對象內部都有一個原型對象,當一個對象查詢某個屬性或方法的時候,JavaScript引擎首先會搜索對象自己是否答案,若是沒有,就會去它的原型對象中繼續搜索,若是沒有,再去它的原型的原型中去找,這就造成了一個原型鏈。直到Object的prototype爲止。

咱們看一段代碼:

if (!Object.create) { 
    Object.create = function (o) {
        if (arguments.length > 1) {
            throw new Error('Object.create implementation' + ' only accepts the first parameter.');
        }
        function F() {} 
        F.prototype = o; 
        return new F();
    }; 
}

Object.create可用於建立一個對象,它的函數原型中接受兩個參數,第一個參數是原型對象,表示新建的對象的原型,必填,第二個參數是屬性數組,表示新建對象的屬性。它在ES5中被引入,所以上邊的代碼是考慮到兼容問題的。

經過上邊的代碼能夠看出來,其內部建立了一個F構造器,而後把原型參數經過F.prototype = o進行賦值,最後使用構造器生成一個對象。所以咱們得出下邊幾個結論:

  • 使用F.prototype = o這樣的方法給對象的原型賦值
  • new F()是怎麼的過程?

    var a = {};
      a.__proto__ = F.prototype;
      F.call(a);

有一個很容易讓人迷惑的地方,咱們先看代碼:

var a = new Object();
console.log(a.prototype); // => undefined
console.log(a.__proto__); // => {}

在上邊咱們不是解釋過了嗎?一個對象內部默認會指向一個原型,可是爲何上邊第二行代碼打印的結果是undefined呢?

這就引出了__proto__的概念,我以爲這篇文章寫的不錯。上邊的代碼輸出爲undefined,可是__proto__卻有值,說明每一個對象內部真正建立的原型是__proto__,而prototype只起到了輔助的做用,徹底能夠把他當作一個普通的屬性來看待。當使用Object.create方法建立對象的時候,參數中的原型會賦值給新對象的__proto__而不是而prototype。

再來看段代碼:

var switchProto = {
    isOn: function isOn() {
        return this.state;
    },
    toggle: function toggle() {
        this.state = !this.state;
        return this;
    },
    meta: {
        name: "James"
    },
    state: false
},
    switch1 = Object.create(switchProto),
    switch2 = Object.create(switchProto);

var state1 = switch1.toggle().isOn();
console.log(state1);

var state2 = switch2.isOn();
console.log(state2);

console.log(switchProto.isOn());

/*當改變一個對象或者數組中的屬性時,會有影響,這說明了這兩個賦值的方式不是copy。*/
switch1.meta.name = "Bond";
console.log(switch2.meta.name);

switch2.meta = { name: "zhangsan"};
console.log(switch1.meta.name);

上邊代碼中的switch1和switch2都指向了同一個原型,當執行完var state1 = switch1.toggle().isOn();後,state1的結果爲true,而state2爲false,這也正好驗證了上邊解釋的JavaScript尋找屬性或方法的原理。查找會使用原型鏈,賦值則不同,若是沒有該屬性,直接在對象內部建立該屬性,這時候跟原型不要緊。

若是修改原型中的對象或數組時,須要特別注意,會對原型產生反作用,可是對對象或數組直接賦值不會產生影響。所以在原型中使用對象或數組時,要十分當心。

原型克隆

原型編程也是有缺點的,若是兩個對象共享了同一原型,那麼更改原型內容的話會影響到其餘的對象,咱們看一個例子:

var testPrototype = {
    name: "James",
    obj: {
        objName: "objName"
    }
};
var b = Object.create(testPrototype);
var c = Object.create(testPrototype);
b.obj.objName = "Test";

console.log(c.obj.objName); // => Test

上邊的代碼演示了共享數據形成的問題,只有理解了如何觸發這些問題,才能更好的避免錯誤的發生。咱們解決上邊出現的問題的思路就是採用值拷貝,複製一份數據。

. extend()方法在jQuery和Underscore中都存在,它的做用就是實現原型的拷貝。咱們看看Underscore內部的實現方法:

_.extend = function(obj) { 
    each(slice.call(arguments, 1), function(source) {
    
        for (var prop in source) { 
            obj[prop] = source[prop];
        } 
    });
    return obj; 
};

each函數會取出對象中的每個屬性,而後賦值給source,最終把全部的屬性賦值給了obj。咱們就不作其餘演示了,這種經過遍歷屬性,而後賦值的方法原理是直接屬性賦值,所以咱們說這種方式沒有使用原型繼承。

這種deepCopy的思想在不一樣語言中仍是比較重要的一個思想,在JavaScript中,咱們應該使用代理原型來共享那些公共的屬性,使用原型拷貝來操縱獨享的數據,這是一條基本的編程原則。

The Flyweight Pattern(享元模式)

假若有一組屬性和方法是被不少實例對象共享的,把他們設計成可複用的模式就是享元模式,相對於給每個實例對象大量相同的屬性和方法,享元模式大大提升了內存性能。

而JavaScript的原型很是完美的契合享元模式。假如咱們要開發一款遊戲,遊戲中的每個敵人都有一些共有的屬性,好比名字,位置,血量值,還有一些共有的方法,好比攻擊,防護等等。若是咱們每建立出一個敵人對象都要把這些屬性進行賦值,無疑會形成大量的性能問題。咱們看看下邊這段程序:

var enemyPrototype = {
    name: "James",
    position: {
        x: 0,
        y: 0
    },
    setPosition: function setPosition(x, y) {
        this.position = {
            x: x,
            y: y
        };
        return this;
    },
    health: 20,
    bite: function bite() {

    },
    evade: function evade() {

    }
},
    spawnEnemy = function () {
        return Object.create(enemyPrototype);
    };

var james1 = spawnEnemy();
var james2 = spawnEnemy();

james1.health = 5;
console.log(james2.__proto__);
console.log(james2.health);
console.log(james1.__proto__.health);

james1.setPosition(10, 10);
console.log(james1.position);
console.log(james1.__proto__.position);
console.log(james2.position);

james1和james2這兩個敵人共享了一個enemyPrototype,這也算是一個默認配置。修改了一個的屬性,不會影響其餘對象的屬性。

值得注意的一點是,在上邊咱們也提到了修改原型中的Object或數組必定要當心,那麼在這個例子中,咱們經過setPosition這個函數解決了這個問題,核心就是這個函數中的this關鍵字。

Object Creation(建立對象)

關於JavaScript中對象的建立,我在這裏談兩點:一種是使用構造器,另外一種是使用字面量。

咱們以前也提到過,使用構造器初始化對象是很面向對象的編程思想,這在JavaScript中並不推薦,緣由是它不能很好的利用原型這一利器。咱們看個例子:

function Car(color, direction, mph) {
    var isParkingBrakeOn = false;
    this.color = color || "black";
    this.direction = direction || 0;
    this.mph = mph || 0;

    this.gas = function gas(amount) {
        amount = amount || 10;
        this.mph += amount;
        return this;
    };

    this.brake = function brake(amount) {
        amount = amount || 10;
        this.mph = (this.mph - amount) < 0 ? 0 : this.mph - amount;
        return this;
    };

    this.toggleParkingBrake = function toggleParkingBrake() {
        isParkingBrakeOn = !isParkingBrakeOn;
        return this;
    };

    this.isParked = function isParked() {
        return isParkingBrakeOn;
    }
}

var car  = new Car();
console.log(car.color); // => black
console.log(car.gas(30).mph); // => 30
console.log(car.brake(20).mph); // => 10
console.log(car.toggleParkingBrake().isParked()); // => true

仔細觀察上邊的代碼,能夠總結出下邊幾點:

  • 在設計構造器函數的時候,函數名第一個字母要大寫,建立對象時要使用new關鍵字
  • 函數內部對外暴露的屬性,方法使用this關鍵字
  • 函數內部能夠添加私有變量

最重要的是理解new Object()這一過程的原理,再次強調一下:

var a = {};
a.__proto__ = F.prototype;
F.call(a);

另外一種建立方式是使用字面量:

var myCar =  {
    isParkingBrakeOn: false,
    color: "black",
    direction: 0,
    mph: 0,

    gas: function gas(amount) {
        amount = amount || 10;
        this.mph += amount;
        return this;
    },

    brake: function brake(amount) {
        amount = amount || 10;
        this.mph = (this.mph - amount) < 0 ? 0 : this.mph - amount;
        return this;
    },

    toggleParkingBrake: function toggleParkingBrake() {
        this.isParkingBrakeOn = !this.isParkingBrakeOn;
        return this;
    },

    isParked: function isParked() {
        return this.isParkingBrakeOn;
    }
}

console.log(myCar.color); // => black
console.log(myCar.gas(30).mph); // => 30
console.log(myCar.brake(20).mph); // => 10
console.log(myCar.toggleParkingBrake().isParked()); // => true

代碼稍微作了改變,實現的效果如出一轍。有一個缺點是,不能使用私有變量,若是我要生成多個Car對象,須要反覆的寫上邊的代碼。那麼咱們應該如何批量的生產對象呢?答案就在下一個小結。

Factories (工廠方法)

咱們本篇討論的主要內容就是Object,上邊咱們已經提到了使用字面量的方式建立對象有一個最大的缺點就是沒法使用私有變量,咱們可使用工廠方法完美解決這個問題。

工廠方法本質上就是一個函數,函數的返回值就是咱們想要建立的對象。這個函數就像工廠一個樣可以批量生產出規格同樣的的產品。

var car = function car(color, direction, mph) {
    var  isParkingBrakeOn = false;
    return {
        color: "black",
        direction: 0,
        mph: 0,

        gas: function gas(amount) {
            amount = amount || 10;
            this.mph += amount;
            return this;
        },

        brake: function brake(amount) {
            amount = amount || 10;
            this.mph = (this.mph - amount) < 0 ? 0 : this.mph - amount;
            return this;
        },

        toggleParkingBrake: function toggleParkingBrake() {
            this.isParkingBrakeOn = !this.isParkingBrakeOn;
            return this;
        },

        isParked: function isParked() {
            return this.isParkingBrakeOn;
        }
    }

}

var myCar = car();

console.log(myCar.color); // => black
console.log(myCar.gas(30).mph); // => 30
console.log(myCar.brake(20).mph); // => 10
console.log(myCar.toggleParkingBrake().isParked()); // => true

咱們把以前的代碼稍做修改,就成了一個工廠方法,每當調用car()就會產生一個對象,這就是工廠方法,他相比於構造器的優點就在於不須要使用new關鍵字。

到目前爲止,咱們已經可使用3中方式建立對象了:

  • 構造器
  • 字面量
  • 工廠方法

還有一個好玩的事情就是在工廠方法中使用原型,必定要記住的一點是,新建的對象會繼承原型中的全部屬性和方法。

var carPrototype = {
        gas: function gas(amount) {
            amount = amount || 10; this.mph += amount; return this;
        },
        brake: function brake(amount) {
            amount = amount || 10;
            this.mph = ((this.mph - amount) < 0)? 0
                : this.mph - amount; return this;
        },
        color: 'pink',
        direction: 0,
        mph: 0
    },
    car = function car(options) {
        return extend(Object.create(carPrototype), options);
    },
    myCar = car({
        color: 'red'
    });

console.log(myCar.color);

上邊這種方式最大的優勢就是在建立對象時,能夠爲新建對象自由擴展屬性和方法,這主要得益於extend函數的做用。

JavaScript是一門動態語言,咱們可使用下邊的方法給carPrototype動態的擴展屬性和方法:

extend(carPrototype, {
    name: 'Porsche',
    color: 'black',
    mph: 220
});

Stamps

Stamps是一個JavaScript庫,他模仿了其餘面嚮對象語言中的類。一般咱們定義了一個類,類裏邊有屬性,方法,初始化方法,有時候某個屬性還多是另外一個類。咱們看看Stamps的示例代碼:

const MyStamp = stampit()       // create new empty stamp
.props({                        // add properties to your future objects
  myProp: 'default value'
})
.methods({                      // add methods to your future objects
  getMyProp() {
    return this.myProp;
  }
})
.init(function ({value}) {      // add initializers to your future objects
  this.myProp = value || this.myProp;
})
.compose(AnotherStamp);         // add other stamp behaviours to your objects

console.log(typeof MyStamp);                            // 'function'
console.log(MyStamp());                                 // { myProp: 'default value' }

console.log(typeof MyStamp().getMyProp);                // 'function'
console.log(MyStamp().getMyProp());                     // default value

console.log(MyStamp({value: 'new value'}));             // { myProp: 'new value' }
console.log(MyStamp({value: 'new value'}).getMyProp()); // new value

MyStamp的風格跟面向對象的類十分類似,謹記一點,MyStamp是一個函數,

// Some privileged methods with some private data.
const Availability = stampit().init(function() {
  var isOpen = false; // private

  this.open = function open() {
    isOpen = true;
    return this;
  };
  this.close = function close() {
    isOpen = false;
    return this;
  };
  this.isOpen = function isOpenMethod() {
    return isOpen;
  }
});

上邊的代碼建立了一個Availability對象,在設計上,咱們給這個對象提供了開和關這兩個方法,而且提供了一個獲取當前狀態的函數isOpen,var isOpen是一個私有變量,用於保存狀態信息。

// Here's a stamp with public methods, and some state:
const Membership = stampit({
  methods: {
    add(member) {
      this.members[member.name] = member;
      return this;
    },
    getMember(name) {
      return this.members[name];
    }
  },
  properties: {
    members: {}
  }
});

這段代碼中,咱們設計了一個會員管理類,提供了兩個公共的方法add getMember和一個公共的屬性members。這些東西在外部都是能夠訪問的。

// Let's set some defaults:
const Defaults = stampit({
  init({name, specials}) {
    this.name = name || this.name;
    this.specials = specials || this.specials;
  },
  properties: {
    name: 'The Saloon',
    specials: 'Whisky, Gin, Tequila'
  }
});

這段代碼爲了演示給對像一個默認值,init({name, specials})這行代碼我不太理解,{name, specials}這是什麼意思?不會報錯?

// Classical inheritance has nothing on this.
// No parent/child coupling. No deep inheritance hierarchies.
// Just good, clean code reusability.
const Bar = stampit(Defaults, Availability, Membership);

// Create an object instance
const myBar = Bar({name: 'Moe\'s'});

// Silly, but proves that everything is as it should be.
myBar.add({name: 'Homer'}).open().getMember('Homer');

重要的事情說三遍、重要的事情說三遍、重要的事情說三遍,使用stampit最大的優勢就是😊😊😊沒有繼承,徹底是組合的思想😊😊😊。

源碼等之後學的差很少了再研究一下。

總結

學html,css,js有段時間了,看的基本都是國外的書籍。我發現若是隻是讀一讀,不把內容寫成文章的話,很快就忘了。在學習新知識的同時也獲得了不少靈感,這對目前的主業也頗有幫助,

可是我有一個問題,哪裏有那種整個web的源碼呢?暫時定的目標是模仿一個網站。

相關文章
相關標籤/搜索