從js資料型別到原型原型鏈

hf_驕陽似火發表於2018-04-07

#系列文章

1 、js資料型別--object

一、資料型別

  在JavaScript中,資料型別可以分為原始型別以及引用型別。其中原始型別包括string,number, boolean, null, undefined, symbol(ES6新增,表示獨一無二的值),這6種資料型別是按照值進行分配的,是存放在棧(stack)記憶體中的簡單資料段,可以直接訪問,資料大小確定,記憶體空間大小可以分配。引用型別包括function,object,array等可以可以使用new建立的資料,又叫物件型別,他們是存放在堆(heap)記憶體中的資料,如var a = {},變數a實際儲存的是一個指標,這個指標指向對記憶體中的資料 {}

傳送門:更多symbol的用法可以看阮一峰ECMAScript 6 入門

  講到資料,那不得不講的就是變數,JavaScript中的變數具有動態型別這一特性,這意味著相同的變數可用作不同的型別:

var x;            // x 為 undefined
x = 6;            // x 為 number
x = "hfhan";      // x 為 string
複製程式碼

  JavaScript中可以用typeof 操作符來檢測一個資料的資料型別,但是需要注意的是typeof null結果是object, 這是個歷史遺留bug:

typeof 123;               // "number"
typeof "hfhan";           // "string"
typeof true;              // "boolean"
typeof null;              // "object"  獨一份的與眾不同
typeof undefined;         // "undefined"
typeof Symbol("hfhan");   // "symbol"
typeof function(){};      // "function"
typeof {};                // "object"
複製程式碼

二、物件型別

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

1、本地物件

  ECMA-262 把本地物件(native object)定義為“獨立於宿主環境的 ECMAScript 實現提供的物件”。

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

  注意:這裡的Object、Function、Array等不是指建構函式,而是指物件的型別

2、內建物件

  ECMA-262 把內建物件(built-in object)定義為“由 ECMAScript 實現提供的、獨立於宿主環境的所有物件,在 ECMAScript 程式開始執行時出現”。這意味著開發者不必明確例項化內建物件,它已被例項化了。ECMA-262 只定義了兩個內建物件,即 Global 和 Math (它們也是本地物件,根據定義,每個內建物件都是本地物件)。

  其中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來查詢。

3、宿主物件

由ECMAScript實現的宿主環境提供的物件,可以理解為:瀏覽器提供的物件。所有的BOM和DOM都是宿主物件。

4、自定義物件

顧名思義,就是開發人員自己定義的物件。JavaScrip允許使用自定義物件,使JavaScript應用及功能得到擴充

5、判斷物件的型別

物件的型別不能使用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]"複製程式碼

三、建構函式

建構函式是描述一類物件統一結構的函式——相當於圖紙

1、物件的建立

  上面我們已經知道了,JavaScript中的物件有很對種型別,比如Function、Object、Array、Date、Set等等,那麼我們如何去建立這些型別的資料?

生成一個函式可以通過function關鍵字:

function a(){
	console.log(1)
}
//或者
var b = function(){
	console.log(2)
}
複製程式碼

  此外建立一個物件(型別為Object的物件),可以通過{};建立一個陣列,可以通過[];建立一個正則物件可以通過/.*/。但是那些沒有特殊技巧的物件,就只能老老實實使用建構函式來建立了。

  JavaScript 語言中,生成例項物件的傳統方法是通過建構函式,即我們通過函式來建立物件,這也證明了函式在JavaScript中具有非常重要的地位,因此說函式是一等公民。

2、建構函式建立物件

  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]"
複製程式碼

3、建構函式和普通函式

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

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

四、原型與原型鏈

1、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物件。

protot

  prototype 是函式獨有的;__proto__是所有物件都有的,是繼承的。呼叫一個物件的某一屬性,如果該物件上沒有該屬性,就會去其原型鏈上找。

  比如上例中,呼叫p.a,物件p上找不到a屬性,就會去找p.__proto__.ap.__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}
複製程式碼

2、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.prototyp

  經年流轉,函式先祖發揮特長,製造出了一系列的函式,如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等內部物件都不允許修。

我們繼續往下看

原型鏈

因為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
複製程式碼

3、自定義建構函式建立物件

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

  這裡我們不對物件的建立方式多做討論,僅以建構函式為例

  當我們使用字面量建立一個物件的時候,其父物件預設為物件先祖,也就是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 的愛恨情仇

相關文章