高程面向對象這塊內容介紹的比較淺顯,我的以爲這本小書是高程的補充,看完以後以爲收穫匪淺,因此作了個筆記,以備後詢javascript
Js中兩種基本數據類型:原始類型
(基本數據類型)和引用類型
;原始類型
保存爲簡單數據值,引用類型
則保存爲對象,其本質是指向內存位置的應用。
其它編程語言用棧存儲原始類型
,用堆存儲引用類型
,而js則不一樣:它使用一個變量對象追蹤變量的生存期。原始值被直接保存在變量對象裏,而引用值則做爲一個指針保存在變量對象內,該指針指向實際對象在內存中的存儲位置。java
Js中一共有5種原始類型:boolean
、number
、string
、null
、undefined
,除了null
類型,均可以用typeof
來判斷
原始類型的變量直接保存原始值(而不是一個指向對象的指針),當原始值被賦給一個變量,該值將被複制到變量中,每一個變量有它本身的一份數據拷貝編程
var color1='red',color2=color1 console.log(color1) // red console.log(color2) // red color1='blue' console.log(color2) // red
對象(引用值)是引用類型的實例。對象是屬性的無序列表,屬性包含鍵和值,若是一個屬性的值是函數,它就被稱爲方法;
Js中的函數實際上是引用值,除了函數能夠運行之外,一個包含數組的屬性和一個包含函數的屬性沒什麼區別。
Js中的構造函數用首字母大寫來跟非構造函數區分:var object = new Object()
由於引用類型不在變量中直接保存對象,因此object
變量實際上並不包含對象的實例,而是一個指向內存中實際對象所在位置的指針。api
var object1 = new Object() var object2 = object1
一個變量賦值給另外一個變量時,兩個變量各得到一個指針的拷貝,而且指向同一個內存中的對象實例。
對象不使用時能夠將引用解除:object = null
,內存中的對象再也不被引用時,垃圾收集器(GC)會把那塊內存挪做他用,在大型項目中尤其重要數組
原始封裝類型共3種:String
、Number
、Boolean
,使用起來跟對象同樣方便,當讀取這三種類型時,原始封裝類型將被自動建立:安全
var name = "Nicholas" var fisrtChar = name.charAt(0) console.log(firstChar) // N
背後發生的故事:閉包
// what js engine does var name = "Nicholas" var temp = new String(name) // 字符串對象 var firstChar = temp.charAt(0) temp = null console.log(firstChar) // N
Js引擎建立了一個字符串的實例讓charAt(0)能夠工做,字符串對象的存在僅用於該語句而且在隨後被銷燬(一種被稱爲自動打包的過程)。能夠測試:app
var name = "Nicholas" name.last = "zakas" console.log(name.last) // undefined
原始封裝類型的屬性會消失是由於被添加屬性的對象馬上就被銷燬了。
背後的故事:編程語言
var name = "Nicholas" var temp = new String(name) temp.last = "zakas" temp = null // temp對象銷燬 var temp = new String(name) console.log(temp.last) // undefined temp = null
其實是在一個馬上就被銷燬的臨時對象上而不是字符串上添加了新的屬性,以後試圖再訪問該屬性,另外一個不一樣的臨時對象被建立,而新屬性並不存在。雖然原始封裝類型會被自動建立,在這些值上進行的instanceof
檢查對應類型的返回值倒是false
ide
var name = 'Nicholas', count = 10, found = false console.log(name instanceof String) // false console.log(count instanceof Number) // false console.log(found instanceof Boolean) // false
這是由於臨時對象僅在值(屬性)被讀取時被建立,instanceof
操做符並無真的讀取任何東西,也就沒有臨時對象的建立。
若是使用手動建立對象和原始封裝類型之間有必定區別,好比:
var found = new Boolean(false) if (found) { console.log("Found") // 執行了,由於對象在if條件判斷時總被認爲是true,不管該對象是否是false,因此儘可能避免手動建立原始封裝類型 }
使函數不一樣於其它對象是函數存在一個[[Call]]
的內部屬性。內部屬性沒法經過代碼訪問而是定義了代碼執行時的行爲。ECMAScript爲Js的對象定義了多種內部屬性,這些內部屬性都用[[ ]]
來標註。[[Call]]
屬性代表該對象能夠被執行,因爲僅函數擁有該屬性,ECMAScript定義typeof操做符對任何具備[[Call]]
屬性的對象返回function
。
函數有兩種字面形式,函數聲明和函數表達式,二者有個很是重要的區別,函數聲明會被提高至上下文的頂部(要麼是函數聲明時所在函數的範圍,要麼是全局範圍),這意味着能夠先使用再聲明函數。
函數能夠像使用對象同樣使用,能夠將它們賦給變量,在對象中添加它們,將它們當成參數傳遞給別的函數,或從別的函數中返回,基本上只要是可使用其它引用值的地方,就可使用函數。
函數的參數實際上被保存在一個arguments
的數組中,arguments
能夠自由增加來包含任意個數的值,它的length
屬性能夠告訴當前有多少個值。arguments
對象自動存在於函數中。也就是說函數的命名參數不過是爲了方便,並不真的限制了函數可接受參數的個數。
注意:arguments
對象不是一個數組的實例,其擁有的方法與數組不一樣,Array.isArray(arguments)
返回false
。
函數指望的參數個數保存在函數的length
屬性中。
Js中不存在簽名,所以也不存在重載,聲明的同名函數後一個會覆蓋前一個。
不過能夠對arguments
對象獲取的參數個數進行判斷來決定怎麼處理。
能夠像添加屬性那樣給對象添加方法,注意定義數據屬性和方法的語法徹底相同。
var person = { name: "Nicholas", sayName: function () { console.log(person.name) } }
以前的例子的sayName()
直接引用了person.name
,在方法和對象之間創建了緊耦合,這種緊耦合使得一個方法很難被不一樣對象使用。
Js全部函數做用域內都有一個this
對象表明該函數的對象。在全局做用域內,this
表明全局對象window
,當一個函數做爲對象的方法被調用時,默認this
的值等於那個對象。改寫:
var person = { name: "Nicholas", sayName: function () { console.log(this.name) } }
因此應該在方法內引用this
而不是直接引用對象。能夠輕易改變變量名,或者將函數用在不一樣對象上,而不用大量改動代碼。
function sayNameForAll() { console.log(this.name) } var person1={ name: "Nicholas", sayName: sayNameForAll } var person2={ name: "Greg" , sayName: sayNameForAll } var name = "Micheal" person1.sayName() // Nicholas person2.sayName() // Greg sayNameForAll() // Micheal
this
在函數被調用時才被設置,所以最後sayNameForAll
函數執行時的this
爲全局對象。
有3種方法能夠改變this
,函數是對象,而對象能夠有方法,因此函數也有方法。
第一個用於操做this
的方法是call()
,它以指定的this
和參數來執行函數,第一個參數爲函數執行時的this
的值,後面的參數爲須要被傳入函數的參數。
function sayNameForAll (label) { console.log(label + ':' + this.name) } var person1 = {name: "Nicholas"} var person2 = {name: "Greg"} var name = "Micheal" sayNameForAll.call(this,"global") // global:Micheal sayNameForAll.call(person1, "person1") // person1:Nicholas sayNameForAll.call(person2,"person2") // person2:Greg
第二個用於操做this
的方法時apply()
,其工做方式與call()
徹底同樣,但它只接受兩個參數:this
的值和一個數組或者相似數組的對象,內含須要被傳入函數的參數(能夠把arguments
對象做爲apply
的第二個參數)。
function sayNameForAll (label) { console.log(label + ":" + this.name) } var person1 = {name:"Nicholas"} var person2 = {name:"Greg"} var name = "Micheal" sayNameForAll.apply(this,["global"]) // global:Micheal sayNameForAll.apply(person1, ["person1"]) // person1:Nicholas sayNameForAll.apply(person2,["person2"]) // person2:Greg
若是你已經有個數組,那麼推介使用apply()
,若是你有的是單獨的變量,則用call()
改變this
的第三個函數方法爲bind()
,bind()
的第一個參數是要傳給新函數的this
的值,其餘參數表明須要被永久設置在新函數中的命名參數,能夠在以後繼續設置任何非永久參數。
function sayNameForAll (label) { console.log(label + ":" + this.name) } var person1 = {name:"Nicholas"} var person2 = {name:"Greg"} var sayNameForPerson1 = sayNameForAll.bind(person1) sayNameForPerson1("person1") // person1:Nicholas var sayNameForPerson2 = sayNameForAll.bind(person2,"person2") sayNameForPerson2() // person2:Greg person2.sayName = sayNameForPerson1; person2.sayName("person2") // person2:Nicholas
sayNameForPerson1()
沒有綁定永久參數,所以能夠繼續傳入label
參數輸出,sayNameForPerson2()
不只綁定了person2
做爲this
,並且綁定了第一個參數爲person2
,所以可使用sayNameForPerson2()
而不用傳入額外參數,可是也不能更改了。person2.sayName
最後因爲this
的值在sayNameForPerson1
的函數表達式中已經綁定爲person1
了,因此雖然sayNameForPerson1
如今是person2
的方法,它依然輸出person1.name
的值。
Js中的對象是動態的,能夠在代碼執行的任意時刻發生改變。
當一個屬性第一次被添加給對象時,Js在對象上隱式調用一個名爲[[Put]]
的內部方法,[[Put]]
方法會在對象上建立一個新節點保存屬性,就像第一次在哈希表上添加一個鍵同樣。這個操做不只指定了初試的值,也定義了屬性的一些特徵。
調用[[Put]]
的結果是在對象上建立了一個自有屬性,該屬性被直接保存在實例內,對該屬性的全部操做都必須經過該對象進行。
當一個已有的屬性被賦予一個新值時,調用的是一個名爲[[Set]]
的方法,該方法將屬性的當前值替換爲新值。
因爲屬性能夠在任什麼時候候添加,所以有時候有必要檢查對象是否已有該屬性:
if(person1.age){ // 不可取 // 執行 }
問題在於Js的類型強制會影響輸出結果,若是if判斷的值爲null、undefined、0、false、NaN或者空字符串時則判斷爲假。因爲一個對象屬性能夠包含這些假值,上例代碼可能致使錯誤的判斷,更可靠的判斷是用in
操做符。in
操做符是在給定對象上查找一個給定名稱的屬性,若是找到則返回true,另外in
操做符在判斷的時候不會評估屬性的值:
var person1={ name: "Nicholas", age: "111", sayName:function(){ consloe.log(this.name) } } console.log('name' in person1) // true console.log('age' in person1) // true console.log('title' in person1) // false console.log('sayName' in person1) // true 方法是值爲函數的屬性,所以一樣能夠用in判斷
可是in
操做符會檢查自有屬性和原型屬性,所以在只想要自有屬性的時候使用hasOwnProperty()
判斷一下,該方法在給定的屬性存在而且爲自有屬性時返回true。
正如屬性能夠在任什麼時候候被添加,也能夠在任什麼時候候被刪除。可是設置一個屬性值爲null並不能將其從對象中刪除,只是調用[[Set]]
將null替換了該屬性原來的值。完全的刪除屬性值須要delete
操做符。delete
操做符針對單個對象調用[[Delete]]
的內部方法,能夠認爲該操做在哈希表中移除了一個鍵值對,當delete
操做符成功時,它返回true。
注意: 某些屬性沒法被
delete
。
var person1= {name: 'Nicholas'} console.log('name' in person1) // true delete person.name console.log('name' in person1) // false console.log(person1.name) // undefined
全部你添加的屬性默認爲可枚舉的,能夠用for-in
循環遍歷,可枚舉屬性的內部特徵[[Enumerable]]
都被設置爲true。for-in
循環會枚舉一個對象中全部的可枚舉屬性並將屬性名賦給一個對象:
var property for (property in object){ console.log('name:' + property) console.log('value' + object[property]) }
若是隻須要獲取一個對象的屬性列表,ES5引入了Object.keys()
方法,它能夠獲取可枚舉屬性的名字(key)的數組。
注意:
Object.keys()
只返回自有屬性不返回原型屬性。
var properties = Object.keys(object) var i, len=properties.length for (i=0; i<len; i++){ console.log('name:' + properties[i]) console.log('value' + object[properties[i]]) }
並非每一個屬性都是可枚舉的,可使用propertyIsEnumerable()
方法檢查一個屬性是否爲可枚舉,每一個對象都有該方法。
var person1= {name: 'Nicholas'} var properties = Object.keys(person1) console.log('name' in person1) // true console.log(person1.propertyIsEnumerable('name')) // true console.log('length' in properties) // true console.log(properties.propertiesIsEnumerable('length')) // false
這裏name
爲可枚舉,由於它是person1
的自有屬性,而properties
的length
爲不可枚舉的,由於它是Array.prototype
的內建屬性,你會發現不少原生屬性默認都是不可枚舉的。
屬性有兩種類型數據屬性和訪問器屬性;
數據屬性包含一個值,例如以前的name
屬性,[[Put]]
方法默認行爲是建立一個數據屬性。
訪問器屬性不包含值而是定義了一個當屬性被讀取時調用的函數getter
和一個當屬性被寫入時調用的函數setter
。
let person1 = { _name: "Nicholas" , // 前置下劃線是約定俗成的,表示該屬性爲私有的,實際上它是公開的 get name() { console.log("reading me") return this._name }, set name(val) { console.log(`setting name to ${val}`) this._name = val } } console.log(person1.name) // reading me Nicholas person1.name='greg' console.log(person1.name) // setting name to Greg
用於定義name
的getter
和setter
的語法看上去像函數可是沒有function
關鍵字,注意get
和set
以後的name
須要跟被訪問的屬性名保持一致。
當你但願賦值操做會觸發一些行爲或者讀取的值須要經過計算所需的返回值獲得時,訪問器屬性將會頗有用。
注意: 不必定要同時定義getter
和setter
,能夠選擇其中之一,若是隻定義getter
,那麼屬性變爲只讀,在非嚴格下寫入將失敗,嚴格下寫入報錯,若是隻定義setter
,那麼屬性爲只寫,兩種模式下讀取都失敗
ES5以前沒法訪問屬性的任何特徵,也沒有辦法指定一個屬性是否爲可枚舉,所以ES5引入多種方法與屬性特徵互動,同時也引入新的特徵來支持額外的功能,如今已經能夠建立出和Js內建屬性同樣的自定義屬性。下面介紹數據屬性和訪問器屬性的特徵。
有兩個屬性時數據屬性和訪問器屬性共有的:[[Enumerable]]
決定你是否能夠遍歷該屬性;[[Configurable]]
決定該屬性是否可配置;
你能夠用delete
刪除一個可配置的屬性,或者隨時改變它,也能夠把可配置的屬性從數據屬性變爲訪問器屬性,反之亦可,全部自有屬性都是可枚舉和可配置的。
若是你想改變屬性特徵,可使用Object.defineProperty()
方法,它接受三個參數:擁有函數的對象、屬性名、包含須要設置的特徵的屬性描述對象。屬性描述對象具備和內部特徵同名的屬性但名字中不包含中括號,因此可使用enumerable
屬性來設置[[Enumerable]]
特徵,用configurable
屬性來設置[[Configurable]]
特徵。假如你想讓一個對象屬性變成不可枚舉且不可配置:
var person1 = { name: 'Nicholas' } var properties = Object.keys(person1) Object.defineProperty(person1, 'name', { enumerable: false }) console.log('name' in person1) // true console.log(person1.propertyIsEnumerable('name')) // false console.log(properties.length) // 0 Object.defineProperty(person1, 'name', { configurable: false }) delete person1.name // 屬性設置爲不可配置以後不能被delete,刪除失敗 console.log('name' in person1) // true console.log(person1.name) // Nicholas Object.defineProperty(person1, 'name', { configurable: true }) // error! 設置爲不可配置以後就不能再設置屬性特徵了,包括[[Configurable]]
數據屬性額外擁有兩個訪問器屬性不具有的特徵:[[Value]]
包含屬性的值,當你在對象上建立屬性時該特徵被自動賦值,全部屬性的值都保存在[[Value]]
中,哪怕該值是一個函數;[[Writable]]
是一個布爾值,指示該屬性是否能夠寫入,全部屬性默認都是可寫的,除非另外指定。
經過這兩個額外屬性,可使用Object.defineProperty()
完整定義一個數據屬性,即便該屬性還不存在。
var person1 = { name: 'Nicholas' } // 等同於 Object.defineProperty(person, 'name', { value: "Nicholas", enumerable: true, configurable: true, writable: true }
當Object.defineProperty()
被調用時,它首先檢查屬性是否存在,若是不存在將根據屬性描述對象指定的特徵建立。當使用Object.defineProperty()
定義新屬性時必定記得爲全部的特徵指定一個值,不然布爾型的特徵會被默認設置爲false。
var person1 = {} Object.defineProperty(person1, 'name', { value: 'Nicholas' }) // 因爲沒有顯式指定特徵,所以屬性爲不可枚舉、不可配置、不可寫的 console.log('name' in person1) // true console.log(person1.propertyIsEnumerable('name')) // false delete person1.name console.log('name' in person1) // true person1.name = 'Greg' console.log(person1.name) // Nicholas
在嚴格模式下視圖改變不可寫屬性會拋出錯誤,而在非嚴格模式下會失敗
訪問器屬性擁有兩個數據屬性不具有的特徵,訪問器屬性不須要儲存值,所以也就沒有[[Value]]
和[[Writable]]
,取而代之的是[[Get]]
和[[Set]]
屬性,內含getter
和setter
函數,同字面量形式同樣,只須要定義其中一個特徵就能夠建立一個訪問器屬性。
若是試圖建立一個同時具備數據屬性和訪問器屬性的屬性,會報錯
以前get set 例子能夠被改寫爲:
let person1 = { _name: "Nicholas" } Object.defineProperty(person1, 'name', { get: function() { console.log("reading me") return this._name }, set: function(val) { console.log(`setting name to ${val}`) this._name = val }, enumerable: true, configurable: true } ) console.log(person1.name) // reading me Nicholas person1.name = 'greg' console.log(person1.name) // setting name to Greg
注意Object.defineProperty()
中的get和set關鍵字,它們是包含函數的數據屬性,這裏不能使用字面量形式。
若是你使用Object.defineProperties()
而不是Object.defineProperty()
能夠爲一個對象同時定義多個屬性,這個方法接受兩個參數:須要改變的對象、一個包含全部屬性信息的對象。後者能夠背當作一個哈希表,鍵是屬性名,值是爲該屬性定義特徵的屬性描述對象。
var person1 = {} Object.defineProperties(person1, { _name: { value: 'Nicholas', enumerable: true, configurable: true, writable: true }, name: { get: function() { console.log('reading me') return this._name }, set: function(val) { console.log(`setting name to ${val}`) this._name = val }, enumerable: true, configurable: true } })
若是須要獲取屬性的特徵,Js中可使用Object.getOwnPropertyDescriptor()
,這個方法只能夠用於自有屬性,它接受兩個參數:對象、屬性名。若是屬性存在,它會返回一個屬性描述對象,內含四個屬性:configurable、enumerable、另外兩個根據屬性類型決定。即便你從沒有爲屬性顯式指定特徵,你依然會獲得包含所有這些特徵值的屬性描述對象。
對象和屬性同樣具備指導行爲的內部特徵,其中,[[Extensible]]
是一個布爾值,它指明該對象自己是否能夠被修改,你建立的全部對象默認都是可擴展的,新的屬性能夠隨時被添加,設置[[Extensible]]
爲false則能夠禁止新屬性的添加。
下面有三種方法能夠用來鎖定對象屬性
第一種方法是Object.preventExtensions()
建立一個不可擴展的對象。該方法接受一個參數:你但願擴展的對象。一旦在一個對象上用這個方法,就永遠不能再給它添加新的屬性了。
let person1 = { _name: "Nicholas" } console.log(Object.isExtensible(person1)) // true Object.preventExtensions(person1) console.log(Object.isExtensible(person1)) // false person1.sayName = function(){ console.log(this.name) } console.log('sayName' in person1) // false
在嚴格模式下試圖給一個不可擴展對象添加屬性會拋出錯誤,而在非嚴格模式下會失敗。應該對不可擴展對象使用嚴格模式,這樣當一個不可擴展對象被錯誤使用時你就會知道
一個被封印的對象是不可擴展的且其全部屬性都不可配置,這意味着不只不能給對象添加屬性,並且也不能刪除屬性或改變類型(從數據屬性改變成訪問屬性或者反之),若是一個對象被封印,那麼只能讀寫它的屬性。
能夠用Object.seal()
方法來封印一個對象,該方法被調用時[[Extensible]]
特徵被設置爲false,其全部屬性的[[Configurable]]
特徵被置爲false,可使用Object.isSealed()
來判斷一個對象是否被封印。
這段代碼封印了person1,所以不能再person1上添加或者刪除屬性。全部的被封印對象都是不可擴展的對象,此時對person1使用Object.isExtensible()
方法將會返回false,且視圖添加sayName()
會失敗。
並且雖然person.name被成功改變成一個新值,可是刪除它會失敗。
確保對被封印的對象使用嚴格模式,這樣當有人誤用該對象時,會報錯
被凍結的對象不能添加或刪除屬性,不能修改屬性類型,也不能寫入任何數據屬性。簡言而之,被凍結對象是一個數據屬性都爲只讀的被封印對象。Object.freeze()
凍結對象。Object.isFrozen()
判斷對象是否被凍結。
被凍結對象僅僅只是對象在某個時間點上的快照,用途有限且不多被使用
構造函數就是用new
建立對象時調用的函數,使用構造函數的好處在於全部用同一個構造函數建立的對象都具備一樣的屬性和方法。
構造函數也是函數,定義的方式和普通函數同樣,惟一的區別是構造函數名應該首字母大寫,以此區分。
function Person(){} var person1 = new Person // 若是沒有要傳遞給構造函數的參數,括號能夠省略 console.log(person1 instanceof Person) // true console.log(person1.constructor === Person) // true
即便Person構造函數沒有顯式返回任何東西,person1也會被認爲是一個新的Person類型的對象,new
操做符會自動建立給定類型的對象並返回它們。每一個對象在建立時都會自動擁有一個構造函數屬性,其中包含了一個指向其構造函數的引用。那些經過字面量形式或者Object構造函數建立出來的泛用對象,其構造函數屬性constructer
指向Object;那些經過自定義構造函數建立出來的對象,其構造函數屬性指向建立它的構造函數。
雖然對象實例及其構造函數之間存在這樣的關係,可是仍是建議使用instanceof
來檢查對象類型,這是由於構造函數屬性能夠被覆蓋,並不必定徹底準確。
在構造函數中只需簡單的給this
添加任何想要的屬性便可:
function Person(name){ this.name = name this.sayName() = function(){ console.log(this.name) } }
在調用構造函數時,new
會自動建立this
對象,且其類型就是構造函數的類型,構造函數自己不須要返回一個對象,new
操做符會幫你返回。
function Person2(name){ this.name=name this.sayName=function(){ console.log(this.name) } } var person2=new Person2('sam') console.log(person2.name) // sam person2.sayName() // sam
每一個對象都有本身的name
屬性值,因此sayName
能夠根據不一樣對象返回不一樣的值。
也能夠在構造函數中顯式調用
return
,若是返回的是一個對象,那麼它會替代新建立的對象實例返回,若是返回的是一個原始類型,那麼它將被忽略,新建立的對象實例將被返回。
構造函數容許使用一致的方法初始化一個類型的實例,在使用對象前設置好全部的屬性,能夠在構造函數中使用Object.defineProperty()
的方法來幫助初始化。
function Person(name) { Object.defineProperty(this, 'name', { get: function() { return name }, set: function(newName) { name = newName }, enumerable: true, configurable: true }) this.sayName = function() { console.log(this.name) } } var person1 =new Person('Nicholas') // 始終確保使用了new操做符,不然就是冒着改變全局對象的風險 console.log(person1 instanceof Person) // true console.log(typeof person1) // object console.log(name) // undefined
當Person不是被new
調用時候,構造函數中的this
指向全局對象,因爲Person構造函數依靠new
提供返回值,person1變量爲undefined。沒有new
,Person只不過是一個沒有返回語句的函數,對this.name
的賦值實際上建立了一個全局對象name。
嚴格模式下,不經過
new
調用Person構造函數會出現錯誤,這是由於嚴格模式並無爲全局對象設置this,this保持爲undefined,而試圖給undefined添加屬性時都會出錯
構造函數容許給對象配置一樣的屬性,當構造函數並無消除代碼冗餘,每一個對象都有本身的sayName()
方法,這意味着100個對象實例就有100個函數作相同的事情,只是使用的數據不一樣。若是全部的對象實例共享同一個方法會更有效率,該方法可使用this.name
來訪問對應的數據,這就須要用到原型對象。
原型對象能夠看作對象的基類,幾乎全部函數(除了一下內建函數)都有一個名爲prototype
的屬性,該屬性是一個原型對象用來建立新的對象實例。
全部建立的對象實例共享該原型對象,且這些對象實例能夠訪問原型對象的屬性。例如,hasOwnProperty()
方法被定義在泛用對象Object
的原型對象中,但卻能夠被任何對象當作本身的屬性訪問。
var book = {title: "the principles of object-oriented js"} console.log('title' in book) console.log(book.hasOwnProperty('title')) // true console.log('hasOwnProperty' in book) // true console.log(book.hasOwnProperty('hasOwnProperty')) // false console.log(Object.prototype.hasOwnProperty('hasOwnProperty')) // true
即便book
中沒有hasOwnProperty()
方法的定義,但仍然能夠經過book.hasOwnProperty()
訪問該方法,這是由於該方法存在於Object.prototype
中。
可使用這樣一個方法來判斷一個屬性是否爲原型屬性:
function hasPrototypeProperty(object, name){ return name in object && !object.hasOwnProperty(name) }
一個對象實例經過內部屬性[[Prototype]]
追蹤其原型對象,該 屬性時一個指向該實例使用的原型對象的指針。當你使用new
建立一個新的對象時,構造函數的原型對象會被賦給該對象的[[Prototype]]
屬性 (JS proto 探究.md )。你能夠調用Object.getPropertyOf()
方法讀取[[prototype]]
屬性的值。
Object.prototype.__proto__ === null
var object={} Object.getPrototypeOf(object) === Object.prototype // true Object.prototype.isPrototypeOf(object) // true
任何一個泛用對象(字面量形式或者new Object()
),其[[Prototype]]
對象始終指向Object.prototype
。也能夠用isPrototypeOf()
方法檢查某個對象是不是另外一個對象的原型對象,該方法被包含在全部對象中。
Note:大部分Js引擎在全部對象上都支持一個__proto__
的屬性,該屬性使你能夠直接讀寫[[Prototype]]
屬性。包括Firefox、Safari、Chrome、Node.js
在讀取一個對象的屬性時,Js引擎會首先在對象的自有屬性中查找屬性名字,若是找到則返回,若是沒有則Js會搜索[[Prototype]]
中的對象,若是找到則返回,找不到則返回undefined
。
var object = {} console.log(object.toString()) // [object Object] object.toString = function() {return "[object Custom]"} console.log(object.toString()) // [object Custom] delete object.toString console.log(object.toString()) // [object Object] delete object.toString console.log(object.toString()) // [object Object]
上例能夠看出,delete
運算符只對只有屬性起做用,沒法刪除一個對象的原型屬性。而且也不能夠給一個對象的原型屬性賦值,對.toString
的賦值只是在對象上建立了一個新的自有屬性,而不是改變原型屬性。
原型對象的共享機制使得它們成爲一次性爲全部對象定義全部方法的理想手段,由於一個方法對全部的對象實例作相同的事,沒理由每一個實例都要有一份本身的方法。將方法放在原型對象中並使用this
方法當前實例是更有效的作法。
function Person(name) {this.name = name} Person.prototype.sayName = function() {console.log(this.name)}; var person1 = new Person("Nicholas") console.log(person1.name) // Nicholas person1.sayName() // Nicholas
也能夠在原型對象上存儲其餘類型的數據,可是在存儲引用值時要注意,由於這些引用值會被多個實例共享,可能你們不但願一個實例可以改變另外一個實例的值。
function Person(name) {this.name = name} Person.prototype.favorites = [] var person1 = new Person("Nicholas") var person2 = new Person("Greg") person1.favorites.push("pizza") person2.favorites.push("quinoa") console.log(person1.favorites) // ["pizza", "quinoa"] console.log(person2.favorites) // ["pizza", "quinoa"]
favorites
屬性被定義到原型對象上,意味着person1.favorites
和person2.favorites
指向同一個數組,你對任意Person對象的favorites插入的值都將成爲原型對象上數組的元素。也可使用字面量的形式替換原型對象:
function Person(name) {this.name=name} Person.prototype= { sayName: function() {console.log(this.name)}, toString: function(){return `[Person ${this.name} ]`} }
雖然用這種字面量的形式定義原型很是簡潔,可是有個反作用須要注意。
var person1 = new Person('Nicholas') console.log(person1 instanceof Person) // true console.log(person1.constructor === Person) // false console.log(person1.constructor === Object) // true
使用字面量形式改寫原型對象改寫了構造函數的屬性,所以如今指向Object
而不是Person
,這是由於原型對象具備個constructor
屬性,這是其餘對象實例所沒有的。當一個函數被建立時,其prototype
屬性也被建立,且該原型對象的constructor
屬性指向該函數本身,當使用字面量形式改寫原型對象Person.prototype
時,其constructor
屬性將被複寫爲泛用對象Object
。爲了不這一點,須要在改寫原型對象時手動重置其constructor
屬性:
function Person(name) {this.name = name} Person.prototype = { constructor: Person, // 爲了避免忘記賦值,最好在第一個屬性就把constructor重置爲本身 sayName() {console.log(this.name)}, toString() {return `[Person ${this.name} ]`} } var person1 = new Person('Nicholas') console.log(person1 instanceof Person) // true console.log(person1.constructor === Person) // true console.log(person1.constructor === Object) // false
構造函數、原型對象、對象實例之間:對象實例和構造函數之間沒有直接聯繫。不過對象實例和原型對象之間以及原型對象和構造函數之間都有直接聯繫。
這樣的鏈接關係也意味着,若是打斷對象實例和原型對象之間的聯繫,那麼也將打斷對象實例及其構造函數之間的關係。
給定類型的全部對象實例共享一個原型對象,因此能夠一次性擴充全部對象實例。[[Prototype]]
屬性只是包含了一個指向原型對象的指針,任何對原型對象的改變都將你可反映到全部引用它的對象實例上。這意味着給原型對象添加的新成員均可以馬上被全部已經存在的對象實例使用。
function Person(name) {this.name = name} Person.prototype = { constructor: Person, sayName() {console.log(this.name)}, toString() {return `[Person ${this.name} ]`} } var person1 = new Person('Nicholas') var person2 = new Person('Greg') console.log('sayHi' in person1) // false console.log('sayHi' in person2) // false Person.prototype.sayHi = () => console.log("Hi") person1.sayHi() // Hi person2.sayHi() // Hi
當對一個對象使用Object.seal()
或Object.freeze()
封印和凍結對象的時候是在操做對象的自有屬性,沒法添加封印對象的自有屬性和更改凍結對象的自有屬性,可是仍然能夠經過在原型對象上添加屬性來擴展對象實例:
function Person(name) {this.name = name} var person1 = new Person("Nicholas") Object.freeze(person1) Person.prototype.sayHi = function() {console.log("Hi")}; person1.sayHi() // Hi
其實,[[Prototype]]
是實例對象的自有屬性,屬性自己person1.[[Prototype]]
被凍結,可是指向的值Person.prototype
並無凍結。
全部內建對象都有構造函數,所以也都有原型對象能夠去改變,例如要在數組上添加一個新的方法只須要改變Array.prototype
便可
Array.prototype.sum = function() { return this.reduce((privious, current) => privious + current) } var numbers = [1, 2, 3, 4, 5, 6] var result = numbers.sum() console.log(result) // 21
sum()函數內部,在調用時this
指向數組的對象實例numbers
,所以this
也能夠調用該數組的其餘方法,好比reduce()。
改變原始封裝類型的原型對象,就能夠給這些原始值添加更多功能,好比:
String.prototype.capitalize = function() { return this.charAt(0).toUpperCase() + this.substring(1) } var message = 'hello world!' console.log(message.capitalize()) // Hello world!
Js內建的繼承方法被稱爲原型對象鏈,又稱爲原型對象繼承。原型對象的屬性能夠由對象實例訪問。實例對象集成了原型對象的屬性,由於原型對象也是一個對象,它也有本身的原型對象並繼承其屬性。這就是原型繼承鏈:對象繼承其原型對象,而原型對象繼承它的原型對象,以此類推。
全部的對象,包括自定義的對象都繼承自Object
,除非另有指定。更確切的說,全部對象都繼承自Object.prototype
,任何以字面量形式定義的對象,其[[Prototype]]
的值都被設爲Object.prototype
,這意味着它繼承Object.prototype的屬性。
var book = {title: 'a book'} console.log(Object.getPrototypeOf(book) === Object.prototype) // true
前幾張用到的幾個方法都是定義在Object.prototype上的,所以能夠被其餘對象繼承:
Methods | Usage |
---|---|
hasOwnProperty() | 檢查是否存在一個給定名字的自有屬性 |
propertyIsEnumerable() | 檢查一個自有屬性是否爲可枚舉 |
isPrototypeOf() | 檢查一個對象是不是另外一個對象的原型對象 |
valueOf() | 返回一個對象的值表達 |
toString() | 返回一個對象的字符串表達 |
這幾種方法由繼承出如今全部的對象中,當你須要對象在Js中以一致的方式工做時,最後兩個尤其重要。
valueOf()
每當一個操做符被用於一個對象時就會調用valueOf()
方法,其默認返回對象實例自己。原始封裝類型重寫了valueOf()
以使得它對String返回一個字符串,對Boolean返回一個布爾,對Number返回一個數字;相似的,對Date對象的valueOf()返回一個epoch時間,單位是毫秒(正如Data.prototype.getTime())。
var now = new Date // now.valueOf() === 1505108676169 var earlier = new Date(2010,1,1) // earlier.valueOf() === 1264953600000 console.log(now>earlier) // true console.log(now-earlier) // 240155076169
now是一個表明當前時間的Date,而earlier是過去的時間,當使用操做符>
時,兩個對象上都調用了valueOf()
方法,你甚至能夠用兩個Date相減來得到它們在epoch時間上的差值。若是你的對象也要這樣使用操做符,你能夠定義本身的valueOf()
方法,定義的時候你並無改變操做符的行爲,僅僅應了操做符默認行爲所使用的值。
toString()
一旦valueOf()
返回的是一個引用而不是原始值的時候,就會回退調用toString()
方法。另外,當Js指望一個字符串時也會對原始值隱式調用toString()
。例如當加號操做符的一邊是一個字符串時,另外一邊就會被自動轉換成字符串,若是另外一邊是一個原始值,會自動轉換成一個字符串表達(true => "true"),若是另外一邊是一個引用值,則會調用valueOf()
,若是其返回一個引用值,則調用toString()
。
var book = {title: 'a book'} console.log("book = " + book) // "book = [object Object]"
由於book是一個對象,所以調用它的toString()
方法,該方法繼承自Object.prototype,大部分Js引擎返回默認值[object Object],若是對這個值不滿意能夠複寫,爲此類字符串提供包含跟多信息。
var book = {title: 'a book', toString(){return `[Book = ${this.title} ]`}} console.log("book = " + book) // book = [Book = a book ]
全部的對象都默認繼承自Object.prototype,因此改變它會影響到全部的對象,這是很是危險的。
若是給Obejct.prototype添加一個方法,它是可枚舉的,能夠粗如今for-in循環中,一個空對象依然會輸出一個以前添加的屬性。儘可能不要修改Object.prototype。
對象字面量形式會隱式指定Object.prototype爲其[[Prototype]]
,也能夠用Object.create()方式顯示指定。Object.create()方法接受兩個參數:須要被設置爲新對象[[Prototype]]
的對象、屬性描述對象,格式如在Object.defineProperties()中使用的同樣(第三章)。
var book = {title: 'a book'} // ↑ 等價於 ↓ var book = Object.create(Object.prototype, { title: { configurable: true, enumerable: true, value: 'a book', writable: true } })
第一種寫法中字面量形式定義的對象自動繼承Object.prototype且其屬性默認設置爲可配置、可寫、可枚舉。第二種寫法顯示使用Object.create()作了相同的操做,兩個book對象的行爲徹底一致。
var person = { name: "Jack", sayName: function(){ console.log(this.name); } } var student = Object.create(person, { name:{value: "Ljc"}, grade: { value: "fourth year of university", enumerable: true, configurable: true, writable: true } }); person.sayName(); // "Jack" student.sayName(); // "Ljc" console.log(person.hasOwnProperty("sayName")); // true console.log(person.isPrototypeOf(student)); // true console.log(student.hasOwnProperty("sayName")); // false console.log("sayName" in student); // true console.log(student.__proto__===person) // true console.log(student.__proto__.__proto__===Object.prototype) // true
對象person2繼承自person1,也就集成了person1的name和sayName(),然而又經過重寫name屬性定義了一個自有屬性,隱藏並替代了原型對象中的同名屬性。因此person1.sayName()輸出Nicholas而person2.sayName()輸出Greg。
在訪問一個對象的時候,Js引擎會執行一個搜索過程,若是在對象實例上發現該屬性,該屬性值就會被使用,若是沒有發現則搜索[[Prototype]]
,若是仍然沒有發現,則繼續搜索該原型對象的[[Prototype]]
,知道繼承鏈末端,末端一般是一個Object.prototype,其[[prototype]]
爲null。這就是原型鏈。
固然也能夠經過Object.create()建立[[Prototype]]
爲null的對象:var obj=Object.create(null)
。該對象obj是一個沒有原型鏈的對象,這意味着toString()
和valueOf
等存在於Object原型上的方法都不存在於該對象上。
Js中的對象繼承也是構造函數繼承的基礎,第四章提到:幾乎全部的函數都有prototype
屬性(經過Function.prototype.bind
方法構造出來的函數是個例外),它能夠被替換和修改。該prototype
屬性被自動設置爲一個繼承自Object.prototype的泛用對象,該對象有個自有屬性constructor
。
// 構造函數 function YourConstructor() {} // Js引擎在背後作的: YourConstructor.prototype = Object.create(Object.prototype, { constructor: { configurable: true, enumerable: true, value: YourConstructor, writable: true } }) console.log(YourConstructor.prototype.__proto__===Object.prototype) // true
你不須要作額外工做,Js引擎幫你把構造函數的prototype
屬性設置爲一個繼承自Object.prototype的對象,這意味着YourConstructor建立出來的任何對象都繼承自Object.prototype,YouConstructor是Object的子類。
因爲prototype可寫,能夠經過改寫它來改變原型鏈:
function Rectangle(length, width) { this.length = length this.width = width } Rectangle.prototype.getArea = function() {return this.length * this.width}; Rectangle.prototype.toString = function() {return `[ Rectangle ${this.length}x${this.width} ]`}; function Square(size) { this.length = size this.width = size } Square.prototype = new Rectangle() Square.prototype.constructor = Square Square.prototype.toString = function() {return `[ Square ${this.length}x${this.width} ]`} var rect = new Rectangle(5, 10) var squa = new Square(6) console.log(rect instanceof Rectangle) // true console.log(rect instanceof Square) // false console.log(rect instanceof Object) // true console.log(squa instanceof Rectangle) // true console.log(squa instanceof Square) // true console.log(squa instanceof Object) // true
MDN:instanceof 運算符能夠用來判斷某個構造函數的 prototype 屬性是否存在另一個要檢測對象的原型鏈上。
Square構造函數的prototype屬性被改寫爲Rectagle的一個實例,此時不須要給Rectangle的調用提供參數,由於它們不須要被使用,並且若是提供了,那麼全部的Square對象實例都會共享這樣的維度。若是用這種方式改寫原型鏈,須要確保構造函數不會再參數缺失時拋出錯誤(不少構造函數包含的初始化邏輯)且構造函數不會改變任何全局狀態。
// inherits from Rectangle function Square(size){ this.length = size; this.width = size; } Square.prototype = new Rectangle(); // 儘管是 Square.prototype 是指向了 Rectangle 的對象實例,即Square的實例對象也能訪問該實例的屬性(若是你提早聲明瞭該對象,且給該對象新增屬性)。 // Square.prototype = Rectangle.prototype; // 這種實現沒有上面這種好,由於Square.prototype 指向了 Rectangle.prototype,致使修改Square.prototype時,實際就是修改Rectangle.prototype。 console.log(Square.prototype.constructor); // 輸出 Rectangle 構造函數 Square.prototype.constructor = Square; // 重置回 Square 構造函數 console.log(Square.prototype.constructor); // 輸出 Square 構造函數 Square.prototype.toString = function(){ return "[Square " + this.length + "x" + this.width + "]"; } var rect = new Rectangle(5, 10); var square = new Square(6); console.log(rect.getArea()); // 50 console.log(square.getArea()); // 36 console.log(rect.toString()); // "[Rectangle 5 * 10]", 但若是是Square.prototype = Rectangle.prototype,則這裏會"[Square 5 * 10]" console.log(square.toString()); // "[Square 6 * 6]" console.log(square instanceof Square); // true console.log(square instanceof Rectangle); // true console.log(square instanceof Object); // true
Square.prototype
並不真的須要被改爲爲一個 Rectangle
對象。事實上,是 Square.prototype
須要指向 Rectangle.prototype
使得繼承得以實現。這意味着能夠用 Object.create()
簡化例子。
// inherits from Rectangle function Square(size){ this.length = size; this.width = size; } Square.prototype= Object.create(Rectangle.prototype, { constructor: { configurable: true, enumerable: true, value: Square, writable: true } })
在對原型對象添加屬性前要確保你已經改寫了原型對象,不然在改寫時會丟失以前添加的方法(由於繼承是將被繼承對象賦值給須要繼承的原型對象,至關於重寫了須要繼承的原型對象)。
因爲JavaScript中的繼承是經過原型對象鏈來實現的,所以不須要調用對象的父類的構造函數。若是確實須要在子類構造函數中調用父類構造函數,那就能夠在子類的構造函數中利用 call、apply方法調用父類的構造函數。
function Rectangle(length, width) { this.length = length this.width = width } Rectangle.prototype.getArea = function() {return this.length * this.width}; Rectangle.prototype.toString = function() {return `[ Rectangle ${this.length}x${this.width} ]`}; function Square(size) {Rectangle.call(this, size, size)} Square.prototype = Object.create(Rectangle.prototype, { constructor: { value: Square, enumerable: true, configurable: true, writable: true } }) Square.prototype.toString = function() {return `[ Square ${this.length}x${this.width} ]`} var rect = new Rectangle(5, 10) var squa = new Square(6) console.log(rect.getArea()) console.log(rect.toString()) console.log(squa.getArea()) console.log(squa.toString())
通常來講,須要修改 prototype
來繼承方法並用構造函數竊取來設置屬性,因爲這種作法模仿了那些基於類的語言的類繼承,因此這一般被稱爲僞類繼承。
其實也是經過指定 call
或 apply
的子對象調用父類方法。
可使用繼承或者混入等其餘技術令對象間行爲共享,也能夠利用Js高級技巧阻止對象結構被改變。
模塊模式是一種用於建立擁有私有數據的單件對象的模式。
基本作法是使用當即調用函數表達式(IIFE)來返回一個對象。原理是利用閉包。
var yourObj = (function(){ // private data variables return { // public methods and properties } }());
模塊模式還有一個變種叫暴露模塊模式,它將全部的變量和方法都放在 IIFE 的頭部,而後將它們設置到須要被返回的對象上。
// 通常寫法 var yourObj = (function(){ var age = 25; return { name: "Ljc", getAge: function(){ return age } } }()); // 暴露模塊模式,保證全部變量和函數聲明都在同一個地方 var yourObj = (function(){ var age = 25; // 私有變量,外部沒法訪問 function getAge(){ return age }; return { name: "Ljc", // 公共變量外部能夠訪問 getAge: getAge // 外部能夠訪問的對象 } }());
模塊模式在定義單個對象的私有屬性十分有效,但對於那些一樣須要私有屬性的自定義類型呢?你能夠在構造函數中使用相似的模式來建立每一個實例的私有數據。
function Person(name){ // define a variable only accessible inside of the Person constructor var age = 22; this.name = name; this.getAge = function(){return age;}; this.growOlder = function(){age++;} } var person = new Person("Ljc"); console.log(person.age); // undefined person.age = 100; console.log(person.getAge()); // 22 person.growOlder(); console.log(person.getAge()); // 23
構造函數在被new的時候建立了一個本地做用於並返回this對象。這裏有個問題:若是你須要對象實例擁有私有數據,就不能將相應方法放在 prototype
上。
若是你須要全部實例共享私有數據(就好像它被定義在原型對象裏那樣),則可結合模塊模式和構造函數,以下:
var Person = (function(){ var age = 22; function InnerPerson(name){this.name = name;} InnerPerson.prototype.getAge = function(){return age;} InnerPerson.prototype.growOlder = function(){age++;}; return InnerPerson; }()); var person1 = new Person("Nicholash"); var person2 = new Person("Greg"); console.log(person1.name); // "Nicholash" console.log(person1.getAge()); // 22 console.log(person2.name); // "Greg" console.log(person2.getAge()); // 22 person1.growOlder(); console.log(person1.getAge()); // 23 console.log(person2.getAge()); // 23
這是一種僞繼承。一個對象在不改變原型對象鏈的狀況下獲得了另一個對象的屬性被稱爲「混入」。所以,和繼承不一樣,混入讓你在建立對象後沒法檢查屬性來源。
function mixin(receiver, supplier){ for(var property in supplier){ if(supplier.hasOwnProperty(property)){ receiver[property] = supplier[property]; } } }
這是淺拷貝,若是屬性的值是一個引用,那麼二者將指向同一個對象。
要注意一件事,使用這種方式,supplier
的訪問器屬性會被複製爲receiver
的數據屬性。
function mixin(reciver, supplier) { if (Object.getOwnPropertyDescriptor) { // 檢查是否支持es5 Object.keys(supplier).forEach(property => { var descriptor = Object.getOwnPropertyDescriptor(supplier, property) Object.defineProperty(reciver, property, descriptor) }) } else { for (var property in supplier) { // 不然使用淺複製 if (supplier.hasOwnProperty(property)) { reciver[property] = supplier[property] } } } }
構造函數也是函數,因此不用 new 也能調用它們來改變 this
的值。在非嚴格模式下, this
被強制指向全局對象。而在嚴格模式下,構造函數會拋出一個錯誤(由於嚴格模式下沒有爲全局對象設置 this,this 保持爲 undefined)。
而不少內建構造函數,例如 Array、RegExp 不須要 new 也能正常工做,這是由於它們被設計爲做用域安全的構造函數。
當用 new 調用一個函數時,this 指向的新建立的對象已經屬於該構造函數所表明的自定義類型。所以,可在函數內用 instanceof 檢查本身是否被 new 調用。
function Person(name){ if(this instanceof Person){ // called with "new" }else{ // called without "new" } }
具體案例:
function Person(name){ if(this instanceof Person){ this.name = name; }else{ return new Person(name); } }