ES6學習筆記2---對象的擴展

一、屬性、方法能夠簡寫
好比屬性簡寫:javascript

var foo = 'bar';
var baz = {foo};
// 等同於
var baz = {foo: foo};


function f(x, y) {
  return {x, y};
}
// 等同於
function f(x, y) {
  return {x: x, y: y};
}

好比方法簡寫:html

var o = {
  method() {
    return "Hello!";
  }
};

// 等同於
var o = {
  method: function() {
    return "Hello!";
  }
};

這種簡寫能夠給咱們帶來哪些便捷呢?java

Example 1:函數的返回值。
在ES5中咱們會這樣寫:jquery

function a(){
    var data = {};
    data.x =1;
    data.y = 10;
    data.z = 100;
    return data;
}
a(); //{x:1,y:10,z:100}

在ES6中咱們會這樣寫:es6

function a() {
  var x = 1,y = 10,z = 100;
  return {x, y, z};
}
a(); //{x:1,y:10,z:100}

Example 2:模塊輸出變量chrome

var ms = {};

function getItem (key) {
  return key in ms ? ms[key] : null;
}

function setItem (key, value) {
  ms[key] = value;
}

function clear () {
  ms = {};
}

module.exports = { getItem, setItem, clear };

本身對這個有點不明白爲何模塊輸出變量很是合適使用這種簡潔方法,歡迎瞭解的朋友賜教,3Q。數組


二、屬性名錶達式
ES6容許用表達式做爲屬性名,表達式要放在[ ]方括號內使用,而ES5只支持直接用標識符做爲屬性名,舉個例子:瀏覽器

//標識符
var o = {
    obj : 1,
    obj2: 'test'
}
//表達式
var obj = 'how are you';
var o = {
  [obj]: 1,
  ['hello world']: 'test'
};
o[obj]; // 1
o['hello world']; //test

還能夠用表達式定義方法名:app

var a = 'ello';
var obj = {
  ['h'+ a]() {
    return 'hi Jack';
  }
};

obj.hello() // hi Jack

注:屬性名錶達式與簡潔表示法,不能同時使用模塊化

// 錯誤寫法
var foo = 'bar';
var bar = 'abc';
var baz = { [foo] };

// 正確寫法
var foo = 'bar';
var baz = { [foo]: 'abc'};

三、方法的name屬性
函數的name屬性,返回函數名。對象方法也是函數,所以也有name屬性。

var person = {
  sayName: function() {
    console.log(this.name);
  },
  get firstName() {
    return "Nicholas"
  }
}
var doSomething = function() {
  return;
};
(new Function()).name // "anonymous"              個人執行結果:"
doSomething.bind().name // "bound doSomething"    個人執行結果:'bound'
person.sayName.name   // "sayName"                個人執行結果:""  雖然sayName有name屬性,可是輸出結果倒是空
person.firstName.name // "get firstName"          個人執行結果:undefined 控制檯顯示firstName沒有name屬性

其中我對這的bind方法有些疑惑,咱們知道bind大都是jquery綁定事件用的,可是這裏的用法必定不是這樣的,因此我就查了一下,果真不一樣:

若是你但願將一個對象的函數賦值給另一個變量後,這個函數的執行上下文仍然爲這個對象,那麼就須要用到bind方法。(與call和apply做用類似)。

若是對象的方法是一個Symbol值,那麼name屬性返回的是這個Symbol值的描述。

const key1 = Symbol('description');
const key2 = Symbol();
var obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name // "[description]"   個人執行結果:""
obj[key2].name // ""

這兩處的運行結果與阮大神的測試結果有些不一樣,多是我直接用chrome控制檯測試的緣故,待我再多用幾種方法執行代碼看看結果的。


四、Object.is()
Object.is用來比較兩個值是否嚴格相等。它與嚴格比較運算符(===)的行爲基本一致。
不過它的不一樣之處只有兩個:一是+0不等於-0,二是NaN等於自身。

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

NaN === NaN // false
Object.is(NaN, NaN) // true

ES5能夠經過下面的代碼,部署Object.is

Object.defineProperty(Object, 'is', {
  value: function(x, y) {
    if (x === y) {
      // 針對+0 不等於 -0的狀況
      return x !== 0 || 1 / x === 1 / y;
    }
    // 針對NaN的狀況
    return x !== x && y !== y;
  },
  configurable: true,
  enumerable: false,
  writable: true
});

五、Object.assign()
Object.assign方法用來將源對象(source)的全部可枚舉屬性,複製到目標對象(target)。它至少須要兩個對象做爲參數,第一個參數是目標對象,後面的參數都是源對象。只要有一個參數不是對象,就會拋出TypeError錯誤。

var target  =  { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

注:若是目標對象與源對象有同名屬性,或多個源對象有同名屬性,則後面的屬性會覆蓋前面的屬性。

var target = { a: 1, b: 1 };
var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

Object.assign只拷貝自身屬性,不可枚舉的屬性(enumerablefalse)和繼承的屬性不會被拷貝。

Object.assign({b: 'c'},
  Object.defineProperty({}, 'invisible', {
    enumerable: false,
    value: 'hello'
  })
)
// { b: 'c' }


Object.assign({b: 'c'},
  Object.defineProperty({}, 'invisible', {
    enumerable: true,
    value: 'hello'
  })
)
// {b: "c", invisible: "hello"}

屬性名爲Symbol值的屬性,也會被Object.assign拷貝。

Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }

對於嵌套的對象,Object.assign的處理方法是替換,而不是添加。

var target = { a: { b: 'c', d: 'e' } }
var source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }

上面代碼中,target對象的a屬性被source對象的a屬性整個替換掉了,而不會獲得{ a: { b: 'hello', d: 'e' } }的結果。這一般不是開發者想要的,須要特別當心。有一些函數庫提供Object.assign的定製版本(好比Lodash_.defaultsDeep方法),能夠解決深拷貝的問題。

看到這產生了疑問:Lodash是什麼?_.defaultsDeep又是怎麼解決的呢?
因而我搜了一些資料:

  • lodash 實際上是一個 JavaScript 實用工具庫,提供一致性,模塊化,性能和配件等功能。相似underscore,是它的下一代。

  • _.defaultsDeep(object, [sources])
    目標Object中設定的值是缺省值,不能被sources的相同property覆蓋,可是用遞歸的方式分配默認值

_.defaultsDeep({ 'user': { 'name': 'barney' } }, { 'user': { 'name': 'fred', 'age': 36 } });
//  { 'user': { 'name': 'barney', 'age': 36 } }

注意,Object.assign能夠用來處理數組,可是會把數組視爲對象。

Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]

其中,4覆蓋1,5覆蓋2,由於它們在數組的同一位置,因此就對應位置覆蓋了。

Object.assign還有不少用處,下面就看一下吧:

  • 爲對象添加屬性

class Point {
  constructor(x, y) {
    Object.assign(this, {x, y});
  }
}

這樣就給Point類的對象實例添加了x、y屬性。

  • 爲對象添加方法

Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    ···
  },
  anotherMethod() {
    ···
  }
});

// 等同於下面的寫法
SomeClass.prototype.someMethod = function (arg1, arg2) {
  ···
};
SomeClass.prototype.anotherMethod = function () {
  ···
};

上面代碼使用了對象屬性的簡潔表示法,直接將兩個函數放在大括號中,再使用assign方法添加到SomeClass.prototype之中。

  • 克隆對象

function clone(origin) {
  return Object.assign({}, origin);
}

上面代碼將原始對象拷貝到一個空對象,就獲得了原始對象的克隆。

不過,採用這種方法克隆,只能克隆原始對象自身的值,不能克隆它繼承的值。若是想要保持繼承鏈,能夠採用下面的代碼。

function clone(origin) {
  let originProto = Object.getPrototypeOf(origin);
  return Object.assign(Object.create(originProto), origin);
}

在JS裏子類利用Object.getPrototypeOf去調用父類方法,用來獲取對象的原型。用它能夠模仿Java的super。

  • 將多個對象合併成一個對象

    • 多個對象合併到某個對象

      const merge =(target, ...sources) => Object.assign(target, ...sources);
    • 多個對象合併到一個新對象

      const merge = (...sources) => Object.assign({}, ...sources);
  • 爲屬性指定默認值

const DEFAULTS = {
  logLevel: 0,
  outputFormat: 'html'
};

function processContent(options) {
  let options = Object.assign({}, DEFAULTS, options);
}

上面代碼中,DEFAULTS對象是默認值,options對象是用戶提供的參數。Object.assign方法將DEFAULTSoptions合併成一個新對象,若是二者有同名屬性,則option的屬性值會覆蓋DEFAULTS的屬性值。

注: 因爲存在深拷貝的問題,DEFAULTS對象和options對象的全部屬性的值,都只能是簡單類型,而不能指向另外一個對象。不然,將致使DEFAULTS對象的該屬性不起做用。


六、屬性的可枚舉性
對象的每一個屬性都有一個描述對象(Descriptor),用來控制該屬性的行爲。Object.getOwnPropertyDescriptor方法能夠獲取該屬性的描述對象。

var obj = { foo: 123 };
 Object.getOwnPropertyDescriptor(obj, 'foo')
// {value: 123, writable: true, enumerable: true, configurable: true}

var o = Object.defineProperty({}, 'display', {
    enumerable: false,
    value: 'block'
  })
Object.getOwnPropertyDescriptor(o,'display')
// {value: "block", writable: false, enumerable: false, configurable: false}

ES5有三個操做,若是enumerablefalse則對其不起做用:
- for...in 循環:只遍歷對象自身的和繼承的可枚舉的屬性。

  • Object.keys():返回對象自身的全部可枚舉的屬性的鍵名。
    - JSON.stringify():只串行化對象自身的可枚舉的屬性。

ES6新增了兩個操做,會忽略enumerable爲false的屬性。

  • Object.assign():只拷貝對象自身的可枚舉的屬性。

  • Reflect.enumerate():返回全部for...in循環會遍歷的屬性。

這五個操做之中,只有for...inReflect.enumerate()會返回繼承的屬性。

實際上,引入enumerable的最初目的,就是讓某些屬性能夠規避掉for...in操做。好比,對象原型的toString方法,以及數組的length屬性,就經過這種手段,不會被for...in遍歷到。

Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false

Object.getOwnPropertyDescriptor([], 'length').enumerable
// false

另外,ES6規定,全部Class的原型的方法都是不可枚舉的。

Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false

總的來講,操做中引入繼承的屬性會讓問題複雜化,大多數時候,咱們只關心對象自身的屬性。因此,儘可能不要用for...in循環,而用Object.keys()代替。


七、__proto__屬性Object.setPrototypeOf(),Object.getPrototypeOf()
(1)__proto__屬性(注:建議不要使用)
__proto__屬性(先後各兩個下劃線),用來讀取或設置當前對象的prototype對象。

// es5的寫法
var obj = Object.create(someOtherObj);
obj.method = function() { ... }


// es6的寫法
var obj = {
  method: function() { ... }
}
obj.__proto__ = someOtherObj;

該屬性沒有寫入ES6的正文,而是寫入了附錄,緣由是__proto__先後的雙引號,說明它本質上是一個內部屬性,而不是一個正式的對外的API,只是因爲瀏覽器普遍支持,才被加入了ES6。標準明確規定,只有瀏覽器必須部署這個屬性,其餘運行環境不必定須要部署,並且新的代碼最好認爲這個屬性是不存在的。所以,不管從語義的角度,仍是從兼容性的角度,都不要使用這個屬性,而是使用下面的Object.setPrototypeOf()(寫操做)、Object.getPrototypeOf()(讀操做)、Object.create()(生成操做)代替。

在實現上,__proto__調用的是Object.prototype.__proto__,具體實現以下。

Object.defineProperty(Object.prototype, '__proto__', {
  get() {
    let _thisObj = Object(this);
    return Object.getPrototypeOf(_thisObj);
  },
  set(proto) {
    if (this === undefined || this === null) {
      throw new TypeError();
    }
    if (!isObject(this)) {
      return undefined;
    }
    if (!isObject(proto)) {
      return undefined;
    }
    let status = Reflect.setPrototypeOf(this, proto);
    if (! status) {
      throw new TypeError();
    }
  },
});
function isObject(value) {
  return Object(value) === value;
}

若是一個對象自己部署了__proto__屬性,則該屬性的值就是對象的原型。

Object.getPrototypeOf({ __proto__: null })
// null

(2)Object.setPrototypeOf()
Object.setPrototypeOf方法的做用與__proto__相同,用來設置一個對象的prototype對象。它是ES6正式推薦的設置原型對象的方法。

// 格式
Object.setPrototypeOf(object, prototype)

// 用法
var o = Object.setPrototypeOf({}, null);

這個方法等同於下面的函數:

function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

例子以下:

var proto = {};
var obj = {x:1,y:2}
Object.setPrototypeOf(obj,proto);

proto.y = 3;
proto.z = 5;
proto.p = 9;

obj // {x:1,y:2,__proto__:{p:9,y:3,z:5}}
obj.y // 2
obj.z // 5
obj.p // 9

(3)Object.getPrototypeOf()
該方法與setPrototypeOf方法配套使用,用於讀取一個對象的prototype對象。
例子以下:

function Dog(){}
var dog = new Dog();

Object.getPrototypeOf(dog) === Dog.prototype; // true

Object.setPrototypeOf(dog,Object.prototype);
Object.getPrototypeOf(dog) === Dog.prototype; // false

Object.prototype

Dog.prototype

顯然Object.prototype與Dog.prototype是不一樣的,因此爲false。


八、Object.observe(),Object.unobserve() 這兩個函數是ES7的一部分,不屬於ES6。

Object.observe方法用來監聽對象(以及數組)的變化。一旦監聽對象發生變化,就會觸發回調函數。

Object.observe方法接受兩個參數,第一個參數是監聽的對象,第二個函數是一個回調函數。

var user = {};
Object.observe(user, function(changes){
  changes.forEach(function(change) {
    user.fullName = user.firstName+" "+user.lastName;
  });
});

user.firstName = 'Michael';
user.lastName = 'Jackson';
user.fullName // 'Michael Jackson'  個人執行結果:undefined

疑惑點:大神的結果是怎麼獲得的呢?而個人倒是undefined。

上面代碼中,Object.observer方法監聽user對象。一旦該對象發生變化,就自動生成fullName屬性。

利用這個方法能夠作不少事情,好比自動更新DOM

var div = $("#foo");

Object.observe(user, function(changes){
  changes.forEach(function(change) {
    var fullName = user.firstName+" "+user.lastName;
    div.text(fullName);
  });
});

上面代碼中,只要user對象發生變化,就會自動更新DOM。若是配合jQuerychange方法,就能夠實現數據對象與DOM對象的雙向自動綁定。

回調函數的changes參數是一個數組,表明對象發生的變化。下面是一個更完整的例子。

var o = {};

function observer(changes){
  changes.forEach(function(change) {
    console.log('發生變更的屬性:' + change.name);
    console.log('變更前的值:' + change.oldValue);
    console.log('變更後的值:' + change.object[change.name]);
    console.log('變更類型:' + change.type);
  });
}

Object.observe(o, observer);

參照上面代碼,Object.observe方法指定的回調函數,接受一個數組(changes)做爲參數。該數組的成員與對象的變化一一對應,也就是說,對象發生多少個變化,該數組就有多少個成員。每一個成員是一個對象(change),它的name屬性表示發生變化源對象的屬性名,oldValue屬性表示發生變化前的值,object屬性指向變更後的源對象,type屬性表示變化的種類。基本上,change對象是下面的樣子。

var change = {
  object: {...},
  type: 'update',
  name: 'p2',
  oldValue: 'Property 2'
}

Object.observe方法目前共支持監聽六種變化。

  • add:添加屬性

  • update:屬性值的變化

  • delete:刪除屬性

  • setPrototype:設置原型

  • reconfigure:屬性的attributes對象發生變化

  • preventExtensions:對象被禁止擴展(當一個對象變得不可擴展時,也就沒必要再監聽了)

Object.observe方法還能夠接受第三個參數,用來指定監聽的事件種類。
舉個例子,當發生delete事件時,纔會調用回調函數。

Object.observe(o, observer, ['delete']);

Object.unobserve方法用來取消監聽。

Object.unobserve(o, observer);

九、對象的擴展運算符
目前,ES7有一個提案,將rest參數/擴展運算符(...)引入對象。Babel轉碼器已經支持這項功能。
(1)Rest參數
Rest參數用於從一個對象取值,至關於將全部可遍歷的、但還沒有被讀取的屬性,分配到指定的對象上面。全部的鍵和它們的值,都會拷貝到新對象上面。

let {a,b,c} = {a:1,b:2,d:3,e:4}
a //1
b //2
c //undefined

let {a,b, ...c} = {a:1,b:2,c:3,d:4}
a //1
b //2
c //{c:3,d:4}

let obj = { a: { b: 9 } };
let { ...x } = obj;
obj.a.b = 6;
console.log(x.a.b);

從上面的代碼能夠看出Rest參數的拷貝是淺拷貝,即若是一個鍵的值是複合類型的值(對象,數組,函數)、那麼Rest參數拷貝的是這個值的引用,而不是這個值的副本。

並且Rest參數不會拷貝繼承自原型對象的屬性。

let s1 = {a:1};
let s2 = {b:2};
Object.setPrototypeOf(s2,s1);
let s3 = { ...s2};
s3 // {b:2}
s2.a // 1

上面代碼中,對象o3是o2的複製,可是隻複製了o2自身的屬性,沒有複製它的原型對象o1的屬性。

(2)擴展運算符
擴展運算符用於取出參數對象的全部可遍歷屬性,拷貝到當前對象之中。

let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

Object.assign({},z);  // { a: 3, b: 4 }

從上面的例子能夠看出此處Object.assign與擴展運算符( ...)的用法同樣,即:{ ...a} 等同於 Object.assign({}, a)

擴展運算符還能夠合併兩個對象。

let a = { x:1 };
let b = { y:2 };

let ab = { ...a, ...b}
ab //{ x:1,y:2 }

擴展運算符還能夠自定義屬性,會在新對象之中,覆蓋掉原有參數。

let a = { x:4, z:8 };
let p = { ...a, x:1, y:2 };
p // { x: 1, z: 8, y: 2 }
//等同於
let c = Object.assign({}, a, { x: 1, y: 2 });
c  // { x: 1, z: 8, y: 2 }

若是把自定義屬性放在擴展運算符前面,就變成了設置新對象的默認屬性值。

let a = { x:4, z:8 };
let p = { x: 1, y: 2, ...a };
p // { x: 4, y: 2, z: 8 }
//等同於
let q = Object.assign({ x:1, y:2 }, a);
q // { x: 4, y: 2, z: 8 }

須要注意:擴展運算符的參數對象中,若是有取值函數get,這個函數是會執行的

// 並不會拋出錯誤,由於x屬性只是被定義,但沒執行
let aWithXGetter = {
  ...a,
  get x() {
    throws new Error('not thrown yet');
  }
};

// 會拋出錯誤,由於x屬性被執行了
let runtimeError = {
  ...a,
  ...{
    get x() {
      throws new Error('thrown now');
    }
  }
};

若是擴展運算符的參數是null或undefined,這個兩個值會被忽略,不會報錯。

let emptyObject = { ...null, ...undefined }; 
emptyObject // 報錯,輸出{}
相關文章
相關標籤/搜索