摘要:除了以上介紹的幾種對象創建方式,此外還有寄生構造函數模式穩妥構造函數模式。
"面向對象" 是以 "對象" 為中心的編程思想,它的思維方式是構造。
"面向對象" 編程的三大特點:"封裝、繼承、多態”:
封裝:屬性方法的抽象
繼承:一個類繼承(復制)另一個類的屬性/方法
多態:方法(接口)重寫
"面向對象" 編程的核心,離不開 "類" 的概念。簡單地理解下 "類",它是一種抽象方法。通過 "類" 的方式,可以創建出多個具有相同屬性和方法的對象。
但是!但是!但是JavaScript中并沒有 "類" 的概念,對的,沒有。
ES6 新增的 class 語法,只是一種模擬 "類" 的語法糖,底層機制依舊不能算是標準 "類" 的實現方式。
在理解JavaScript中如何實現 "面向對象" 編程之前,有必要對JavaScript中的對象先作進一步地了解。
什么是對象對象是"無序屬性"的集合,表現為"鍵/值對"的形式。屬性值可包含任何類型值(基本類型、引用類型:對象/函數/數組)。
有些文章指出"JS中一切都是對象",略有偏頗,修正為:"JS中一切引用類型都是對象"更為穩妥些。
函數 / 數組都屬于對象,數組就是對象的一種子類型,不過函數稍微復雜點,它跟對象的關系,有點"雞生蛋,蛋生雞"的關系,可先記住:"對象由函數創建"。
簡單對象的創建字面量聲明(常用)
new 操作符調用 Object 函數
// 字面量 let person = { name: "以樂之名" }; // new Object() let person = new Object(); person.name = "以樂之名";
以上兩種創建對象的方式,并不具備創建多個具有相同屬性的對象。
TIPS:new 操作符會對所有函數進行劫持,將函數變成構造函數(對函數的構造調用)。
對象屬性的訪問方式. 操作符訪問 (也稱 "鍵訪問")
[] 操作符訪問(也稱 "屬性訪問")
. 操作符 VS [] 操作符:. 訪問屬性時,屬性名需遵循標識符規范,兼容性比 [] 略差;
[] 接受任意UTF-8/Unicode字符串作為屬性名;
[] 支持動態屬性名(變量);
[] 支持表達式計算(字符串連接 / ES6的Symbol)
TIPS: 標識符命名規范 —— 數字/英文字母/下劃線組成,開頭不能是數字。
// 任意UTF-8/Unicode字符串作為屬性名 person["$my-name"]; // 動態屬性名(變量) let attrName = "name"; person[attrName]; // 表達式計算 let attrPrefix = "my_"; person[attrPrefix + "name"]; // person["my_name"] person[Symbol.name]; // Symbol在屬性名的應用屬性描述符
ES5新增 "屬性描述符",可針對對象屬性的特性進行配置。
屬性特性的類型Configurable 可配置(可刪除)?[true|false]
Enumerable 可枚舉 [true|false]
Writable 可寫? [true|false]
Value 值?默認undefined
Get [[Getter]] 讀取方法
Set [[Setter]] 設置方法
訪問器屬性優先級高于數據屬性
訪問器屬性會優于 writeable/value
獲取屬性值時,如果對象屬性存在 get(),會忽略其 value 值,直接調用 get();
設置屬性值時,如果對象屬性存在 set(),會忽略 writable 的設置,直接調用 set();
訪問器屬性日常應用:
屬性值聯動修改(一個屬性值修改,會觸發另外屬性值修改);
屬性值保護(只能通過 set() 制定邏輯修改屬性值)
定義屬性特性Object.defineProperty() 定義單個屬性
Object.defineProperties() 定義多個屬性
let Person = {}; Object.defineProperty(Person, "name", { writable: true, enumerable: true, configurable: true, value: "以樂之名" }); Person.name; // 以樂之名
TIPS:使用 Object.defineProperty/defineProperties 定義屬性時,屬性特性 configurable/enumerable/writable 值默認為 false,value 默認為 undefined。其它方式創建對象屬性時,前三者值都為 true。
可使用Object.getOwnPropertyDescriptor() 來獲取對象屬性的特性描述。
原型JavaScript中模擬 "面向對象" 中 "類" 的實現方式,是利用了JavaScript中函數的一個特性(屬性)——prototype(本身是一個對象)。
每個函數默認都有一個 prototype 屬性,它就是我們所說的 "原型",或稱 "原型對象"。每個實例化創建的對象都有一個 __proto__ 屬性(隱式原型),它指向創建它的構造函數的 prototype 屬性。
new + 函數(實現"原型關聯")let Person = function(name, age) { this.name = name; this.age = age; }; Person.prototype.say = function() {}; let father = new Person("David", 48); let mother = new Person("Kelly", 46);
new操作符的執行過程,會對實例對象進行 "原型關聯",或稱 "原型鏈接"。
new的執行過程創建(構造)一個全新的空對象
“這個新對象會被執行"原型"鏈接(新對象的__proto__會指向函數的prototype)”
構造函數的this會指向這個新對象,并對this屬性進行賦值
如果函數沒有返回其他對象,則返回這個新對象(注意構造函數的return,一般不會有return)
原型鏈"對象由函數創建",既然 prototype 也是對象,那么它的 __proto__ 原型鏈上應該還有屬性。Person.prototype.__proto__ 指向 Function.prototype,而Function.prototype.__proto__ 最終指向 Object.prototype。
TIPS:Object.prototype.__proto__ 指向 null(特例)。
日常調用對象的 toString()/valueOf() 方法,雖然沒有去定義它們,但卻能正常使用。實際上這些方法來自 Object.prototype,所有普通對象的原型鏈最終都會指向 Object.prototype,而對象通過原型鏈關聯(繼承)的方式,使得實例對象可以調用 Object.prototype 上的屬性 / 方法。
訪問一個對象的屬性時,會先在其基礎屬性上查找,找到則返回值;如果沒有,會沿著其原型鏈上進行查找,整條原型鏈查找不到則返回 undefined。這就是原型鏈查找。
基礎屬性與原型屬性 hasOwnProperty()判斷對象基礎屬性中是否有該屬性,基礎屬性返回 true。
涉及 in 操作都是所有屬性(基礎 + 原型)for...in... 遍歷對象所有可枚舉屬性
in 判斷對象是否擁有該屬性
Object.keys(...)與Object.getOwnPropertyNames(...)Object.keys(...) 返回所有可枚舉屬性
Object.getOwnPropertyNames(...) 返回所有屬性
屏蔽屬性修改對象屬性時,如果屬性名與原型鏈上屬性重名,則在實例對象上創建新的屬性,屏蔽對象對原型屬性的使用(發生屏蔽屬性)。屏蔽屬性的前提是,對象基礎屬性名與原型鏈上屬性名存在重名。
創建對象屬性時,屬性特性對屏蔽屬性的影響對象原型鏈上有同名屬性,且可寫,在對象上創建新屬性(屏蔽原型屬性);
對象原型鏈上有同名屬性,且只讀,忽略;
對象原型鏈上有同名屬性,存在訪問器屬性 set(),調用 set()
批量創建對象的方式創建多個具有相同屬性的對象
1. 工廠模式function createPersonFactory(name, age) { var obj = new Object(); obj.name = name; obj.age = age; obj.say = function() { console.log(`My name is ${this.name}, i am ${this.age}`); }; return obj; } var father = createPersonFactory("David", 48); var mother = createPersonFactory("Kelly", 46); father.say(); // "My name is David, i am 48" mother.say(); // "My name is Kelly, i am 46"
缺點:
無法解決對象識別問題
屬性值為函數時無法共用,不同實例對象的 say 方法沒有共用內存空間
obj.say = function(){...} 實例化一個對象時都會開辟新的內存空間,去存儲function(){...},造成不必要的內存開銷。
father.say == mother.say; // false2. 構造函數(new)
function Person(name, age) { this.name = name; this.age = age; this.say = function() { console.log(`My name is ${this.name}, i am ${this.age}`); } } let father = new Person("David", 48);
缺點:屬性值為引用類型(say方法)時無法共用,不同實例對象的 say 方法沒有共用內存空間(與工廠模式一樣)。
3. 原型模式function Person() {} Person.prototype.name = "David"; Person.prototype.age = 48; Person.prototype.say = function() { console.log(`My name is ${this.name}, i am ${this.age}`); }; let father = new Person();
優點:解決公共方法內存占用問題(所有實例屬性的 say 方法共用內存)
缺點:屬性值為引用類型時,因內存共用,一個對象修改屬性會造成其它對象使用屬性發生改變。
Person.prototype.like = ["sing", "dance"]; let father = new Person(); let mother = new Person(); father.like.push("travel"); // 引用類型共用內存,一個對象修改屬性,會影響其它對象 father.like; // ["sing", "dance", "travel"] mother.like; // ["sing", "dance", "travel"]4. 構造函數 + 原型(經典組合)
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.say = function() { console.log(`My name is ${this.name}, i am ${this.age}`); }
原理:結合構造函數和原型的優點,"構造函數初始化屬性,原型定義公共方法"。
5. 動態原型構造函數 + 原型的組合方式,區別于其它 "面向對象" 語言的聲明方式。屬性方法的定義并沒有統一在構造函數中。因此動態原型創建對象的方式,則是在 "構造函數 + 原型組合" 基礎上,優化了定義方式(區域)。
function Person(name, age) { this.name = name; this.age = age; // 判斷原型是否有方法,沒有則添加; // 原型上的屬性在構造函數內定義,僅執行一次 if (!Person.prototype.say) { Person.prototype.say = function() { console.log(`My name is ${this.name}, i am ${this.age}`); } } }
優點:屬性方法統一在構造函數中定義。
除了以上介紹的幾種對象創建方式,此外還有"寄生構造函數模式"、"穩妥構造函數模式"。日常開發較少使用,感興趣的伙伴們可自行了解。
"類" 的繼承傳統的面向對象語言中,"類" 繼承的原理是 "類" 的復制。但JavaScript模擬 "類" 繼承則是通過 "原型關聯" 來實現,并不是 "類" 的復制。正如《你不知道的JavaScript》中提出的觀點,這種模擬 "類" 繼承的方式,更像是 "委托",而不是 "繼承"。
以下列舉JavaScript中常用的繼承方式,預先定義兩個類:
"Person" 父類(超類)
"Student" 子類(用來繼承父類)
// 父類統一定義 function Person(name, age) { // 構造函數定義初始化屬性 this.name = name; this.age = age; } // 原型定義公共方法 Person.prototype.eat = function() {}; Person.prototype.sleep = function() {};原型繼承
// 原型繼承 function Student(name, age, grade) { this.grade = grade; }; Student.prototype = new Person(); // Student原型指向Person實例對象 Student.prototype.constructor = Student; // 原型對象修改,需要修復constructor屬性 let pupil = new Student(name, age, grade);
子類的原型對象為父類的實例對象,因此子類原型對象中擁有父類的所有屬性
無法向父類構造函數傳參,初始化屬性值
屬性值是引用類型時,存在內存共用的情況
無法實現多繼承(只能為子類指定一個原型對象)
構造函數繼承// 構造函數繼承 function Student(name, age, grade) { Person.call(this, name, age); this.grade = grade; }
調用父類構造函數,傳入子類的上下文對象,實現子類參數初始化賦值。僅實現部分繼承,無法繼承父類原型上的屬性。可 call 多個父類構造函數,實現多繼承。
屬性值為引用類型時,需開辟多個內存空間,多個實例對象無法共享公共方法的存儲,造成不必要的內存占用。
原型 + 構造函數繼承(經典)// 原型 + 構造函數繼承 function Student(name, age, grade) { Person.call(this, name, age); // 第一次調用父類構造函數 this.grade = grade; } Student.prototype = new Person(); // 第二次調用父類構造函數 Student.prototype.constructor = Student; // 修復constructor屬性
結合原型繼承 + 構造函數繼承兩者的優點,"構造函數繼承并初始化屬性,原型繼承公共方法"。
父類構造函數被調用了兩次。
待優化:父類構造函數第一次調用時,已經完成父類構造函數中 "屬性的繼承和初始化",第二次調用時只需要 "繼承父類原型屬性" 即可,無須再執行父類構造函數。
寄生組合式繼承(理想)// 寄生組合式繼承 function Student(name, age, grade) { Person.call(this, name, age); this.grade = grade; } Student.prototype = Object.create(Person.prototype); // Object.create() 會創建一個新對象,該對象的__proto__指向Person.prototype Student.prototype.constructor = Student; let pupil = new Student("小明", 10, "二年級");
創建一個新對象,將該對象原型關聯至父類的原型對象,子類 Student 已使用 call 來調用父類構造函數完成初始化,所以只需再繼承父類原型屬性即可,避免了經典組合繼承調用兩次父類構造函數。(較完美的繼承方案)
ES6的class語法class Person { constructor(name, age) { this.name = name; this.grade = grade; } eat () { //... } sleep () { //... } } class Student extends Person { constructor (name, age, grade) { super(name, age); this.grade = grade; } play () { //... } }
優點:ES6提供的 class 語法使得類繼承代碼語法更加簡潔。
Object.create(...)Object.create()方法會創建一個新對象,使用現有對象來提供新創建的對象的__proto__
Object.create 實現的其實是"對象關聯",直接上代碼更有助于理解:
let person = { eat: function() {}; sleep: function() {}; } let father = Object.create(person); // father.__proto__ -> person, 因此father上有eat/sleep/talk等屬性 father.eat(); father.sleep();
上述代碼中,我們并沒有使用構造函數 / 類繼承的方式,但 father 卻可以使用來自 person 對象的屬性方法,底層原理依賴于原型和原型鏈的魔力。
// Object.create實現原理/模擬 Object.create = function(o) { function F() {} F.prototype = o; return new F(); }
Object.create(...) 實現的 "對象關聯" 的設計模式與 "面向對象" 模式不同,它并沒有父類,子類的概念,甚至沒有 "類" 的概念,只有對象。它倡導的是 "委托" 的設計模式,是基于 "面向委托" 的一種編程模式。
文章篇幅有限,僅作淺顯了解,后續可另開一章講講 "面向對象" VS "面向委托",孰優孰劣,說一道二。
對象識別(檢查 "類" 關系) instanceofinstanceof 只能處理對象與函數的關系判斷。instanceof 左邊是對象,右邊是函數。判斷規則:沿著對象的 __proto__ 進行查找,沿著函數的 prototype 進行查找,如果有關聯引用則返回 true,否則返回 false。
let pupil = new Student(); pupil instanceof Student; // true pupil instanceof Person; // true Student繼承了PersonObject.prototype.isPrototypeOf(...)
Object.prototype.isPrototyepOf(...) 可以識別對象與對象,也可以是對象與函數。
let pupil = new Student(); Student.prototype.isPrototypeOf(pupil); // true
判斷規則:在對象 pupil 原型鏈上是否出現過 Student.prototype , 如果有則返回 true, 否則返回 false
ES6新增修改對象原型的方法: Object.setPrototypeOf(obj, prototype),存在有性能問題,僅作了解,更推薦使用 Object.create(...)。
Student.prototype = Object.create(Person.prototype); // setPrototypeOf改寫上行代碼 Object.setPrototypeOf(Student.prototype, Person.prototype);后語
"面向對象" 是程序編程的一種設計模式,具備 "封裝,繼承,多態" 的特點,在ES6的 class 語法未出來之前,原型繼承確實是JavaScript入門的一個難點,特別是對新入門的朋友,理解起來并不友好,模擬繼承的代碼寫的冗余又難懂。好在ES6有了 class 語法糖,不必寫冗余的類繼承代碼,代碼寫少了,眼鏡片都亮堂了。
老話說的好,“會者不難”。深入理解面向對象,原型,繼承,對日后代碼能力的提升及編碼方式優化都有益處。好的方案不只有一種,明白個中緣由,帶你走進新世界大門。
參考文檔:
《你不知道的JavaScript(上卷)》
《JavaScript高級程序設計》
JavaScript常見的六種繼承方式
深入理解javascript原型和閉包
本文首發Github,期待Star!
https://github.com/ZengLingYong/blog
作者:以樂之名
本文原創,有不當的地方歡迎指出。轉載請指明出處。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/101831.html
摘要:進擊的巨人第三篇,本篇就作用域作用域鏈閉包等知識點,一一擊破。在此我們遵照的方式,暫且稱是閉包。所以,一名合格的前端,除了會用閉包,還要正確的解除閉包引用。 進擊的巨人第三篇,本篇就作用域、作用域鏈、閉包等知識點,一一擊破。 showImg(https://segmentfault.com/img/bVburWd?w=1280&h=854); 作用域 作用域:負責收集并維護由所有聲明的...
摘要:情況沒有明確作用對象的情況下,通常為全局對象例如函數的回調函數,它的就是全局對象。正因如此,機器可以作為這類對象的標志,即面向對象語言中類的概念。所以機器又被稱為構造函數。原型鏈也就是繼承鏈。 JS面向對象二:this/原型鏈/new原理 阮一峰JavaScript教程:面向對象編程 阮一峰JavaScript教程:實例對象與 new 命令 阮一峰JavaScript教程:this 關...
摘要:每一個由構造函數創建的對象都會默認的連接到該神秘對象上。在構造方法中也具有類似的功能,因此也稱其為類實例與對象實例一般是指某一個構造函數創建出來的對象,我們稱為構造函數的實例實例就是對象。表示該原型是與什么構造函數聯系起來的。 本文您將看到以下內容: 傳統構造函數的問題 一些相關概念 認識原型 構造、原型、實例三角結構圖 對象的原型鏈 函數的構造函數Function 一句話說明什么...
摘要:創建一個新的對象即實例對象把新對象的指向后面構造函數的原型對象。簡單來驗證一下等同與對象沒有原型對象的原型對像等同于構造函數是等同于,構造函數是七原型鏈的作用其實,原型鏈的根本作用就是為了屬性的讀取。 首先說一下,函數創建的相關知識 在JavaScript中,我們創建一個函數A(就是聲明一個函數), 那么 js引擎 就會用構造函數Function來創建這個函數。所以,所有的函數的con...
摘要:有關函數柯里化的詳解,請回閱前端進擊的巨人五學會函數柯里化。構造函數中的通過操作符可以實現對函數的構造調用。在了解構造函數中的前,有必要先了解下實例化對象的過程。 showImg(https://segmentfault.com/img/bVburMp?w=800&h=600); 常見this的誤解 指向函數自身(源于this英文意思的誤解) 指向函數的詞法作用域(部分情況) th...
閱讀 2809·2021-10-26 09:48
閱讀 1671·2021-09-22 15:22
閱讀 4028·2021-09-22 15:05
閱讀 608·2021-09-06 15:02
閱讀 2607·2019-08-30 15:52
閱讀 2107·2019-08-29 18:38
閱讀 2755·2019-08-28 18:05
閱讀 2332·2019-08-26 13:55