JavaScript 中創(chuàng)建對象的方式有很多,比如對象字面量模式或者使用 Object 創(chuàng)建:
// 創(chuàng)建 obj1 對象
let obj1 = {
name:"",
showName(){ return this.name }
}
// 創(chuàng)建 obj2 對象
let obj2 = new Object()
obj2.name = ""
obj2.showName = function(){ return this.name }
使用這兩種方式(特別是對象字面量方式)創(chuàng)建對象十分方便,可以拿來即用。但也有一些缺點:
- 過程過于繁瑣,如果需要創(chuàng)建多個對象,就需要書寫多次創(chuàng)建代碼
- 封裝性不夠,因為按照常規(guī)理念, 對象應該由一個公共的接口(類、函數)來進行統一創(chuàng)建,需要創(chuàng)建對象時,直接初始化某個類或者調用函數來進行創(chuàng)建。
上面的兩個缺陷都指向了一點:我們需要一個函數(類)來進行對象創(chuàng)建,基于這個理念,出現了使用工廠模式來創(chuàng)建對象的方式。
工廠模式
工廠模式很簡單,對原料(原生 Object 對象)進行一些加工(參數),然后返回一個產品(被加工后的對象)。
function createPerson(name,age){
// 創(chuàng)建一個原生對象
let o = new Object()
o.name = name;
o.age = age
return o;
}
如果我們需要創(chuàng)建某個對象,只需調用相應的工廠函數:
let p1 = createPerson("MIKE",20)
let p2 = createPerson("JACK",22)
工廠模式的缺點
工廠模式的解決了批量創(chuàng)建對象的問題,但也有一個明顯的缺點:沒有“類”的概念,除了能夠批量創(chuàng)建對象,無法對這些對象進行判斷,無法知道這些對象是由誰(類)創(chuàng)建出來的?;谶@個問題,出現了構造函數模式。
構造函數模式
函數是 JavaScript 中的一等公民,可以做很多事情,其中有一項功能就是可以被 new 操作符調用。在 ES6 之前,JavaScript 中是沒有 class 關鍵字的,于是有了通過 new 操作符來調用函數創(chuàng)建一個對象的方式,很明顯,通過 new 操作符調用的函數就是所謂的“類”。
function Person(name,age){
this.name = name;
this.age = age;
this.intro = function(){
console.log(`name:${this.name},age:${this.age}`)
}
}
接下來通過 Person 類來創(chuàng)建對象:
let p1 = new Person("MIKE",20)
let p2 = new Person("JACK","22")
構造函數中的 this 關鍵字就指向了當前被創(chuàng)建的對象。
通過這種方式創(chuàng)建對象以后,我們就可以知道對象是被哪個“類”創(chuàng)建的了。
p1 instanceof Person //true
p1 instanceof Object //true
p1.constructor === Person //true
當對象被創(chuàng)建后,其會擁有一個 constructor 屬性,指向其的構造函數。不過由于 JavaScript 太靈活了,constructor 屬性是可以被修改的,因此通過 constructor 來對對象的類進行判斷是不準確的,使用
instanceof 操作符更加可靠。
p1.constructor = Array
p1.constructor // Array
p1.constructor === Person //false
p1 instanceof Person // true
構造函數創(chuàng)建對象的流程
使用構造函數創(chuàng)建對象,大概有如下幾個流程:
- 創(chuàng)建一個新對象
- 將構造函數的作用域賦值給這個對象(因此
this就指向了這個對象) - 執(zhí)行構造函數中的代碼,為對象添加屬性方法
- 函數執(zhí)行完畢,對象被銷毀
構造函數作為函數
構造函數本身也是函數,因此其可以作為函數調用,由于構造函數中使用 this 關鍵字,我們可以通過構造函數為某個對象進行賦值。this 為哪個對象賦值,決定于這個函數執(zhí)行時的上下文。
function Person(name,age){
this.name = name;
this.age = age;
this.intro = function(){
console.log(`name:${this.name},age:${this.age}`)
}
}
let p1 = Person("MIKE","20")
p1 //undefined
window.name //"MIKE"
window.age //"20"
如果不指定函數的上下文,默認為 window 對象,因此 Person 函數執(zhí)行時,為 window 對象添加了屬性和方法。
let o = {}
Person.call(o,"JACK","22")
o //{name:"JACK",age:"22"}
這里明確指定函數的上下文為對象 o 后,調用 Person 函數就為 o 對象添加屬性方法了。
構造函數模式的問題
使用構造函數模式創(chuàng)建對象看起來是個不錯的方式,但這樣有沒有缺陷呢?也是有的。這個缺陷就在于每次使用構造函數創(chuàng)建對象時,都會為每個對象重新創(chuàng)建一份屬性和方法的副本,無法實現復用。
為什么會這樣呢?因為構造函數本質也是一個函數,函數在運行時會有一個獨立的作用域,創(chuàng)建多個對象時會多次調用構造函數,并把這些函數的作用域賦值給對象,然后為對象添加屬性。但是這些函數的作用域是獨立的,因此我們在構造函數體內所做的任何變量聲明、函數聲明,都會在運行時重新創(chuàng)建一次,然后添加到對象上。
function Person(name,age){
this.name = name;
this.age = age;
this.intro = function(){
console.log(`name:${this.name},age:${this.age}`)
}
}
let p1 = new Person("MIKE","20")
let p2 = new Person("JACK","22")
p1.intro === p2.intro // false
以上就是使用構造函數創(chuàng)建對象無法實現復用的原因,既然無法復用的原因是由于在獨立作用于中創(chuàng)建變量和函數,那我們把這些變量和函數放到函數的獨立作用于之外不就可以了?確實如此。
function Person(name,age){
this.name = name
this.age = age
this.intro = intro
}
function intro(){
console.log(`name:${this.name},age:${this.age}`)
}
let p1 = new Person("MIKE","20")
let p2 = new Person("JACK","22")
p1.intro === p2.intro // true
上面的 intro 方法就實現了代碼復用,節(jié)約了資源。但這種方式也是有問題的:增加了全局變量,而且破壞了封裝性。后面將會介紹更多創(chuàng)建對象的方法,一步步進行完善。
完。