摘要:屬性表明該對象可以被執行,由于僅函數擁有該屬性,定義操作符對任何具有屬性的對象返回。對象自動存在于函數中。注意對象不是一個數組的實例,其擁有的方法與數組不同,返回。所有函數作用域內都有一個對象代表該函數的對象。
高程面向對象這塊內容介紹的比較淺顯,個人覺得這本小書是高程的補充,看完之后覺得收獲匪淺,所以做了個筆記,以備后詢
1. 原始類型和引用類型Js中兩種基本數據類型:原始類型(基本數據類型)和引用類型;
原始類型保存為簡單數據值,引用類型則保存為對象,其本質是指向內存位置的應用。
其它編程語言用棧存儲原始類型,用堆存儲引用類型,而js則不同:它使用一個變量對象追蹤變量的生存期。原始值被直接保存在變量對象里,而引用值則作為一個指針保存在變量對象內,該指針指向實際對象在內存中的存儲位置。
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) // red1.2 引用類型
對象(引用值)是引用類型的實例。對象是屬性的無序列表,屬性包含鍵和值,如果一個屬性的值是函數,它就被稱為方法;
Js中的函數其實是引用值,除了函數可以運行以外,一個包含數組的屬性和一個包含函數的屬性沒什么區別。
Js中的構造函數用首字母大寫來跟非構造函數區分:var object = new Object()
因為引用類型不在變量中直接保存對象,所以object變量實際上并不包含對象的實例,而是一個指向內存中實際對象所在位置的指針。
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)可以工作,字符串對象的存在僅用于該語句并且在隨后被銷毀(一種被稱為自動打包的過程)。可以測試:
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
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,所以盡量避免手動創建原始封裝類型 }2. 函數
使函數不同于其它對象是函數存在一個[[Call]]的內部屬性。內部屬性無法通過代碼訪問而是定義了代碼執行時的行為。ECMAScript為Js的對象定義了多種內部屬性,這些內部屬性都用[[ ]]來標注。[[Call]]屬性表明該對象可以被執行,由于僅函數擁有該屬性,ECMAScript定義typeof操作符對任何具有[[Call]]屬性的對象返回function。
2.1 函數聲明與函數表達式函數有兩種字面形式,函數聲明和函數表達式,兩者有個非常重要的區別,函數聲明會被提升至上下文的頂部(要么是函數聲明時所在函數的范圍,要么是全局范圍),這意味著可以先使用再聲明函數。
2.2 函數就是值函數可以像使用對象一樣使用,可以將它們賦給變量,在對象中添加它們,將它們當成參數傳遞給別的函數,或從別的函數中返回,基本上只要是可以使用其它引用值的地方,就可以使用函數。
2.3 參數函數的參數實際上被保存在一個arguments的數組中,arguments可以自由增長來包含任意個數的值,它的length屬性可以告訴當前有多少個值。
arguments對象自動存在于函數中。也就是說函數的命名參數不過是為了方便,并不真的限制了函數可接受參數的個數。
注意: arguments對象不是一個數組的實例,其擁有的方法與數組不同,Array.isArray(arguments)返回false。
函數期望的參數個數保存在函數的length屬性中。
2.4 重載Js中不存在簽名,因此也不存在重載,聲明的同名函數后一個會覆蓋前一個。
不過可以對arguments對象獲取的參數個數進行判斷來決定怎么處理。
可以像添加屬性那樣給對象添加方法,注意定義數據屬性和方法的語法完全相同。
var person = { name: "Nicholas", sayName: function () { console.log(person.name) } }2.5.1 this對象
之前的例子的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為全局對象。
2.5.2 改變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的值。
3. 理解對象Js中的對象是動態的,可以在代碼執行的任意時刻發生改變。
3.1 定義屬性當一個屬性第一次被添加給對象時,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。
3.3 刪除屬性正如屬性可以在任何時候被添加,也可以在任何時候被刪除。但是設置一個屬性值為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) // undefined3.4 屬性枚舉
所有你添加的屬性默認為可枚舉的,可以用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并不是每個屬性都是可枚舉的,可以使用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的內建屬性,你會發現很多原生屬性默認都是不可枚舉的。
3.5 屬性類型屬性有兩種類型數據屬性和訪問器屬性;
數據屬性包含一個值,例如之前的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,那么屬性為只寫,兩種模式下讀取都失敗3.6 屬性特征ES5之前無法訪問屬性的任何特征,也沒有辦法指定一個屬性是否為可枚舉,因此ES5引入多種方法與屬性特征互動,同時也引入新的特征來支持額外的功能,現在已經可以創建出和Js內建屬性一樣的自定義屬性。下面介紹數據屬性和訪問器屬性的特征。
3.6.1 通用特征有兩個屬性時數據屬性和訪問器屬性共有的:
[[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]]3.6.2 數據屬性特征數據屬性額外擁有兩個訪問器屬性不具備的特征:
[[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在嚴格模式下視圖改變不可寫屬性會拋出錯誤,而在非嚴格模式下會失敗3.6.3 訪問器屬性訪問器屬性擁有兩個數據屬性不具備的特征,訪問器屬性不需要儲存值,因此也就沒有[[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關鍵字,它們是包含函數的數據屬性,這里不能使用字面量形式。
3.6.4 定義多重屬性如果你使用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 } })3.6.5 獲取屬性特征如果需要獲取屬性的特征,Js中可以使用Object.getOwnPropertyDescriptor(),這個方法只可以用于自有屬性,它接受兩個參數:對象、屬性名。如果屬性存在,它會返回一個屬性描述對象,內含四個屬性:configurable、enumerable、另外兩個根據屬性類型決定。即使你從沒有為屬性顯式指定特征,你依然會得到包含全部這些特征值的屬性描述對象。
3.7 禁止修改對象對象和屬性一樣具有指導行為的內部特征,其中,[[Extensible]]是一個布爾值,它指明該對象本身是否可以被修改,你創建的所有對象默認都是可擴展的,新的屬性可以隨時被添加,設置[[Extensible]]為false則可以禁止新屬性的添加。
3.7.1 禁止擴展
下面有三種方法可以用來鎖定對象屬性第一種方法是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在嚴格模式下試圖給一個不可擴展對象添加屬性會拋出錯誤,而在非嚴格模式下會失敗。應該對不可擴展對象使用嚴格模式,這樣當一個不可擴展對象被錯誤使用時你就會知道3.7.2 對象封印一個被封印的對象是不可擴展的且其所有屬性都不可配置,這意味著不僅不能給對象添加屬性,而且也不能刪除屬性或改變類型(從數據屬性改變成訪問屬性或者反之),如果一個對象被封印,那么只能讀寫它的屬性。
可以用Object.seal()方法來封印一個對象,該方法被調用時[[Extensible]]特征被設置為false,其所有屬性的[[Configurable]]特征被置為false,可以使用Object.isSealed()來判斷一個對象是否被封印。
這段代碼封印了person1,因此不能再person1上添加或者刪除屬性。所有的被封印對象都是不可擴展的對象,此時對person1使用Object.isExtensible()方法將會返回false,且視圖添加sayName()會失敗。
而且雖然person.name被成功改變成一個新值,但是刪除它會失敗。確保對被封印的對象使用嚴格模式,這樣當有人誤用該對象時,會報錯3.7.3 對象凍結被凍結的對象不能添加或刪除屬性,不能修改屬性類型,也不能寫入任何數據屬性。簡言而之,被凍結對象是一個數據屬性都為只讀的被封印對象。
Object.freeze() 凍結對象。
Object.isFrozen() 判斷對象是否被凍結。被凍結對象僅僅只是對象在某個時間點上的快照,用途有限且很少被使用4. 構造函數和原型對象 4.1 構造函數構造函數就是用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來訪問對應的數據,這就需要用到原型對象。
4.2 原型對象原型對象可以看做對象的基類,幾乎所有函數(除了一下內建函數)都有一個名為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) }4.2.1 [[Prototype]]屬性一個對象實例通過內部屬性[[Prototype]]追蹤其原型對象,該 屬性時一個指向該實例使用的原型對象的指針。當你使用new創建一個新的對象時,構造函數的原型對象會被賦給該對象的[[Prototype]]屬性 (JS proto 探究.md )。你可以調用Object.getPropertyOf()方法讀取[[prototype]]屬性的值。
Object.prototype.__proto__ === nullvar 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的賦值只是在對象上創建了一個新的自有屬性,而不是改變原型屬性。
4.2.2 在構造函數中使用原型對象原型對象的共享機制使得它們成為一次性為所有對象定義所有方法的理想手段,因為一個方法對所有的對象實例做相同的事,沒理由每個實例都要有一份自己的方法。將方法放在原型對象中并使用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構造函數、原型對象、對象實例之間:對象實例和構造函數之間沒有直接聯系。不過對象實例和原型對象之間以及原型對象和構造函數之間都有直接聯系。
這樣的連接關系也意味著,如果打斷對象實例和原型對象之間的聯系,那么也將打斷對象實例及其構造函數之間的關系。
4.2.3 改變原型對象給定類型的所有對象實例共享一個原型對象,所以可以一次性擴充所有對象實例。[[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并沒有凍結。
4.2.4 內建對象的原型對象所有內建對象都有構造函數,因此也都有原型對象可以去改變,例如要在數組上添加一個新的方法只需要改變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) // 21sum()函數內部,在調用時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!5. 繼承 5.1 原型對象鏈和Object.prototypeJs內建的繼承方法被稱為原型對象鏈,又稱為原型對象繼承。原型對象的屬性可以由對象實例訪問。實例對象集成了原型對象的屬性,因為原型對象也是一個對象,它也有自己的原型對象并繼承其屬性。這就是原型繼承鏈:對象繼承其原型對象,而原型對象繼承它的原型對象,以此類推。
所有的對象,包括自定義的對象都繼承自Object,除非另有指定。更確切的說,所有對象都繼承自Object.prototype,任何以字面量形式定義的對象,其[[Prototype]]的值都被設為Object.prototype,這意味著它繼承Object.prototype的屬性。var book = {title: "a book"} console.log(Object.getPrototypeOf(book) === Object.prototype) // true5.1.1 繼承自Object.prototype的方法前幾張用到的幾個方法都是定義在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) // 240155076169now是一個代表當前時間的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 ]5.1.2 修改Object.prototype所有的對象都默認繼承自Object.prototype,所以改變它會影響到所有的對象,這是非常危險的。
5.2 對象繼承
如果給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。
5.3 構造函數繼承
在訪問一個對象的時候,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) // trueMDN: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); // trueSquare.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 } })在對原型對象添加屬性前要確保你已經改寫了原型對象,否則在改寫時會丟失之前添加的方法(因為繼承是將被繼承對象賦值給需要繼承的原型對象,相當于重寫了需要繼承的原型對象)。5.4 構造函數竊取由于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 來繼承方法并用構造函數竊取來設置屬性,由于這種做法模仿了那些基于類的語言的類繼承,所以這通常被稱為偽類繼承。
5.5 訪問父類方法其實也是通過指定 call 或 apply 的子對象調用父類方法。
6. 對象模式可以使用繼承或者混入等其他技術令對象間行為共享,也可以利用Js高級技巧阻止對象結構被改變。
6.1 私有成員和特權成員 6.1.1 模塊模式模塊模式是一種用于創建擁有私有數據的單件對象的模式。
基本做法是使用立即調用函數表達式(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 // 外部可以訪問的對象 } }());6.1.2 構造函數的私有成員模塊模式在定義單個對象的私有屬性十分有效,但對于那些同樣需要私有屬性的自定義類型呢?你可以在構造函數中使用類似的模式來創建每個實例的私有數據。
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()); // 236.2 混入這是一種偽繼承。一個對象在不改變原型對象鏈的情況下得到了另外一個對象的屬性被稱為“混入”。因此,和繼承不同,混入讓你在創建對象后無法檢查屬性來源。
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] } } } }6.3 作用域安全的構造函數構造函數也是函數,所以不用 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); } }PS:歡迎大家關注我的公眾號【前端下午茶】,一起加油吧~
另外可以加入「前端下午茶交流群」微信群,長按識別下面二維碼即可加我好友,備注加群,我拉你入群~
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/91738.html
摘要:面向對象精要讀書筆記下面代碼的實際執行過程是什么使用原始值和原始封裝類型是有區別的因為是被解析成一個對象的,所以肯定是真的函數是對象,函數有兩種字面形式,第一種是函數聲明,以關鍵字開頭后面跟函數名字。 Javascript面向對象精要讀書筆記 1、下面代碼的實際執行過程是什么? var name = fan var str = name.charAt(0) console.l...
摘要:使用時,會自動創建對象,其類型為構造函數類型,指向對象實例缺少關鍵字,指向全局對象。構造函數本身也具有屬性指向原型對象。 在JavaScript面向對象精要(一)中講解了一些與面向對象相關的概念和方法,這篇講講原型和繼承。 構造函數和原型對象 構造函數也是函數,用new創建對象時調用的函數,與普通函數的一個區別是,其首字母應該大寫。但如果將構造函數當作普通函數調用(缺少new關鍵字...
摘要:使函數不同于其他對象的決定性特性是函數存在一個被稱為的內部屬性。其中,是一個布爾值,指明改對象本身是否可以被修改值為。注意凍結對象和封印對象均要在嚴格模式下使用。 數據類型 在JavaScript中,數據類型分為兩類: 原始類型:保存一些簡單數據,如true,5等。JavaScript共有5中原始類型: boolean:布爾,值為true或false number:數字,值...
摘要:解除引用的最佳手段是將對象變量設置為。字面形式允許你在不需要使用操作符和構造函數顯示創建對象的情況下生成引用值。函數就是值可以像使用對象一樣使用函數因為函數本來就是對象,構造函數更加容易說明。 JavaScript(ES5)的面向對象精要 標簽: JavaScript 面向對象 讀書筆記 2016年1月16日-17日兩天看完了《JavaScript面向對象精要》(參加異步社區的活動送...
閱讀 2584·2023-04-25 20:50
閱讀 3929·2023-04-25 18:45
閱讀 2213·2021-11-17 17:00
閱讀 3323·2021-10-08 10:05
閱讀 3073·2019-08-30 15:55
閱讀 3487·2019-08-30 15:44
閱讀 2355·2019-08-29 13:51
閱讀 1111·2019-08-29 12:47