「思惟導圖學前端 」6k字一文搞懂Javascript對象,原型,繼承

前言

去年開始我給本身畫了一張知識體系的思惟導圖,用於規劃本身的學習範圍和方向。可是我犯了一個大錯,個人思惟導圖只是一個全局的藍圖,而在學習某個知識點的時候沒有系統化,知識太過於零散,另外一方面也很容易遺忘,回頭複習時沒有一個提綱,總體的學習效率不高。意識到這一點,我最近開始用思惟導圖去學習和總結具體的知識點,效果還不錯。試想一下,一張思惟導圖的某個端點是另外一張思惟導圖,這樣串起來的知識鏈條是多麼「酸爽」!固然,YY一下就行了,我保證你沒有足夠的時間給全部知識點都畫上思惟導圖,挑重點便可。javascript

提綱思路

當咱們要研究一個問題或者知識點時,關注點無非是:java

  1. 是什麼?程序員

  2. 作什麼?web

  3. 爲何?編程

很明顯,搞懂「是什麼」是最最基礎的,而這部分卻很重要。萬丈高樓平地起,若是連基礎都不清楚,何談應用實踐(「作什麼」),更加也不會理解問題的本質(「爲何」)。設計模式

而要整理一篇高質量的思惟導圖,必須充分利用「總-分」的思路,首先要造成一個基本的提綱,而後從各個方面去延伸拓展,最後獲得一棵較爲完整的知識樹。分解知識點後,在細究的過程當中,你可能還會驚喜地發現一個知識點的各個組成部分之間的關聯,對知識點有一個更爲飽滿的認識。數組

梳理提綱須要對知識點有一個總體的認識。若是是學習比較陌生的領域知識,個人策略是從相關書籍或官方文檔的目錄中提煉出提綱。瀏覽器

下面以我複習javascript對象這塊知識時的一些思路爲例說明。app

javascript對象

在複習javascript對象這塊知識時,我從過往的一些使用經驗,書籍,文檔資料中提煉出了這麼幾個方面做爲提綱,分別是:編輯器

  • 對象的分類

  • 對象的三個重要概念:類,原型,實例

  • 建立對象的方法

  • 對象屬性的訪問和設置

  • 原型和繼承

  • 靜態方法和原型方法

由此展開獲得了這樣一個思惟導圖:

js對象
js對象

對象的分類

對象主要分爲這麼三大類:

  • 內置對象:ECMAScript規範中定義的類或對象,好比Object, Array, Date等。

  • 宿主對象:由javascript解釋器所嵌入的宿主環境提供。好比瀏覽器環境會提供windowHTMLElement等瀏覽器特有的宿主對象。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 構造函數

能夠經過關鍵詞new調用javascript對象的構造函數來得到對象實例。好比:

  1. 建立內置對象實例
var o = new Object();
複製代碼
  1. 建立自定義對象實例
function Person(name) {
 this.name = name; }; new Person('Faker'); 複製代碼

Object.create

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.propAundefined,由於子類實例不是父類的實例,沒法繼承父類原型屬性。

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只返回enumerabletrue的屬性,而且會返回原型對象上的屬性。

原型相關

  • Object.getPrototypeOf:返回指定對象的原型。
function Child() {}
var child = new Child(); Object.getPrototypeOf(child) === Child.prototype; // true 複製代碼
  • Object.setPrototypeOf:設置指定對象的原型。這是一個比較危險的動做,同時也是一個性能不佳的方法,不推薦使用。

行爲控制

如下列舉的這三個方式是一個遞進的關係,咱們按序來看:

  • Object.preventExtensions:讓一個對象變的不可擴展,也就是永遠不能再添加新的屬性。

  • Object.seal:封閉一個對象,阻止添加新屬性並將全部現有屬性標記爲不可配置。也就是說Object.sealObject.preventExtensions的基礎上,給對象屬性都設置了configurablefalse

這裏有一個坑是:對於configurablefalse的屬性,雖然不能從新設置它的configurableenumerable特性,可是能夠把它的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定義了不少原型方法。

Object原型方法
Object原型方法

hasOwnProperty

該方法會返回一個布爾值,指示對象自身屬性中是否具備指定的屬性(也就是,是否有指定的鍵),常配合for ... in語句一塊兒使用,用來遍歷對象自身可枚舉屬性。

isPrototypeOf

該方法用於測試一個對象是否存在於另外一個對象的原型鏈上。Object.prototype.isPrototypeOfObject.getPrototypeOf不一樣點在於:

  • Object.prototype.isPrototypeOf判斷的是原型鏈關係,而且返回一個布爾值。

  • Object.getPrototypeOf是獲取目標對象的直接原型,返回的是目標對象的原型對象

PropertyIsEnumerable

該方法返回一個布爾值,表示指定的屬性是否可枚舉。它檢測的是對象屬性的enumerable特性。

valueOf & toString

對象轉原始值會用到的方法,以前寫過一篇筆記,具體見js數據類型很簡單,卻也不簡單

toLocaleString

toLocaleString方法返回一個該對象的字符串表示。此方法被用於派生對象爲了特定語言環境的目的(locale-specific purposes)而重載使用。常見於日期對象。

最後

經過閱讀本文,讀者們能夠對Javascript對象有一個基本的認識。對象是Javascript中很是複雜的部分,絕非一篇筆記或一張思惟導圖可囊括,諸多細節不便展開,可關注我留言交流,回覆「思惟導圖」可獲取我整理的思惟導圖。

關注&交流
關注&交流
相關文章
相關標籤/搜索