JavaScript中,除了五種原始類型(即數字,字串,布林值,null,undefined)之外的都是物件了,所以,不把物件學明白怎麼繼續往下學習呢?
一.概述
物件是一種複合值,它將許多值(原始值或其他物件)聚合在一起,可透過屬性名稱存取這些值。而屬性名可以是包含空字串在內的任意字串。 JavaScript物件也可以稱為一種資料結構,正如我們常聽到的「雜湊(hash)」、「散列表(hashtable)」、「字典 (dictionary)」、「關聯數組(associative array)」。
JavaScript中物件可以分為三類:
①內建對象,例如陣列、函數、日期等;
②宿主對象,即JavaScript解釋器所嵌入的宿主環境(如瀏覽器)定義的,例如HTMLElement等;
③自訂對象,即程式設計師用程式碼定義的;
物件的屬性可以分為兩類:
①自有屬性(own property):直接在物件中定義的屬性;
②繼承屬性(inherited property):在物件的原型物件中定義的屬性(關於原型物件下面會詳談);
二.物件的建立
既然學習對象,又怎麼能不懂如何創建對象呢?面試前端職位的同學,可能都被問過這個基礎問題:
建立JavaScript物件的兩種方法是什麼? (或者:說說創建JavaScript物件的方法?)
這個問題我就被問過兩次。 「創建物件的兩種方法」這種說法網路上有很多,但是據我所看書籍來說是有三種方法的!下面我們就來具體談談這三種方法:
1.物件直接量
物件直接量由若干名/值對組成的映射表,名/值對中間用冒號分隔,名/值對之間用逗號分隔,整個映射表用花括號括起來。屬性名可以是JavaScript標識符也可以是字串直接量,也就是說下面兩種創建物件obj的寫法是完全一樣的:
var obj = {x: 1, y: 2};
var obj = {'x': 1, 'y':2};
2.透過new建立物件
new運算子後跟隨一個函數調用,即建構函數,建立並初始化一個新物件。例如:
1 var o = new Object(); //建立一個空對象,和{}一樣
2 var a = new Array(); //建立一個空數組,和[]一樣
3 var d = new Date(); //建立一個表示目前時間的Date物件
關於建構函式相關的內容以後再說。
3.Object.create()
ECMAScript5定義了一個名為Object.create()的方法,它創建一個新對象,其中第一個參數是這個對象的原型對象(好像還沒解釋原型對象…下面馬上就說),第二個可選參數用以對物件的屬性進行進一步的描述,第二個參數下面再說(因為這第三種方法是ECMAScript5中定義的,所以以前大家才常說創建對象的兩種方法的吧?個人覺得應該是這個原因)。這個方法使用很簡單:
1 var o1 = Object.create({x: 1, y: 2}); //物件o1繼承了屬性x和y
2 var o2 = Object.create(null); //物件o2沒有原型
下面三種的完全一樣的:
1 var obj1 = {};
2 var obj2 = new Object();
3 var obj3 = Object.create(Object.prototype);
為了解釋為啥這三種方式是完全一樣的,我們先來解釋下JavaScript中的原型物件(哎,讓客官久等了!),記得一位大神說過:
Javascript是一種基於物件(object-based)的語言,你遇到的所有東西幾乎都是物件。但是,它又不是真正的物件導向程式設計(OOP)語言,因為它的語法中沒有class(類別)。
物件導向的程式語言JavaScript,沒有類別! ! !那麼,它是怎麼實現繼承的呢?沒錯,就是透過原型物件。基本上每個JavaScript物件(null除外)都和另一個物件相關聯,「另一個」物件就是所謂的原型物件(原型物件也可以簡稱為原型,並沒有想像的那麼複雜,它也只是一個物件而已)。每一個物件都從原型物件繼承屬性,而一個物件的prototype屬性的值(這個屬性在物件建立時預設自動生成,並不需要顯示的自訂)就是這個物件的原型對象,即obj.prototype就是對象obj的原型物件。
原型物件先說到這,回到上面的問題,有了對原型物件的認識,下面就是不需要過多解釋的JavaScript語言規定了:
①所有透過物件直接量所建立的物件的原型物件就是Object.prototype物件;
②透過關鍵字new和建構函式所建立的物件的原型物件就是建構函式prototype屬性的值,所以透過建構函式Object所建立的物件的原型就是Object.prototype了;
現在也補充了第三種建立物件的方法Object.create()第一個參數的意義。
三.屬性的查詢與設定
學會如何創造物件還不夠啊,因為物件只有擁有一些屬性才能真正起到作用滴!那麼,就繼續往下學習物件的屬性吧!
可以透過點(.)或方括號([])運算子來取得和設定屬性的值。對於點(.)來說,右側必須是一個以屬性名命名的標識符(注意:JavaScript語言的標識符有自己的合法規則,並不同於帶引號的字串);對於方括號([] )來說,方括號內必須是字串表達式(字串變數當然也可以嘍,其他可以轉換成字串的值例如數字什麼的也是都可以滴),這個字串就是屬性的名字。如以下例子:
var obj = {x: 1, y: 2}; obj.x = 5; obj['y'] = 6
概述中說過,JavaScript物件具有」自有屬性“,也有「繼承屬性」。當查詢物件obj的屬性x時,首先會尋找物件obj自有屬性中是否有x,如果沒有,就會尋找物件obj的原型物件obj.prototype是否有屬性x,如果沒有,就會再找出物件obj .prototype的原型物件obj.prototype.prototype是否有屬性x,就這樣直到找到x或查找到的原型物件是undefined的物件為止。可以看到,一個物件上面繼承了很多原型對象,這些原型對象就構成了一個”鏈“,這也就是我們平時所說的“原型鏈”,這種繼承也就是JavaScript中“原型式繼承” ( prototypal inheritance)。
物件o查詢某一屬性時正如上面所說會沿著原型鏈一步步查找,但是其設定某一屬性的值時,只會修改自有屬性(如果物件沒有這個屬性,那就會添加這個屬性並賦值),並不會修改原型鏈上其他物件的屬性。
四.存取器屬性getter和setter
上面我們所說的都是很普通的物件屬性,這種屬性稱為做「資料屬性」(data property),資料屬性只有一個簡單的值。然而在ECMAScript 5中,屬性值可以用一個或兩個方法替代,這兩個方法就是getter和setter,有getter和setter定義的屬性稱為「存取器屬性」(accessor property)。
當程式查詢存取器屬性的值時,JavaScript呼叫getter方法(無參數)。這個方法的回傳值就是屬性存取表達式的值。當程式設定一個存取 器屬性的值時,JavaScript呼叫setter方法,將賦值表達式右邊的值當作參數傳入setter。如果屬性同時具有getter和setter 方法,那麼它就是一個讀/寫屬性;如果它只有getter方法,那麼它就是一個只讀屬性,給只讀屬性賦值不會報錯,但是並不能成功;如果它只有setter方法,那麼它是一個只寫屬性,讀取只寫屬性總是回傳undefined。來看個實際的例子:
var p = { x: 1.0, y: 2.0, get r(){ return Math.sqrt(this.x*this.x + this.y*this.y); }; set r(newvalue){ var oldvalue = Math.sqrt(this.x*this.x + this.y*this.y); var ratio = newvalue/oldvalue; this.x *= ratio; this.y *= ratio; }, get theta(){ return Math.atan2(this.y, this.x); }, print: function(){ console.log('x:'+this.x+', y:'+this.y); } };
如範例所寫,存取器屬性定義一個或兩個和屬性同名的函數,這個函數定義並沒有使用function關鍵字,而是使用get和set,也沒有使用冒號將屬性名稱和函數體分隔開。比較一下,下面的print屬性就是一個函數方法。註:這裡的getter和setter裡this關鍵字的用 法,JavaScript把這些函數當作物件的方法來調用,也就是說,在函數體內的this指向這個物件。下面看下實例運行結果:
正如控制台的輸出,r、theta同x,y一樣只是一個值屬性,print是一個方法屬性。
ECMAScript 5增加的這種存取器,雖然比普通屬性更為複雜了,但是也使得操作物件屬性鍵值對更加嚴謹了。
五.刪除屬性
程式猿擼碼一般都是實現增、刪、改、查功能,前面已經說了增、改、查,下面就說說刪除吧!
delete運算子可以刪除物件的屬性,它的運算元應該是一個屬性存取表達式。但是,delete只是斷開屬性和宿主物件的聯繫,而不會去操作屬性中的屬性:
var a = {p:{x:1}}; var b = a.p; delete a.p;
执行这段代码后b.x的值依然是1,由于已删除属性的引用依然存在,所以有时这种不严谨的代码会造成内存泄露,所以在销毁对象的时候,要遍历属性中的属性,依次删除。
delete表达式返回true的情况:
①删除成功或没有任何副作用(比如删除不存在的属性)时;
②如果delete后不是一个属性访问表达式;
var obj = {x: 1,get r(){return 5;},set r(newvalue){this.x = newvalue;}}; delete obj.x; //删除对象obj的属性x,返回true delete obj.x; //删除不存在的属性,返回true delete obj.r; //删除对象obj的属性r,返回true delete obj.toString; //没有任何副作用(toString是继承来的,并不能删除),返回true delete 1; //数字1不是属性访问表达式,返回true
delete表达式返回false的情况:
①删除可配置性(可配置性是属性的一种特性,下面会谈到)为false的属性时;
delete Object.prototype; //返回false,prototype属性是不可配置的
//通过var声明的变量或function声明的函数是全局对象的不可配置属性
var x = 1;
delete this.x; //返回false
function f() {}
delete this.f; //返回false
六.属性的特性
上面已经说到了属性的可配置性特性,因为下面要说的检测属性和枚举属性还要用到属性的特性这些概念,所以现在就先具体说说属性的特性吧!
除了包含名字和值之外,属性还包含一些标识它们可写、可枚举、可配置的三种特性。在ECMAScript 3中无法设置这些特性,所有通过ECMAScript 3的程序创建的属性都是可写的、可枚举的和可配置的,且无法对这些特性做修改。ECMAScript 5中提供了查询和设置这些属性特性的API。这些API对于库的开发者非常有用,因为:
①可以通过这些API给原型对象添加方法,并将它们设置成不可枚举的,这让它们更像内置方法;
②可以通过这些API给对象定义不能修改或删除的属性,借此“锁定”这个对象;
在这里我们将存取器属性的getter和setter方法看成是属性的特性。按照这个逻辑,我们也可以把属性的值同样看做属性的特性。因此,可以认 为属性包含一个名字和4个特性。数据属性的4个特性分别是它的值(value)、可写性(writable)、可枚举性(enumerable)和可配置 性(configurable)。存取器属性不具有值特性和可写性它们的可写性是由setter方法是否存在与否决定的。因此存取器属性的4个特性是读取 (get)、写入(set)、可枚举性和可配置性。
为了实现属性特性的查询和设置操作,ECMAScript 5中定义了一个名为“属性描述符”(property descriptor)的对象,这个对象代表那4个特性。描述符对象的属性和它们所描述的属性特性是同名的。因此,数据属性的描述符对象的属性有 value、writable、enumerable和configurable。存取器属性的描述符对象则用get属性和set属性代替value和 writable。其中writable、enumerable和configurable都是布尔值,当然,get属性和set属性是函数值。通过调用 Object.getOwnPropertyDescriptor()可以获得某个对象特定属性的属性描述符:
从函数名字就可以看出,Object.getOwnPropertyDescriptor()只能得到自有属性的描述符,对于继承属性和不存在的属性它都返回undefined。要想获得继承属性的特性,需要遍历原型链(不会遍历原型链?不要急,下面会说到的)。
要想设置属性的特性,或者想让新建属性具有某种特性,则需要调用Object.definePeoperty(),传入需要修改的对象、要创建或修改的属性的名称以及属性描述符对象:
可以看到:
①传入Object.defineProperty()的属性描述符对象不必包含所有4个特性;
②可写性控制着对属性值的修改;
③可枚举性控制着属性是否可枚举(枚举属性,下面会说的);
④可配置性控制着对其他特性(包括前面说过的属性是否可以删除)的修改;
如果要同时修改或创建多个属性,则需要使用Object.defineProperties()。第一个参数是要修改的对象,第二个参数是一个映射表,它包含要新建或修改的属性的名称,以及它们的属性描述符,例如:
var p = Object.defineProperties({},{ x: {value: 1, writable: true, enumerable: true, configurable: true}, y: {value: 2, writable: true, enumerable: true, configurable: true}, r: {get: function(){return 88;}, set: function(newvalue){this.x =newvalue;},enumerable: true, configurable: true}, greet: {value: function(){console.log('hello,world');}, writable: true, enumerable: true, configurable: true} });
相信你也已经从实例中看出:Object.defineProperty()和Object.defineProperties()都返回修改后的对象。
前面我们说getter和setter存取器属性时使用对象直接量语法给新对象定义存取器属性,但并不能查询属性的getter和setter方法 或给已有的对象添加新的存取器属性。在ECMAScript 5中,就可以通过Object.getOwnPropertyDescriptor()和Object.defineProperty()来完成这些工作 啦!但在ECMAScript 5之前,大多数浏览器(IE除外啦)已经支持对象直接量语法中的get和set写法了。所以这些浏览器还提供了非标准的老式API用来查询和设置 getter和setter。这些API有4个方法组成,所有对象都拥有这些方法。__lookupGetter__()和 __lookupSetter__()用以返回一个命名属性的getter和setter方法。__defineGetter__()和 __defineSetter__()用以定义getter和setter。这四个方法都是以两条下划线做前缀,两条下划线做后缀,以表明它们是非标准方 法。下面是它们用法:
七.检测属性
JavaScript对象可以看做属性的集合,那么我们有时就需要判断某个属性是否存在于某个对象中,这就是接下来要说的检测属性。
检测一个对象的属性也有三种方法,下面就来详细说说它们的作用及区别!
1.in运算符
in运算符左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回true,否则返回false。
为了试验,我们先给对象Object.prototype添加一个可枚举属性m,一个不可枚举属性n;然后,给对象obj定义两个可枚举属性x,一个不可枚举属性y,并且对象obj是通过对象直接量形式创建的,继承了Object.prototype。下面看实例:
从运行结果可以看出:in运算符左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性(不论这些属性是否可枚举)中包含这个属性则返回true,否则返回false。
2.hasOwnProperty()
对象的hasOwnProperty()方法用来检测给定的名字是否是对象的自有属性(不论这些属性是否可枚举),对于继承属性它将返回false。下面看实例:
3.propertyIsEnumerable()
propertyIsEnumerable()是hasOwnProperty()的增强版,只有检测到是自有属性且这个属性可枚举性为true时它才返回true。还是实例:
八.枚举属性
相对于检测属性,我们更常用的是枚举属性。枚举属性我们通常使用for/in循环,它可以在循环体中遍历对象中所有可枚举的自有属性和继承属性,把属性名称赋值给循环变量。继续上实例:
我原来认为for/in循环跟in运算符有莫大关系的,现在看来它们的规则并不相同啊!当然,如果这里不想遍历出继承的属性,那就在for/in循环中加一层hasOwnProperty()判断:
for(prop in obj){ if(obj.hasOwnProperty(prop)){ console.log(prop); } }
除了for/in循环之外,ECMAScript 5还定义了两个可以枚举属性名称的函数:
①Object.getOwnpropertyNames(),它返回对象的所有自有属性的名称,不论是否可枚举;
②Object.keys(),它返回对象对象中可枚举的自有属性的名称;
还是实例:
九.对象的三个特殊属性
每个对象都有与之相关的原型(prototype)、类(class)和可扩展性(extensible attribute)。这三个就是对象的特殊属性(它们也只是对象的属性而已,并没有想象的复杂哦)。
1.原型属性
正如前面所说,对象的原型属性是用来继承属性的(有点绕…),这个属性如此重要,以至于我们经常把“o的原型属性”直接叫做“o的原型”。原型属性 是在实例创建之初就设置好的(也就是说,这个属性的值是JavaScript默认自动设置的,后面我们会说如何自己手动设置),前面也提到:
①通过对象直接量创建的对象使用Object.prototype作为它们的原型;
②通过new+构造函数创建的对象使用构造函数的prototype属性作为它们的原型;
③通过Object.create()创建的对象使用第一个参数(如果这个参数为null,则对象原型属性值为undefined;如果这个参数为 undefined,则会报错:Uncaught TypeError: Object prototype may only be an Object or null: undefined)作为它们的原型;
那么,如何查询一个对象的原型属性呢?在ECMAScript 5中,将对象作为参数传入Object.getPrototypeOf()可以查询它的原型,例如:
但是在ECMAScript 3中,没有Object.getPrototypeOf()函数,但经常使用表达式obj.constructor.prototype来检测一个对象的原型,因为每个对象都有一个constructor属性表示这个对象的构造函数:
①通过对象直接量创建的对象的constructor属性指向构造函数Object();
②通过new+构造函数创建的对象的constructor属性指向构造函数;
③通过Object.create()创建的对象的constructor属性指向与其原型对象的constructor属性指向相同;
要检测一个对象是否是另一个对象的原型(或处于原型链中),可以使用isPrototypeOf()方法。例如:
还有一个非标准但众多浏览器都已实现的对象的属性__proto__(同样是两个下划线开始和结束,以表明其为非标准),用以直接查询/设置对象的原型。
2.类属性
对象的类属性(class attribute)是一个字符串,用以表示对象的类型信息。ECMAScript 3 和ECMAScript 5 都未提供设置这个属性的方法,并只有一种间接的方法可以查询它。默认的toString()方法(继承自Object.prototype)返回了这种格 式的字符串:[object class] 。因此,要想获得对象的类,可以调用对象的toString()方法,然后提取已返回字符串的第8到倒数第二个位置之间的字符。不过,很多对象继承的 toString()方法重写了(比如:Array、Date等),为了能调用正确的toString()版本,必须间接地调用 Function.call()方法。下面代码可以返回传递给它的任意对象的类:
function classof(obj){ if(o === null){ return 'Null'; } if(o === undefined){ return 'Undefined'; } return Object.prototype.toString.call(o).slice(8, -1); }
classof()函數可以傳入任何類型的參數。下面是使用實例:
總結:從運行結果可以看出透過三種方式建立的物件的類別屬性都是'Object'。
3.可擴充性
物件的可擴充性用以表示是否可以為物件新增屬性。所有內建物件和自訂物件都是顯示可擴展的(除非將它們轉換為不可擴展),宿主物件的可擴充性是由JavaScript引擎定義的。 ECMAScript 5中定義了用來查詢和設定物件可擴充性的函數:
①(查詢)透過將物件傳入Object.isExtensible(),來判斷該物件是否是可擴展的。
②(設定)如果想將物件轉換為不可擴展,則需要呼叫Object.preventExtensions(),將待轉換的物件作為參數傳進去。注意:
a.一旦將物件轉換為不可擴展的,就無法再將其轉換回可擴展的了;
b.preventExtensions()只會影響到物件本身的可擴展性,如果給一個不可擴展的物件的原型添加屬性,這個不可擴展的物件同樣會繼承這些新屬性;
進一步,Object.seal()和Object.preventExtensions()類似,除了能將物件設為不可擴充的,還可以將物件的 所有自有屬性都設為不可配置的。對於那些已經封閉(sealed)起來的對像是不能解封的。可以使用Object.isSealed()來偵測物件是否 封閉。
更進一步,Object.freeze()將更嚴格地鎖定物件-「凍結」(frozen)。除了將物件設定為不可擴充並將其屬性設為不可配置之外,還可以將它自有的所有資料屬性設為唯讀(若物件的存取器屬性有setter方法,存取器屬性將不受影響,仍可透過給屬性賦值呼叫它們)。使用 Object.isFrozen()來偵測物件是否總結。
總結:Object.preventExtensions()、Object.seal()和Object.freeze()都傳回傳入的對象,也就是說,可以透過嵌套的方式呼叫它們:
var obj = Object.seal(Object.create(Object.freeze({x:1}),{y:{value: 2, writable: true}));
這語句中使用Object.create()函數傳入了兩個參數,即第一個參數是建立的對象的原型對象,第二個參數是在創建對像是直接給其定義的屬性,並且附帶定義了屬性的特性。
十.物件的序列化
前面說完了物件的屬性以及物件屬性的特性,東西還是蠻多的,不知道你是否已看暈。不過,以下就是比較輕鬆的話題了!
物件序列化(serialization)是指將物件的狀態轉換為字串,也可以將字串還原為物件。 ECMAScript 5提供了內建函數JSON.stringify()和JSON.parse()用來序列化和還原物件。這些方法都使用JSON作為資料交換格式,JSON 的全名為「JavaScript Object Notation」-JavaScript物件表示法,它的語法和JavaScript物件與陣列直接量的語法非常相近:
其中,最後的jsonObj是obj的深拷貝
JSON的語法是JavaScript的子集,它並不能表示JavaScript裡的所有值。支援物件、陣列、字串、無限大數字、true、false和null,並且它們可以序列化和還原。注意:
①NaN、Infinity和-Infinity序列化的結果是null;
②JSON.stringify()只能序列化物件可枚舉的自有屬性;
③日期物件序列化的結果是ISO格式的日期字串(參考Date.toJSON()函數),但JSON.parse()仍然保留它們的字串形態,而不能將它們還原為原始日期對象;
④函數、RegExp、Error物件和undefined值不能序列化和還原;
以上就是本文的全部內容,希望對大家的學習有所幫助。