/*
* @ javascript中沒有類的概念,所以基在對象創建方面與面向對象語言有所不同
* @ 對象創建的常用方法及各自的局限性
* @ 使用Object或對象字面量創建對象
* @ 工廠模式創建對象
* @ 構造函數模式創建對象
* @ 原型模式創建對象
* @ 構造與原型混合模式創建對象
*/
使用Object或對象字面量創建對象
/* * @ 使用Object或對象字面量創建對象 * @ 最基本的創建對象的方法 */ // 創建一個student對象, new一個Object var student = new Object(); student.name = "easy"; student.age = "20"; // 如果嫌這種方法麻煩, 有種封裝不良的感覺,試試字面量創建
對象字面量方式來創建對象
/* * @ 對象字面量方式來創建對象 * @ 缺點,無法重復 */ var student = { name: "easy", age: "20" } // 當我們要創建同類的student1, student2,...,studentN時, 不得不將以上的代碼重復N次 var student2 = { name: "easy2", age: "20" } // ... var studentn = { name: "easyn", age: "20" } // 這樣麻煩而且重復太多, 能不能像工廠間那樣, 有一個車床不斷生產出對象.
工廠模式創建對象
/* * @ 工廠模式創建對象 * @ JS中沒有類的概念, 那么我們不妨就使用一種函數將以上對象創建過程封裝起來以便於重復調用 * @ 同時可以給出特定接口來初始化對象 */ function createStudent(name, age){ var obj = new Object(); obj.name = name; obj.age = age; return obj; } var student1 = createStudent("easy1", 20); var student2 = createStudent("easy2", 20); // ... var studentn = createStudent("easyn", 20);
// 這樣就可以通過createStudent函數源源不斷地"生產"對象了, 看起來很完美了,但我們不僅希望"產品"的生產可以像工廠間一樣源源不斷,還想知道生產的產品究竟是哪一種類型
// 比如說: 我們同時又定義了"生產"水果對象的createFruit()函數 function createFruit(name, color){ var obj = new Object(); obj.name = name; obj.color = color; return obj; } var v1 = createStudent("easy1", 20); var v2 = createFruit("apple", "green"); console.log(v1 instanceof createStudent); // false console.log(v2 instanceof createFruit); // false console.log(v1 instanceof Object); // true console.log(v2 instanceof Object); // true // 如果我們用instanceof操作符去檢測, 他們統統都是Object類型
// 我們希望v1是student類型, 而v2是fruit類型, 那我們可以用構造函數來創建對象
/* * @ "構造函數" 和 "普通函數" 有什么區別 * @ 一.實際上並不存在創建構造函數的特殊語法, 構造函數就是一個普通函數, 與普通函數唯一的區別在於調用方法 * @ 二.對於任意函數,使用new操作符調用, 那么它就是構造函數; 不使用new操作符調用, 那么它就是普通函數 * @ 三.按照慣例,我們約定構造函數名以大寫字母開頭 * @ 四.使用new操作符調用構造函數時,會經歷4個階段 * @ 1.創建一個新對象 * @ 2.將構造函數作用賦給新對象(使用tihs指向該新對象) * @ 3.執行構造函數代碼 * @ 4.返回新對象 */
構造函數模式創建對象
// 將工廠模式函數重寫, 並添加一個方法屬性 function Student(name, age){ this.name = name; this.age = age; this.alertName = function(){ alert(this.name); } } function Fruit(name, color){ this.name = name; this.color = color; this.alertName = function(){ alert(this.name); } } // 分別new出Student和Fruit的對象 var v1 = new Student("easy", 20); var v2 = new Fruit("apple", "green"); // 這樣就可以用instanceof操作符來檢測以上對象類型區分出不同的對象 console.log(v1 instanceof Student); // true console.log(v2 instanceof Student); // false console.log(v1 instanceof Fruit); // false console.log(v2 instanceof Fruit); // true console.log(v1 instanceof Object); // true console.log(v2 instanceof Object); // true
// 這樣解決了工廠模式無法區分對象類型, 但這樣就完美了嗎, 在JS中,函數是對象, 當我們實例化不只一個Strudent對象的時候
var v1 = new Student("easy1", 20); var v2 = new Student("easy2", 20); //... var vn = new Student("easyn", 20); // 其中共同的alertName()函數也被實例化了n次, 我們可以用以下方法來檢測不同的Strudent對象並不共用alertName()函數 console.log(v1.alertName == v2.alertName); // false
// 這無疑是一種內存的浪費,我們知道this對象是在運行時基於函數的執行環境進行綁定. 在全局函數中,this對象等同於window, 在對象方法中,this指向該對象
// 試將對象方法移到構造函數外部 function Student(name, age){ this.name = name; this.age = age; this.alertName = alertName; } function alertName(){ alert(this.name); } var stu1 = new Student("easy1", 20); var stu2 = new Student("easy2", 20); // 在調用"stu1.alert()時", this對象才被綁定到stu1上, 通過alertName()函數定義為全局函數, 這樣對象中的alertName屬性則被設置為指向該全局的指針, 由此stu1和stu2共享了該全局函數, 解決了內在浪費的問題
// 但是,通過全局函數的方式解決對象內部共享的問題, 終究不像一個好的解決方法, 更好的方案是通過原型對象模式來解決
原型模式創建對象
/* * 原型模式創建對象 * @ 函數的原型對象 * @ 對象實例和原型對象的關聯 * @ 使用原型模型創建對象 * @ 原型模型創建對象的局限性 */ /* * 函數的原型對象 * @ 每一個函數都有一個prototype屬性, 該屬性是一個指針,該指針指向了一個對象 * @ 創建的構造函數, 該對象中包含可以由所有實例共享的屬性和方法 * @ 在默認情況下,所有原型對象會自動包含一個constructor屬性, 該屬性也是一個指針, 指向prototype所在的函數 */ /* * 對象實例和原型對象的關聯 * @ 在調用構造函數創建新的實例時, 該實例的內部會自動包含一個[[Prototype]]屬性, 該指針指向構造函數的原型對象, 指針關聯的是"實例與構造函數的原型對象" 而不是"實例與構造函數" */
使用原型模型創建對象
/* * 使用原型模型創建對象 * @ 直接在原型對象中添加屬性和方法 */ function Student(){ } Student.prototype.name = "easy"; Student.prototype.age = 20; Student.prototype.alertName = function(){ alert(this.name); } var stu1 = new Student(); var stu2 = new Student(); // true 二者共享同一個函數 console.log(stu1.alertName == stu2.alertName); // 我們在Strudent的prototype對象添加了name, age屬性以及alertName方法, 但創建的stu1和stu2中並不包含name, age屬性以及alertName()方法, 只包含一個[[prototype]]指針屬性, 當我們調用stu1.name或stu1.alertName時, 是如何找到對應的屬性和方法的呢? // 當我們需要讀取對象的某個屬性時, 都會執行一次搜索,首先在該對象中查找該屬性, 若找到,返回該屬性, 否則, 到[[prototype]] 指向的原型對象中繼續查找 // 看出另一層意思是: 如果對象實例中包含和原型對象中同名的屬性或方法, 則對象實例中的該同名屬性或方法會屏蔽原型對象中的同名屬性或方法, 原因就是"首先在該對象中查找該屬性, 若找到,返回該屬性值"
通過對象字面量重寫原型對象
function Student(){} Student.prototype = { constructor: Student, name: "easy", age: 20, alertName: function(){ alert(this.name); } } // 要特別注意,我們這里相當於用對象字面量重新創建了一個Object對象,然后使Student的prototype指針指向該對象。該對象在創建的過程中,自動獲得了新的constructor屬性,該屬性指向Object的構造函數。因此,我們在以上代碼中,增加了constructor : Student使其重新指回Student構造函數。
// 原型模型創建對象的局限性
// 原型模型在對象實例共享數據方面給我們帶來了很大的便利, 但通常情況下不同的實例會希望擁有屬性自己的單獨的屬性, 我們將構造函數的模型和原型模型結合使用即可兼得數據共享和"不共享"
構造與原型混合模式創建對象
/* * 構造與原型混合模式創建對象 * 我們結合原型模式在共享方法屬性以及構造函數模式在實例方法屬性方面的優勢,使用以下的方法創建對象 * 在構造函數中定義實例屬性,在原型中定義共享屬性的模式,是目前使用最廣泛的方式。通常情況下,我們都會默認使用這種方式來定義引用類型變量。 */ // 我們希望每個stu擁有屬於自己的name和age屬性 function Student(name, age){ this.name = name; this.age = age; } // 所有的stu應該共享一個alertName()方法 Student.prototype = { constructor: Student, alertName: function(){ alert(this.name); } } var stu1 = new Student("Jim", 20); var stu2 = new Student("Tom", 21); stu1.alertName(); stu2.alertName(); alert(stu1.alertName == stu2.alertName); //true 共享函數