前言
作為一名前端開發者,JavaScript中對象創建是很重要。在JavaScript這門基于原型的語言中,對象幾乎無處不在。今天,我將帶領大家回顧JavaScript對象創建的7種方式,從最原始的字面量到現代的ES6 class,每一步演進都解決了前一步的痛點,這就像是JavaScript語言設計的進化史。
1. 對象字面量:最原始的創建方式
代碼示例
// 使用字面量進行創建
// 通過字面量創建的對象,都會有原型開銷,并且他們的原型都是指向Object.prototype
var Sheep = {name: "小羊",school: "青青草原",age: 18
}// 驗證原型鏈
const sheepPrototype = Object.getPrototypeOf(Sheep)
console.log(sheepPrototype === Object.prototype) // 輸出: true
深入講解
最初接觸JavaScript時,對象字面量是我學會的第一種創建對象的方式。它的創建方式很直觀,你需要什么屬性就直接寫什么屬性。
原型鏈解析:
// 深入理解字面量對象的原型
const myObj = { a: 1 }
console.log(myObj.__proto__ === Object.prototype) // true
console.log(myObj.hasOwnProperty('a')) // true - 來自Object.prototype
console.log(myObj.toString()) // "[object Object]" - 來自Object.prototype
優缺點分析
優點:
- 語法簡潔直觀,易于理解和使用
- 適合創建單個、獨特的對象
- 創建速度快,無需額外的函數調用
缺點:
- 無法實現代碼復用,每個對象都需要重新定義
- 不適合創建大量相似的對象
- 缺乏封裝性,所有屬性都是公開的
特點:
對象字面量就像是"手工定制",每個對象都是獨一無二的藝術品。但當我需要創建100只羊時,難道要寫100次相同的代碼嗎?這讓我意識到,我們需要一種"批量生產"的方式。
2. 工廠模式:批量生產的開始
代碼示例
// 使用工廠模式創建對象 - 避免了new關鍵字,并且具有封裝性
function Sheep(name, age) {if (name === '喜羊羊') {return { name, age, feature: ['聰明'],eat: function() {console.log(`${this.name}在吃青草`)}}} else {return { name, age, feature: ['貪吃鬼'],eat: function() {console.log(`${this.name}在吃零食`)}}}
}const sheep1 = Sheep('喜羊羊', 3)
const sheep2 = Sheep('懶羊羊', 4)// 類型檢測問題
console.log(sheep1 instanceof Object); // true (所有對象都是Object的實例)
console.log(sheep1 instanceof Sheep); // false! 無法識別具體類型
深入講解
工廠模式是我第一次體會到"函數即工廠"的概念。把它想象成一個制造羊的工廠,你告訴工廠想要什么樣的(參數),工廠就給你制造出來(返回對象)。這種模式能夠根據不同的輸入條件,創建不同特性的對象。
方法重復問題演示:
function createSheep(name) {return {name: name,eat: function() { // 每次調用都創建新函數console.log('吃草')}}
}const s1 = createSheep('羊1')
const s2 = createSheep('羊2')
console.log(s1.eat === s2.eat) // false - 方法沒有復用!
優缺點分析
優點:
- 解決了代碼復用問題,可以批量創建對象
- 可以根據參數動態決定對象的屬性
- 隱藏了對象創建的細節,提供了一定的封裝性
缺點:
- 無法識別對象的具體類型(都是Object的實例)
- 每個對象都包含相同的方法副本,造成內存浪費
- 沒有利用原型鏈,無法實現真正的繼承
核心特點與反思
工廠模式就像一個"代工廠",能批量生產但產品上沒有品牌標識。我開始思考:如何讓創建的對象能夠被識別出"出身"?這就引出了構造函數的概念。
3. 構造函數模式:引入類型標識
代碼示例
// 使用構造函數創建對象 - 使用new的好處是模板化、高效率、類型識別和封裝
// 在構造函數中創建的方法在每次創建對象的時候,都會重新創建一次方法
function Sheep(name, age) {this.name = name;this.age = age;this.eat = function() {console.log(`${age}歲的${name}在吃草`)}
}const sheep1 = new Sheep('喜羊羊', 3)
const sheep2 = new Sheep('沸羊羊', 4)// 類型識別成功
console.log(sheep1 instanceof Sheep) // true
console.log(sheep1.constructor === Sheep) // true// 但方法依然沒有復用
console.log(sheep1.eat === sheep2.eat) // false - 每個實例都有獨立的eat方法
深入講解
構造函數讓我第一次感受到JavaScript中"類"的概念(雖然ES6之前沒有真正的類)。通過new關鍵字,仿佛在說:“請按照這個藍圖(構造函數)給我制造一個對象”。new操作符背后發生了什么?
new操作符的內部機制:
// new操作符的模擬實現
function myNew(Constructor, ...args) {// 1. 創建一個新對象const obj = {}// 2. 將新對象的原型指向構造函數的prototypeobj.__proto__ = Constructor.prototype// 3. 將構造函數的this綁定到新對象上并執行const result = Constructor.apply(obj, args)// 4. 如果構造函數返回對象,則返回該對象;否則返回新創建的對象return result instanceof Object ? result : obj
}// 使用示例
const mySheep = myNew(Sheep, '美羊羊', 3)
console.log(mySheep instanceof Sheep) // true
解決方法重復的嘗試:
// 解決辦法:將方法綁定到構造函數的原型上
function Sheep(name, age) {this.name = name;this.age = age;
}Sheep.prototype.eat = function() {console.log(`${this.age}歲的${this.name}在吃草`)
}const sheep1 = new Sheep('喜羊羊', 3)
const sheep2 = new Sheep('沸羊羊', 4)
console.log(sheep1.eat === sheep2.eat) // true - 方法復用成功!
優缺點分析
優點:
- 解決了對象類型識別問題(instanceof可以正確判斷)
- 代碼結構更清晰,符合面向對象的思維
- 可以通過prototype添加共享方法
缺點:
- 方法定義在構造函數內部時,每個實例都會創建方法的副本
- 內存利用率低,相同的方法被重復創建
核心特點與反思
構造函數就像給產品打上了"品牌標簽",但每個產品都自帶了一套完整的"使用說明書"(方法),這顯然是一種浪費。需要找到一種方式,讓所有同類產品共享同一份"說明書"。
4. 原型模式:共享的力量
代碼示例
// 原型模式 - 所有屬性和方法都定義在原型上
function Sheep() {}// 所有屬性和方法都定義在原型上
Sheep.prototype.name = '陽光中學';
Sheep.prototype.age = 3;
Sheep.prototype.feature = ['聰明'] // 引用類型屬性
Sheep.prototype.eat = function() {console.log('我正在吃青春蛋糕~');
};const sheep1 = new Sheep()
const sheep2 = new Sheep()// 方法共享成功
console.log(sheep1.eat === sheep2.eat) // true// 但引用類型屬性共享帶來了問題
sheep1.feature.push('玩游戲')
console.log(sheep2.feature) // ['聰明', '玩游戲'] - 意外修改了所有實例!
深入講解
原型模式讓我真正理解了JavaScript的精髓:原型鏈。把原型想象成一個"公共倉庫",所有實例都可以從這個倉庫中獲取方法和屬性。這就像一個家族的"傳家寶",所有家族成員都能使用,但不能據為己有。
原型鏈查找機制:
// 深入理解原型鏈查找
const s = new Sheep()// 屬性查找順序演示
console.log(s.hasOwnProperty('name')) // false - name不是實例自有屬性
console.log('name' in s) // true - 但能通過原型鏈找到// 原型鏈:s -> Sheep.prototype -> Object.prototype -> null
console.log(s.__proto__ === Sheep.prototype) // true
console.log(Sheep.prototype.__proto__ === Object.prototype) // true
console.log(Object.prototype.__proto__ === null) // true
優缺點分析
優點:
- 完美解決了方法共享問題,內存利用率高
- 原型鏈機制支持屬性和方法的查找
- 所有實例共享原型上的屬性和方法
缺點:
- 引用類型的屬性被所有實例共享,容易造成意外修改
- 無法在創建實例時傳遞初始化參數
- 所有實例的屬性初始值都相同
核心特點與反思
純原型模式就像"共產主義",所有資源都是共享的。有些東西(比如個人名字)應該是私有的,有些東西(比如吃飯的方法)可以是共享的。這讓我意識到,我需要一種"混合經濟體制"。
5. 組合模式:完美的平衡
代碼示例
// 組合模式(構造函數+原型) - ES6 class出現之前的最佳實踐
// 獨立屬性+通用方法
function Sheep(name, age) {// 實例屬性 - 每個實例獨有this.name = name;this.age = age;this.friends = []; // 引用類型也是獨有的
}// 共享方法 - 定義在原型上
Sheep.prototype.eat = function() {console.log(`${this.age}歲的${this.name}在吃草`)
}Sheep.prototype.addFriend = function(friendName) {this.friends.push(friendName)
}const sheep1 = new Sheep('喜羊羊', 3)
const sheep2 = new Sheep('沸羊羊', 4)// 驗證方法共享
console.log(sheep1.eat === sheep2.eat) // true// 驗證屬性獨立
sheep1.addFriend('美羊羊')
console.log(sheep1.friends) // ['美羊羊']
console.log(sheep2.friends) // [] - 不受影響
深入講解
組合模式是最優雅的解決方案(在ES6之前)。它采用了"各司其職"的策略:構造函數負責定義實例屬性(每個對象的"個性"),原型負責定義方法(所有對象的"共性")。這就像現代社會的分工合作,效率最高。
深入理解組合模式的優勢:
// 組合模式的靈活性展示
function Animal(type) {this.type = type;this.energy = 100;
}// 可以動態添加原型方法
Animal.prototype.sleep = function() {this.energy += 20;console.log(`${this.type}睡覺后,能量恢復到${this.energy}`)
}// 可以覆蓋原型方法
Animal.prototype.toString = function() {return `[Animal: ${this.type}]`
}const cat = new Animal('貓')
console.log(cat.toString()) // [Animal: 貓]
優缺點分析
優點:
- 完美解決了共享和獨立的平衡問題
- 每個實例有自己的屬性副本,方法則共享
- 支持向構造函數傳遞參數
- 是ES6 class出現之前的最佳實踐
缺點:
- 需要分別管理構造函數和原型
- 代碼分散在兩個地方,不夠聚合
核心特點與反思
組合模式讓我理解了"取長補短"的智慧。它就像一個成熟的企業:有私有財產(實例屬性),也有公共設施(原型方法)。但我還在想,有沒有更靈活的方式來控制對象的創建和繼承?
6. Object.create():精確控制原型鏈
代碼示例
// Object.create() - 可以顯式指定原型對象// 示例1:顯式指定原型
const Sheep = {name: '喜羊羊',eat: function() {console.log('我愛吃飯')}
}const sheep = Object.create(Sheep)
sheep.name = '懶羊羊' // 覆蓋原型上的name
console.log(sheep.name) // 懶羊羊
console.log(Object.getPrototypeOf(sheep) === Sheep); // true// 示例2:創建無原型對象 - Object.prototype也不繼承
const sheep1 = Object.create(null);
const sheep2 = {};// 檢查原型
console.log(Object.getPrototypeOf(sheep1)); // null
console.log(Object.getPrototypeOf(sheep2)); // [Object: null prototype] {} (即Object.prototype)// 嘗試調用基礎方法
console.log(typeof sheep1.toString); // "undefined"
console.log(typeof sheep2.toString); // "function"// 示例3:實現對象的淺克隆,保持原型鏈不變
function shallowClone(original) {const clone = Object.create(Object.getPrototypeOf(original));Object.assign(clone, original);return clone;
}// 克隆無原型對象
const sheep1 = Object.create(null);
sheep1.name = '喜羊羊';const clonedSheep1 = shallowClone(sheep1);
console.log(clonedSheep1.name); // 喜羊羊
console.log(clonedSheep1 === sheep1); // false
console.log(Object.getPrototypeOf(clonedSheep1)); // null
console.log(typeof clonedSheep1.toString); // undefined// 克隆標準對象
const sheep2 = {};
sheep2.name = '懶羊羊';const clonedSheep2 = shallowClone(sheep2);
console.log(clonedSheep2.name); // 懶羊羊
console.log(clonedSheep2 === sheep2); // false - 是一個全新的、獨立的對象
console.log(Object.getPrototypeOf(clonedSheep2) === Object.prototype); // true
console.log(typeof clonedSheep2.toString); // function
深入講解
Object.create()對原型鏈的"完全掌控"。能夠精確地指定一個對象的原型,甚至可以創建一個"無根之木"(沒有原型的對象)。
Object.create()的高級用法:
// 使用第二個參數定義屬性描述符
const sheepPrototype = { eat: function() { console.log('吃草') } }
const animal = Object.create(sheepPrototype, {age: {value: 3,writable: true,enumerable: true,configurable: true},id: {value: Math.random(),writable: false, // 只讀屬性enumerable: false // 不可枚舉}
})console.log(animal.age) // 3
animal.age = 4 // 可以修改
console.log(animal.age) // 4animal.id = 999 // 嘗試修改只讀屬性
console.log(animal.id) // 仍然是原來的隨機數// 枚舉測試
for(let key in animal) {console.log(key) // 只會打印age和原型上的屬性,不會打印id
}
優缺點分析
優點:
- 提供了對原型鏈的精確控制
- 可以創建真正的"純凈"對象(無原型)
- 支持屬性描述符,可以定義只讀、不可枚舉等特性
- 是實現繼承的底層機制
缺點:
- 語法相對復雜,不夠直觀
- 需要手動設置構造函數
- 對于簡單場景來說過于底層
核心特點與反思
Object.create()就像是對象創建的"原子操作",它讓我看到了JavaScript對象系統的底層機制。但對于日常開發,希望有更簡潔、更符合傳統OOP思維的語法。
7. ES6 Class:現代化的語法糖
代碼示例
// ES6 Class - class中定義的方法,不是綁定到構造函數本身,
// 而是被自動添加到了構造函數的prototype(原型)對象上
class Sheep {constructor(name, age) {// 實例屬性this.name = name;this.age = age;this.friends = [];}// 實例方法 - 自動添加到prototypesayHello() {console.log(`Hello, my name is ${this.name}`);}eat() {console.log(`${this.name}在吃草`);}addFriend(friend) {this.friends.push(friend);}// 靜態方法static compare(sheep1, sheep2) {return sheep1.age - sheep2.age;}// getterget info() {return `${this.name} (${this.age}歲)`;}// setterset info(value) {[this.name, this.age] = value.split(',');}
}// 使用
const sheep1 = new Sheep('喜羊羊', 3);
const sheep2 = new Sheep('美羊羊', 2);// 驗證方法共享
console.log(sheep1.sayHello === sheep2.sayHello); // true// 使用靜態方法
console.log(Sheep.compare(sheep1, sheep2)); // 1// 使用getter/setter
console.log(sheep1.info); // 喜羊羊 (3歲)
sheep1.info = '懶羊羊,5';
console.log(sheep1.name); // 懶羊羊
console.log(sheep1.age); // "5"
深入講解
ES6 Class它本質上還是基于原型的,但語法上更接近傳統的面向對象語言。把它理解為一個"語法糖",背后還是我們熟悉的原型機制。
Class本質的揭示:
// Class本質上還是函數
console.log(typeof Sheep) // "function"// Class定義的方法在prototype上
console.log(Sheep.prototype.sayHello) // [Function: sayHello]// 用傳統方式實現同樣的效果
function TraditionalSheep(name, age) {this.name = name;this.age = age;
}TraditionalSheep.prototype.sayHello = function() {console.log(`Hello, my name is ${this.name}`);
};// 驗證:方法不屬于實例的自有屬性,而是在prototype上
const sheep = new Sheep("喜羊羊", 2)
console.log(sheep.hasOwnProperty('sayHello')); // false
console.log(sheep.__proto__.hasOwnProperty('sayHello')); // true
Class的高級特性:
// 繼承
class SmartSheep extends Sheep {constructor(name, age, iq) {super(name, age); // 調用父類構造函數this.iq = iq;}// 方法重寫eat() {super.eat(); // 調用父類方法console.log('...一邊吃一邊思考');}// 新方法solve() {console.log(`${this.name}解決了問題!`);}
}const smartSheep = new SmartSheep('喜羊羊', 3, 150);
smartSheep.eat();
// 喜羊羊在吃草
// ...一邊吃一邊思考
優缺點分析
優點:
- 語法清晰、簡潔、易于理解
- 更好的代碼組織,所有相關代碼在一個地方
- 原生支持繼承、靜態方法、getter/setter
- 符合其他編程語言的OOP習慣
缺點:
- 本質還是原型,可能給其他語言背景的開發者造成誤解
- 不支持私有屬性(雖然有提案)
- 必須使用new調用(不像普通函數那樣靈活)
核心特點與反思
ES6 Class就像給JavaScript穿上了一件"現代化的外衣",讓它看起來更像Java或C++。這件外衣下面,還是JavaScript獨特的原型鏈機制。理解這一點,對深入掌握JavaScript至關重要。
總結:7種方式的橫向對比
創建方式 | 代碼復用 | 類型識別 | 內存效率 | 參數傳遞 | 繼承支持 | 語法復雜度 | 適用場景 |
---|---|---|---|---|---|---|---|
對象字面量 | ? | ? | ? | ? | ? | 簡單 | 創建單個對象 |
工廠模式 | ? | ? | ? | ? | ? | 簡單 | 批量創建相似對象 |
構造函數 | ? | ? | ? | ? | 部分 | 中等 | 需要類型識別的場景 |
原型模式 | ? | ? | ? | ? | ? | 中等 | 方法共享為主的場景 |
組合模式 | ? | ? | ? | ? | ? | 中等 | ES5最佳實踐 |
Object.create | ? | 部分 | ? | 部分 | ? | 復雜 | 需要精確控制原型鏈 |
ES6 Class | ? | ? | ? | ? | ? | 簡單 | 現代JavaScript開發 |
通過這7種對象創建方式的學習,深刻理解了JavaScript語言設計的演進。每一種方式都有其存在的價值和適用場景。作為開發者,我們不應該盲目追求"最新"或"最好",而應該根據實際需求選擇最合適的方案。