從數據類型講原型原型鏈

系列文章

1 、js數據類型--objectjavascript

1、數據類型

  在JavaScript中,數據類型能夠分爲原始類型以及引用類型。其中原始類型包括string,number, boolean, null, undefined, symbol(ES6新增,表示獨一無二的值),這6種數據類型是按照值進行分配的,是存放在棧(stack)內存中的簡單數據段,能夠直接訪問,數據大小肯定,內存空間大小能夠分配。引用類型包括function,object,array等能夠可使用new建立的數據,又叫對象類型,他們是存放在堆(heap)內存中的數據,如var a = {},變量a實際保存的是一個指針,這個指針指向對內存中的數據 {}html

傳送門:更多symbol的用法能夠看阮一峯ECMAScript 6 入門java

  講到數據,那不得不講的就是變量,JavaScript中的變量具備動態類型這一特性,這意味着相同的變量可用做不一樣的類型:es6

var x;            // x 爲 undefined
x = 6;            // x 爲 number
x = "hfhan";      // x 爲 string

  JavaScript中能夠用typeof 操做符來檢測一個數據的數據類型,可是須要注意的是typeof null結果是object, 這是個歷史遺留bug:web

typeof 123;               // "number"
typeof "hfhan";           // "string"
typeof true;              // "boolean"
typeof null;              // "object"  獨一份的不同凡響
typeof undefined;         // "undefined"
typeof Symbol("hfhan");   // "symbol"
typeof function(){};      // "function"
typeof {};                // "object"

2、對象類型

先理解下什麼是宿主環境:由web瀏覽器或是桌面應用系統造就的js引擎執行的環境即宿主環境。segmentfault

一、本地對象

  ECMA-262 把本地對象(native object)定義爲「獨立於宿主環境的 ECMAScript 實現提供的對象」。數組

  本地對象包含但不限於Object、Function、Array、String、Boolean、Number、Date、RegExp、各類錯誤類對象(Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError、URIError)promise

  注意:這裏的Object、Function、Array等不是指構造函數,而是指對象的類型瀏覽器

二、內置對象

  ECMA-262 把內置對象(built-in object)定義爲「由 ECMAScript 實現提供的、獨立於宿主環境的全部對象,在 ECMAScript 程序開始執行時出現」。這意味着開發者沒必要明確實例化內置對象,它已被實例化了。ECMA-262 只定義了兩個內置對象,即 Global 和 Math (它們也是本地對象,根據定義,每一個內置對象都是本地對象)。app

  其中Global對象是ECMAScript中最特別的對象,由於實際上它根本不存在,但你們要清楚,在ECMAScript中,不存在獨立的函數,全部函數都必須是某個對象的方法。相似於isNaN()、parseInt()和parseFloat()方法等,看起來都是函數,而實際上,它們都是Global對象的方法。並且Global對象的方法還不止這些。有關Global對象的具體方法和屬性,感興趣的同窗能夠看一下這裏:JavaScript 全局對象參考手冊

  對於web瀏覽器而言,Global有一個代言人window,可是window並非ECMAScripta規定的內置對象,由於window對象是相對於web瀏覽器而言的,而js不只僅能夠用在瀏覽器中。

  Global與window的關係能夠看這裏:概念區分:JavaScript中的global對象,window對象以及document對象

  能夠看出,JavaScript中真正的內置對象其實只有兩個:Global 和 Math,但是觀看網上的文章資料,千篇一概的都在講JavaScript的11大內置對象(不是說只有11個,而是經常使用的有11個:Object、Function、Array、String、Boolean、Number、Date、RegExp、Error、Math、Global,ES6中出現的Set 、Map、Promise、Proxy等應該也算是比較經常使用的),這是不嚴謹的,JavaScript中本地對象、內置對象和宿主對象一文中,把本地對象、內置對象統稱爲「內部對象」,算是比較貼切的。

  更多「內部對象」能夠查看MDN>JavaScript>引用>內置對象 內容,或者經過瀏覽器控制檯打印window來查找。

三、宿主對象

由ECMAScript實現的宿主環境提供的對象,能夠理解爲:瀏覽器提供的對象。全部的BOM和DOM都是宿主對象。

四、自定義對象

顧名思義,就是開發人員本身定義的對象。JavaScrip容許使用自定義對象,使JavaScript應用及功能獲得擴充

五、判斷對象的類型

對象的類型不能使用typeof來判斷,由於除了Function外其餘類型的對象所獲得的結果全爲"object"

typeof function(){};      // "function"
typeof {};                // "object"
typeof new RegExp;        // "object"
typeof new Date;          // "object"
typeof Math;              // "object"
typeof new Error;         // "object"
…

一個使用最多的檢測對象類型的方法是 Object.prototype.toString

Object.prototype.toString.apply(new Function);     // "[object Function]"
Object.prototype.toString.apply(new Object);       // "[object Object]"
Object.prototype.toString.apply(new Date);         // "[object Date]"
Object.prototype.toString.apply(new Array);        // "[object Array]"
Object.prototype.toString.apply(new RegExp);       // "[object RegExp]"
Object.prototype.toString.apply(new ArrayBuffer);  // "[object ArrayBuffer]"
Object.prototype.toString.apply(Math);             // "[object Math]"
Object.prototype.toString.apply(JSON);             // "[object JSON]"
var promise = new Promise(function(resolve, reject) {
    resolve();
});
Object.prototype.toString.apply(promise);          // "[object Promise]"
…

3、構造函數

構造函數是描述一類對象統一結構的函數——至關於圖紙

一、對象的建立

  上面咱們已經知道了,JavaScript中的對象有很對種類型,好比Function、Object、Array、Date、Set等等,那麼咱們如何去建立這些類型的數據?

生成一個函數能夠經過function關鍵字:

function a(){
    console.log(1)
}
//或者
var b = function(){
    console.log(2)
}

  此外建立一個對象(類型爲Object的對象),能夠經過{};建立一個數組,能夠經過[];建立一個正則對象能夠經過/.*/。可是那些沒有特殊技巧的對象,就只能老老實實使用構造函數來建立了。

  JavaScript 語言中,生成實例對象的傳統方法是經過構造函數,即咱們經過函數來建立對象,這也證實了函數在JavaScript中具備很是重要的地位,所以說函數是一等公民。

二、構造函數建立對象

  JavaScript中的對象在使用的時候,大部分都須要先進行實例化(除了已經實例化完成的Math對象以及JSON對象):

var a = new Function("console.log('a') ");  //構造函數建立Function對象
var b = new Object({a:1});                  //構造函數建立Object對象
var c = new Date();                         //構造函數建立Date對象
var d = new Set();                          //構造函數建立Set對象
var e = new Array(10);                      //構造一個初始長度爲10的數組對象

  能夠看出,只要使用new關鍵字來實例化一個構造函數就能夠建立一個對象了,JavaScript中內部對象的構造函數是瀏覽器已經封裝好的,咱們能夠直接拿過來使用。

使用構造函數建立的數據全是對象,即便用new關鍵字建立的數據全是對象,其中new作了4件事:

1)、先建立空對象
2)、用空對象調用構造函數,this指向正在建立的空對象 
    按照構造函數的定義,爲空對象添加屬性和方法
3)、將新建立對象的__proto__屬性指向構造函數的prototype對象。
4)、將新建立對象的地址,保存到等號左邊的變量中

除了瀏覽器自己自帶的構造函數,咱們還可使用一個普通的函數來建立對象:

function Person(){};
var p1 = new Person()

  這個例子中Person就是一個普普統統的空函數,可是他依然能夠做爲構造函數來建立對象,咱們打印下p1的類型,能夠看出使用自定義的構造函數,所建立的對象類型爲 Object

Object.prototype.toString.apply(p1);  // "[object Object]"

三、構造函數和普通函數

  實際上並不存在建立構造函數的特殊語法,其與普通函數惟一的區別在於調用方法。對於任意函數,使用new操做符調用,那麼它就是構造函數,又叫工廠函數;不使用new操做符調用,那麼它就是普通函數。

  按照慣例,咱們約定構造函數名以大寫字母開頭,普通函數以小寫字母開頭,這樣有利於顯性區分兩者。例如上面的new Object (),new Person ()。

4、原型與原型鏈

一、prototype 與 __proto

原型是指原型對象,原型對象從哪裏來?

  每一個函數在被建立的時候,會同時在內存中建立一個空對象,每一個函數都有一個prototype 屬性,這個屬性指向這個空對象,那麼這個空對象就叫作函數的原型對象,而每個原型對象中都會有一個constructor屬性,指向該函數

function b(){console.log(1)};
b.prototype.constructor === b;   // true

抽象理解:構造函數是妻子,原型對象是丈夫,prototype是找丈夫,constructor是找妻子。

手動更改函數的原型對象:

var a = {a:1};
b. prototype = a;  //更改b的原型對象爲a
a. constructor;    // function Object() { [native code] }

爲何這裏a. constructor不指向b函數?

  這是由於變量a所對應的對象是事先聲明好的,不是跟隨函數一塊兒建立的,因此他沒有constructor屬性,這時候尋找constructor屬性就會到父對象上去找,而全部對象默認都繼承自Object. Prototype,因此最後找的就是Object. Prototype. Constructor,也就是Object函數。

剛纔講到了繼承,繼承又是怎麼一回事呢?

全部對象都有一個__proto__ 屬性,這個屬性指向其父元素,也就是所繼承的對象,通常爲構造函數的prototype對象。

prototype 與 __proto

  prototype 是函數獨有的;__proto__ 是全部對象都有的,是繼承的。調用一個對象的某一屬性,若是該對象上沒有該屬性,就會去其原型鏈上找。

  好比上例中,調用p.a,對象p上找不到a屬性,就會去找p.__proto__.a,p.__proto__.a也找不到,就會去找p.__proto__.__proto__.a,依次類推,直到找到Object.prototype.a也沒找到,就會返回undefined。

  原型鏈是由各級子對象的__proto__屬性連續引用造成的結構,全部對象原型鏈的頂部都是Object.prototype。

  咱們知道,當子對象被實例化以後再去修改構造函數的prototype屬性是不會改變子對象與原型對象的繼承關係的,可是經過修改子對象的__proto__屬性,咱們能夠解除子對象與原型對象之間的繼承關係。

var A = function(){};    // 構造函數
A.prototype = {a:1};     // 修改原型對象
var a = new A;           // 實例化子對象a,此時a繼承自{a:1}
a.a                      // 1
A.prototype = {a:2}      // 更該構造函數的原型對象
a.a                      // 1   此時,a還是繼承自{a:1}
a.__proto__ = {a:3}      // 修改a的原型鏈
a.a                      // 3   此時,a繼承自{a:3}

二、Object.prototype與Function.prototype

  一切誕生於虛無!

  上面講了,全部對象原型鏈的頂部都是Object.prototype,那麼Object.prototype是怎麼來的,憑空造的嗎?還真是!

Object.prototype.__proto__ === null;   // true

  上面講了,咱們能夠經過修改對象的__proto__屬性來更改繼承關係,可是,Object.prototype的__proto__屬性不容許更改,這是瀏覽器對Object.prototype的保護措施,修改Object.prototype的__proto__屬性會拋出錯誤。同時,Object.prototype.__proto__也只能進行取值操做,由於null 和 underfined沒有對應的包裝類型,所以不能調用任何方法及屬性

  在控制檯打印下Object.prototype.__proto__的保護屬性:

Object.getOwnPropertyDescriptor(Object.prototype,"__proto__");

注:保護屬性及getOwnPropertyDescriptor爲ES5中內容。

1

  能夠看到,其numerable、configurable屬性均爲false,也就是Object.prototype.__proto__屬性不可刪除,不可修改屬性特性,而且屬性作了get、set的處理。

  Object.prototype與Function.prototype是原型鏈中最難理解也是最重要的兩個對象。下面咱們用抽象的方法來理解這兩個對象:

  天地伊始,萬物初開,誕生了一個對象,不知其姓名,只知道他的類型爲"[object Object]",他是一切對象的先祖,爲初代對象,繼承於虛無(null)。

  後來,又誕生了一個對象,也不知其姓名,只知道他的類型爲"[object Function]",他是一切函數的先祖,繼承於對象先祖,爲二代對象。

Object.prototype與Function.prototype

  經年流轉,函數先祖發揮特長,製造出了一系列的函數,如Object、Function、Array、Date、String、Number等,都說龍生九子各有不一樣,這些函數雖然說各個都貌美如花,神統統天,但功能上仍是有很大的區別的。

  其中最須要關注的是Object以及Function。原來函數先祖在創造Function的時候,悄悄的把Function的prototype屬性指向了本身,也把本身的constructor屬性指向了Function。若是說Function是函數先祖爲本身創造的妻子,那麼Object就是函數先祖爲對象先祖創造的妻子,一樣的,Object的prototype屬性指向了對象先祖,對象先祖也把本身的constructor屬性指向Object,表示他贊成了這門婚事。

  此後,世人都稱對象先祖爲Object.prototype,函數先祖爲Function.prototype。

  從上能夠看出,對象先祖是一開始就存在的,而不是同Object一塊兒被建立的,因此手動更改Object.prototype的指向後:

Object.prototype = {a:1};    //修改Object.prototype的指向
var a = {};                  //經過字面量建立對象
a.a                          //undefined 此時a仍然繼承於對象先祖
var b = new Object();        //經過new來建立對象
b.a                          //結果是???

  這裏我本來覺得會打印1,可是實際上打印的仍是undefined,而後在控制檯打印下Object.prototype,發現Object.prototype仍然指向對象先祖,也就是說Object.prototype = {a:1}指向更改失敗,我猜想和上面Object.prototype的__proto__屬性不容許更改,緣由是同樣的,是瀏覽器對Object.prototype的保護措施。

  在控制檯打印下Object.prototype的保護屬性:

Object.getOwnPropertyDescriptor(Object,"prototype");

2

  能夠看到,其writable、enumerable、configurable屬性均爲false,也就是其prototype屬性不可修改,不可刪除,不可修改屬性特性。

  其實不光Object.prototype不能修改,Function. Prototype、String. Prototype等內部對象都不容許修。

咱們繼續往下看

3

由於Object、Function、Array、String等都繼承自Function.prototype,因此有

Object.__proto__ === Function.prototype;       // true
Function.__proto__ === Function.prototype;     // true
Array.__proto__ === Function.prototype;        // true
String.__proto__ === Function.prototype;       // true

全部的對象都繼承於Object.prototype,因此有

Function.prototype.__proto__ === Object.prototype;     // true
Array.prototype.__proto__ === Object.prototype;        // true
String.prototype.__proto__ === Object.prototype;       // true

三、自定義構造函數建立對象

  當咱們自定義一個對象的時候,這個對象在整個原型鏈上的位置是怎麼樣的呢?

  這裏咱們不對對象的建立方式多作討論,僅以構造函數爲例

  當咱們使用字面量建立一個對象的時候,其父對象默認爲對象先祖,也就是Object.prototype

var a = {};
a.__proto__ === Object.prototype;  // true

  上面講了,自定義構造函數所建立的對象他的類型均爲"[object Object]",在函數創建的時候,會在內存中同步創建一個空對象,其過程能夠看做:

function F(){};  // prototype 賦值  F.prototype = {},此時{}繼承於Object.prototype

  當咱們使用構造函數建立一個對象時,會把構造函數的prototype屬性賦值給子對象的__proto__屬性,即:

var a = new F();   //__proto__賦值 a.__proto__ = F.prototype;

  由於F.prototype繼承於Object.prototype,因此有

a.__proto__.__proto__ === Object.prototype;  // true

z

  綜上咱們能夠看出,原型鏈就是根據__proto__維繫的由子對象-父對象的一條單向通道,不過要理解這條通道,咱們還須要理解構造對象,類,prototype,constructor等,這些都是原型鏈上的美麗的風景。

  最後但願你們能夠在javascript的大道上肆意馳騁。

其餘好文

JavaScript 世界萬物誕生記
Prototype 與 Proto 的愛恨情仇

相關文章
相關標籤/搜索