JavaScript中的對象分類
在瀏覽器環境中,我們無法單純依靠JavaScript代碼實現div對象,只能靠document.createElement來創建。說明了JavaScript的對象機制並非簡單的屬性集合+原型。
日常工作中,接觸到的主要API,幾乎都是由今天所講解的這些對象提供的。理解這些對象的性質,我們才能真正理解我們使用的API的一些特性。
我們可以把對象分成幾類。
- 宿主對象(host Objects):由JavaScript宿主環境提供的對象,它們的行爲完全由宿主環境決定。
- 內置對象(Built-in Objects):由JavaScript語言提供的對象。
- 固有對象(Intrinsic Objects):由標準規定,隨着JavaScript運行時創建而自動創建的對象實例。
- 原生對象(Native Objects):可以由用戶通過Array、RegExp等內置構造器或者特殊語法創建的對象。
- 普通對象(Ordinary Objects):由{}語法、Object構造器或者class關鍵字定義類創建的對象,它能夠被原型繼承。
下面我會爲你一一講解普通對象之外的對象類型。
宿主對象
JavaScript宿主對象千奇百怪,但是前端最熟悉的無疑是瀏覽器環境中的宿主了。
在瀏覽器環境中,我們都知道全局對象是window,window上又有很多屬性,如document。
實際上,這個全局對象window上的屬性,一部分來自JavaScript語言,一部分來自瀏覽器環境。
宿主對象也分爲固有的和用戶可創建的兩種,比如document.createElement就可以創建一些dom對象。
宿主也會提供一些構造器,比如我們可以使用new Image來創建img元素
內置對象·固有對象
固有對象是由標準規定,隨着JavaScript運行時創建而自動創建的對象實例。
固有對象在任何JS代碼執行前就已經被創建出來了,它們通常扮演者類似基礎庫的角色。我們前面提到的“類”其實就是固有對象的一種。
內置對象·原生對象
把JavaScript中,能夠通過語言本身的構造器創建的對象稱作原生對象。在JavaScript標準中,提供了30多個構造器。按照我的理解,按照不同應用場景,我把原生對象分成了以下幾個種類。
通過這些構造器,我們可以用new運算創建新的對象,所以我們把這些對象稱作原生對象。
幾乎所有這些構造器的能力都是無法用純JavaScript代碼實現的,它們也無法用class/extend語法來繼承。
用對象來模擬函數與構造器:函數對象與構造器對象
在JavaScript中,還有一個看待對象的不同視角,這就是用對象來模擬函數和構造器。
函數對象的定義是:具有[[call]]私有字段的對象,構造器對象的定義是:具有私有字段[[construct]]的對象。
JavaScript用對象模擬函數的設計代替了一般編程語言中的函數,它們可以像其它語言的函數一樣被調用、傳參。任何宿主只要提供了“具有[[call]]私有字段的對象”,就可以被 JavaScript 函數調用語法支持。
我們可以這樣說,任何對象只需要實現[[call]],它就是一個函數對象,可以去作爲函數被調用。而如果它能實現[[construct]],它就是一個構造器對象,可以作爲構造器被調用。
對於爲JavaScript提供運行環境的程序員來說,只要字段符合,我們在上文中提到的宿主對象和內置對象(如Symbol函數)可以模擬函數和構造器。
當然了,用戶用function關鍵字創建的函數必定同時是函數和構造器。不過,它們表現出來的行爲效果卻並不相同。
對於宿主和內置對象來說,它們實現[[call]](作爲函數被調用)和[[construct]](作爲構造器被調用)不總是一致的。比如內置對象 Date 在作爲構造器調用時產生新的對象,作爲函數時,則產生字符串,見以下代碼:
console.log(new Date); // 1
console.log(Date())
而瀏覽器宿主環境中,提供的Image構造器,則根本不允許被作爲函數調用。
console.log(new Image);
console.log(Image());//拋出錯誤
再比如基本類型(String、Number、Boolean),它們的構造器被當作函數調用,則產生類型轉換的效果。
值得一提的是,在ES6之後 => 語法創建的函數僅僅是函數,它們無法被當作構造器使用。
對於用戶使用 function 語法或者Function構造器創建的對象來說,[[call]]和[[construct]]行爲總是相似的,它們執行同一段代碼:
function f(){
return 1;
}
var v = f(); //把f作爲函數調用
var o = new f(); //把f作爲構造器調用
我們大致可以認爲,它們[[construct]]的執行過程如下:
- 以 Object.protoype 爲原型創建一個新對象;
- 以新對象爲 this,執行函數的[[call]];
- 如果[[call]]的返回值是對象,那麼,返回這個對象,否則返回第一步創建的新對象。
這樣的規則造成了個有趣的現象,如果我們的構造器返回了一個新的對象,那麼new創建的新對象就變成了一個構造函數之外完全無法訪問的對象,這一定程度上可以實現“私有”。
function cls(){
this.a = 100;
return {
getValue:() => this.a
}
}
var o = new cls;
o.getValue(); //100
//a在外面永遠無法訪問到
特殊行爲的對象
除了上面介紹的對象之外,在固有對象和原生對象中,有一些對象的行爲跟正常對象有很大區別。
它們常見的下標運算(就是使用中括號或者點來做屬性訪問)或者設置原型跟普通對象不同,這裏我簡單總結一下。
- Array:Array的length屬性根據最大的下標自動發生變化。
- Object.prototype:作爲所有正常對象的默認原型,不能再給它設置原型了。
- String:爲了支持下標運算,String的正整數屬性訪問會去字符串裏查找。
- Arguments:arguments的非負整數型下標屬性跟對應的變量聯動。
- 模塊的namespace對象:特殊的地方非常多,跟一般對象完全不一樣,儘量只用於import吧。
- 類型數組和數組緩衝區:跟內存塊相關聯,下標運算比較特殊。
bind後的function:跟原來的函數相關聯。
總結
不使用new運算符,儘可能找到獲得對象的方法。
例子:
var o = {}
var o = function(){}
// 1. 利用字面量
var a = [], b = {}, c = /abc/g
// 2. 利用dom api
var d = document.createElement('p')
// 3. 利用JavaScript內置對象的api
var e = Object.create(null)
var f = Object.assign({k1:3, k2:8}, {k3: 9})
var g = JSON.parse('{}')
// 4.利用裝箱轉換
var h = Object(undefined), i = Object(null), k = Object(1), l = Object('abc'), m = Object(true)
// 使用 Object 構造器
var o = new Object();
// 使用 function
var o = new function f() {};
// 使用 method
var o = Object.create(null)
// 使用 ES6 class
class myOwnObject {
constructor(a) { this.a = a; }
}
var o = new myOwnObject(‘hey yo’);
小實驗:獲取全部JavaScript固有對象
我們從JavaScript標準中可以找到全部的JS對象定義。JS語言規定了全局對象的屬性。
三個值:
Infinity、NaN、undefined。
九個函數:
- eval
- isFinite
- isNaN
- parseFloat
- parseInt
- decodeURI
- decodeURIComponent
- encodeURI
- encodeURIComponent
一些構造器:
Array、Date、RegExp、Promise、Proxy、Map、WeakMap、Set、WeapSet、Function、Boolean、String、Number、Symbol、Object、Error、EvalError、RangeError、ReferenceError、SyntaxError、TypeError
URIError、ArrayBuffer、SharedArrayBuffer、DataView、Typed Array、Float32Array、Float64Array、Int8Array、Int16Array、Int32Array、UInt8Array、UInt16Array、UInt32Array、UInt8ClampedArray。
四個用於當作命名空間的對象:
- Atomics
- JSON
- Math
- Reflect
我們使用廣度優先搜索,查找這些對象所有的屬性和Getter/Setter,就可以獲得JavaScript中所有的固有對象。
var set = new Set();
var objects = [
eval,
isFinite,
isNaN,
parseFloat,
parseInt,
decodeURI,
decodeURIComponent,
encodeURI,
encodeURIComponent,
Array,
Date,
RegExp,
Promise,
Proxy,
Map,
WeakMap,
Set,
WeakSet,
Function,
Boolean,
String,
Number,
Symbol,
Object,
Error,
EvalError,
RangeError,
ReferenceError,
SyntaxError,
TypeError,
URIError,
ArrayBuffer,
SharedArrayBuffer,
DataView,
Float32Array,
Float64Array,
Int8Array,
Int16Array,
Int32Array,
Uint8Array,
Uint16Array,
Uint32Array,
Uint8ClampedArray,
Atomics,
JSON,
Math,
Reflect];
objects.forEach(o => set.add(o));
for(var i = 0; i < objects.length; i++) {
var o = objects[i]
for(var p of Object.getOwnPropertyNames(o)) {
var d = Object.getOwnPropertyDescriptor(o, p)
if( (d.value !== null && typeof d.value === "object") || (typeof d.value === "function"))
if(!set.has(d.value))
set.add(d.value), objects.push(d.value);
if( d.get )
if(!set.has(d.get))
set.add(d.get), objects.push(d.get);
if( d.set )
if(!set.has(d.set))
set.add(d.set), objects.push(d.set);
}
}