你應該知道的JS —— 物件

SGAMER-rain發表於2018-03-29

物件的隱式轉換

當物件之間相加obj1 + obj12或相減obj1 - obj2或者alert(obj)會發生什麼?。 當某個物件出現在了需要原始型別才能進行操作的上下文時,JavaScript 會將物件轉換成原始型別。在進行轉換中的物件有有特殊的方法

  • 對於物件而言,不存在布林轉換,因為所有物件在上下文中布林值都為ture,所以只有數字和字串轉換
  • 數字轉換髮生在減去物件或者應用數學函式時,例如,Date物件可以被減去,結果是date1 - data2兩個日期之間的時間差
  • 字串轉換,當我們alert(obj)在類似的上下文中輸出物件時,通常會發生這種情況
ToPrimitive

當我們在需要原始型別的上下文中使用物件時,例如在alert或者數學運算中,使用ToPrimitive演算法將其轉換為原始型別值。該演算法允許我們使用特殊的物件方法自定義轉換 根據上下文,轉換具有所謂的提示

  • string 當一個操作期望一個字串時,對於物件到字串的轉換,如alert
alert(obj);

// 或者使用物件來作為屬性
anotherObj[obj] = 123;
複製程式碼
  • number 當一個操作需要數字時,用於物件到數學的轉換。例如
let num = Number(obj);

let n = +obj
let delta = date1 - date2;

let greater = user1 > user2;
複製程式碼
  • default 在少數情況下發生,當操作不確定期望的型別時。+這種運算子即可以進行字串拼接也可以進行數學運算,所以字串和數字都可以。或者當一個物件與字串,數字或符號來判斷是否相等時
let total = car1 + car2;

if(user == 1) { ... };
複製程式碼

大於小於運算子<>可以同時處理字串和數字。不過,他使用number提示,而不是default提示,這是歷史原因 在JavaScript中,除了一個特例(Date物件),其他的內建物件都按照與default相同的方式實現轉換number

為了進行轉換, JavaScript會嘗試查詢並呼叫三個物件方法

  1. 呼叫obj[Symbol.toPrimitive](init) 如果方法存在
  2. 否則,如果提示是string
    • 嘗試obj.toString()obj.valueOf()
  3. 否則, 如果提示是numberdefault
    • 嘗試obj.valueOf()obj.toString()
Symbol.toPrimitive

例子如下:

let user = {
  name: 'john',
  money: 1000,
  [Symbol.toPrimitive](hint){
    console.log(hint);
    return hint === 'string' ? this.name : this.money
  }
}

console.log(`${user}`);
// string
// join
console.log(+user);
// Number
// 1000
console.log(user === 1000);
// default
// true
複製程式碼
toString()和valueOf()

toString()以及valueOf從遠古時代到來,他們不是符號,而是常規字串命名的方法。他們提供了一種替代老式的方式來實現轉換。 如果沒有Symbol.toPrimitive那麼JavaScript會嘗試查詢他們並按順序嘗試:

  • toString -> valueOf 為字串提示
  • valueOf -> toString除此以外
let user = {
  name: 'john',
  money: 1000,
  toString(){
  ``return this.name;
  },
  valueOf(){
    return this.money;
  }
}
console.log(`${user}` + 1); // john1
console.log(+user); // 1000
console.log(user == 1000); // true
複製程式碼

最後附上一張JavaScript原始型別轉換表

你應該知道的JS —— 物件

物件的遍歷

for...in迴圈

為了遍歷物件的所有鍵,存在一個特殊的迴圈形式: for...in

let user = {
  name: 'john',
  age: 30,
  isAdmin: true
}

for(let key in user){
  // keys
  alert(key); // name, age, isAdmin
  alert(user[key]); // john, 20, true
}
複製程式碼

物件遍歷的順序並不是按照新增順序建立的,而是具有一定規則的, 先是整數屬性排序,其他的則以建立順序出現

let codes = {
  "49": "Germany",
  "41": "Switzerland",
  "44": "Great Britain",
  // ...
  "1": "USA"
}
for(let code in codes){
  console.log(code); // 1 , 41, 44, 49;
}
複製程式碼

使用for...in遍歷物件是無法直接獲取屬性值,因為他實際上遍歷的是物件中所有可列舉屬性,你需要手動獲取屬性值。而在ES6中我們可以藉助for...ofIterator來直接獲取屬性值。 簡單介紹下Iterator,在ES6中新添了MapSet,加上原有的資料結構, 使用者還可以組合使用他們,因此需要統一的介面機制,來處理不同的資料結構。遍歷器(Iterator)就是這樣一種結構,只要在資料結構中部署它,就可以完成遍歷操作 Iteerator的遍歷過程是這樣的

  1. 建立一個指標物件,指向當前資料結構的起始位置
  2. 第一次呼叫指標物件的next方法,可以將指標指向資料結構的第一個成員
  3. 第二次呼叫指標物件的next方法,指標就指向資料結構第二個成員
  4. 不斷呼叫指標物件的next方法,直到他指向資料結構的結束位置 每一次呼叫next方法就會泛函一個包含valuedone兩個屬性的物件。其中,value屬性是當前成員的值,done屬性是一個布林值,表示遍歷是否結束。 當我們使用for...of迴圈遍歷某種資料結構時,該迴圈會自動去尋找Iterator介面。 結合這兩點我們可以建立物件的遍歷器介面
let obj = {
      a: 1,
      b: 2
    }
Object.defineProperty(obj, Symbol.iterator, {
  value(){
    var o = this;
    var idx = 0;
    var k = Object.keys(o);
    return {
      next(){
        return {
          value: o[k[idx++]],
          done: (idx > k.length)
        }
      }
    }
  }
})
複製程式碼

物件的克隆

物件與原始型別之間的根本區別之一是他們他們通過引用儲存和複製 原始型別值: string, number, boolean被分配/複製為整體值 例如:

let message = "hello";
let pharse = message;
複製程式碼

因此我們有兩個獨立的變數,每個變數都儲存字串hello

你應該知道的JS —— 物件

物件不是這樣的 物件儲存的是其值的記憶體地址,而非值本身

let user = {
  name: 'john'
}
複製程式碼

你應該知道的JS —— 物件

當一個變數被賦值為物件時,賦值的是其值的記憶體地址(引用),而不是物件本身 我們可以將一個物件想象成一個櫥櫃,那麼變數就是櫥櫃的鑰匙,賦值變數就會賦值鑰匙,但不是櫥櫃本身

let user = { name: "John" };
let admin = user;
複製程式碼

現在我們有兩個變數,每個變數都引用同一個物件

你應該知道的JS —— 物件

我們可以使用任何變數來訪問控制櫥櫃並修改其內容

let user = { name: 'john' };
let admin = user;

admin.name = 'pete';

alert(user.name); // 'pete'
複製程式碼

只有兩個物件是同一個物件時,他們才是相等的

例如兩個變數引用同一個物件,他們是相等的

let a = {};
let b = a;

console.log( a == b) // true
console.log( a === b) // true
複製程式碼

這裡兩個獨立的物件是不相等的,儘管他們都是空的

let a = {};
let b = {};

console.log( a == b); // false
複製程式碼

因此複製一個物件變數會建立對同一個物件的引用。 但是如果我們需要複製一個物件呢?我們需要建立一個新物件並通過遍歷他的屬性來複制現有物件的結構 例如:

let user = {
  name: 'john',
  age: 30
}

let clone = {}; 

for(let key in user){
  clone[key] = user[key];
}

clone.name = 'pete';

console.log(user.name);
複製程式碼

我們也可以使用Object.assgin方法,

let user = {
  name: 'john',
  age: 30
}

let clone = Object.assign({}, user);
複製程式碼

到現在為止,我們認為所有的屬性user都是原始型別的,但屬性可以是對其他物件的引用,如何處理他們 例如這個

let user = {
  name: 'john',
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);
console.log( user.sizes === clone.sizes );

user.sizes.width++;
console.log(clone.sizes.width) 51
複製程式碼

為了解決這個問題,我們應該遞迴檢查每個值的型別,如果他是一個物件,就複製他的結構,這就是所謂的深度克隆 簡單的例子:

let user = {
  name: 'john',
  size: {
    height: 182,
    width: 50
  }
}
let cloneobj = {};

function clone(source, target){
  let keys = Object.keys(source);
  for(let key of keys){
    if(typeof source[key] == 'object'){
      target[key] = clone(source[key], {});
    }else{
      target[key] = source[key];
    }
  }
  return target;
}
    
clone(user, cloneobj);
user.size.width++;
console.log(cloneobj);

複製程式碼

而在HTML5規範中提出了一種用於深層克隆的標準演算法,用於處理上述情況和更復雜的情況,稱為結構化克隆演算法

關於結構化克隆的好處是在於他處理迴圈物件並支援大量內建型別,問題在於演算法並不對使用者直接暴露,只能作為API的一部分

MessageChannel

只要你呼叫postMessage結構化克隆演算法就可以使用,我們可以建立一個MessageChannel併傳送訊息。在接收端,訊息包含我們原始資料物件的結構化克隆

function strucuralClone(obj){
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  })
}
const user = {
  a: 1,
  b: {
    c: 2,
  }
}
user.c = user;
const clone = strucuralClone(user);
clone.then( (result) => console.log(result))
複製程式碼
History API

如果你曾經使用history.pushState(),那麼您可以提供一個狀態物件來儲存URL。事實證明,這個狀態物件在結構上被同步克隆。同時我們必須小心,不要混淆可能使用狀態物件的任何程式邏輯,所以我們需要在完成克隆後恢復原始狀態。為了防止發生任何事件,請使用history.replaceState()而不是history.pushState();

function strucuralClone(obj){
  const oldState = history.state;
  history.replaceState(obj, document.title);
  const copy = history.state;
  history.replaceState(oldState, document.title);
  return copy;
}
const user = {
  a: 1,
  b: {
    c: 2,
  }
}
user.c = user;
const clone = strucuralClone(user);
console.log(clone)
複製程式碼
Notification API
function strucuralClone(obj){
  return new Notification('', {data: obj, silent: true});
}
const user = {
  a: 1,
  b: {
    c: 2,
  }
}
user.c = user;
user.a = 2;
const clone = strucuralClone(user);
user.a = 3;
console.log(clone)
複製程式碼

結構化克隆優於JSON的地方

  • 結構化克隆可以複製RegExp物件
  • 結構化克隆可以複製Blob,File以及FileList物件
  • 結構化克隆可以複製ImageData物件。
  • 結構化克隆可以正確地複製有迴圈引用的物件

結構化克隆所不能做到的

  • Error以及Function物件是不能被結構化克隆演算法複製的
  • 企圖克隆DOM節點同樣會丟擲錯誤
  • 物件的某些特定引數也不會被保留
  • 原型鏈上的屬性也不會被追蹤以及複製

效能比較

你應該知道的JS —— 物件

這些克隆方式只是黑科技,在專案中還是乖乖用lodash提供的clone方法把。

物件的不可變性

有時候你會希望屬性或者物件是不可改變的,在ES5中可以通過多種方法來實現

物件常量

結合writeable: falseconfigurable:false就可以建立一個真正的常量屬性(不可修改, 重定義, 或者刪除);

var myObject = {};

Object.defineProperty(myObject, "freez_number", {
  value: 42,
  writable: false,
  configurable: false
})

複製程式碼
禁止擴充套件

如果你想禁止一個物件新增新屬性並且保留已有屬性,可以使用Object.preventExtensions(..)

var myObject = {
  a: 2
}

Object.preventExtensions(myObject);

myObject.b = 3;
myObject.b // undefiend

複製程式碼
密封

Object.seal(..)會建立一個密封物件,這個方法實際上會在一個現有物件上呼叫Object.preventExtensions(..)並把所有屬性標記為configurable: false 所以密封后不僅不能新增新屬性也不能重新配置或者刪除任何現有屬性(雖然可以修改屬性的值)

凍結

Object.freeze()會建立一個凍結物件,這個方法實際上會在一個現有物件上呼叫Object.seal() 並把所有資料訪問屬性標記為writeable: false這樣就無法修改他們的值 這個方法是你可以應用在物件上的級別最高的不可變性,他會禁止對於對本身及其任意直接屬性的修改。 重要的一點: 所有的方法建立的都是淺不可變性,也就是說,他們只會影響目標物件和他的直接屬性。如果目標物件引用了其他物件(陣列, 物件. 函式, 等) 其他物件的內容不受影響, 仍然是可變的

不過我們可以深度凍結一個物件,具體方法為,首先在這個物件上呼叫Object.freeze(..)然後遍歷他引用的所有物件並在這些物件上呼叫Object.freeze(...),但是一定要小心,因為這樣做有可能會在無意中凍結其他物件(共享物件)

為什麼需要不可變性

下面的程式碼能夠體現不可變性的重要性

var arr = [1, 2, 3];

foo(arr);

console.log(arr[0]);
複製程式碼

從表面上講,你可能認為arr[0]的值仍然為1但事實是否如此不得而知,因為foo(...)可能會改變那你傳入其中的arr所引用的陣列,所以我麼需要上面的方法來讓物件不可變

var arr = Object.freeze([1, 2, 3]);

foo(arr)

console.log(arr[0]);
複製程式碼

可以非常確定arr[0]就是1 這是非常重要的,因為這可以使我們更容易的理解程式碼當我們將物件傳遞到我們看不到或者不能控制的地方,我們依然能夠相信這個值不會改變

不可變性帶來的效能問題

每當我們開始建立一個新值(陣列,物件)取代修改已經存在的值時,很明顯的問題是,效能上會有問題。 如果在你的程式中,只會發生一次或幾次單一的狀態變化,那麼扔掉一箇舊物件或舊陣列完全沒必要擔心,效能損失會非常非常小————頂多幾微妙。但是如果頻繁的進行這樣的操作,那麼效能問題就需要考慮了。像陣列這樣的資料結構,我們期望除了能夠儲存其原始的資料,然後能追蹤其每次改變並根據之前的版本建立一個分支 在內部,他可能就像一個物件引用的連結串列樹,樹中的每個節點都表示原始值的改變。

你應該知道的JS —— 物件

如果是開發的話,我們也可以使用Immutable.js這種成熟的庫來進行開發

Getter 和 Setter

在ES5中可以使用gettersetter部分改寫預設操作, 但是隻能應用在單個屬性上,無法應用在整個物件上(ES6中proxy的出現可以改寫整個物件),getter是一個隱藏函式,會在獲取屬性值時呼叫,setter也是一個隱藏的函式,會在設定屬性值時呼叫。 當你給一個屬性定義getter, setter或者兩者都有時,這個屬性會被定義為訪問描述符。對於訪問描述符來說,javascript會忽略他們的valuewriteable特性,取而代之的是關心setget(還有configurable和enumerable)特性

let myObject = {
  get a(){
    return this._a;
  }
  set a(val){
    this._a = val * 2;
  }
}

myObject.a = 2;
myObject.a; //4
複製程式碼

存在性

看下面程式碼

var myObject = {
  a: undefiend
}

myObject.a // undefiend;
myObject.b // undefiend
複製程式碼

這時我們可以看出,如myObject.a的屬性訪問返回值可能是undefiend,但是這個值有可能是屬性中儲存的undefiend,也可能是因為屬性不存在所以返回undefined,那麼怎麼區別這兩種物件呢

var myObject = {
  a: 2
}

('a' in myObject);  // true
('b' in myObject);  // false

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

in操作符會檢查屬性是否在物件及其[[property]]原型鏈中,相比之下,hasOwnProperty(...)只會檢查屬性是否在myObject物件中,不會檢查[[prototype]]鏈。

參考資料:

相關文章