與其它的語言相比,JavaScript老是顯得不那麼合羣。好比:javascript
在被詬病和爭論中,有人喊出JavaScript並不是「非面向對象」的語言,而是「基於對象」的語言。可是,對於如何定義「面向對象」和「基於對象」,基本上很難有人可以回答。html
這須要明白JavaScript語言的設計思想,才能更清楚究竟是否須要模擬類,以及爲何須要模擬類。在早期人們習慣於其餘語言的面向對象編程方式,而對JavaScript感到困惑,並嘗試用貼近類的方式去編程。java
咱們先看看JavaScript對對象的定義:「語言和宿主的基礎設施由對象來提供,而且 JavaScript 程序便是一系列互相通信的對象集合」。這裏的意思根本不是表達弱化面向對象的意思,反而是表達對象對於語言的重要性。程序員
到底什麼是面向對象?Objcet在英文中,是一切事物的總稱,這和麪向對象編程的抽象思惟有相通之處。中文翻譯「對象」卻沒有這樣的普適性。在不一樣的編程語言中,設計者也利用各類不一樣的語言特性來描述對象,最爲成功的流派是使用「類」的方式來描述對象,這誕生了諸如C++、Java等流行的編程語言。編程
而JavaScript早年則選擇了一個更爲冷門的流派:原型。這是不合羣的緣由之一。數組
然而不幸的是,由於一些公司政治緣由,JavaScript推出之時受管理層之命被要求模仿 Java,因此,JavaScript 創始人 Brendan Eich 在「原型運行時」的基礎上引入了 new、this 等語言特性,使之「看起來更像 Java」。瀏覽器
所以,咱們至少須要明白一件事,咱們以前所熟知的「面向對象」的編程方式,實際上是「基於類」的面向對象,它並非面向對象的所有,確切地說,基於類只是面向對象編程的一個流派而已。而想要理解JavaScript對象,就必須清空咱們認識「基於類的面向對象」相關的概念,回到人類對對象的樸素認識和無關語言的基礎理論,咱們就可以理解JavaScript面向對象設計的思路。app
「基於類」的編程提倡使用一個關注分類和類之間關係開發模型。在這類語言中,老是先有類,再從類去實例化一個對象。類與類之間又可能會造成繼承、組合等關係。類又每每與語言的類型系統整合,造成必定編譯時的能力。編程語言
與此相對,「基於原型」的編程看起來更爲提倡程序員去關注一系列對象實例的行爲,然後纔去關心如何將這些對象,劃分到最近的使用方式類似的原型對象,而不是將它們分紅類。函數
基於原型和基於類都可以知足基本的複用和抽象需求,可是適用的場景不太相同。這就像專業人士可能喜歡在看到老虎的時候,喜歡用貓科豹屬豹亞種來描述它,可是對一些不那麼正式的場合,「大貓」可能更爲接近直觀的感覺一些。咱們的 JavaScript 並不是第一個使用原型的語言,在它以前,self、kevo 等語言已經開始使用原型來描述對象了。
拋開模擬Java的複雜語法設施(new、Function Object、函數的prototype屬性等),原型系統能夠說至關簡單,用兩條能夠歸納:
這個模型在之前的各個歷史版本中並無大的改變,在ES6提供了一些列內置函數,能夠更直接地訪問操做原型:
利用這三個方法,咱們能夠徹底拋開類的思惟,利用原型來實現抽象和複用。例如:
// 這段代碼建立了一個「貓」對象,又根據貓作了一些修改建立了虎,以後咱們徹底能夠用 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 風格的類接口來操縱原型運行時,能夠說很是彆扭。
關於惟一標識性,若是分別定義兩個結構和值如出一轍的對象,他們兩個是不相等的(a1 === a2爲false)
關於狀態和行爲,不一樣語言會有不一樣的描述,Java中稱他們爲「屬性」和「方法」,在JavaScript中,狀態和行爲統一抽象爲「屬性」。
前三個特徵是任何面嚮對象語言都具有的,而JavaScript對象獨有的特點是:對象具備高度的動態性,賦予了使用者在運行時爲對象添改狀態和行爲的能力。
例如,下面的例子展現了向一個對象添加屬性,這樣操做徹底OK:
let o = { a: 1 }; o.b = 2; console.log(o.a, o.b); //1 2
同時,JavaScript的屬性被設計成比別的語言更加複雜的形式,它提供了數據屬性(描述屬性)和訪問器屬性兩類屬性描述符(能夠理解爲針對屬性的屬性,這兩類屬性描述符不能同時存在,只能選取一種)。
數據屬性在很早的版本中實現,訪問器屬性在ES6新增。詳情可見下方總結,簡單來講就是:數據屬性能夠規定某屬性的值、是否可寫、是否可枚舉、是否能被修改配置(包含是否能刪除);訪問器屬性則主要定義了訪問時的行爲和結果。
如今能夠回答JavaScript是否須要模擬類這個問題了。其實,JavaScript本不須要模擬類,可是由於你們習慣於類的編程方式(以及一些其餘緣由,總結在下方),ES6正式開始使用class關鍵字,new + function的怪異搭配能夠徹底拋棄了。咱們推薦在任何場景都使用ES6的語法來定義類。但在這裏須要說明,class關鍵字的使用,其本質仍是基於原型,class extends 只是語法糖,徹底不存在拋棄原型一說。
1.大部分面嚮對象語言都是基於類的流派,而基於原型的比較小衆。
基於原型本是一個優秀的抽象對象的形式,可是「基於類」的面向對象已經先入爲主成爲大部分人的思惟,在缺少系統性學習的前提下,嘗試用基於類的思想去理解並掌握基於原型的處理方式,只會讓人更加懷疑和困惑。
其實,不止有人對基於原型有疑問,也有人對基於類表達過疑惑,只是對於大部分人來講質疑一個如此成功的流派顯得多餘,慢慢地認爲面向對象理所應當就是這樣。
2.早期因爲公司政治緣由,模仿java的一些語法和方式,不只怪異,更加深了人們的困惑。
JavaScript 對象並不是只有一種,好比在瀏覽器環境中咱們沒法單純依靠js代碼實現 div 對象,只能靠 document.createElement 來建立,這說明了 JavaScript 的對象機制並不是簡單的屬性集合 + 原型。
JavaScript 中的對象能夠分爲如下幾類,這也與 JavaScript 語言的組成有關:
在瀏覽器環境中,咱們知道全局對象是 window ,window 上又有不少屬性,這裏的屬性一部分來自 JavaScript 語言,一部分來自瀏覽器環境。JavaScript 標準中規定了全局對象屬性,來自瀏覽器宿主部分的能夠理解爲 W3C 的 HTML 標準(或非標準)的API,例如DOM、BOM。
固有對象是由標準規定,隨着 JavaScript 運行時而自動建立的對象實例。這些對象在任何JavaScript代碼執行以前就已經被建立出來了,他們一般扮演相似基礎庫的角色,例如 global 對象(瀏覽器環境中是 window 對象)、JSON、Math、Number等。
ECMA標準提供了一份並不全面的固有對象表 ECMA
可以經過語言自己的構造器建立的對象稱做原生對象。在JavaScript標準中,提供了30多個構造器,以下:
幾乎全部這些構造器的能力都是沒法用純JavaScript代碼實現的,它們也沒法用 class/extend 語法來繼承。咱們能夠認爲,全部這些原生對象都是爲了特定能力或者性能,設計出來的「特權對象」。
let name = "...", age = 20; let obj = { name: name, age: age, func: function () { ... } }; // ES5表示 let obj = { name, age, func() { ... } }; // ES6表示
使用方括號,屬性名稱能夠用表達式表示,也能夠用變量名。
let name = "abc", obj = {}; obj[name] = "123"; // obj :{ abc: "123" } obj["h" + "ello"] = "aa"; // obj:{ abc: "123", "hello": "aa" }
函數有 name 屬性,返回函數名。對象內的方法也是函數,也有 name 屬性。
const person = { sayName() { console.log('hello!'); }, }; person.sayName.name // "sayName"
對象內的每一個屬性都有一個屬性描述符,分爲兩類:數據屬性(也叫描述屬性)、訪問器屬性。二者不能一塊兒用。
咱們經過 Object.getOwnPropertyDescriptor() 方法來獲取某個對象的某個屬性的描述符。
let obj = { a: "hello" }; const descriptor = Object.getOwnPropertyDescriptor(obj, 'a'); console.log(descriptor); { value: "hello" writable: true enumerable: true configurable: 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
咱們知道,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關鍵字表示原型對象時,只能用在對象的方法之中,用在其餘地方都會報錯。
遍歷涉及到屬性是否可枚舉、是不是自身的屬性、鍵是不是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(),另外五種遍歷,遵循一樣的次序規則:
一覽表:
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及extends只是語法糖。詳見 ECMAScript新語法、特性總結
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 對象與 Proxy 對象同樣,也是 ES6 爲了操做對象而提供的新的 API。
輔助說明示例:
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); } });
一共有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對象後執行給定函數
應用場景舉例: