JavaScript之Object拆解


轉載煩請註明原文連接:
https://github.com/Xing-Chuan/blog/blob/master/JavaScript/JavaScript%E4%B9%8BObject%E6%8B%86%E8%A7%A3.md
---javascript

最近把研究 Object 的體會總結了一下, 有 Object 相關聯的屬性、方法和 ES6 後新 Api .java

屬性類型

JavaScript 中有兩種數據類型: 數據屬性和訪問器屬性git

數據屬性

數據屬性有如下幾個描述行爲的屬性:es6

  • Configurable 描述這個屬性是否可被 delete 刪除, 默認爲 true
  • Enumerable 描述這個屬性是否可被枚舉, 默認爲 true
  • writable 描述這個屬性是否可被修改, 默認爲 true
  • value 描述這個屬性的值

若是想要修改這些系統默認屬性, 能夠經過 ES5 的方法 Object.defineProperty(obj, property, option).github

注意:chrome

  • 能夠屢次調用 Object.defineProperty() 修改屬性, 但將 Configurable 設置爲 false 是不可逆的.
  • 調用 Object.defineProperty() 修改屬性方法時, 若是不指定這些描述屬性, 則默認值都會設置爲 false.

訪問器屬性

訪問器有如下幾個屬性:api

  • Configurable 描述這個屬性是否可被 delete 刪除, 默認爲 true
  • Enumerable 描述這個屬性是否可被枚舉, 默認爲 true
  • get 在讀取屬性時調用的函數, 默認爲 undefined
  • set 在設置屬性時調用的函數, 默認爲 undefined
let book = { _year: 2004, edition: 1 };

/*
 * 第一個參數: 對象
 * 第二個參數: 對象的屬性
 * 第三個參數: 須要設置的描述屬性
 */

Object.defineProperty(book, 'year', {
  get: function() { return this._year; },
  set: function(newValue) {
    if (newValue > 2004) {
      this._year = newValue;
      this.edition += newValue - 2004;
    }
  }
});

book.year = 2009;
console.log(book.edition); // 6

注意:數組

  • get 和 set 必須同時設置, 不然會出現不能讀或不能寫的狀況

若是要一次修改多個參數的描述屬性, 可使用 Object.defineProperties()瀏覽器

let book = {};

Object.defineProperties(book, {
  _year: {
    value: 2004
  },
  edition: {
    value: 1
  }
});

構造函數

構造函數也是普通的函數, 若是沒有經過 new 操做符生成新的實例對象, 那構造函數就是一個普通的函數.
爲了區分構造函數於普通函數, 咱們一般用大駝峯命名法給構造函數命名.
String、Array 等幾乎全部對象都是 Object 的實例, Object 就是一個構造函數.安全

new Array() instanceof Object // true
new String() instanceof Object // true
new Function() instanceof Object // true
function Info() {
  this.name = 'XingChuan';
  this.age = '10';
}

let info1 = new Info();
let info2 = new Info();
console.log(info1.name); // XingChuan
console.log(info1.age); // 10
console.log(info2.name); // XingChuan
console.log(info2.age); // 10

原型對象

既然談到構造函數就要談到原型了, 每一個函數都有一個原型(prototype)屬性, 原型存在的意義就是, 原型上的屬性和方法由全部構造函數的實例對象所共享, 若是修改了原型對象上的方法, 那全部實例都會實時更改.

咱們平時使用 Array 和 String 的方法, 都是在其原型對象上, 全部咱們建立的全部數組和字符串均可以享有這些方法.

image.png

原型鏈

在 chrome, firefox, safari 中, 有一個瀏覽器私有屬性 __proto__ 指向構造函數的 prototype, __proto__ 並非官方屬性, 爲了兼容性考慮, 開發中最好不要使用 __proto__.

下面咱們來具象化一下原型鏈的構成:

函數的原型鏈

構造函數也是普通的函數, 只是咱們拿來生成實例, 因此纔有這個稱謂, 而全部的函數都是 Function 的實例.

function info() {

}
// 原型鏈
info.__proto__ => Function.prototype => Object.prototype

image.png

全部的函數都是 Function 的實例, 而 Function 是 Object 的實例, 因此有了這條原型鏈.

內置對象的原型鏈

頗有意思的是, Object、Array、String、Function 都是函數, 因此他們都是 Function 的實例, Function 比較特殊, 它也是自身的實例.

Math 是個例外, 它並非一個函數, 而是一個對象.

console.log(Array instanceof Function) // true
console.log(String instanceof Function) // true
console.log(Object instanceof Function) // true
console.log(Function instanceof Function) // true
console.log(Math instanceof Function) // false
console.log(Math.__proto__ === Object.prototype); // true
// 原型鏈
Array.__proto__ => Function.prototype => Object.protytype

實例對象的原型鏈

function Info() {
}
Info.prototype.name = 'Xingchuan';
Info.prototype.age = 10;
Info.prototype.showName = function() {
  console.log(this.name);
};
let info1 = new Info();
info1.showName() // XingChuan
let info2 = new Info();
info2.name = 'test';
info2.showName() // test
// 原型鏈
info1.__proto__ => Info.prototype => Object.prototype

實例對象的 __proto__ 會指向構造函數的 prototype, 全部的原型對象都是 Object.prototype 的實例, 因此構成了這一條原型鏈.

注意:

  • 原型鏈的訪問是有就近原則的, 若是像上面實例中已經有 name 屬性, 則不會繼續訪問 prototype 上的 name 屬性, 若是實例中沒有這個屬性, 則會按照原型鏈一直找下去, 若是沒有就是 undefined.

元素也是 Object 的實例

console.dir(document.getElementsByTagName('span')[0]);
// span元素 => HTMLSpanElement => HTMLElement => Element => Node => EventTarget => Object.prototype
console.dir(document.getElementsByTagName('span')[0] instanceof Object);
// true

元素的原型鏈很長, 不過能夠看到元素也是 Object 的實例.

小結

  • 原型鏈有就近原則, 若是在實例上訪問到某屬性, 就不會繼續原型鏈尋找該屬性了
  • 原型是能夠從新指定的
  • prototype 的 constructor 指向 構造函數
  • 實例的 __proto__ 指向構造函數的 prototype
  • 全部函數的 __proto__ 指向 Function.prototype
  • 函數的 prototype 都是基於 Object.prototype
  • Function 也是本身的實例
  • 一切起源於 Object.prototype
  • 每一個原型都有 constructor 屬性, 指向原型所屬的函數, 用字面量對象改成原型的引用, 會丟失 constructor 這個屬性

來張示意圖結尾( 侵刪 ):

image.png

this 的指向

一書中對 this 指向的問題有比較詳細的描寫, 推薦看看.

在討論 this 指向的問題以前, 先要明確一下, 在嚴格模式下, 未指定環境對象而調用函數,則 this 值不會轉型爲 window.
除非明確把函數添加到某個對象或者調用 apply()或 call(),不然 this 值將是 undefined.

  • 普通函數中的 this
let x = 1;
function show() {
  console.log(this.x);
}
show(); // 1

普通函數中的 this 指向 window

  • 構造函數中的 this
function Info(){
  this.x = 1;
}
let info1 = new Info();
console.log(info1.x); // 1

構造函數中的 this 指向 new 出來的實例對象, new 這個操做符會改變 this 的指向.

  • 對象方法中的 this
let x = 2;
let obj = {
  x: 1,
  y: function() {
    console.log(this.x);
  }
};
obj.y(); // 1

對象方法中的 this 指向調用它的對象.
  • call、apply、bind(ES5) 中的 this

call、apply、bind(ES5) 的做用就是改變 this 的指向, this 會指向第一個參數.

若是 call、apply、bind(ES5) 調用方法時沒有傳參, 默認 this 指向 window.

bind 只會改變 this 的指向, 並不會執行方法, call 和 apply 則會改變指向時也執行方法.

  • 事件函數中的 this

事件函數中的 this 指向綁定事件的元素.

  • 箭頭函數中的 this

箭頭函數是 ES6 中新增的方法, 不一樣於其餘狀況中的 this 在調用時才決定指向, 箭頭函數 this 指向定義時的外圍, 在不確認指向的狀況下, 請慎用.

ECMAScript 6 入門對箭頭函數的使用限制作了說明:
(1)函數體內的this對象,就是定義時所在的對象,而不是使用時所在的對象。
(2)不能夠看成構造函數,也就是說,不可使用new命令,不然會拋出一個錯誤。
(3)不可使用arguments對象,該對象在函數體內不存在。若是要用,能夠用Rest參數代替。
(4)不可使用yield命令,所以箭頭函數不能用做Generator函數。

數據類型判斷

在開發中, 咱們能夠用 typeof 來判斷類型, 但有很大的侷限性.

typeof 1
// "number"
typeof '1'
// "string"
typeof true
// "boolean"
typeof undefined
// "undefined"
typeof null
// "object"
typeof (function(){})
// "function"
typeof []
// "object"
typeof {}
// "object"

typeof 只對一些簡單數據類型有效, 爲了能夠判斷各類內置對象, 咱們須要採起一些 手段 , 使用 Object 原型上的 toString 方法.

Object.prototype.toString.call(1);
// "[object Number]"
Object.prototype.toString.call('1');
// "[object String]"
Object.prototype.toString.call(true);
// "[object Boolean]"
Object.prototype.toString.call(function() {});
// "[object Function]"
Object.prototype.toString.call(null);
// "[object Null]"
Object.prototype.toString.call(undefined);
// "[object Undefined]"
Object.prototype.toString.call(new Date());
// "[object Date]"
Object.prototype.toString.call(Math);
// "[object Math]"
Object.prototype.toString.call([]);
// "[object Array]"
Object.prototype.toString.call({});
// "[object Object]"

能夠所有搞定了.

ES6 以後的新特性

對象屬性的簡潔寫法

簡潔的寫法能夠減小代碼量也能夠更加優雅, 但代碼是給計算機看的, 同時也是給人看的, 容易發生歧義的地方必定要注意.

  • 屬性的簡寫

若是屬性名與屬性值相同, 能夠忽略不寫.
屬性值是字符串時不可簡寫.

// old
let name = 'XingChuan';
let obj = {
  name: name
};
// new
let name = 'XingChuan';
let obj = {
  name
};
// error
let name = 'XingChuan';
let obj = {
  name:'name'
};
  • 函數的簡寫
// old
let obj = {
  show: function() {
    console.log('show');
  }
};
// new
let obj = {
  show() {
    console.log('show');
  }
};

字面量形式定義對象

ES5只支持這種字面量定義:

let obj = {
  name: 'XingChuan',
  age: 10
};

ES6支持這種寫法:

let obj ={
  [name]: 'XingChuan',
  ['a' + 'ge']: 10
};
// 做爲屬性名的表達式會自動 toString() , 應避免使用對象做爲表達式, 由於 String({}) === '[object Object]'

注意:

  • 屬性名錶達式與簡潔表示法不能同時使用, 會報錯
  • 屬性名錶達式爲對象時屬性名會重複的問題

Object.is('','') 同值相等API

Object.is() 基本等同於 === , 除卻兩點:

+0 === -0 // true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

Object.assign(target, source1, source2) 複製可枚舉屬性

let target = {
  info: {
    name: 'name1'
  }
};
let source1 ={
  info: {
    name: 'name2',
    age: 30
  }
};
let source2 ={
  info: {
    name: 'name3'
  }
};

Object.assign(target, source1, source2);
// { info: {name: 'name3'}}
//
// Object.assign 是淺拷貝, 若是屬性的值是對象, 就會添加新的引用, 而不是在原有地址上添加屬性.
// target會自動轉換爲對象, 因此不能爲 null 或 undefined , 會報錯
// source爲 null 或 undefined 時, 由於沒法轉換爲對象, 會跳過, 但不會報錯
// 若 source 爲字符串, 會以數組的形式複製到 target 中
//

屬性的可枚舉性

每一個對象屬性都有一個描述對象 Description , 能夠控制是否可被枚舉, 數據屬性的其中之一.

let obj = {
  name: 'XingChuan'
};
Object.getOwnPropertyDescriptor(obj,'name');

// {
//   configurable: true,
//   enumerable: true, // 若是可枚舉爲 false , 某些操做會忽略掉這個屬性
//   value: "XingChuan",
//   writable: true
// }

ES5 中有 3 個屬性會忽略 enumerablefalse 的屬性:

  • for...in 循環
  • Object.keys()
  • JSON.stringify()

ES6 新增的 Object.assign() 也會忽略描述中不可枚舉的屬性.

數組中的 length 屬性不會被 for...in 獲取就是由於不可枚舉的描述.

Object.getOwnPropertyDescriptor([1,2,3],'length');

// {
//   configurable: false,
//   enumerable: false,
//   value: 3,
//   writable: true
// }

另外, ES6 中也規定了 Class 原型上的方法是不可枚舉的.

屬性的遍歷

  1. for...in

for...in 循環遍歷對象自身的和繼承的可枚舉屬性(不含 Symbol 屬性)

  1. Object.keys(obj)

Object.keys返回一個數組,包括對象自身的(不含繼承的)全部可枚舉屬性(不含 Symbol 屬性)

  1. Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames返回一個數組,包含對象自身的全部屬性(不含 Symbol 屬性,可是包括不可枚舉屬性)

  1. Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols返回一個數組,包含對象自身的全部 Symbol 屬性

  1. Reflect.ownKeys(obj)

Reflect.ownKeys返回一個數組,包含對象自身的全部屬性,無論屬性名是 Symbol 或字符串,也不論是否可枚舉

以上 5 種方法在遍歷順序上, 遵循如下 3 條規則:

  • 首先遍歷全部屬性名爲數值的屬性,按照數字排序
  • 其次遍歷全部屬性名爲字符串的屬性,按照生成時間排序
  • 最後遍歷全部屬性名爲 Symbol 值的屬性,按照生成時間排序

__proto__, Object.getPrototypeOf(), Object.setPrototypeOf()

__proto__

__proto__ 指向當前實例的原型對象, 其沒有被 ES6 列爲正式 API, 但由於被瀏覽器廠商普遍使用, 被收入附錄.

某些瀏覽器廠商一樣指向原型對象, 多是另外一種命名方式, 因此爲了兼容性考慮, 最好不要經過它去操做原型.

Object.setPrototypeOf()

Object.setPrototypeOf() 是 ES6 設置原型對象的方法

let obj = {
  x: 10
};
let option = {
  x: 20,
  y: 30,
  z: 40
};
Object.setPrototypeOf(obj, option);

obj.x // 10
obj.y // 30
obj.z // 40

// 因原型鏈訪問順序的優先級, obj.x 爲 10 而不是 20, 如 obj 不存在 x 的屬性, obj.x 就會爲 20.

Object.getPrototypeOf()

Object.getPrototypeOf(obj) 是 ES6 返回原型對象的方法

Object.keys(), Object.values(), Object.entries()

Object.keys() 是 ES5 中遍歷屬性的方法, ES6 新增了 Object.values(), Object.entries().

  • Object.values

返回對象自身的(不包含繼承的), 可枚舉的鍵值

  • Object.entries()

返回對象自身的(不包含繼承的), 可枚舉的鍵值對數組

對象的拓展運算符

ES8 中將數組的拓展運算符引入到了對象中.

解構

let {a, b, ...x} = {a: 1, b:2, c: 3, d: 4};
console.log(x);
// {c:3,d:4}

注意:

  1. 拓展運算符會複製對象全部未讀取的鍵值對, 因此右側必須是對象或可轉換爲對象, 不能爲 null 或 undefined
  2. 拓展運算符必須置爲最後一個位置, 不然會報錯
  3. 拓展運算符不會複製原型上的方法

克隆對象

let x = {name: 'XingChuan', age: 88};
let cloneObj = { ...x };

合併對象

let x = {name: 'XingChuan', age: 88};
let y = {job: 'developer'};
let cloneObj = { ...x, ...y };

拓展運算符表達式

let obj = { ...{x > 1 ? {a: 1} : {} } };

擴展運算符的參數對象之中,若是有取值函數get,這個函數是會執行的.

let runtimeError = {
  ...a,
  ...{
    get x() {
      throws new Error('thrown now');
    }
  }
};

Object.getOwnPropertyDescriptors()

ES5 中 Object.getOwnPropertyDescriptor(obj, property) 能夠獲取對象屬性的描述對象.

ES8 中 新增了 Object.getOwnPropertyDescriptors(obj) 能夠獲取對象全部屬性的描述對象, 描述對象包括 get 和 set 屬性.

Null 傳導運算符

咱們要讀取對象的一個屬性或調用其方法, 爲了避免報錯, 應該先判斷對象是否存在, 而後再讀取其屬性.

若是咱們想讀取 obj.info.xingchuan.name, 安全的寫法應該是下面這樣

let name = obj && obj.info && obj.info.xingchuan && obj.info.xingchuan.name || 'default';

如今提案中引入了 Null 傳導運算符, 簡化了寫法, 能夠寫爲下面這種方式.

let name = obj ?. info ?. xingchuan ?. name || 'default';

參考資料

  • MDN
  • JavaScript高級程序設計
  • ECMAScript6入門
  • JavaScript忍者禁術
相關文章
相關標籤/搜索