一、資料類型
在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對象。

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中内容。
可以看到,其numerable、configurable屬性均為false,也就是
Object.prototype.__proto__
屬性不可删除,不可修改屬性特性,并且屬性做了get、set的處理。
Object.prototype與Function.prototype是原型鍊中最難了解也是最重要的兩個對象。下面我們用抽象的方法來了解這兩個對象:
天地伊始,萬物初開,誕生了一個對象,不知其姓名,隻知道他的類型為”[object Object]”,他是一切對象的先祖,為初代對象,繼承于虛無(null)。
後來,又誕生了一個對象,也不知其姓名,隻知道他的類型為”[object Function]”,他是一切函數的先祖,繼承于對象先祖,為二代對象。
經年流轉,函數先祖發揮特長,制造出了一系列的函數,如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");
可以看到,其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]”,在函數建立的時候,會在記憶體中同步建立一個空對象,其過程可以看作:
當我們使用構造函數建立一個對象時,會把構造函數的prototype屬性指派給子對象的
__proto__
屬性,即:
因為F.prototype繼承于Object.prototype,是以有
綜上我們可以看出,原型鍊就是根據
__proto__
維系的由子對象-父對象的一條單向通道,不過要了解這條通道,我們還需要了解構造對象,類,prototype,constructor等,這些都是原型鍊上的美麗的風景。
最後希望大家可以在javascript的大道上肆意馳騁。
其他好文
JavaScript 世界萬物誕生記
Prototype 與 Proto 的愛恨情仇