JS系列之如何創(chuàng)建對象

前言

俗話說“在js語言中,一切都對象”,而且創(chuàng)建對象的方式也有很多種,所以今天我們做一下梳理

最簡單的方式

JavaScript創(chuàng)建對象最簡單的方式是:對象字面量形式或使用Object構(gòu)造函數(shù)

對象字面量形式
var person = new Object();
    person.name = "jack";
    person.sayName = function () {
    alert(this.name)
}
使用Object構(gòu)造函數(shù)
var person = {
    name: "jack";
    sayName: function () {
      alert(this.name)
    }
}

明顯缺點:創(chuàng)建多個對象時,會出現(xiàn)代碼重復(fù),于是乎,‘工廠模式’應(yīng)運而生

工廠模式

通俗一點來理解工廠模式,工廠:“我創(chuàng)建一個對象,創(chuàng)建的過程全由我來負責(zé),但任務(wù)完成后,就沒我什么事兒了啊O(∩_∩)O哈哈~”

function createPerson (name) {
  var o = new Object();
  o.name = name;
  o.sayName = function () {
    alert(this.name)
  }
  return o
}

var p1 = new createPerson("jack");

明顯缺點:所有的對象實例都是Object類型,幾乎類型區(qū)分可言啊!你說無法區(qū)分類型,就無法區(qū)分啊,我偏不信!那咱們就來看代碼吧:

var p1 = new createPerson("jack");
var p2 = new createPerson("lucy");

console.log(p1 instanceof Object);  //true
console.log(p2 instanceof Object);  //true

你看,是不是這個理兒;所以為了解決這個問題,我們采用‘構(gòu)造函數(shù)模式’

構(gòu)造函數(shù)模式

構(gòu)造函數(shù)模式,就是這個函數(shù)我只管創(chuàng)建某個類型的對象實例,其他的我一概不管(注意到?jīng)]有,這里已經(jīng)有點類型的概念了,感覺就像是在搞小團體嘛)

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

function Animal (name) {
  this.name = name;
  this.sayName = function () {
    alert(this.name)
  }
}

var p1 = new Person("jack")
p1.sayName()  //"jack"

var a1 = new Animal("doudou")
a1.sayName()  //"doudou"

console.log(p1 instanceof Person)  //true
console.log(a1 instanceof Animal)  //true
console.log(p1 instanceof Animal)  //false(p1顯然不是Animal類型,所以是false)
console.log(a1 instanceof Person)  //false(a1也顯然不是Person類型,所以同樣是false)

上面這段代碼證明:構(gòu)造函數(shù)模式的確可以做到對象類型的區(qū)分。那么該模式是不是已經(jīng)完美了呢,然而并不是,我們來一起看看下面的代碼:

//接著上面的代碼
console.log(p1.sayName === a1.sayName)  //false

發(fā)現(xiàn)問題了嗎?p1sayName竟然和a1sayName不是同一個,這說明什么?說明‘構(gòu)造函數(shù)模式’根本就沒有‘公用’的概念,創(chuàng)建的每個對象實例都有自己的一套屬性和方法,‘屬性是私有的’,這個我們可以理解,但方法你都要自己搞一套,這就有點沒必要了
明顯缺點:上面已經(jīng)描述了,為了解決這個問題,又出現(xiàn)了一種新模式‘原型模式’,該模式簡直就是一個階段性的跳躍,下面我們來看分一下‘原型模式’

原型模式

這里要記住一句話:構(gòu)造函數(shù)中的屬性和方法在每個對象實例之間都不是共享的,都是各自搞一套;而要想實現(xiàn)共享,就要將屬性和方法存到構(gòu)造函數(shù)的原型中。這句話什么意思呢?下面我們來詳細解釋
當(dāng)建立一個構(gòu)造函數(shù)時(普通函數(shù)亦然),會自動生成一個prototype(原型),構(gòu)造函數(shù)與prototype是一對一的關(guān)系,并且此時prototype中只有一個constructor屬性(哪有,明明還有一個__proto__呢,這個我們先不在此討論,后面會有解釋)

WX20170527-205641@2x.png

這個constructor是什么?它是一個類似于指針的引用,指向該prototype的構(gòu)造函數(shù),并且該指針在默認的情況下是一定存在的

console.log(Person.prototype.constructor === Person)  //true

剛才說過prototype自動生成的,其實還有另外一種手動方式來生成prototype

function Person (name) {
  this.name = name
}
Person.prototype = {
  //constructor: Person,
  age: 30
}
console.log(Person.prototype)  //Object {age: 30}
console.log(Person.prototype.constructor === Person)  //false

Tips:為了證明的確可以為構(gòu)造函數(shù)手動創(chuàng)建prototype,這里給prototype加了name屬性。
可能你已經(jīng)注意到了一個問題,這行代碼:

console.log(Person.prototype.constructor === Person)  //false

結(jié)果為什么是false???大哥,剛才的prototype是默認生成的,然后我們又用了另外一種方式:手動設(shè)置。具體分析一下手動設(shè)置的原理:
1.構(gòu)造函數(shù)的prototype其實也是一個對象

WX20170527-212325@2x.png

2.當(dāng)我們這樣設(shè)置prototype時,其實已經(jīng)將原先Person.prototype給切斷了,然后又重新引用了另外一個對象

WX20170527-212837@2x.png

3.此時構(gòu)造函數(shù)可以找到prototype,但prototype找不到構(gòu)造函數(shù)了

Person.prototype = {
  //constructor: Person,  // 因為constructor屬性,我沒聲明啊,prototype就是利用它來找到構(gòu)造函數(shù)的,你竟然忘了聲明
  age: 30
}

4.所以,要想顯示手動設(shè)置構(gòu)造函數(shù)的原型,又不失去它們之間的聯(lián)系,我們就要這樣:

function Person (name) {
  this.name = name
}
Person.prototype = {
  constructor: Person,  //constructor一定不要忘了!!
  age: 30
}

畫外音:“說到這里,你還沒有講原型模式是如何實現(xiàn)屬性與方法的共享啊”,不要急,馬上開始:

對象實例-構(gòu)造函數(shù)-原型,三者是什么樣的關(guān)系呢?

WX20170527-214615@2x.png

WX20170527-215233@2x.png

看明白這張圖的意思嗎?
1.當(dāng)對象實例訪問一個屬性時(方法依然),如果它自身沒有該屬性,那么它就會通過__proto__這條鏈去構(gòu)造函數(shù)的prototype上尋找
2.構(gòu)造函數(shù)與原型是一對一的關(guān)系,與對象實例是一對多的關(guān)系,而并不是每創(chuàng)建一個對象實例,就相應(yīng)的生成一個prototype
這就是原型模式的核心所在,結(jié)論:在原型上聲明屬性或方法,可以讓對象實例之間共用它們

然后原型模式就是完美的嗎?并不是,它有以下兩個主要問題:
問題1:如果對象實例有與原型上重名的屬性或方法,那么,當(dāng)訪問該屬性或方法時,實例上的會屏蔽原型上的

function Person (name) {
  this.name = name
}
Person.prototype = {
  constructor: Person,
  name: 'lucy'
}
var p1 = new Person('jack');
console.log(p1.name);  //jack

問題2:由于實例間是共享原型上的屬性和方法的,所以當(dāng)其中一個對象實例修改原型上的屬性(基本值,非引用類型值或方法時,其他實例也會受到影響

WX20170527-222024@2x.png

原因就是,當(dāng)實例自身的基本值屬性與原型上的重名時,實例就會創(chuàng)建該屬性,留著今后自己使用,而原型上的屬性不會被修改;但如果屬性是引用類型值,如:ArrayObject,當(dāng)發(fā)生重名時,實例是不會拷貝一份新的留給自己使用的,還是堅持實例間共享,所以就會出現(xiàn)上圖中的情況

以上兩個問題就是原型模式的明顯缺點,為了改掉這些缺點,我們一般會采用一種組合模式“組合使用構(gòu)造函數(shù)模式和原型模式”,其實在原型模式這一節(jié),該模式已經(jīng)有所應(yīng)用了

組合使用構(gòu)造函數(shù)模式和原型模式

這種模式可謂是集構(gòu)造函數(shù)模式和原型模式之所長,用構(gòu)造函數(shù)模式來定義對象實例的屬性或方法,而共享的屬性或方法就交給原型模式

function Person (name) {
  this.name = name  //實例的屬性,在構(gòu)造函數(shù)中聲明
}

Person.prototype = {
  constructor: Person,
  sayName: function () {  //共享的方法存在原型中
    alert(this.name)
  }
}

注:此模式目前是ECMAScript中使用最廣泛、認同度最高的一種創(chuàng)建自定義類型的方法


下面要介紹的幾個模式是針對不同場景的,而不是說組合使用構(gòu)造函數(shù)模式和原型模式有什么缺點,又用這幾個模式來彌補,不是這樣的

動態(tài)原型模式

特點:共享的方法是在構(gòu)造函數(shù)中檢測并聲明的,原型并沒有被顯示創(chuàng)建

function Person (name) {
  this.name = name;
  if (typeof this.sayName !== 'function') {  //檢查方法是否存在
      console.log('sayName方法不存在')
      Person.prototype.sayName = function () {
      alert(this.name)
    }
  } else {
    console.log('sayName方法已存在')
  }
}

var p1 = new Person('jack');  //'sayName方法不存在'
p1.sayName(); //因為sayName不存在,我們來創(chuàng)建它,所以這里輸出'jack'
var p2 = new Person('lucy');  //'sayName方法已存在'
p2.sayName();  //這時sayName已存在,所以輸出'lucy'

當(dāng)Person構(gòu)造函數(shù)第一次被調(diào)用時,Person.prototype上就會被添加sayName方法;《Javascript高級程序設(shè)計》一書說到:使用動態(tài)原型模式時,不能使用對象字面量重寫原型。我們來理解一下:

WX20170529-123047@2x.png

分析:
1.p1實例創(chuàng)建,此時原型沒有sayName方法,那我們就為原型添加一個
2.隨后,我們以字面量的形式重寫了原型,這時舊的原型并沒有被銷毀,而且它和p1還保持著聯(lián)系
3.之后的實例,也就是這里的p2,都是與新原型保持聯(lián)系;所以p1、p2有各自的構(gòu)造器原型,即使它們的構(gòu)造器是同一個

WX20170529-122827@2x.png

所以切記:當(dāng)我們采用動態(tài)原型模式時,千萬不要以字面量的形式重寫原型

寄生構(gòu)造函數(shù)模式

了解此模式之前,我們先來想一個問題:構(gòu)造函數(shù)為什么要用new關(guān)鍵字調(diào)用?代碼說話:

WX20170530-173851@2x.png
WX20170530-174054@2x.png

我們發(fā)現(xiàn)什么?如果不是new方法調(diào)用構(gòu)造函數(shù),那么就要顯式的return,否則構(gòu)造函數(shù)就不會有返回值;但如果使用new,那就沒有這個問題了

下面我們再來看寄生構(gòu)造函數(shù)模式:

function Person (name) {
  var o = new Object();
  o.name = name;
  o.sayName = function () {
    alert(this.name)
  };
  return o
}

var p1 = new Person('jack');  //與工廠模式唯一不同之處:使用new調(diào)用
p1.sayName(); //jack

其實new不new都無所謂,因為我們已經(jīng)顯式的return o

WX20170531-204035@2x.png

那么寄生構(gòu)造函數(shù)模式到底有什么應(yīng)用場景呢?據(jù)《javascript高級程序設(shè)計》一書記載,舉例:如果我們想創(chuàng)建一個具有額外方法的特殊數(shù)組,那么我們可以這樣做:

function SpecialArray () {
  var values = new Array();
  Array.prototype.push.apply(values,arguments);
  values.toPipedString = function () {
    return this.join('|')
  }
  return values
}

var colors = new SpecialArray('red','blue','green');
alert(colors.toPipedString())  //'red|blue|green'

最后重要的一點:該模式和構(gòu)造函數(shù)和原型無緣,也就是不能區(qū)分實例類型,因為該模式生成的實例,它的構(gòu)造函數(shù)都是Object,原型都是Object.prototype

WX20170531-205208@2x.png

穩(wěn)妥構(gòu)造函數(shù)模式

該模式與寄生構(gòu)造函數(shù)相比,主要有兩點不同:
1.創(chuàng)建對象實例的方法不引用this
2.不使用new操作符調(diào)用構(gòu)造函數(shù)
按照穩(wěn)妥構(gòu)造函數(shù)的要求,可以將前面的Person構(gòu)造函數(shù)重寫如下:

function Person (name) {
  var o = new Object();
  o.sayName = function () {
    alert(name)  //這里其實涉及到了閉包的知識,因此產(chǎn)生了私有屬性的概念
  }
  return o
}

此模式最適合在一些安全的環(huán)境中(這些環(huán)境中會禁止使用this和new),同理,此模式與構(gòu)造函數(shù)和原型也無緣

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

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

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