ES6学习笔记---对象的扩展

1、属性、方法可以简写
比如属性简写:

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};
}

比如方法简写:

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

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

这种简写可以给我们带来哪些便捷呢?

Example 1:函数的返回值。
在ES5中我们会这样写:

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

在ES6中我们会这样写:

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

Example 2:模块输出变量

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。


2、属性名表达式
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

还可以用表达式定义方法名:

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'};

3、方法的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控制台测试的缘故,待我再多用几种方法执行代码看看结果的。


4、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
});

5、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对象的该属性不起作用。


6、属性的可枚举性
对象的每个属性都有一个描述对象(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()代替。


7、__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是不同的,所以为false。


8、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);

9、对象的扩展运算符
目前,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 // 报错,输出{}
posted @ 2017-08-11 17:50  我爱吃小丸子  阅读(225)  评论(0编辑  收藏  举报