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
只拷贝自身属性,不可枚举的属性(enumerable
为false
)和继承的属性不会被拷贝。
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
方法将DEFAULTS
和options
合并成一个新对象,如果两者有同名属性,则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有三个操作,如果enumerable
为false
则对其不起作用:
- for...in
循环:只遍历对象自身的和继承的可枚举的属性。
-
Object.keys()
:返回对象自身的所有可枚举的属性的键名。
-JSON.stringify()
:只串行化对象自身的可枚举的属性。
ES6新增了两个操作,会忽略enumerable为false的属性。
-
Object.assign()
:只拷贝对象自身的可枚举的属性。 -
Reflect.enumerate()
:返回所有for...in循环会遍历的属性。
这五个操作之中,只有for...in
和Reflect.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
。如果配合jQuery
的change
方法,就可以实现数据对象与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 // 报错,输出{}