談論JavaScript對象——我的總結

前言

疑惑、懷疑與思考

JavaScript究竟是面向對象仍是基於對象?

與其它的語言相比,JavaScript老是顯得不那麼合羣。好比:javascript

  • 不一樣於其它的面嚮對象語言,JavaScript一直沒有類的概念(ES6以前),ES6的到來也並無改變它是基於原型的本質,這點是最讓開發人員困惑的地方
  • _proto_ 和 prototype 傻傻分不清
  • 對象能夠是由 new 關鍵字實例化,也能夠直接由花括號定義
  • JavaScript對象能夠自由添加屬性,而其餘的語言不行

在被詬病和爭論中,有人喊出JavaScript並不是「非面向對象」的語言,而是「基於對象」的語言。可是,對於如何定義「面向對象」和「基於對象」,基本上很難有人可以回答。html

JavaScript究竟是否須要模擬類class?

這須要明白JavaScript語言的設計思想,才能更清楚究竟是否須要模擬類,以及爲何須要模擬類。在早期人們習慣於其餘語言的面向對象編程方式,而對JavaScript感到困惑,並嘗試用貼近類的方式去編程。java

溯源與再思考

什麼是面向對象?

咱們先看看JavaScript對對象的定義:「語言和宿主的基礎設施由對象來提供,而且 JavaScript 程序便是一系列互相通信的對象集合」。這裏的意思根本不是表達弱化面向對象的意思,反而是表達對象對於語言的重要性。程序員

到底什麼是面向對象?Objcet在英文中,是一切事物的總稱,這和麪向對象編程的抽象思惟有相通之處。中文翻譯「對象」卻沒有這樣的普適性。在不一樣的編程語言中,設計者也利用各類不一樣的語言特性來描述對象,最爲成功的流派是使用「類」的方式來描述對象,這誕生了諸如C++、Java等流行的編程語言。編程

而JavaScript早年則選擇了一個更爲冷門的流派:原型。這是不合羣的緣由之一。數組

然而不幸的是,由於一些公司政治緣由,JavaScript推出之時受管理層之命被要求模仿 Java,因此,JavaScript 創始人 Brendan Eich 在「原型運行時」的基礎上引入了 new、this 等語言特性,使之「看起來更像 Java」。瀏覽器

所以,咱們至少須要明白一件事,咱們以前所熟知的「面向對象」的編程方式,實際上是「基於類」的面向對象,它並非面向對象的所有,確切地說,基於類只是面向對象編程的一個流派而已。而想要理解JavaScript對象,就必須清空咱們認識「基於類的面向對象」相關的概念,回到人類對對象的樸素認識和無關語言的基礎理論,咱們就可以理解JavaScript面向對象設計的思路。app

什麼是原型?什麼是類?

「基於類」的編程提倡使用一個關注分類和類之間關係開發模型。在這類語言中,老是先有類,再從類去實例化一個對象。類與類之間又可能會造成繼承、組合等關係。類又每每與語言的類型系統整合,造成必定編譯時的能力。編程語言

與此相對,「基於原型」的編程看起來更爲提倡程序員去關注一系列對象實例的行爲,然後纔去關心如何將這些對象,劃分到最近的使用方式類似的原型對象,而不是將它們分紅類。函數

基於原型和基於類都可以知足基本的複用和抽象需求,可是適用的場景不太相同。這就像專業人士可能喜歡在看到老虎的時候,喜歡用貓科豹屬豹亞種來描述它,可是對一些不那麼正式的場合,「大貓」可能更爲接近直觀的感覺一些。咱們的 JavaScript 並不是第一個使用原型的語言,在它以前,self、kevo 等語言已經開始使用原型來描述對象了。

JavaScript的原型與對象

JavaScript原型:

拋開模擬Java的複雜語法設施(new、Function Object、函數的prototype屬性等),原型系統能夠說至關簡單,用兩條能夠歸納:

  • 若是全部對象都有私有字段 [[prototype]],就是對象的原型;
  • 讀一個屬性,若是對象自己沒有,則會繼續訪問對象的原型,直到原型爲空或者找到爲止。

這個模型在之前的各個歷史版本中並無大的改變,在ES6提供了一些列內置函數,能夠更直接地訪問操做原型:

  • Object.create 根據指定的原型建立新對象,原型能夠是 null;
  • Object.getPrototypeOf 得到一個對象的原型;
  • Object.setPrototypeOf 設置一個對象的原型

利用這三個方法,咱們能夠徹底拋開類的思惟,利用原型來實現抽象和複用。例如:

// 這段代碼建立了一個「貓」對象,又根據貓作了一些修改建立了虎,以後咱們徹底能夠用 Object.create 來建立另外的貓和虎對象,咱們能夠經過「原始貓對象」和「原始虎對象」來控制全部貓和虎的行爲
var cat = {
    say(){
        console.log("meow~");
    },
    jump(){
        console.log("jump");
    }
}

var tiger = Object.create(cat,  {
    say:{
        writable:true,
        configurable:true,
        enumerable:true,
        value:function(){
            console.log("roar!");
        }
    }
})


var anotherCat = Object.create(cat);

anotherCat.say();

var anotherTiger = Object.create(tiger);

anotherTiger.say();

這段代碼建立了一個「貓」對象,又根據貓作了一些修改建立了虎,以後咱們徹底能夠用 Object.create 來建立另外的貓和虎對象,咱們能夠經過「原始貓對象」和「原始虎對象」來控制全部貓和虎的行爲。可是,在更早的版本中,程序員只能經過 Java 風格的類接口來操縱原型運行時,能夠說很是彆扭。

JavaScript對象的特徵:

  • 對象具備惟一標識性:這個標識不是看變量名,而是內存地址
  • 對象有狀態
  • 對象具備行爲
  • 對象具備動態性

關於惟一標識性,若是分別定義兩個結構和值如出一轍的對象,他們兩個是不相等的(a1 === a2爲false)

關於狀態和行爲,不一樣語言會有不一樣的描述,Java中稱他們爲「屬性」和「方法」,在JavaScript中,狀態和行爲統一抽象爲「屬性」。

前三個特徵是任何面嚮對象語言都具有的,而JavaScript對象獨有的特點是:對象具備高度的動態性,賦予了使用者在運行時爲對象添改狀態和行爲的能力。

例如,下面的例子展現了向一個對象添加屬性,這樣操做徹底OK:

let o = { a: 1 };
o.b = 2;
console.log(o.a, o.b); //1 2

同時,JavaScript的屬性被設計成比別的語言更加複雜的形式,它提供了數據屬性(描述屬性)和訪問器屬性兩類屬性描述符(能夠理解爲針對屬性的屬性,這兩類屬性描述符不能同時存在,只能選取一種)

數據屬性在很早的版本中實現,訪問器屬性在ES6新增。詳情可見下方總結,簡單來講就是:數據屬性能夠規定某屬性的值、是否可寫、是否可枚舉、是否能被修改配置(包含是否能刪除);訪問器屬性則主要定義了訪問時的行爲和結果。

既然ES6有了class,咱們是否是能夠拋棄原型了?

如今能夠回答JavaScript是否須要模擬類這個問題了。其實,JavaScript本不須要模擬類,可是由於你們習慣於類的編程方式(以及一些其餘緣由,總結在下方),ES6正式開始使用class關鍵字,new + function的怪異搭配能夠徹底拋棄了。咱們推薦在任何場景都使用ES6的語法來定義類。但在這裏須要說明,class關鍵字的使用,其本質仍是基於原型,class extends 只是語法糖,徹底不存在拋棄原型一說。

如今,咱們能夠總結一下爲何JavaScript對象讓人困惑了:

1.大部分面嚮對象語言都是基於類的流派,而基於原型的比較小衆。

  基於原型本是一個優秀的抽象對象的形式,可是「基於類」的面向對象已經先入爲主成爲大部分人的思惟,在缺少系統性學習的前提下,嘗試用基於類的思想去理解並掌握基於原型的處理方式,只會讓人更加懷疑和困惑。

  其實,不止有人對基於原型有疑問,也有人對基於類表達過疑惑,只是對於大部分人來講質疑一個如此成功的流派顯得多餘,慢慢地認爲面向對象理所應當就是這樣。

2.早期因爲公司政治緣由,模仿java的一些語法和方式,不只怪異,更加深了人們的困惑。

全面認識JavaScript對象

JavaScript對象分類

JavaScript 對象並不是只有一種,好比在瀏覽器環境中咱們沒法單純依靠js代碼實現 div 對象,只能靠 document.createElement 來建立,這說明了 JavaScript 的對象機制並不是簡單的屬性集合 + 原型。

JavaScript 中的對象能夠分爲如下幾類,這也與 JavaScript 語言的組成有關:

  • 宿主對象(host Objects):由 JavaScript 宿主環境提供的對象,它們的行爲徹底由宿主環境決定。
  • 內置對象(Built-in Objects):由 JavaScript 語言提供的對象。
    • 固有對象(Intrinsic Objects):由標準規定,隨着 JavaScript 運行時自動建立的對象
    • 原生對象(Native Objects):能夠由用戶經過內置構造器建立的對象
    • 普通對象(Ordinary Objects):由 {} 語句、Object 構造器或者 class 關鍵字定義的對象,它可以被原型繼承

宿主對象

在瀏覽器環境中,咱們知道全局對象是 window ,window 上又有不少屬性,這裏的屬性一部分來自 JavaScript 語言,一部分來自瀏覽器環境。JavaScript 標準中規定了全局對象屬性,來自瀏覽器宿主部分的能夠理解爲 W3C 的 HTML 標準(或非標準)的API,例如DOM、BOM。

固有對象

固有對象是由標準規定,隨着 JavaScript 運行時而自動建立的對象實例。這些對象在任何JavaScript代碼執行以前就已經被建立出來了,他們一般扮演相似基礎庫的角色,例如 global 對象(瀏覽器環境中是 window 對象)、JSON、Math、Number等。

ECMA標準提供了一份並不全面的固有對象表 ECMA

原生對象

可以經過語言自己的構造器建立的對象稱做原生對象。在JavaScript標準中,提供了30多個構造器,以下:

幾乎全部這些構造器的能力都是沒法用純JavaScript代碼實現的,它們也沒法用 class/extend 語法來繼承。咱們能夠認爲,全部這些原生對象都是爲了特定能力或者性能,設計出來的「特權對象」。

ES6以來的擴展(普通對象)

1.簡潔的表示方法

let name = "...", age = 20;
let obj = { name: name, age: age, func: function () { ... } }; // ES5表示
let obj = { name, age, func() { ... } }; // ES6表示

2.屬性名錶達式

使用方括號,屬性名稱能夠用表達式表示,也能夠用變量名。

let name = "abc", obj = {};
obj[name] = "123";    // obj :{ abc: "123" }
obj["h" + "ello"] = "aa";  // obj:{ abc: "123", "hello": "aa" }

3.方法的 name 屬性

函數有 name 屬性,返回函數名。對象內的方法也是函數,也有 name 屬性。

const person = {
  sayName() {
    console.log('hello!');
  },
};

person.sayName.name   // "sayName"

4.屬性描述符

對象內的每一個屬性都有一個屬性描述符,分爲兩類:數據屬性(也叫描述屬性)、訪問器屬性。二者不能一塊兒用。

數據屬性

咱們經過 Object.getOwnPropertyDescriptor() 方法來獲取某個對象的某個屬性的描述符。

let obj = { a: "hello" };
const descriptor = Object.getOwnPropertyDescriptor(obj, 'a');
console.log(descriptor);
{
    value: "hello"
    writable: true
    enumerable: true
    configurable: true
}

能夠看到,屬性描述符(數據屬性)由四個值組成:

  • value  該屬性的值
  • writable  屬性是否可寫,若是爲false就成爲了只讀屬性。默認爲true
  • enumerable  屬性是否可枚舉,默認爲true
  • configurable  屬性是否可配置。爲true表示此屬性的屬性描述符能夠被修改,屬性也能夠被刪除,false則不可刪除不可修改。默認爲true

如何設置屬性描述符呢?經過 Object.defineProperty()/Object.defineProperties() 方法:

Object.defineProperty(obj, prop, descriptor) 在對象上定義/修改一個屬性,並返回該對象
例:
    let obj = {};
    let des = Object.defineProperty(obj, "a", {value: 123, writable: false});
    obj.a // 123
    console.log(des)  // {value: 123, writable: false,enumerable: true,configurable: true} // enumerable和configurable默認爲true

Object.defineProperties(obj, props) 在對象上定義/修改多個屬性,並返回該對象
例:
    let obj = {};
    let des = Object.defineProperty(obj, {
        a: {value: 123, writable: true},
        b: {value: 456, writable: true},
    });

訪問器屬性

訪問器屬性一樣有四個:value、writable、get、set

其中最有用的是 getter/setter 函數,當經過對象取值/賦值時,會觸發對應的函數。例如:

let obj = {
    _name: "hello",
    get name() { return this._name },
    set name(value) { this._name = value },
};
obj.name // "hello"
obj.name = "haha";
obj.name // "haha"

須要注意,當定義了 setter/getter 函數後,name屬性真實存在,但在取值/賦值函數內部沒法獲取到同名屬性 name ,也就是說,不能將 getter/setter 函數名和屬性名相同,這點與 Proxy 不一樣。在上面的例子中,當訪問 name 屬性時實際訪問的是 _name 屬性。

另外,上面提到,不能同時使用兩種屬性描述符,不然會報錯,如:

let obj = {};
Object.defineProperty(obj, "a", {
  get : function(){
    return bValue;
  },
  set : function(newValue){
    bValue = newValue;
  },
  writable: true,  // 該屬性只能用於數據描述符
});
// throws a TypeError: value appears only in data descriptors, get appears only in accessor descriptors

5.super關鍵字

咱們知道,this 關鍵字老是指向函數所在的當前對象,ES6新增了一個相似的關鍵字 super,指向當前對象的原型對象。

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello" obj對象經過super關鍵字引用了原型對象的foo屬性

注意:當super關鍵字表示原型對象時,只能用在對象的方法之中,用在其餘地方都會報錯。

6.遍歷

遍歷涉及到屬性是否可枚舉、是不是自身的屬性、鍵是不是Symbol等問題。

如下四個操做沒法遍歷對象的不可枚舉屬性:

for ... in循環:只遍歷對象自身的和繼承的可枚舉屬性(不包含Symbol屬性)
Object.keys():返回自身可枚舉屬性的鍵(不包含Symbol屬性)
JSON.stringify():只序列化自身的可枚舉屬性(不包含Symbol屬性)
Object.assign():只拷貝自身的可枚舉屬性(包含Symbol屬性)

另外還有三種操做能夠遍歷不可枚舉屬性:

Object.getOwnPropertyNames():返回自身的全部屬性(不含Symbol屬性)
Object.getOwnPropertySymbols():返回自身全部的Symbol屬性
Reflect.ownKeys():返回自身的全部屬性(包含Symbol)

以上7種方法,除了JSON.stringify()和assign(),另外五種遍歷,遵循一樣的次序規則:

  • 首先遍歷全部數值鍵,按照數值升序排列。
  • 其次遍歷全部字符串鍵,按照加入時間升序排列。
  • 最後遍歷全部 Symbol 鍵,按照加入時間升序排列。

7.擴展方法

一覽表:

Object.is()        判斷兩個值是否相同,與===基本一致,能夠說是對其的完善
Object.assign()      複製、合併對象,返回新對象。(1.只會複製可枚舉屬性2.屬性都是淺拷貝)
Object.getOwnPropertyDescriptor()    返回某對象的某個自有屬性的描述屬性
Object.getPrototypeOf()      返回指定對象的原型對象
Object.setPrototypeOf()      設置指定對象的原型對象
Object.keys()          返回一個對象的全部可枚舉屬性名稱
Object.values()         返回一個對象的全部可枚舉屬性的值
Object.entries()        返回一個對象的全部可枚舉屬性的鍵值對數組(能夠看作是上面兩個方法的結合)
Object.fromEntries()    entries()的逆操做,用於將一個鍵值對數組還原爲對象,所以特別適合將Map結構轉爲對象

詳情見這篇博客 JavaScript字符串、數組、對象方法總結

Class及繼承

class關鍵字的使用正是迎合了「模擬類」的需求,但不改變其基於原型的本質,class及extends只是語法糖。詳見 ECMAScript新語法、特性總結

Proxy

Proxy使得咱們擁有強大的對象操做能力。Proxy英文意思爲「代理」,表示它能夠代理某些操做。Proxy 在目標對象前架設一層攔截,外界對該對象的訪問,都必須先通過這層攔截,它提供了一種機制,能夠對外界的訪問進行過濾和改寫。這等同於在語言層面作出修改,屬於一種「元編程」(meta programmin),即對編程語言進行編程。

const proxy = new Proxy(target, handler);  // 生成 Proxy 實例

栗子(攔截讀取操做):

var person = {
  name: "張三"
};
 
var proxy = new Proxy(person, {
  get: function(target, propKey, receiver) {
    if (property in target) {
      return target[property];
    } else {
      throw new ReferenceError("不存在的");
    }
  },
  set: function(target, propKey, value, receiver) {
    console.log("setter", target, propKey, value);
    retrun target[propKey] = value;
  }
});
 
proxy.name // "張三"
proxy.age // 拋出一個錯誤
proxy.name = "李四"
proxy.name // "李四"

Proxy支持的攔截操做一覽表,一共13種:

get(target, propKey, receiver):攔截對象屬性的讀取
set(target, propKey, value, receiver):攔截對象屬性的設置,返回一個布爾值。
has(target, propKey):攔截propKey in proxy的操做,返回一個布爾值。
deleteProperty(target, propKey):攔截delete proxy[propKey]的操做,返回一個布爾值。
ownKeys(target):攔截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循環,返回一個數組。該方法返回目標對象全部自身的屬性的屬性名,而Object.keys()的返回結果僅包括目標對象自身的可遍歷屬性。
getOwnPropertyDescriptor(target, propKey):攔截Object.getOwnPropertyDescriptor(proxy, propKey),返回屬性的描述對象。
defineProperty(target, propKey, propDesc):攔截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一個布爾值。
preventExtensions(target):攔截Object.preventExtensions(proxy),返回一個布爾值。
getPrototypeOf(target):攔截Object.getPrototypeOf(proxy),返回一個對象。
isExtensible(target):攔截Object.isExtensible(proxy),返回一個布爾值。
setPrototypeOf(target, proto):攔截Object.setPrototypeOf(proxy, proto),返回一個布爾值。若是目標對象是函數,那麼還有兩種額外操做能夠攔截。
apply(target, object, args):攔截 Proxy 實例做爲函數調用的操做,好比proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
construct(target, args):攔截 Proxy 實例做爲構造函數調用的操做(new命令),好比new proxy(...args)。

Reflect

Reflect 對象與 Proxy 對象同樣,也是 ES6 爲了操做對象而提供的新的 API。

Reflect 的做用:

  • 將 Object 語言內部的方法拿出來放到 Reflect 對象上,即從 Reflect 對象上拿 Ojbect 對象內部方法。現階段這些方法同時存在 Object 和 Reflect 上,將來新的方法將只部署在 Reflect 上。
  • 原 Object 方法報錯的狀況,在 Reflect 上會返回 false。使得代碼運行更穩定。
  • 讓 Object 操做都變成函數行爲。原 Object 操做有一些是命令式,好比 in 和 delete,Reflect 用 has() 和 deleteProperty() 替代。
  • Reflect 對象上的方法與 Proxy 行爲一一對應,只要是 Proxy 上的方法就會對應地出如今Reflect上。這可使得兩種對象相互配合完成默認的行爲,做爲修改行爲的基礎。即,無論 Proxy 如何修改默認行爲,你總能夠在 Reflect 上獲取默認行爲。

輔助說明示例:

2.某些Object方法調用可能會拋出異常,在Reflect上會返回false
// 老寫法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}

// 新寫法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}
3.將命令式操做改爲函數行爲
// 老寫法
'assign' in Object // true

// 新寫法
Reflect.has(Object, 'assign') // true
4.與Proxy配合,獲取對象的一些默認行爲
var loggedObj = new Proxy(obj, {
  get(target, name) {
    console.log('get', target, name);
    return Reflect.get(target, name);
  },
  deleteProperty(target, name) {
    console.log('delete' + name);
    return Reflect.deleteProperty(target, name);
  },
  has(target, name) {
    console.log('has' + name);
    return Reflect.has(target, name);
  }
});

Reflect對象方法

一共有13個靜態方法:

Reflect.get(target, name, receiver)           查找並返回target對象的name屬性,若是沒有該屬性,則返回undefined。
Reflect.set(target, name, value, receiver)    設置target對象的name屬性等於value
Reflect.defineProperty(target, name, desc)    基本等同於Object.defineProperty,用來爲對象定義屬性。將來,後者會被逐漸廢除,請從如今開始就使用Reflect.defineProperty代替它。
Reflect.deleteProperty(target, name)          等同於delete obj[name],刪除對象的屬性
Reflect.has(target, name)                     對應name in obj裏面的in運算符,判斷屬性是否存在於對象中
Reflect.construct(target, args)               等同於new target(...args),這提供了一種不使用new,來調用構造函數的方法。
Reflect.ownKeys(target)                       用於返回對象的全部屬性,基本等同於Object.getOwnPropertyNames與Object.getOwnPropertySymbols之和。
Reflect.isExtensible(target)                  對應Object.isExtensible,返回一個布爾值,表示當前對象是否可擴展。
Reflect.preventExtensions(target)             對應Object.preventExtensions方法,用於讓一個對象變爲不可擴展。它返回一個布爾值,表示是否操做成功。
Reflect.getOwnPropertyDescriptor(target, name) 基本等同於Object.getOwnPropertyDescriptor,用於獲得指定屬性的描述對象,未來會替代掉後者。
Reflect.getPrototypeOf(target)                用於讀取對象的__proto__屬性,對應Object.getPrototypeOf(obj)。
Reflect.setPrototypeOf(target, prototype)     用於設置目標對象的原型(prototype),對應Object.setPrototypeOf(obj, newProto)方法,返回布爾值,表示是否設置成功。
Reflect.apply(target, thisArg, args)          用於綁定this對象後執行給定函數

應用場景舉例:

  • 能夠實現觀察者模式,即觀察數據的變化,一旦發生變化,自動執行對應的函數。
相關文章
相關標籤/搜索