整理Object的一些方法

weixin_33766168發表於2017-01-25

ES5 和 ES6 中 Object 新增了很多新的方法,現在很多開原始碼中有用到了這些,今天來整理一番。

Object.assign()

這是ES6新新增的方法,Object.assign()用來複制源物件的所有可列舉屬性複製到目標物件中,方法返回目標物件。語法如下:

Object.assign(target, ...source);

source物件可以有很多個,比如:

let target = {name: 'target'};
let source1 = {age: 23};
let source2 = {email: 'zhanglun1410@gmail.com'};
// ...
// let sourceN ={.....};
traget = Object.assign(target, source1, source2);

如果源物件和目標物件的屬性的key相同,目標物件的屬性將會被源物件中的屬性覆蓋。對於多個源物件來說,如果有相同的key,右邊的屬性將覆蓋左邊的屬性。這個方法只能將源物件的可列舉物件和自己的屬性複製給目標物件。

什麼是可列舉物件()?
可列舉屬性是指那些內部 “可列舉” 標誌設定為true的屬性,對於通過直接的賦值和屬性初始化的屬性,該標識值預設為即為true,對於通過Object.defineProperty等定義的屬性,該標識值預設為false。可列舉的屬性可以通過 for...in 迴圈進行遍歷(除非該屬性名是一個 Symbol)。

對於源物件,Object.assign使用[[Get]],而在目標物件上使用[[Set]],也就是說,使用這個方法會源物件的getter和目標物件的setters。所以其本質就是定義或者複製一個新的屬性。如果等待合併的源物件包含了getters,那就不太適合用來將源物件合併到原型中。假如複製的屬性到原型裡,包括它們的可列舉屬性,那麼應該使用 Object.getOwnPropertyDescriptor() 和 Object.defineProperty() 。String 和 Symbol 屬性都是會被複制的。

如果遇到了一個錯誤,比如目標物件的某個屬性是不可修改的,會丟擲一個TypeError的錯誤嗎,目標物件保持不變

var foo = {}
Object.defineProperty(foo, 'name', {
  writable: false,
  value: 'zhanglun'
});
Object.assign(foo, {name: 'zhangxiaolun'}); // TypeError: Cannot assign to read only property '1' of object '#<Object>'

如果源物件是null或者undefined,Object.assign()不會丟擲錯誤:

var foo = {name: 'zhanglun'};
Object.assign(foo, null, undefined);
console.log(foo); // foo: {name: 'zhanglun'}

Object.create()

通過指定的原型物件和屬性,建立一個新的物件。語法如下:

Object.create(proto, [,. propertiesObject]);

第一個引數是一個物件,可以是一個普通的物件,比如:{name: 'zhanglun'},也可以是一個新建立的物件的原型(prototype),比如:new Array().prototype。無論是那種,都是 JavaScript 中的 Object,其屬性都被新增到返回的物件原型中;第二個引數是可選的,但是不能是undefined,該物件自身擁有的可列舉屬性會被新增到新建立的物件上,其原型鏈上的屬性是無效的。如果第一個引數不是null或者一個物件值,將會丟擲TypeError異常。

Object.create()最直接的作用是基於一個物件建立新的物件,更多時候用在了原型鏈繼承上,先來看看 JavaScript
中建立物件的幾種方法:

  • 物件字面量

var o = {a: 1};

// o這個物件繼承了Object.prototype上面的所有屬性
// 所以可以這樣使用 o.hasOwnProperty('a').
// hasOwnProperty 是Object.prototype的自身屬性。
// Object.prototype的原型為null。
// 原型鏈如下:
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];

// 陣列都繼承於Array.prototype 
// (indexOf, forEach等方法都是從它繼承而來).
// 原型鏈如下:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
  return 2;
}

// 函式都繼承於Function.prototype
// (call, bind等方法都是從它繼承而來):
// f ---> Function.prototype ---> Object.prototype ---> null
  • 建構函式

在 JavaScript 中,構造器其實就是一個普通的函式。當使用 new 操作符 來作用這個函式時,它就可以被稱為構造方法(建構函式)。如果沒有 new 關鍵字而是直接呼叫的話,相當於是在當前作用域上呼叫,此時函式中如果有 this 的話,this 指向的是當前作用域。

function Graph() {
  this.vertexes = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v){
    this.vertexes.push(v);
  }
};

var g = new Graph();
// g是生成的物件,他的自身屬性有'vertices'和'edges'.
// 在g被例項化時,g.[[Prototype]]指向了Graph.prototype.
Graph();
console.log(window.vertexes); // 在全域性作用域中呼叫,意外地增加了全域性變數
  • 使用 Object.create()

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (繼承而來)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因為d沒有繼承Object.prototype
  • ES6 中的 Class 關鍵字

ES6 引入了一套新的關鍵字用來實現 class。這是一個語法糖,其本質還是基於原型的。這些新的關鍵字包括 class, constructor, static, extends, 和 super。關於 Class的使用,回頭再開一篇文章深入學習。

"use strict";

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);

使用 Object.create() 實現繼承

下面是一個使用 Object.create()實現類的繼承的例子。

function Shape() {
  this.x = 0;
  this.y = 0;
}

// 父類的原型方法
Shape.prototype.move = function(x, y) {
  this.x += x;
  this.y += y;
  console.info('Shape moved.');
};

// 子類
function Rectangle() {
  Shape.call(this); // 呼叫建構函式
}

// 子類繼承父類
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

var rect = new Rectangle();

console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle);// true
console.log('Is rect an instance of Shape?', rect instanceof Shape);// true
rect.move(1, 1); // Outputs, 'Shape moved.'

Object.create VS new

前面說到的,Object.create實際上是將第一個引數(無論是原型物件還是普通物件)的屬性新增到新建立物件的原型中,這也就意味著,通過new Function()中定義的屬性和方法是無法通過 create()方法新增到新建立物件中的。

Object.create建立一個新的物件,這個物件“繼承”了第一個引數。建構函式新建立的物件“繼承”建構函式的prototype。

let o = new SomeConstructor();  // o 直接繼承自`SomeConstructor.prototype`。

兩者的最明顯的不同之處在於:如果 Object.create()的第一個引數是null,新建立的物件不會“繼承”自任何地方,沒有原型,也沒有往上追溯的原型;在使用建構函式時,如果將其原型設定為 null,SomeConstructor.prototype = null;,新建立的物件將會“繼承”自 Object 的 prototype。

Object.freeze()

字面意思就是將一個 object“凍住”:不能新增新的屬性;不能刪除現有的屬性;不能修改現有屬性,包括屬性的enumerability, configurability和 writability。這個方法返回一個不可修改的物件,使用語法:

Object.freeze(obj)

任何嘗試修改該物件的操作都會失敗,可能是靜默失敗,也可能會丟擲異常。在嚴格模式中會丟擲異常(本地測試下來嚴格模式也不會報錯)。資料屬性的值不可更改,訪問器屬性(有getter和setter)也同樣,但由於是函式呼叫,給人一種修改了這個屬性的錯覺。如果一個屬性的值是個物件,則這個物件中的屬性是可以修改的,除非它也是個凍結物件。

let foo = {
  name: 'zhanglun',
  age: 23,
}

console.log(foo.name); // 'zhanglun'
console.log(foo.age); // 23

foo.name = 'zhanglun1410';
foo.age = 24;

console.log(foo.name); // 'zhanglun1410'
console.log(foo.age); // 24

Object.freeze(foo);

foo.name = 'zzzz'; // 靜默失敗
console.log(foo.name); // 'zhanglun1410'

'use strict';
foo.name = 'zzzz'; // TypeError 或者 靜默失敗
console.log(foo.name); // 'zhanglun1410'

Object.freeze VS const

被凍結的物件是不可改變的。但是它不一定是常量。對常量而言,它的所有引用,無論是直接的還是間接的都是引用的不可改變的物件。string,number,和 boolean 總是不可變的(當你把一個變數從字串 A 修改到字串 B時,A 和 B 都是不可變的,A 還是 A,B 也還是 B,只不過變數的之前指向的是 A,修改之後指向了 B)。通常來說,一般不會建立一個物件常量,也不存在freezeAll()這樣的方法。

const 用於宣告常量,將變數繫結到一個不可修改的物件,常量最終指向的是一個不可修改的物件,比如一個被凍結的物件,而 Object.freeze 作用在物件的值上,將一個物件變成不可修改的物件。

深度凍結物件

前面提到的,Object.freeze作用在物件的屬性上,使物件的屬性不可修改。而如果屬性值也是一個物件的話,依然能夠修改,除非這個物件也被凍結了。因此,可以把 Object.freeze 理解成是“淺凍結”。可以編寫額外的程式碼來實現“深凍結”:

obj1 = {
  internal: {}
};

Object.freeze(obj1);
obj1.internal.a = 'aValue';

obj1.internal.a // 'aValue'

// 深度凍結
function deepFreeze(obj) {

  // 獲取到物件的屬性的名字
  var propNames = Object.getOwnPropertyNames(obj);

  // 先凍結內部的物件
  propNames.forEach(function(name) {
    var prop = obj[name];

    // Freeze prop if it is an object
    if (typeof prop == 'object' && prop !== null)
      deepFreeze(prop);
  });

  // 凍結 obj
  return Object.freeze(obj);
}

obj2 = {
  internal: {}
};

deepFreeze(obj2);
obj2.internal.a = 'anotherValue';
obj2.internal.a; // undefined

Object.freeze 的注意事項

在 ES5 中,如果引數不是一個物件,是一個原始資料型別,會丟擲 TypeError。在 ES6 中,不是物件的引數的會被當做是一個已經被凍結的普通物件,只是返回這個引數。

Object.defineProperty()

Object.defineProperty是ES5新增的一個方法,可以給物件的屬性增加更多的控制。語法如下:

Object.defineProperty(obj, prop, descriptor)

前面兩個引數很簡單,修改的物件和修改或者新增的屬性,著重介紹一下第三個引數:屬性描述符。

ES5 中定義了一個名叫“屬性描述符”的物件,用於描述了的各種特徵,它本身是一個 Object。屬性描述符物件有4個屬性:

  • configurable:可配置性,控制著其描述的屬性的修改,表示能否修改屬性的特性,能否把屬性修改為訪問器屬性,或者能否通過delete刪除屬性從而重新定義屬性。預設值為true。

  • enumerable:可列舉性,表示能否通過for-in遍歷得到屬性。預設值為true。

  • writable:可寫性,表示能否修改屬性的值。預設值為true。

  • value:資料屬性,表示屬性的值。預設值為undefined。

和兩個存取器屬性,分別是get和set,可以代替value和writable。

  • get:在讀取屬性時呼叫的函式。只指定get則表示屬性為只讀屬性。預設值為undefined。

  • set:在寫入屬性時呼叫的函式。只指定set則表示屬性為只寫屬性。預設值為undefined。

屬性描述符只能在Object.definePropertyObject.defineProperties中使用。

var o = {}; // Creates a new object

// Example of an object property added with defineProperty with a data property descriptor
// 新增屬性 a,值為37,並設定屬性描述符
Object.defineProperty(o, 'a', {
  value: 37,
  writable: true,
  enumerable: true,
  configurable: true
});


var bValue = 38;
Object.defineProperty(o, 'b', {
  get: function() { return bValue; },
  set: function(newValue) { bValue = newValue; },
  enumerable: true,
  configurable: true
});
o.b; // 38
// o 物件中存在屬性 b,他的值為38;
// 只要 o.b沒有重新定義,它的值永遠都是38

// 訪問器不能和 value或者 writable混在一起用
Object.defineProperty(o, 'conflict', {
  value: 0x9f91102,
  get: function() { return 0xdeadbeef; }
});
// 丟擲一個錯誤 Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #<Object>

Object.defineProperties()

相比 Object.defineProperty, Object.defineProperties可以說是前者的升級版,可以一次同時定義多個屬性,語法略有不同:

let obj = {};
Object.defineProperties(obj, {
  "property1": {
    value: true,
    writable: true
  },
  "property2": {
    value: "Hello",
    writable: false
  },
  "property3": {
    get: function() {
      return 'Hello, Object.defineProperties';
    },
    set:function() {
      this.property2 = 'xxxxxx';
    }
  }
  // etc. etc.
});

參考資料:

  1. https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

  2. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

  3. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties

  4. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create

  5. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze

  6. http://stackoverflow.com/a/17952160

相關文章