JavaScript 中的物件概念的確很容易讓人困惑。看下面一個例子:
var strPrimitive = "I'm mamacat"; typeof strPrimitive; // "string" strPrimitive instanceof String; // false var strObject = new String("I'm mamacat"); typeof strObject; // "object" strObject instanceof String; // true strPrimitive.substr(8, 3); // "cat"
同樣的字串賦值到對象,一會兒是字符串類型一會兒是對象,而明明不是對象類型的變數還是可以使用對象屬性,為什麼會這樣呢?
【相關課程推薦:JavaScript影片教學】
#類型與內建物件
##JavaScript 中一共有六種主要(語言)類型,即string, number, boolean, null, undefined 和 object,其中前五個基本類型都不是物件(對 null 進行typeof 得到的是"object",這是語言本身的BUG)。而在此之外,則有許多特殊的物件子類型,例如陣列、函數和內建物件等。
有些內建物件的名字看著和簡單基本型別一樣,就例如 String,Boolean,Object 之類。這些內建物件從表現形式看就和別的物件導向語言中的「類別」概念差不多,而正如上篇文章所屬,它們實際上使只是一些能被用來建構一個對應子類型的內建函數而已(不要困惑,函數也是對象,這裡不矛盾)。於是就可以回到最初的例子,strObject 是內建函數/內建物件 String 所建構的變量,對應 String 子型,所以它是一個對象,而 strPrimitive 則是一個原始字面值而已。 當然,上面範例中最下面我們看起來對 strPrimitive 呼叫了 substr() 函數,這裡則是因為,JavaScript 引擎會在需要時,把原始字面量轉換成對應的對象,而轉換之後我們自然就可以使用屬性來存取對應的方法了。物件屬性
那麼,就上方的例子而言,String 物件實例就會有substr() 函數可以用,但根據先前的文章可以知道,這些「函數」本身並不屬於某個對象,而這些函數實質是對應對象的屬性。當然,即便我們說某種類型的物件本身俱備各種屬性,實際上這些屬性也多是各自獨立存在的,只不過以引用的形式關聯在了一起而已,這和之前了解的內容也不矛盾。這些被關聯起來的東西,被稱為物件的 屬性。物件的複製
插播一條快報,儘管之前的文章提到過,上方也又一次反覆強調過屬性只是以引用的形式關聯起來的獨立存在,我們有時仍然會「理所當然」的認為屬性是物件的一部分,而最容易因此踩坑的地方之一就是物件的複製了。仔細思考即可知道,當我們複製物件時,由於其屬性本身只是引用關聯,故「複製」得到的物件所包含的屬性引用指向的和原本物件的屬性引用其實還是同一個位置:var ori = { a : 1}; var ori_copy = ori; ori.a = 61; ori_copy.a; // 61顯然這很可能和我們的期望不一樣,而我們想要真正的拷貝物件則沒有完美適用性的方案,很多時候的常規做法則是把物件序列化一下,然後再以此反序列化得到新的物件來實現物件的拷貝(例如使用json)。 ES6 中新增了
Object.assign() 來進行物件的淺拷貝,做法是把物件的所有可枚舉屬性等號賦值到新物件中。不過仍需注意的是,等號賦值並不會賦值屬性的元資訊(屬性描述符,後述),在需要的情況下應特別留意。
屬性存取和數組
訪問物件所關聯的屬性的方式即透過. 或[] 運算子進行訪問,obj.a 和obj["a" ] 訪問的屬性實質上是一樣的,而這兩種訪問形式的區別也只有訪問的屬性名稱裡能不能有奇怪的符號而已。 [] 運算子內丟的是個字串,其實屬性名稱也永遠都是字串。當然,這個概念可能比較意外的就是,數組的下標存取其實並不是例外,數字還是被轉換成了字串才被使用的。// 对象的属性访问: var tejilang = {1 : "Teji Wolf"}; tejilang instanceof Array; // false tejilang["1"]; // "Teji Wolf" tejilang[1]; // "Teji Wolf" // 这回保证它是 Array var macat = ["codingcat"]; macat instanceof Array; // true macat.length; // 1 macat[0]; // "codingcat" macat["0"]; // "codingcat" macat.length = 20; macat; // (20) ["codingcat", empty × 19]
数组下标既然不属例外情况,那数组对象必然有其它属性控制数组本身的行为,例如上例中,macat 数组的长度就是 length 属性所体现的,通过修改它的值也就改变了对象本身对外的表现形式。当然,由于数组本身就是对象,所以我们还是可以把数组当键值对来用,只是这种做法通常是没有意义且会让人感到困惑的。JavaScript 引擎通常都根据对象的类型做了不同程度的优化,故除了代码逻辑可读性外,合理的使用也是多少可以改善性能的。
能够通过字符访问属性还是存在一些别的好处的,比如 ES6 的可计算属性名。当然 ES6 不在本文的关注范围内,所以这里就不再讨论了。
属性描述符
有时我们可能不希望某个属性被随意修改,有时候我们需要额外配置一些属性的信息,自 ES5 起,所有的属性就都具备了“属性描述符”(Property Descriptor)来控制属性本身的这些元信息。
数据描述符
来看这个例子:
var chris = {}; Object.defineProperty(chris, "IQ", { value: 228, writable: false, configurable: true, enumerable: true }); chris.IQ = 61; // 静默失败了,如果是严格模式则会 TypeError chris.IQ; // 228
通过 defineProperty 可以对一个对象的属性配置其对应的属性描述符(元信息),而属性描述符则包含访问描述符和数据描述符,上面的例子中,defineProperty 的第三个参数就定义了数据的若干数据描述符,其中 writable 表示可写,configurable 表示属性是否可配置(注意,修改成不可配置是单向操作),enumerable 则表示属性是否应当出现在枚举中,比如 for..in 中。
显然我们可以通过属性描述符实现对属性的保护,而同时也存在一些方便函数来做近似的事。如 Object.preventExtensions() 会保留原有属性但禁止添加新属性,Object.seal() 会密封对象,在禁止添加新属性的基础上把原有属性标记为不可配置,Object.freeze() 会冻结对象,即在密封的基础上把数据访问属性标记为不可写。
[[Get]], [[Put]] 和访问描述符
在我们访问和赋值一个对象的属性时,实际上是通过 [[Get]] 和 [[Put]] 操作进行的,例如属性访问时,[[Get]] 会先找有没有这个属性,如果没有则会遍历对象的 [[Prototype]] 链(原型链,这次不谈这个概念)来找,实在找不到则返回 undefined 。而这个行为实际是允许我们通过设置 getter (get())和 setter (set())函数来改变的,它们被称为 访问描述符。
当我们提供访问描述符时,对应的访问操作就不再受到 value 和 writable 属性的影响了,另外需要注意的是,尽管它们也是属性描述符,但定义 getter 和 setter 并不要求一定要通过 defineProperty 设置:
var obj = { get a() { // 给 a 属性定义 getter return this._a_; }, set a(val) { // a 属性的 setter this._a_ = val * 2; } } obj.a = 2; obj.a; // 4
属性存在性
因为属性的值也可能是 undefined,不存在的属性直接访问得到的也是 undefined,所以直接通过简单的属性访问是无法区分是否存在的,这时我们即可通过 in 或者 hasOwnProperty() 检查属性是否存在对象中了:
var obj = {a : 2}; "a" in obj; // true obj.hasOwnProperty("a"); // true
尽管仍没有讲到原型链的概念,这里仍然应注意,in 操作符会检查原型链中是否存在属性,而 hasOwnProperty 则不会。另外在一些情况下,有的对象会没有 hasOwnProperty 这个属性(此处不提原因),这时可以用过 Object.prototype.hasOwnProperty.call(objName, propertyName) 来实现检查。
本文来自 js教程 栏目,欢迎学习!
以上是JavaScript中物件屬性詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!