- 看透JavaScript:原理、方法與實踐
- 韓路彪
- 4352字
- 2020-11-28 15:50:46
5.2 對象的屬性
對象是通過其屬性來發揮作用的,因此對象的屬性是對象的核心。
5.2.1三種屬性類型
ES中對象的屬性其實有三種類型,前面使用的屬性只是其中的一種。屬性的三種類型分別為:命名數據屬性(named data properties)、命名訪問器屬性(named accessor properties)和內部屬性(internal properties)。下面我們分別學習。
1.命名數據屬性
命名數據屬性是我們平時使用最多的屬性,由屬性名和屬性值組成。前面例子中所使用的都是這種屬性,這里就不再舉例了。
2.命名訪問器屬性
命名訪問器屬性是使用getter、setter或者其中之一來定義的屬性。getter和setter是對應的方法,setter方法用于給屬性賦值,而getter方法用于獲取屬性的值。如果只有getter、setter其中之一,就只能進行單一的操作,例如,只有getter方法的屬性就是只讀屬性,只有setter方法的屬性就是只可以寫入的屬性。例如下面的例子。
function log(msg){ console.log(msg); } var colorManager = { _colorNum:3, _accessColors:["red", "green", "blue"], _color:"red", //colorNum為只讀屬性,只需定義get方法 get colorNum(){ return this._colorNum; }, //accessColors為只可寫入的屬性,只需定義set方法 set accessColors(colors){ this._colorNum = colors.length; this._accessColors = colors; log("accessColors被修改了"); }, //color為可讀寫屬性,同時定義了get、set方法 set color(color){ if(this._accessColors.indexOf(color)<0){ //判斷設置的color是否在允許的范圍內 log("color不在允許范圍內"); return; } log("color值被修改為" + color); this._color = color; }, get color(){ log("正在獲取color值"); if(this._accessColors.indexOf(this._color)<0){ return null; } return this._color; } } log(colorManager.color); //正在獲取color值red colorManager.accessColors = ["white", "black", "red", "yellow", "orange"];
//accessColors被修改了 log(colorManager.colorNum); //5 colorManager.color = "blue"; //color不在允許范圍內 colorManager.color = "orange"; //color值被修改為orange log(colorManager.color); //正在獲取color值orange
在這個例子中,我們定義了一個colorManager對象。它有三個訪問器屬性:colorNum、accessColors和color。其中,colorNum表示可以使用的color的數量,只有getter方法是只讀屬性;accessColors表示可以使用的color的數組,只有setter方法是只寫屬性;color表示當前的color值,是可讀寫屬性。當給相應的屬性設置值的時候就會調用相應的setter方法,調用屬性值的時候就會調用相應的getter方法。我們在各個訪問器方法中除了修改(或讀取)屬性值之外,還做了一些邏輯判斷以及打印日志的相關工作。
從這個例子可以看到getter和setter方法本身并不可以保存屬性的內容。通常另外定義一個以下畫線開頭的屬性來保存訪問器屬性的值,而且為了方便,一般會將保存訪問器屬性值的屬性的名字設置為訪問器屬性名前加下畫線。例如,在上面例子中使用_color來保存color訪問器屬性的值,使用_colorNum來保存colorNum訪問器屬性的值。這并不是強制性的,保存值的屬性的名稱也可以用其他名稱。但是,為了方便和代碼容易理解最好還是按照這個規則來命名。
這里大家可以會有一個疑問,那就是在定義了保存訪問器屬性值的屬性之后,如果直接操作這個屬性,不就可以繞過訪問器來操作其值了嗎?例如,直接操作_color屬性不就可以繞過訪問器方法了嗎?對于這個問題,我們學習后面相應的內容之后就可以解決了,這里暫時可以先不考慮。
3.內部屬性
內部屬性是對象的一種特殊屬性。它沒有自己的名字,當然也就不可以像前兩種屬性那樣直接訪問了。正是因為內部屬性沒有名字所以前面兩種屬性才叫作命名屬性。內部屬性使用兩對方括號表示。例如,[[Extensible]]表示Extensible內部屬性。
內部屬性的作用是用來控制對象本身的行為。所有對象共有的內部屬性共12個:[[Prototype]]、[[Class]]、[[Extensible]]、[[Get]]、[[GetOwnProperty]]、[[GetProperty]]、[[Put]]、[[CanPut]]、[[HasProperty]]、[[Delete]]、[[DefaultValue]]和[[DefineOwnProperty]]。除了這12個之外,不同的對象可能還會有自己的內部屬性,例如,Function類型對象的[[HasInstance]]、RegExp類型對象的[[Match]]等。通用的12個內部屬性中的前3個可用來指示對象本身的一些特性,后9個屬性可對對象進行特定的操作。它們在進行相應的操作時會自動調用,這里就不詳細介紹了。下面主要給大家解釋一下前3個屬性。
(1)[[Prototype]]
[[Prototype]]屬性就是前面講過的使用function創建對象時function中的prototype屬性。在創建完的實例對象中,這個屬性并不可以直接調用,但可以使用Object的getPrototypeOf方法來獲取,例如下面的例子。
function Car(){} Car.prototype = {color:"black"}; var car = new Car(); console.log(typeof car.prototype); //undefined console.log(Object.getPrototypeOf(car)); //Object { color="black"}
這個例子中,使用Car新建了car對象,Car的prototype屬性對象中有一個color屬性,這個對象就是car實例的[[Prototype]]屬性,不能使用car.prototype獲取,可使用Object. getPrototypeOf(car)獲取。在有些瀏覽器(例如Firefox)中還可以使用_ _proto_ _屬性來獲取,例如,這里使用car. _ _proto_ _同樣可以獲取[[Prototype]]屬性。但是,因為_ _proto_ _屬性不是通用屬性,所以最好還是使用Object的getPrototypeOf方法來獲取。
(2)[[Class]]
[[Class]]屬性可用來區分不同對象的類型,不能直接訪問,toString方法默認會返回這個值。默認Object.prototype.toString方法返回的字符串是[object, [[Class]] ],即方括號里面兩個值,第一個是固定的object,第二個是[[Class]],因此使用toString方法就可以獲取對象的[[Class]]屬性。但是,因為ES中的內置對象在prototype中重寫了toString方法,所以內置對象的返回值可能不是這個形式。瀏覽器中的宿主對象并沒有重寫此方法,在瀏覽器中調用它們的toString方法可以獲取[[Class]]的值,例如下面的例子。
function log(msg){ console.log(msg); } log(window.toString()); //[object Window] log(document.toString()); //[object HTMLDocument] log(navigator.toString()); //[object Navigator]
我們自己創建的object類型對象默認都屬于Object類型,因此,它們的toString方法默認都會返回[object Object]。另外,內置對象因為重寫了toString方法,所以不會返回這種結構的返回值,例如,字符串對象會返回字符串自身,數組會返回數組元素連接成的字符串等。對于這種情況,我們可以使用Object.prototype.toString的apply屬性方法來調用Object原生的toString方法,這樣就會得到[object, [[Class]] ]這樣的結果,例如下面的例子。
var str = "", arr = []; console.log(Object.prototype.toString.apply(str)); //[object String] console.log(Object.prototype.toString.apply(arr)); //[object Array]
多知道點
逆向調用的apply和call方法
apply和call方法都可以理解為function對象中的隱藏方法,其實它們是Function對象的prototype屬性對象中的方法,而function對象是Function的實例對象,因此function可以調用Function的prototype屬性對象中的這兩個方法。
這兩個方法的作用相同,都用于逆向調用。正常的方法調用是通過“對象.方法名(參數)”結構調用的,也就是需要使用對象來調用相應的方法。但是,使用這兩個方法正好反過來,它們都可以實現用方法來調用對象,也就是將一個對象傳遞給方法,然后該方法就可以作為對象的方法來調用,這樣就不需要先將方法添加為對象的屬性,然后再調用了,例如下面的例子。
var obj = {v:237}; function logV(){ console.log(this.v); } logV.apply(obj); //237 logV.call(obj); //237
這個例子中的obj對象并沒有logV方法,但是通過logV方法的apply和call屬性方法調用obj對象就可以實現跟將logV方法設置為obj對象的屬性然后再調用相同的效果。
方法在調用時還可能需要傳遞參數,使用apply和call來調用也可以傳遞參數,但這兩個方法傳遞參數的方式不一樣,這也是它們唯一的區別。使用apply調用時參數需要作為一個數組傳遞,而使用call調用時參數直接按順序傳入即可,調用語法分別如下。
fun.apply(thisArg, [argsArray]); fun.call(thisArg[, arg1[, arg2[, ...]]]);
它們的第一參數都是this對象,也就是調用方法的對象,后面的參數都是傳遞給方法的參數。我們來看個例子。
function sell(goods, num){ return this.price.get(goods)*num; } var tmall = {price:new Map([ ["iphone6_Plus_16g",5628], ["iphone6_Plus_64g",6448], ["小米Note_頂配版",2999] ])} var jd = {price:new Map([ ["iphone6_Plus_16g",5688], ["iphone6_Plus_64g",6423],
["小米Note_頂配版",2999] ])} console.log(sell.apply(jd, ["iphone6_Plus_64g", 1])); //6423 console.log(sell.call(jd, "iphone6_Plus_64g", 1)); //6423 console.log(sell.apply(jd, ["小米Note_頂配版", 2])); //5998 console.log(sell.call(tmall, "iphone6_Plus_16g", 3)); //16884
在這個例子中,我們首先定義了一個sell方法,用于計算價格,它有兩個參數,分別表示商品名和數量,計算時需要先從當前對象的price屬性中獲取單價,再乘以數量。然后,我們定義了兩個對象tamll和jd,它們都只包含一個price屬性,它是Map類型用于表示不同商城中商品的單價。最后,我們使用sell方法的apply和call分別調用tmall和jd對象計算了各自的價格。如果還需要計算其他平臺(對象)的價格,只需要創建相應的對象就可以了,而不需要將sell方法分別添加到它們的屬性中。
(3)[[Extensible]]
[[Extensible]]屬性用來標示對象是否可擴展,即是否可以給對象添加新的命名屬性,默認為true,也就是可以擴展。我們可以使用Object的preventExtensions方法將一個對象的[[Extensible]]值變為false,這樣就不可以擴展了。另外,可以使用Object的isExtensible方法來獲取[[Extensible]]的值。我們來看個例子。
var person = {nationality: "中國"}; console.log(Object.isExtensible(person)); //true person.name = "歐陽修"; //現在可以添加屬性 Object.preventExtensions(person); //將[[Extensible]]設置為false console.log(Object.isExtensible(person)); //false p.age = "108"; //拋出異常
這個例子中,我們定義了person對象,它的[[Extensible]]屬性本來為true,我們可以給它添加命名屬性。當調用preventExtensions方法對其操作后[[Extensible]]屬性就變為了false,這時就不能給它添加新的命名屬性了。
需要注意的是,一旦使用preventExtensions方法將[[Extensible]]的值設置為false后就無法改回true了。
5.2.2 5種創建屬性的方式
本節主要指創建命名屬性。對象的命名屬性一共有5種創建方式。
1.使用花括號創建
這種方式是在使用花括號創建對象時創建屬性,例如下面的例子。
var obj = { v:1.0, getV: function () { return this.v; }, _name:"object", get name(){ this._name; }, set name(name){ this._name = name; } }
這個例子中,使用花括號創建了obj對象,其中包含直接量屬性(v)、function對象屬性(getV)以及訪問器屬性(name)。注意,在定義訪問器屬性的getter和setter方法時沒有冒號。
2.使用點操作符創建
當使用點操作符給一個對象的屬性賦值時,如果對象存在此屬性則會修改屬性的值,否則會添加相應的屬性并賦予對應的值,例如下面的例子。
var person = {name:"張三"}; person.name = "李四"; //修改原有屬性的值 person.age = 88; //添加新屬性
這個例子中,首先使用花括號定義了person對象,其中包含name屬性,當給它的name屬性賦予新值時會改變其name屬性的值,而當給age屬性賦值時,由于person原來沒有age屬性,所以會先添加age屬性,然后將其值設置為88。
在function中使用this創建屬性其實也是這種添加屬性方式的一種特殊用法。因為在function創建object類型對象時,其中的this就代表創建出來的對象,而且剛創建出來的對象是沒有自定義的命名屬性的,所以使用this和點操作符就可以將屬性添加到創建的對象中,例如下面的例子。
function Person(){ this.name = "孫悟空"; } var person = new Person(); console.log(person.name); //孫悟空
在這個例子中,首先定義了function類型的Person,然后用其創建了person對象,創建完成后會自動調用Person方法體中的this.name = "孫悟空";語句,這時,由于this所代表的person對象并沒有name屬性,所以會自動給它添加name屬性,這也就是創建的person對象具有name屬性的原因了。
3. Object的create方法
我們在前面已經介紹過Object的create方法,它有兩個參數,第一個參數中的屬性為創建的對象的[[Prototype]]屬性,第二個參數為屬性描述對象。
4. Object的defineProperty、defineProperties方法
我們可以使用Object的defineProperty和defineProperties方法給對象添加屬性。define Property方法可添加單個屬性,defineProperties方法可以添加多個屬性。
Object的defineProperty方法一共有三個參數,第一個是要添加屬性的對象,第二個是要添加屬性的屬性名,第三個是屬性的描述。前兩個參數都很簡單,第三個我們會在后面詳細講解,先來看個例子。
var obj = {}; Object.defineProperty(obj, "color", { enumerable: true, value: "green" }); console.log(Object.getOwnPropertyNames(obj)); //["color"] console.log(obj.color); //green
在這個例子中,我們使用defineProperty方法給obj對象添加了color屬性。
Object的defineProperties方法可以創建多個屬性,它有兩個參數,第一個參數是要添加屬性的對象,第二個參數是屬性描述對象,和create方法中的第二個參數一樣,例如下面的例子。
var obj = {}; Object.defineProperties(obj, { name:{ enumerable: true, writable: false, value: "lucy" }, color:{ enumerable: true, value: "green" } }); console.log(Object.getOwnPropertyNames(obj)); //["name", "color"] obj.name = "peter"; console.log(obj.name); //lucy
這個例子使用Object的defineProperties方法給obj對象添加了name和color兩個屬性。在這個例子中,因為name屬性的writable為false,所以obj的name屬性是不可以修改的。當我們將其值修改為peter后,打印出的還是原來的lucy,這說明修改并沒有作用。而且,使用defineProperties方法添加屬性時writable的默認值就是false。
5.通過prototype屬性創建
使用function創建的object實例對象可以使用function對象的prototype屬性對象中的屬性,這一點我們在前面已經多次證實過。嚴格來說,function對象的prototype中的屬性并不會添加到創建的實例對象中,但創建的對象可以調用,這樣就相當于可以將prototype中的屬性添加到創建的對象中。因此,如果給function的prototype添加了屬性,那么也就相當于給創建的對象添加了屬性,而且在對象創建完成之后還可以再添加,例如下面的例子。
function Shop(){} var shop = new Shop(); Shop.prototype.type = "網絡銷售"; console.log(shop.type); //網絡銷售
這個例子中,首先使用Shop創建了shop對象,然后給Shop的prototype添加了type屬性,這時調用shop.type也可以獲取屬性值。在調用shop.type時,因為shop沒有type屬性,shop就會實時到Shop的prototype中查找,而不是提前將Shop的prototype屬性對象保存起來,所以創建完shop對象后再修改Shop的prototype屬性,已修改的屬性也可以被shop實例對象調用。這一點在前面已經介紹過。
- Learning C# by Developing Games with Unity 2020
- SOA實踐
- 程序員考試案例梳理、真題透解與強化訓練
- 劍指Java:核心原理與應用實踐
- Serverless computing in Azure with .NET
- Lighttpd源碼分析
- C++編程兵書
- Qt5 C++ GUI Programming Cookbook
- Mastering Concurrency in Python
- 計算機系統解密:從理解計算機到編寫高效代碼
- Processing開發實戰
- Building Scalable Apps with Redis and Node.js
- Developing RESTful Web Services with Jersey 2.0
- Oracle API Management 12c Implementation
- Drools 8規則引擎:核心技術與實踐