你不懂js系列學習筆記-this與對象原型- 03

第 3 章:對象

原文:You-Dont-Know-JSjavascript

1 語法

對象能夠經過兩種形式定義:聲明(文字)形式和構造形式java

聲明(文字)形式:git

var myObj = {
  key: value
  // ...
};
複製代碼

構造形式:github

var myObj = new Object();
myObj.key = value;
複製代碼

2 類型

這裏書上說 JavaScript 有六種主要類型,ES6 引入了一種新的原始數據類型Symbol,表示獨一無二的值。它是 JavaScript 語言的第七種數據類型,前六種是:undefinednull、布爾值(Boolean)、字符串(String)、數值(Number)、對象(Object)。數組

關於 js 的類型 https://developer.mozilla.org/zh-CN/docs/Glossary/Primitive數據結構

JavaScript 數據類型和數據結構函數

JavaScript 標準內置對象工具

null 自己是基本類型:null 有時會被看成一種對象類型,可是這其實只是語言自己的一個 bug,即對 null 執行 typeof null 時會返回字符串 "object"學習

原理是這樣的,不一樣的對象在底層都表示爲二進制,在 JavaScript 中二進制前三位都爲 0 的話會被判 斷爲 object 類型,null 的二進制表示是全 0,天然前三位也是 0,因此執行 typeof 時會返回「object」。測試

2.1 內置對象

JavaScript 中還有一些對象子類型,一般被稱爲內置對象

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

在 JavaScript 中,它們實際上只是一些內置函數。這些內置函數能夠看成構造函數 (由 new 產生的函數調用)來使用,從而能夠構造一個對應子類型的新對象

var strPrimitive = "I am a string";
typeof strPrimitive; // "string"
strPrimitive instanceof String; // false

var strObject = new String("I am a string");
typeof strObject; // "object"
strObject instanceof String; // true

// 檢查 sub-type 對象
Object.prototype.toString.call(strObject); // [object String]
複製代碼

原始值 "I am a string" 並非一個對象,它只是一個字面量,而且是一個不可變的值。 若是要在這個字面量上執行一些操做,好比獲取長度、訪問其中某個字符等,那須要將其轉換爲 String 對象。在必要時語言會自動把字符串字面量轉換成一個 String 對象。

Object.prototype.toString…的用法有個小技巧:https://gist.github.com/Yunkou/67d5da9d05b922479d771d8bcde3308d 判斷 js 類型

核心的代碼:

Object.prototype.toString.call(obj).slice(8, -1);
複製代碼

基本類型值 "I am a string" 不是一個對象,它是一個不可變的基本字面值。爲了對它進行操做,好比檢查它的長度,訪問它的各個獨立字符內容等等,都須要一個 String 對象。

幸運的是,在必要的時候語言會自動地將 "string" 基本類型強制轉換爲 String 對象類型,這意味着你幾乎從不須要明確地建立對象。JS 社區的絕大部分人都 強烈推薦 儘量地使用字面形式的值,而非使用構造的對象形式。

考慮下面的代碼:

var strPrimitive = "I am a string";

console.log(strPrimitive.length); // 13

console.log(strPrimitive.charAt(3)); // "m"
複製代碼

在這兩個例子中,咱們在字符串的基本類型上調用屬性和方法,引擎會自動地將它強制轉換爲 String 對象,因此這些屬性/方法的訪問能夠工做。

nullundefined 沒有對象包裝的形式,僅有它們的基本類型值。相比之下,Date 的值 僅能夠 由它們的構造對象形式建立,由於它們沒有對應的字面形式。

3 內容

3.1 屬性訪問

咱們須要使用 . 或 [ ] 操做符。

兩種語法的主要區別在於,. 操做符後面須要一個 標識符(Identifier) 兼容的屬性名,而 [".."] 語法基本能夠接收任何兼容 UTF-8/unicode 的字符串做爲屬性名。舉個例子,爲了引用一個名爲「Super-Fun!」的屬性,你不得不使用 ["Super-Fun!"] 語法訪問,由於 Super-Fun! 不是一個合法的 Identifier 屬性名。 [".."] 語法能夠傳變量。

在對象中,屬性名 老是 字符串。若是你使用 string 之外的(基本)類型值,它會首先被轉換爲字符串。這甚至包括在數組中經常使用於索引的數字,因此要當心不要將對象和數組使用的數字搞混了。

var myObject = {};

myObject[true] = "foo";
myObject[3] = "bar";
myObject[myObject] = "baz";

myObject["true"]; // "foo"
myObject["3"]; // "bar"
myObject["[object Object]"]; // "baz"
複製代碼

若是你試圖在一個數組上添加屬性,可是屬性名 看起來 像一個數字,那麼最終它會成爲一個數字索引(也就是改變了數組的內容):

var myArray = ["foo", 42, "bar"];

myArray["3"] = "baz";

myArray.length; // 4

myArray[3]; // "baz"
複製代碼

3.2 屬性描述符(Property Descriptors)

在 ES5 以前,JavaScript 語言沒有給出直接的方法,讓你的代碼能夠考察或描述屬性性質間的區別,好比屬性是否爲只讀。

在 ES5 中,全部的屬性都用 屬性描述符(Property Descriptors) 來描述。

考慮這段代碼:

var myObject = {
  a: 2
};

Object.getOwnPropertyDescriptor(myObject, "a");
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
複製代碼

正如你所見,咱們普通的對象屬性 a 的屬性描述符(稱爲「數據描述符」,由於它僅持有一個數據值)的內容要比 value2多得多。它還包含另外三個性質:

  1. writable
  2. enumerable
  3. configurable

當咱們建立一個普通屬性時,能夠看到屬性描述符的各類性質的默認值,同時咱們能夠用 Object.defineProperty(..) 來添加新屬性,或使用指望的性質來修改既存的屬性(若是它是 configurable 的!)。

3.2.1 可寫性(Writable)

writable 控制着你改變屬性值的能力。

考慮這段代碼:

var myObject = {};

Object.defineProperty(myObject, "a", {
  value: 2,
  writable: false, // 不可寫!
  configurable: true,
  enumerable: true
});

myObject.a = 3;

myObject.a; // 2
複製代碼

如你所見,咱們對 value 的修改悄無聲息地失敗了。若是咱們在 strict mode 下進行嘗試,會獲得一個錯誤:

"use strict";

var myObject = {};

Object.defineProperty(myObject, "a", {
  value: 2,
  writable: false, // 不可寫!
  configurable: true,
  enumerable: true
});

myObject.a = 3; // TypeError
複製代碼

這個 TypeError 告訴咱們,咱們不能改變一個不可寫屬性。

注意: 咱們一下子就會討論 getters/setters,可是簡單地說,你能夠觀察到 writable:false 意味着值不可改變,和你定義一個空的 setter 是有些等價的。實際上,你的空 setter 在被調用時須要扔出一個 TypeError,來和 writable:false 保持一致。

3.2.2 可配置性(Configurable)

只要屬性當前是可配置的,咱們就可使用相同的 defineProperty(..) 工具,修改它的描述符定義。

var myObject = {
  a: 2
};

myObject.a = 3;
myObject.a; // 3

Object.defineProperty(myObject, "a", {
  value: 4,
  writable: true,
  configurable: false, // 不可配置!
  enumerable: true
});

myObject.a; // 4
myObject.a = 5;
myObject.a; // 5

Object.defineProperty(myObject, "a", {
  value: 6,
  writable: true,
  configurable: true,
  enumerable: true
}); // TypeError
複製代碼

最後的 defineProperty(..) 調用致使了一個 TypeError,這與 strict mode 無關,若是你試圖改變一個不可配置屬性的描述符定義,就會發生 TypeError。要當心:如你所看到的,將 configurable 設置爲 false一個單向操做,不可撤銷!

注意: 這裏有一個須要注意的微小例外:即使屬性已是 configurable:falsewritable 老是能夠沒有錯誤地從 true 改變爲 false,但若是已是 false 的話不能變回 true

configurable:false 阻止的另一個事情是使用 delete 操做符移除既存屬性的能力。

var myObject = {
  a: 2
};

myObject.a; // 2
delete myObject.a;
myObject.a; // undefined

Object.defineProperty(myObject, "a", {
  value: 2,
  writable: true,
  configurable: false,
  enumerable: true
});

myObject.a; // 2
delete myObject.a;
myObject.a; // 2
複製代碼

如你所見,最後的 delete 調用(無聲地)失敗了,由於咱們將 a 屬性設置成了不可配置。

delete 僅用於直接從目標對象移除該對象的(能夠被移除的)屬性。若是一個對象的屬性是某個其餘對象/函數的最後一個現存的引用,而你 delete 了它,那麼這就移除了這個引用,因而如今那個沒有被任何地方所引用的對象/函數就能夠被做爲垃圾回收。可是,將 delete 當作一個像其餘語言(如 C/C++)中那樣的釋放內存工具是 恰當的。delete 僅僅是一個對象屬性移除操做 —— 沒有更多別的含義。

3.2.3 可枚舉性(Enumerable)

咱們將要在這裏提到的最後一個描述符性質是 enumerable(還有另外兩個,咱們將在一下子討論 getter/setters 時談到)。

它的名稱可能已經使它的功能很明顯了,這個性質控制着一個屬性是否能在特定的對象-屬性枚舉操做中出現,好比 for..in循環。設置爲 false 將會阻止它出如今這樣的枚舉中,即便它依然徹底是能夠訪問的。設置爲 true 會使它出現。

全部普通的用戶定義屬性都默認是可 enumerable 的,正如你一般但願的那樣。但若是你有一個特殊的屬性,你想讓它對枚舉隱藏,就將它設置爲 enumerable:false

3.3 存在性(Existence)

咱們能夠查詢一個對象是否擁有特定的屬性,而 沒必要 取得那個屬性的值:

var myObject = {
  a: 2
};

"a" in myObject; // true
"b" in myObject; // false

myObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("b"); // false
複製代碼

in 操做符會檢查屬性是否存在於對象 ,或者是否存在於 [[Prototype]] 鏈對象遍歷的更高層中(詳見第五章)。相比之下,hasOwnProperty(..) 僅僅 檢查 myObject 是否擁有屬性,但 不會 查詢 [[Prototype]] 鏈。咱們會在第五章詳細講解 [[Prototype]] 時,回來討論這個兩個操做重要的不一樣。

經過委託到 Object.prototype,全部的普通對象均可以訪問 hasOwnProperty(..)(詳見第五章)。可是建立一個不連接到 Object.prototype 的對象也是可能的(經過 Object.create(null) —— 詳見第五章)。這種狀況下,像 myObject.hasOwnProperty(..) 這樣的方法調用將會失敗。

在這種場景下,一個進行這種檢查的更健壯的方式是 Object.prototype.hasOwnProperty.call(myObject,"a"),它借用基本的 hasOwnProperty(..) 方法並且使用 明確的 this 綁定(詳見第二章)來對咱們的 myObject 實施這個方法。

注意: in 操做符看起來像是要檢查一個值在容器中的存在性,可是它實際上檢查的是屬性名的存在性。在使用數組時注意這個區別十分重要,由於咱們會有很強的衝動來進行 4 in [2, 4, 6] 這樣的檢查,可是這老是不像咱們想象的那樣工做。

3.3.1 枚舉(Enumeration)

先前,在學習 enumerable 屬性描述符性質時,咱們簡單地解釋了"可枚舉性(enumerability)"的含義。如今,讓咱們來更加詳細地從新講解它。

var myObject = {};

Object.defineProperty(
  myObject,
  "a",
  // 使 `a` 可枚舉,如通常狀況
  { enumerable: true, value: 2 }
);

Object.defineProperty(
  myObject,
  "b",
  // 使 `b` 不可枚舉
  { enumerable: false, value: 3 }
);

myObject.b; // 3
"b" in myObject; // true
myObject.hasOwnProperty("b"); // true

// .......

for (var k in myObject) {
  console.log(k, myObject[k]);
}
// "a" 2
複製代碼

你會注意到,myObject.b 實際上 存在,並且擁有能夠訪問的值,可是它不出如今 for..in 循環中(然而使人詫異的是,它的 in 操做符的存在性檢查經過了)。這是由於 「enumerable」 基本上意味着「若是對象的屬性被迭代時會被包含在內」。

注意:for..in 循環實施在數組上可能會給出意外的結果,由於枚舉一個數組將不只包含全部的數字下標,還包含全部的可枚舉屬性。因此一個好主意是:將 for..in 循環 用於對象,而爲存儲在數組中的值使用傳統的 for 循環並用數字索引迭代。

另外一個能夠區分可枚舉和不可枚舉屬性的方法是:

var myObject = {};

Object.defineProperty(
  myObject,
  "a",
  // 使 `a` 可枚舉,如通常狀況
  { enumerable: true, value: 2 }
);

Object.defineProperty(
  myObject,
  "b",
  // 使 `b` 不可枚舉
  { enumerable: false, value: 3 }
);

myObject.propertyIsEnumerable("a"); // true
myObject.propertyIsEnumerable("b"); // false

Object.keys(myObject); // ["a"]
Object.getOwnPropertyNames(myObject); // ["a", "b"]
複製代碼

propertyIsEnumerable(..) 測試一個給定的屬性名是否直 接存 在於對象上,而且是 enumerable:true

Object.keys(..) 返回一個全部可枚舉屬性的數組,

Object.getOwnPropertyNames(..) 返回一個 全部 屬性的數組,不論能不能枚舉。

inhasOwnProperty(..) 區別於它們是否查詢 [[Prototype]] 鏈,

Object.keys(..)Object.getOwnPropertyNames(..) 考察直接給定的對象。

(當下)沒有與 in 操做符的查詢方式(在整個 [[Prototype]] 鏈上遍歷全部的屬性,如咱們在第五章解釋的)等價的、內建的方法能夠獲得一個 全部屬性 的列表。你能夠近似地模擬一個這樣的工具:遞歸地遍歷一個對象的 [[Prototype]] 鏈,在每一層都從 Object.keys(..) 中取得一個列表——僅包含可枚舉屬性。

3.4 迭代(Iteration)

for..in 循環迭代一個對象上(包括它的 [[Prototype]] 鏈)全部的可迭代屬性。但若是你想要迭代值呢?

在數字索引的數組中,典型的迭代全部的值的辦法是使用標準的 for 循環,好比:

var myArray = [1, 2, 3];

for (var i = 0; i < myArray.length; i++) {
  console.log(myArray[i]);
}
// 1 2 3
複製代碼

可是這並無迭代全部的值,而是迭代了全部的下標,而後由你使用索引來引用值,好比 myArray[i]

ES5 還爲數組加入了幾個迭代幫助方法,包括 forEach(..)every(..)、和 some(..)。這些幫助方法的每個都接收一個回調函數,這個函數將施用於數組中的每個元素,僅在如何響應回調的返回值上有所不一樣。

forEach(..) 將會迭代數組中全部的值,而且忽略回調的返回值。every(..) 會一直迭代到最後,或者 當回調返回一個 false(或「falsy」)值,而 some(..) 會一直迭代到最後,或者 當回調返回一個 true(或「truthy」)值。

這些在 every(..)some(..) 內部的特殊返回值有些像普通 for 循環中的 break 語句,它們能夠在迭代執行到末尾以前將它結束掉。

若是你使用 for..in 循環在一個對象上進行迭代,你也只能間接地獲得值,由於它實際上僅僅迭代對象的全部可枚舉屬性,讓你本身手動地去訪問屬性來獲得值。

注意: 與以有序數字的方式(for 循環或其餘迭代器)迭代數組的下標比較起來,迭代對象屬性的順序是 不肯定 的,並且可能會因 JS 引擎的不一樣而不一樣。對於須要跨平臺環境保持一致的問題,不要依賴 觀察到的順序,由於這個順序是不可靠的。

可是若是你想直接迭代值,而不是數組下標(或對象屬性)呢?ES6 加入了一個有用的 for..of 循環語法,用來迭代數組(和對象,若是這個對象有定義的迭代器):

var myArray = [1, 2, 3];

for (var v of myArray) {
  console.log(v);
}
// 1
// 2
// 3
複製代碼

for..of 循環要求被迭代的 東西 提供一個迭代器對象(從一個在語言規範中叫作 @@iterator 的默認內部函數那裏獲得),每次循環都調用一次這個迭代器對象的 next() 方法,循環迭代的內容就是這些連續的返回值。

數組擁有內建的 @@iterator,因此正如展現的那樣,for..of 對於它們很容易使用。可是讓咱們使用內建的 @@iterator 來手動迭代一個數組,來看看它是怎麼工做的:

var myArray = [1, 2, 3];
var it = myArray[Symbol.iterator]();

it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // { done:true }
複製代碼

注意: 咱們使用一個 ES6 的 SymbolSymbol.iterator 來取得一個對象的 @@iterator 內部屬性。咱們在本章中簡單地提到過 Symbol 的語義(見「計算型屬性名」),一樣的原理也適用於這裏。你老是但願經過 Symbol 名稱,而不是它可能持有的特殊的值,來引用這樣特殊的屬性。另外,儘管這個名稱有這樣的暗示,但 @@iterator 自己 不是迭代器對象, 而是一個返回迭代器對象的 方法 —— 一個重要的細節!

正如上面的代碼段揭示的,迭代器的 next() 調用的返回值是一個 { value: .. , done: .. } 形式的對象,其中 value 是當前迭代的值,而 done 是一個 boolean,表示是否還有更多內容能夠迭代。

注意值 3done:false 一塊兒返回,猛地一看會有些奇怪。你不得不第四次調用 next()(在前一個代碼段的 for..of 循環會自動這樣作)來獲得 done:true,以使本身知道迭代已經完成。這個怪異之處的緣由超出了咱們要在這裏討論的範圍,可是它源自於 ES6 生成器(generator)函數的語義。

雖然數組能夠在 for..of 循環中自動迭代,但普通的對象 沒有內建的 @@iterator。這種故意省略的緣由要比咱們將在這裏解釋的更復雜,但通常來講,爲了將來的對象類型,最好不要加入那些可能最終被證實是麻煩的實現。

複習

JS 中的對象擁有字面形式(好比 var a = { .. })和構造形式(好比 var a = new Array(..))。字面形式幾乎老是首選,但在某些狀況下,構造形式提供更多的構建選項。

許多人聲稱「Javascript 中的一切都是對象」,這是不對的。對象是六種(或七中,看你從哪一個方面說)基本類型之一。對象有子類型,包括 function,還能夠被行爲特化,好比 [object Array] 做爲內部的標籤表示子類型數組。

對象是鍵/值對的集合。經過 .propName["propName"] 語法,值能夠做爲屬性訪問。無論屬性何時被訪問,引擎實際上會調用內部默認的 [[Get]] 操做(在設置值時調用 [[Put]] 操做),它不只直接在對象上查找屬性,在沒有找到時還會遍歷 [[Prototype]] 鏈(見第五章)。

屬性有一些能夠經過屬性描述符控制的特定性質,好比 writableconfigurable。另外,對象擁有它的不可變性(它們的屬性也有),能夠經過使用 Object.preventExtensions(..)Object.seal(..)、和 Object.freeze(..) 來控制幾種不一樣等級的不可變性。

屬性沒必要非要包含值 —— 它們也能夠是帶有 getter/setter 的「訪問器屬性」。它們也能夠是可枚舉或不可枚舉的,這控制它們是否會在 for..in 這樣的循環迭代中出現。

你也可使用 ES6 的 for..of 語法,在數據結構(數組,對象等)中迭代 ,它尋找一個內建或自定義的 @@iterator 對象,這個對象由一個 next() 方法組成,經過這個 next() 方法每次迭代一個數據。

相關文章
相關標籤/搜索