天天看點

從js資料類型到原型原型鍊一、資料類型二、對象類型三、構造函數四、原型與原型鍊其他好文

一、資料類型

  在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 = ;            // x 為 number
x = "hfhan";      // x 為 string
           

  JavaScript中可以用typeof 操作符來檢測一個資料的資料類型,但是需要注意的是

typeof null

結果是object, 這是個曆史遺留bug:

typeof ;               // "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()
}
//或者
var b = function(){
    console.log()
}
           

  此外建立一個對象(類型為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的數組對象
           

  可以看出,隻要使用new關鍵字來執行個體化一個構造函數就可以建立一個對象了,JavaScript中内部對象的構造函數是浏覽器已經封裝好的,我們可以直接拿過來使用。

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

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

除了浏覽器本身自帶的構造函數,我們還可以使用一個普通的函數來建立對象:

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

  這個例子中Person就是一個普普通通的空函數,但是他依然可以作為構造函數來建立對象,我們列印下

p1

的類型,可以看出使用自定義的構造函數,所建立的對象類型為

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對象。

從js資料類型到原型原型鍊一、資料類型二、對象類型三、構造函數四、原型與原型鍊其他好文

  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:};     // 修改原型對象
var a = new A;           // 執行個體化子對象a,此時a繼承自{a:1}
a.a                      // 1
A.prototype = {a:}      // 更該構造函數的原型對象
a.a                      // 1   此時,a仍是繼承自{a:1}
a.__proto__ = {a:}      // 修改a的原型鍊
a.a                      // 3   此時,a繼承自{a:3}
           

2、Object.prototype與Function.prototype

  一切誕生于虛無!

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

  上面講了,我們可以通過修改對象的

__proto__

屬性來更改繼承關系,但是,Object.prototype的

__proto__

屬性不允許更改,這是浏覽器對Object.prototype的保護措施,修改Object.prototype的

__proto__

屬性會抛出錯誤。同時,

Object.prototype.__proto__

也隻能進行取值操作,因為null 和 underfined沒有對應的包裝類型,是以不能調用任何方法及屬性

  在控制台列印下

Object.prototype.__proto__

的保護屬性:

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

注:保護屬性及getOwnPropertyDescriptor為ES5中内容。

從js資料類型到原型原型鍊一、資料類型二、對象類型三、構造函數四、原型與原型鍊其他好文

  可以看到,其numerable、configurable屬性均為false,也就是

Object.prototype.__proto__

屬性不可删除,不可修改屬性特性,并且屬性做了get、set的處理。

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

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

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

從js資料類型到原型原型鍊一、資料類型二、對象類型三、構造函數四、原型與原型鍊其他好文

  經年流轉,函數先祖發揮特長,制造出了一系列的函數,如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:};    //修改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"); 
           
從js資料類型到原型原型鍊一、資料類型二、對象類型三、構造函數四、原型與原型鍊其他好文

  可以看到,其writable、enumerable、configurable屬性均為false,也就是其prototype屬性不可修改,不可删除,不可修改屬性特性。

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

我們繼續往下看

從js資料類型到原型原型鍊一、資料類型二、對象類型三、構造函數四、原型與原型鍊其他好文

因為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]”,在函數建立的時候,會在記憶體中同步建立一個空對象,其過程可以看作:

  當我們使用構造函數建立一個對象時,會把構造函數的prototype屬性指派給子對象的

__proto__

屬性,即:

  因為F.prototype繼承于Object.prototype,是以有

從js資料類型到原型原型鍊一、資料類型二、對象類型三、構造函數四、原型與原型鍊其他好文

  綜上我們可以看出,原型鍊就是根據

__proto__

維系的由子對象-父對象的一條單向通道,不過要了解這條通道,我們還需要了解構造對象,類,prototype,constructor等,這些都是原型鍊上的美麗的風景。

  最後希望大家可以在javascript的大道上肆意馳騁。

其他好文

JavaScript 世界萬物誕生記

Prototype 與 Proto 的愛恨情仇