去年開始我給本身畫了一張知識體系的思惟導圖,用於規劃本身的學習範圍和方向。可是我犯了一個大錯,個人思惟導圖只是一個全局的藍圖,而在學習某個知識點的時候沒有系統化,知識太過於零散,另外一方面也很容易遺忘,回頭複習時沒有一個提綱,總體的學習效率不高。意識到這一點,我最近開始用思惟導圖去學習和總結具體的知識點,效果還不錯。試想一下,一張思惟導圖的某個端點是另外一張思惟導圖,這樣串起來的知識鏈條是多麼「酸爽」!固然,YY一下就行了,我保證你沒有足夠的時間給全部知識點都畫上思惟導圖,挑重點便可。javascript
當咱們要研究一個問題或者知識點時,關注點無非是:java
是什麼?程序員
作什麼?web
爲何?編程
很明顯,搞懂「是什麼」是最最基礎的,而這部分卻很重要。萬丈高樓平地起,若是連基礎都不清楚,何談應用實踐(「作什麼」),更加也不會理解問題的本質(「爲何」)。設計模式
而要整理一篇高質量的思惟導圖,必須充分利用「總-分」的思路,首先要造成一個基本的提綱,而後從各個方面去延伸拓展,最後獲得一棵較爲完整的知識樹。分解知識點後,在細究的過程當中,你可能還會驚喜地發現一個知識點的各個組成部分之間的關聯,對知識點有一個更爲飽滿的認識。數組
梳理提綱須要對知識點有一個總體的認識。若是是學習比較陌生的領域知識,個人策略是從相關書籍或官方文檔的目錄中提煉出提綱。瀏覽器
下面以我複習javascript
對象這塊知識時的一些思路爲例說明。app
在複習javascript
對象這塊知識時,我從過往的一些使用經驗,書籍,文檔資料中提煉出了這麼幾個方面做爲提綱,分別是:編輯器
對象的分類
對象的三個重要概念:類,原型,實例
建立對象的方法
對象屬性的訪問和設置
原型和繼承
靜態方法和原型方法
由此展開獲得了這樣一個思惟導圖:
對象主要分爲這麼三大類:
內置對象:ECMAScript規範中定義的類或對象,好比Object
, Array
, Date
等。
宿主對象:由javascript解釋器所嵌入的宿主環境提供。好比瀏覽器環境會提供window
,HTMLElement
等瀏覽器特有的宿主對象。Nodejs會提供global
全局對象
自定義對象:由javascript開發者自行建立的對象,用以實現特定業務。就好比咱們熟悉的Vue,它就是一個自定義對象。咱們能夠對Vue這個對象進行實例化,用於生成基於Vue的應用。
javascript在ES6以前沒有class
關鍵字,但這不影響javascript能夠實現面向對象編程,javascript的類名對應構造函數名。
在ES6以前,若是咱們要定義一個類,實際上是藉助函數來實現的。
function Person(name) {
this.name = name; } Person.prototype.sayHello = function() { console.log(this.name + ': hello!'); } var person = new Person('Faker'); person.sayHello(); 複製代碼
ES6明肯定義了class
關鍵字。
class Person {
constructor(name) { this.name = name; } sayHello() { console.log(this.name + ': hello!'); } } var person = new Person('Faker'); person.sayHello(); 複製代碼
原型是類的核心,用於定義類的屬性和方法,這些屬性和方法會被實例繼承。
定義原型屬性和方法須要用到構造函數的prototype
屬性,經過prototype
屬性能夠獲取到原型對象的引用,而後就能夠擴展原型對象了。
function Person(name) {
this.name = name; } Person.prototype.sexList = ['man', 'woman']; Person.prototype.sayHello = function() { console.log(this.name + ': hello!'); } 複製代碼
類是抽象的概念,至關於一個模板,而實例是類的具體表現。就好比Person
是一個類,而根據Person
類,咱們能夠實例化多個對象,可能有小明,小紅,小王等等,類的實例都是一個個獨立的個體,可是他們都有共同的原型。
var xiaoMing = new Person('小明');
var xiaoHong = new Person('小紅'); // 擁有同一個原型 Object.getPrototypeOf(xiaoMing) === Object.getPrototypeOf(xiaoHong); // true 複製代碼
對象直接量也稱爲對象字面量。直接量就是不須要實例化,直接寫鍵值對便可建立對象,堪稱「簡單粗暴」。
var xiaoMing = { name: '小明' };
複製代碼
每寫一個對象直接量至關於建立了一個新的對象。即便兩個對象直接量看起來如出一轍,它們指向的堆內存地址也是不同的,而對象是按引用訪問的,因此這兩個對象是不相等的。
var xiaoMing1 = { name: '小明' };
var xiaoMing2 = { name: '小明' }; xiaoMing1 === xiaoMing2; // false 複製代碼
能夠經過關鍵詞new
調用javascript對象的構造函數來得到對象實例。好比:
var o = new Object();
複製代碼
function Person(name) {
this.name = name; }; new Person('Faker'); 複製代碼
Object.create
用於建立一個對象,接受兩個參數,使用語法以下;
Object.create(proto[, propertiesObject]);
複製代碼
第一個參數proto
用於指定新建立對象的原型;
第二個參數propertiesObject
是新建立對象的屬性名及屬性描述符組成的對象。
proto
能夠指定爲null
,可是意味着新對象的原型是null
,它不會繼承Object
的方法,好比toString()
等。
propertiesObject
參數與Object.defineProperties
方法的第二個參數格式相同。
var o = Object.create(Object.prototype, {
// foo會成爲所建立對象的數據屬性 foo: { writable:true, configurable:true, value: "hello" }, // bar會成爲所建立對象的訪問器屬性 bar: { configurable: false, get: function() { return 10 }, set: function(value) { console.log("Setting o.bar to", value); } } }); 複製代碼
屬性查詢也能夠稱爲屬性訪問。在javascript中,對象屬性查詢很是靈活,支持點號查詢,也支持字符串索引查詢(之因此說是「字符串索引」,是由於寫法看起像數組,索引是字符串而不是數字)。
經過點號加屬性名訪問屬性的行爲很像一些靜態類型語言,如java,C等。屬性名是javascript標識符,必須直接寫在屬性訪問表達式中,不能動態訪問。
var o = { name: '小明' };
o.name; // "小明" 複製代碼
而根據字符串索引查詢對象屬性就比較靈活了,屬性名就是字符串表達式的值,而一個表達式是能夠接受變量的,這意味着能夠動態訪問屬性,這賦予了javascript程序員很大的靈活性。下面是一個很簡單的示例,而這種特性在業務實踐中做用很大,好比深拷貝的實現,你每每不知道你要拷貝的對象中有哪些屬性。
var o = { chineseName: '小明', englishName: 'XiaoMing' };
['chinese', 'english'].forEach(lang => { var property = lang + 'Name'; console.log(o[property]); // 這裏使用了字符串索引訪問對象屬性 }) 複製代碼
對了,屬性查詢不只能夠查詢自由屬性,也能夠查詢繼承屬性。
var protoObj = { age: 18 };
var o = Object.create(protoObj); o.age; // 18,這裏訪問的是原型屬性,也就是繼承獲得的屬性 複製代碼
經過屬性訪問表達式,咱們能夠獲得屬性的引用,就能夠據此設置屬性了。這裏主要注意一下只讀屬性和繼承屬性便可,細節再也不展開。
前面也提到了,原型是實現繼承的基礎。那麼如何去理解原型呢?
首先,要明確原型概念中的三角關係,三個主角分別是構造函數,原型,實例。我這裏畫了一張比較簡單的圖來幫助理解下。
原型這東西吧,我感受「沒人能幫你理解,只有你本身去試過纔是懂了」。
不過這裏說說我剛學習原型時的疑惑,疑惑的是爲何構造函數有屬性prototype
指向原型,而實例又能夠經過__proto__
指向原型,究竟prototype
和__proto__
誰是原型?其實這明顯是沒有理解對象是按引用訪問這個特色了。原型對象永遠只有一個,它存儲於堆內存中,而構造函數的prototype
屬性只是得到了原型的引用,經過這個引用能夠操做原型。
一樣地,__proto__
也只是原型的引用,可是要注意了,__proto__
不是ECMAScript
規範裏的東西,因此千萬不要用在生產環境中。
至於爲何不能夠經過__proto__
訪問原型,緣由也很簡單。經過實例直接得到了原型的訪問和修改權限,這自己是一件很危險的事情。
舉個例子,這裏有一個類LatinDancer
,意思是拉丁舞者。通過實例化操做,獲得了多個拉丁舞者。
function LatinDancer(name) {
this.name = name; }; LatinDancer.prototype.dance = function() { console.log(this.name + '跳拉丁舞...'); } var dancer1 = new LatinDancer('小明'); var dancer2 = new LatinDancer('小紅'); var dancer3 = new LatinDancer('小王'); dancer1.dance(); // 小明跳拉丁舞... dancer2.dance(); // 小紅跳拉丁舞... dancer3.dance(); // 小王跳拉丁舞... 複製代碼
你們歡快地跳着拉丁舞,忽然小王這個傢伙心血來潮,說:「我要作b-boy,我要跳Breaking」。因而,他私下改了原型方法dance()
。
dancer3.__proto__.dance = function() { console.log(this.name + '跳breaking...'); } 複製代碼dancer1.dance(); // 小明跳breaking... dancer2.dance(); // 小紅跳breaking... dancer3.dance(); // 小王跳breaking... 複製代碼
這個時候就不對勁了,小明和小紅正跳着拉丁,忽然身體不受控制了,跳起了Breaking,內心暗罵:「沃尼瑪,勞資不是跳拉丁的嗎?」
這裏只是舉個例子哈,沒有對任何舞種或者舞者不敬的意思,抱歉抱歉。
因此,你們應該也明白了爲何不能使用__proto__
了吧。
在javascript中,任何對象都有原型,除了Object.prototype
,它沒有原型,或者說它的原型是null
。
那麼什麼是原型鏈呢?javascript程序在查找一個對象的屬性或方法時,會首先在對象自己上進行查找,若是找不到則會去對象的原型上進行查找。按照這樣一個遞歸關係,若是原型上找不到,就會到原型的原型上找,這樣一直查找下去,就會造成一個鏈,它的終點是null
。
還要注意的一點是,構造函數也是一個對象,也存在原型,它的原型能夠經過Function.prototype
得到,而Function.prototype
的原型則能夠經過Object.prototype
得到。
說到繼承,可能你們腦子裏已經冒出來「原型鏈繼承」,「借用構造函數繼承」,「寄生式繼承」,「原型式繼承」,「寄生組合繼承」這些概念了吧。說實話,一開始我也是這麼記憶,可是發現好像不是那麼容易理解啊。最後,我發現,只要從原型三角關係入手,就能理清實現繼承的思路。
咱們知道,對象實例能訪問的屬性和方法一共有三個來源,分別是:調用構造函數時掛載到實例上的屬性,原型屬性,對象實例化後自身新增的屬性。
很明顯,第三個來源不是用來作繼承的,那麼前兩個來源用來作繼承分別有什麼優缺點呢?很明顯,若是隻基於其中一種來源作繼承,都不可能全面地繼承來自父類的屬性或方法。
首先明確下繼承中三個主體:父類,子類,子類實例。那麼怎麼才能讓子類實例和父類搭上關係呢?
所謂繼承,簡單說就是能經過子類實例訪問父類的屬性和方法。而利用原型鏈能夠達成這樣的目的,因此只要父類原型、子類原型、子類實例造成原型鏈關係便可。
代碼示例:
function Father() {
this.nationality = 'Han'; }; Father.prototype.propA = '我是父類原型上的屬性'; function Child() {}; Child.prototype = new Father(); Child.prototype.constructor = Child; // 修正原型上的constructor屬性 Child.prototype.propB = '我是子類原型上的屬性'; var child = new Child(); console.log(child.propA, child.propB, child.nationality); // 均可以訪問到 child instanceof Father; // true 複製代碼
能夠看到,在上述代碼中,咱們作了這樣一個特殊處理Child.prototype.constructor = Child;
。一方面是爲了保證constructor
的指向正確,畢竟實例由子類實例化得來,若是constructor
指向父類構造函數也不太合適吧。另外一方面是爲了防止某些方法顯示調用constructor
時帶來的麻煩。具體解釋見Why is it necessary to set the prototype constructor?
關鍵點:讓子類原型成爲父類的實例,子類實例也是父類的實例。
缺點:沒法繼承父類構造函數給實例掛載的屬性。
在調用子類構造函數時,經過call調用父類構造函數,同時指定this值。
function Father() {
this.nationality = 'Han'; }; Father.prototype.propA = '我是父類原型上的屬性'; function Child() { Father.call(this); }; Child.prototype.propB = '我是子類原型上的屬性'; var child = new Child(); console.log(child.propA, child.propB, child.nationality); 複製代碼
這裏的child.propA
是undefined
,由於子類實例不是父類的實例,沒法繼承父類原型屬性。
child instanceof Father; // false
複製代碼
關鍵點:構造函數的複用。
缺點:子類實例不是父類的實例,沒法繼承父類原型屬性。
所謂組合繼承,就是綜合上述兩種方法。實現代碼以下:
function Father() {
this.nationality = 'Han'; }; Father.prototype.propA = '我是父類原型上的屬性'; function Child() { Father.call(this); }; Child.prototype = new Father(); Child.prototype.constructor = Child; // 修正原型上的constructor屬性 Child.prototype.propB = '我是子類原型上的屬性'; var child = new Child(); console.log(child.propA, child.propB, child.nationality); // 都能訪問到 複製代碼
一眼看上去沒什麼問題,可是Father()
構造函數實際上是被調用了兩次的。第一次發生在Child.prototype = new Father();
,此時子類原型成爲了父類實例,執行父類構造函數Father()
時,得到了實例屬性nationality
;第二次發生在var child = new Child();
,此時執行子類構造函數Child()
,而Child()
中經過call()
調用了父類構造函數,因此子類實例也得到了實例屬性nationality
。這樣理解起來可能有點晦澀難懂,咱們能夠看看子類實例的對象結構:
能夠看到,子類實例和子類原型上都掛載了執行父類構造函數時得到的屬性nationality
。然而咱們作繼承的目的是很單純的,即「讓子類繼承父類屬性和方法」,但並不該該給子類原型掛載沒必要要的屬性而致使污染子類原型。
有人會說「這麼一點反作用怕什麼」。固然,對於這麼簡單的父類而言,這種反作用微乎其微。假設父類有幾百個屬性或方法呢,這種白白耗費性能和內存的行爲是有必要的嗎?答案顯而易見。
關鍵點:實例屬性和原型屬性都得以繼承。
缺點:父類構造函數被執行了兩次,污染了子類原型。
原型式繼承是相對於原型鏈繼承而言的,與原型鏈繼承的不一樣點在於,子類原型在建立時,不會執行父類構造函數,是一個純粹的空對象。
function Father() {
this.nationality = 'Han'; }; Father.prototype.propA = '我是父類原型上的屬性'; function Child() {}; Child.prototype = Object.create(Father.prototype); Child.prototype.constructor = Child; // 修正原型上的constructor屬性 Child.prototype.propB = '我是子類原型上的屬性'; var child = new Child(); console.log(child.propA, child.propB, child.nationality); // 均可以訪問到 child instanceof Father; // true 複製代碼
在ES5
以前,能夠這樣模擬Object.create
:
function create(proto) {
function F() {} F.prototype = proto; return new F(); } 複製代碼
關鍵點:利用一個空對象過渡,解除子類原型和父類構造函數的強關聯關係。這也意味着繼承能夠是純對象之間的繼承,無需構造函數介入。
缺點:沒法繼承父類構造函數給實例掛載的屬性,這一點和原型鏈繼承並沒有差別。
寄生式繼承有借鑑工廠函數的設計模式,將繼承的過程封裝到一個函數中並返回對象,而且能夠在函數中擴展對象方法或屬性。
var obj = {
nationality: 'Han' }; function inherit(proto) { var o = Object.create(proto); o.extendFunc = function(a, b) { return a + b; } return o; } var inheritObj = inherit(obj); 複製代碼
這裏inheritObj
不只繼承了obj
,並且也擴展了extendFunc
方法。
關鍵點:工廠函數,封裝過程函數化。
缺點:若是在工廠函數中擴展對象屬性或方法,沒法獲得複用。
用以解決組合繼承過程當中存在的「父類構造函數屢次被調用」問題。
function inherit(childType, fatherType) {
childType.prototype = Object.create(fatherType.prototype); childType.prototype.constructor = childType; } function Father() { this.nationality = 'Han'; } Father.prototype.propA = '我是父類原型上的屬性'; function Child() {} inherit(Child, Father); // 繼承 Child.prototype.propB = '我是子類原型上的屬性'; var child = new Child(); console.log(child); 複製代碼
關鍵點:解決父類構造函數屢次執行的問題,同時讓子類原型變得更加純粹。
何謂「靜態方法」?靜態方法爲類全部,不歸屬於任何一個實例,須要經過類名直接調用。
function Child() {}
Child.staticMethod = function() { console.log("我是一個靜態方法") } var child = new Child(); Child.staticMethod(); // "我是一個靜態方法" child.staticMethod(); // Uncaught TypeError: child.staticMethod is not a function 複製代碼
Object
類有不少的靜態方法,我學習的時候習慣把它們分爲這麼幾類(固然,這裏沒有所有列舉開來,只挑了常見的方法)。
Object.create()
:基於原型和屬性描述符集合建立一個新對象。
Object.assign()
:合併多個對象,會影響源對象。因此在合併對象時,爲了不這個問題,通常會這樣作:
var mergedObj = Object.assign({}, a, b);
複製代碼
Object.defineProperty
:經過屬性描述符來定義或修改對象屬性,主要涉及value
, configurable
, writable
, enumerable
四個特性。
Object.defineProperties
:是defineProperty
的升級版本,一次性定義或修改多個屬性。
Object.getOwnPropertyDescriptor
:獲取屬性描述符,是一個對象,包含value
, configurable
, writable
, enumerable
四個特性。
Object.getOwnPropertyNames
:返回一個由指定對象的全部自身屬性的屬性名(包括不可枚舉屬性但不包括Symbol值做爲名稱的屬性)組成的數組。
Object.keys
:會返回一個由一個給定對象的自身可枚舉屬性組成的數組,與getOwnPropertyNames
最大的不一樣點在於:keys
只返回enumerable
爲true
的屬性,而且會返回原型對象上的屬性。
Object.getPrototypeOf
:返回指定對象的原型。
function Child() {}
var child = new Child(); Object.getPrototypeOf(child) === Child.prototype; // true 複製代碼
Object.setPrototypeOf
:設置指定對象的原型。這是一個比較危險的動做,同時也是一個性能不佳的方法,不推薦使用。
如下列舉的這三個方式是一個遞進的關係,咱們按序來看:
Object.preventExtensions
:讓一個對象變的不可擴展,也就是永遠不能再添加新的屬性。
Object.seal
:封閉一個對象,阻止添加新屬性並將全部現有屬性標記爲不可配置。也就是說Object.seal
在Object.preventExtensions
的基礎上,給對象屬性都設置了configurable
爲false
。
這裏有一個坑是:對於configurable
爲false
的屬性,雖然不能從新設置它的configurable
和enumerable
特性,可是能夠把它的writable
特性從true
改成false
(反之不行)。
Object.freeze
:凍結一個對象,不能新增,修改,刪除屬性,也不能修改屬性的原型。這裏還有一個深凍結
deepFreeze
的概念,有點相似深拷貝的意思,遞歸凍結。
Object.isExtensible
:檢測對象是否可擴展。
Object.isSealed
:檢測對象是否被封閉。
Object.isFrozen
:檢測對象是否被凍結。
Object.entries
Object.values
Object.fromEntries
原型方法是指掛載在原型對象上的方法,能夠經過實例調用,本質上是藉助原型對象調用。例如:
function Child() {}
Child.prototype.protoMethod = function() { console.log("我是一個原型方法") } var child = new Child(); child.protoMethod(); // "我是一個原型方法" 複製代碼
ECMAScript給Object
定義了不少原型方法。
該方法會返回一個布爾值,指示對象自身屬性中是否具備指定的屬性(也就是,是否有指定的鍵),常配合for ... in
語句一塊兒使用,用來遍歷對象自身可枚舉屬性。
該方法用於測試一個對象是否存在於另外一個對象的原型鏈上。Object.prototype.isPrototypeOf
與Object.getPrototypeOf
不一樣點在於:
Object.prototype.isPrototypeOf
判斷的是原型鏈關係,而且返回一個布爾值。
Object.getPrototypeOf
是獲取目標對象的直接原型,返回的是目標對象的原型對象
該方法返回一個布爾值,表示指定的屬性是否可枚舉。它檢測的是對象屬性的enumerable
特性。
對象轉原始值會用到的方法,以前寫過一篇筆記,具體見js數據類型很簡單,卻也不簡單。
toLocaleString
方法返回一個該對象的字符串表示。此方法被用於派生對象爲了特定語言環境的目的(locale-specific purposes)而重載使用。常見於日期對象。
經過閱讀本文,讀者們能夠對Javascript對象有一個基本的認識。對象是Javascript中很是複雜的部分,絕非一篇筆記或一張思惟導圖可囊括,諸多細節不便展開,可關注我留言交流,回覆「思惟導圖」可獲取我整理的思惟導圖。