重學前端筆記(六)-JavaScript中的對象分類

JavaScript中的對象分類

在瀏覽器環境中,我們無法單純依靠JavaScript代碼實現div對象,只能靠document.createElement來創建。說明了JavaScript的對象機制並非簡單的屬性集合+原型。
日常工作中,接觸到的主要API,幾乎都是由今天所講解的這些對象提供的。理解這些對象的性質,我們才能真正理解我們使用的API的一些特性。

我們可以把對象分成幾類。

  1. 宿主對象(host Objects):由JavaScript宿主環境提供的對象,它們的行爲完全由宿主環境決定。
  2. 內置對象(Built-in Objects):由JavaScript語言提供的對象。
  3. 固有對象(Intrinsic Objects):由標準規定,隨着JavaScript運行時創建而自動創建的對象實例。
  4. 原生對象(Native Objects):可以由用戶通過Array、RegExp等內置構造器或者特殊語法創建的對象。
  5. 普通對象(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);
    }
}
發佈了12 篇原創文章 · 獲贊 4 · 訪問量 6485
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章