一、属性的简洁表示法
当对象中属性的key名称和value对应的变量名称相同时,可以只写key名称,如:
var foo = '123';
var obj = {
foo
};
// 这相当于:
var obj = {
foo: foo
}
除了属性可以简写,方法也可以简写,即:
var obj = {
foo() {
// ...
}
}
// 相当于
var obj = {
foo: function() {
// ...
}
}
需要注意的是:简写的属性名总是字符串,所以是允许下列这种写法的:
var obj = {
class() {
// ...
}
}
因为它等同于:
var obj = {
'class': function() {
// ...
}
}
如果要表示一个generator函数,则需要在方法名前面加上*
:
var obj = {
* gen() {
// ...
}
}
ES6中,还允许使用表达式作为属性名,具体方法是使用[]
包裹表达式放入属性中,如:
const name = 'some key';
const obj = {
[name]: 'Hello, world'
}
obj[name]; // 'Hello, world'
除此之外,还可以使用[]
定义函数名,如:
const name = 'foo';
const obj = {
[name]() {
//...
}
}
obj.foo();
当[]
中包裹的内容是一个对象时,会将这个对象转化为string,典型的情况就是转化为[object Object]
,如:
const key = {};
const obj = {
[key]: 'test'
};
obj['[object Object]']; // 'test'
当然,转化为string调用的是toString()
方法,所以也可以这么做:
const key = {
toString() {
return 'name';
}
};
const obj = {
[key]: 'RuphiLau'
};
obj.name; // 'RuphiLau'
二、方法的name
属性
函数也是一个对象,所以它也拥有属性,而其name
属性,返回的则是这个方法名称,如:
const obj = {
foo(){}
};
obj.foo.name; // 'foo'
如果一个方法使用了setter
和getter
,则它的name
属性取不到值,即:
const obj = {
_realName: '',
get name() {
return this._realName;
},
set name(nval) {
this._realName = nval;
}
};
obj.name.name; // undefined
这时候,可以使用属性描述对象来获取,即:
const descriptor = Object.getOwnPropertyDescriptor(obj, 'name');
descriptor.set.name; // 'set name'
descriptor.get.name; // 'get name'
此外,如果使用bind()
来得到的函数,其name
属性将为:bound 方法名称
,如:
(function foo(){}).bind(null).name; // 'bound foo'
而用Function()
构造函数得到的函数,其name
则为anonymous
:
(new Function()).name; // 'anonymous'
而如果对象的方法是一个Symbol
值,则name
属性返回的是这个Symbol
值的描述,如:
const key1 = Symbol('desc');
const key2 = Symbol();
const obj = {
[key1](){},
[key2](){}
};
obj[key1].name; // '[desc]'
obj[key2].name; // ''
三、Object.is()
由于历史上缺少比较两个同值相等的最佳方法(如==
会先转化数据类型后再比较,而即便===
,在面对NaN
的时候也是没有办法),那么,ES6中引入了Object.is()
,有:
Object.is(1, 1); // true
Object.is('str', 'str'); // true
Object.is(NaN, NaN); // true
Object.is({}, {}); // false
而还有一个很重要的结论就是,在Object.is()
中,+0
和-0
是不相等的,这是因为Object.is()
设计的本质是:同值相等:
Object.is(+0, -0); // false
+0 === -0 ; // true
实现Object.is()
的pollyfill可以为:
Object.defineProperty(Object, 'is', {
value: function(x, y) {
if (x === y) {
// 针对+0不等于-0的情况,利用Infinity不等于-Infinity
return x !== 0 || (1 / x === 1 / y);
}
// 针对NaN的情况
return x !== x && y !== y;
},
configurable: true,
enumerable: false,
writable: true
});
四、Object.assign()
这个方法可以用于对象的合并,它能够将源对象的所有 可枚举属性,复制到目标对象中,其语法为:
Object.assign(target, source1, source2 [, sourceN]*)
转化与合并策略:
1)当只有target
一个参数的时候,直接返回的是target
- 当
target
为number
/boolean
/string
类型时,会先转成对象,如:
typeof Object.assign(123); // 'object'
typeof Object.assign(true); // 'object'
- 当
target
为undefined
或者null
类型时会报错,因为它们无法被转成对象 - 当
target
为string
类型时,会得到一个Array-like对象:
Object.assign('abc');
// { 0: 'a', 1: 'b', 2: 'c', length: 3 }
2)第二个参数起的对象,会被合并到target
里,如果存在同名属性,则后面的覆盖前面的
const t = {a:1};
const a = {a:2, b:3};
const b = {b:4, c:5};
const c = {c:6, d:7};
Object.assign(t, a, b, c);
// 将得到:{ a:2, b:4, c:6, d:7 }
3)如果是非源参数(除了target外的参数),如果不能被转为对象,则处理方法是跳过而不会报错。而虽然能够被转为对象,number
/boolean
类型的参数都不会产生合并效果,而string
则可以:
const t = {};
const v1 = 123;
const v2 = false;
const v3 = 'abc';
const v4 = undefined;
const v5 = null;
Object.assign(t, v1, v2, v3, v4, v5);
// {0: "a", 1: "b", 2: "c"}
之所以只有string类型的能被合并,是因为使用Object()
构造函数转化为对象的时候,只有string类型的可以产生可枚举对象,而其他类型的只会产生[[PrimitiveValue]]
属性,这个属性是不可枚举的,所以不能够被合并到target里:
Object(true); // Boolean {[[PrimitiveValue]]: true}
Object(10); // Number {[[PrimitiveValue]]: 10}
Object('abc'); // String {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
此外,Object.assign()
执行的是浅拷贝,而非深拷贝,即如果一个属性的值是一个对象,那么拷贝的是它的引用。所以有:
const t = {};
const s = {
obj: {
a: 1
}
};
Object.assign(t, s);
t.obj.a++;
console.log(s.obj.a); // 2
当有嵌套的深对象时,Object.assign()
执行的是一层合并,即如果有多层嵌套重名,直接覆盖第一层,如:
const t = {
obj: {
a: 1,
b: 2
}
};
const s = {
obj: {
c: 3
}
};
Object.assign(t, s);
/*
这种情况下,t对象最终的结果为:
{
obj: { c: 3 }
}
而非:
{
obj: { a: 1, b: 2, c: 3 }
}
*/
当遇到数组的时候,Object.assign()
也可以处理,需要注意的是:Object.assign()
将数组认定为对象,即:
Object.assign([1, 2, 3], [4, 5, 6]);
/*
相当于视为:
Object.assign({0:1, 1:2, 2:3}, {0:4, 1:5, 2:6})
所以会得到:[4, 5, 6]
*/
常见用途
Object.assign()
有多种用途,列举如下:
1)为对象添加属性和方法
const t = {/* ... */};
Object.assign(t, {
a: 1,
b: 2
});
const Car = function(){}
Object.assign(Car.prototype, {
showInfo() { /* ... */ },
addSpeed() { /* ... */ },
/* ... */
});
2)克隆对象
可以使用以下的方式克隆一个对象,如:
function clone(origin) {
return Object.assign({}, origin);
}
但是这种方式并不能保持继承链,如果要保持继承链,那么可以像下面这么做:
function clone(origin) {
const originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
3)多个对象的合并
如果想要合并多个对象到源对象,可以这么写:
function mergeToTarget(target, ...source) {
return Object.assign(target, ...source);
}
如果想要合并多个对象返回一个新对象,可以这么写:
function merge(...source) {
return Object.assign({}, ...source);
}
五、属性的可枚举性与遍历
1、可枚举性
对象的每个属性都有一个属性描述符(descriptor),可以通过Object.getOwnPropertyDescriptor(对象, 要获取的属性名)
来获得,如:
const obj = { foo: 123 }
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
/*
将得到:
{
configurable: true,
enumerable: true,
value: 123,
writable: true
}
*/
其中,enumerable
就表示可枚举性
,当为true时,表示该属性可枚举,否则不可枚举。当不可枚举的时候,以下的操作会跳过该属性:
for-in
遍历 对象自身 的和 继承的 可枚举属性Object.keys()
得到对象自身所有可枚举属性的键名JSON.stringify()
只串行化对象自身的可枚举属性Object.assign()
忽略不可枚举的属性,只拷贝对象自身的可枚举属性
注意,ES6中规定:所有class的原型方法都是不可枚举的
2、属性的遍历
ES6一共有5种方法可以遍历对象的属性:
1)for-in
遍历对象自身的和可枚举的继承属性(不含Symbol
属性)
2)Object.keys()
得到对象自身中所有的可枚举属性(不含Symbol
属性)
3)Object.getOwnPropertyNames(obj)
得到对象自身所有属性的key数组(包含不可枚举属性,但是不含Symbol
属性)
4)Object.getOwnPropertySymbols(obj)
得到一个数组,包含对象自身的所有Symbol
属性
5)Reflect.ownKeys(obj)
得到一个数组,包含对象自身的所有属性的key,包含为Symbol的key名称,也包含不可枚举属性
遍历次序:
- 首先遍历key为数值的属性,按数值排序
- 其次遍历key为字符串的属性,按照生成顺序排序
- 其次遍历key为Symbol值的属性,按照生成顺序排序
六、Object.create()
ES5中新增了Object.create()
方法,它的语法为:
Object.create(proto, [ propertiesObject ]);
它可以用来创建一个对象,并将对象的__proto__
属性指定为proto
,如:
const obj1 = { a: 123 };
const obj2 = Object.create(obj1);
obj2.b = 456;
console.dir(obj2);
/*
得到:
{
b: 456,
__proto__: obj1
}
*/
为什么有new
运算符了,还需要使用Object.create()
呢?这是因为,使用new
运算符无法指定对象的__proto__
,在实现寄生组合式继承的时候,我们通常采用变通的方法来实现__proto__
属性的指定:
const F = function(){};
F.prototype = Parent.prototype;
const childPrototype = new F; // 这样子可以使得childPrototype.__proto__ = F.prototype即Parent.prototype
而现在使用Object.create()
的话,寄生组合式继承就很好实现了:
Object.prototype.extends = function(Parent) {
const Child = this;
const childPrototype = Object.create(Parent.prototype);
childPrototype.construtor = Child;
Child.prototype = childPrototype;
}
此外,Object.create()
还可以接收第二个参数,第二个参数和给Object.defineProperties()
传入的参数是一样的,如:
const a = {
name: 'a'
};
const b = Object.create(a, {
name: {
configurable: true,
enumerable: true,
value: 'b',
writable: true
}
});
/*
这种情况下,b就会等于:
{
name: 'b'
__proto__: a
}
而a则为:{ name: 'a' }
*/
总结一下:Object.create()
创建一个对象,并将新建对象的proto指向第一个参数指定的对象,而第二个参数描述对象指定的属性,则会成为这个新建对象的自身属性。所以可以实现其polyfill为:
if (!Object.create) {
Object.create = function(proto, propertiesObject) {
if (!(
proto === null ||
typeof proto === 'object' ||
typeof proto === 'function'
)) {
throw TypeError('Argument must be an object, or null');
}
var tmp = new Object();
tmp.__proto__ = proto;
if (typeof propertiesObject === 'object') {
Object.defineProperties(tmp, propertiesObject);
}
}
}
七、Object.getOwnPropertyDescriptors()
在ES8中,引入了Object.getOwnPropertyDescriptors()
,可以一次性获得某个对象下所有属性的描述对象,如:
const obj = {
foo: 123,
get bar() {
return 'bar';
}
};
Object.getOwnPropertyDescriptors(obj);
/*
{
foo: {
configurable: true,
enumerable: true,
value: 123,
writable: true
},
bar: {
configurable: true,
enumerable: true,
get: funtion bar(){ return 'bar'; },
set: undefined
}
}
*/
而如果要在ES8前使用这个方法的话,polyfill可以这么实现:
Object.getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function(obj) {
const result = {};
for (let key of Reflect.ownKeys(obj)) {
result[key] = Object.getOwnPropertyDescriptor(obj, key);
}
return result;
};
引入这个方法后,我们就可以结合Object.defineProperties()
方法来实现拷贝get、set方法,如:
const obj = {
get foo() {
return 'foo';
}
}
const target = {};
Object.defineProperties(target, Object.getOwnPropertyDescriptors(obj));
此外,它还有如下的应用:
1、对象克隆
可以实现浅拷贝的克隆,如:
function shallowClone(obj) {
return Object.create(
Object.getPrototypeof(obj),
Object.getOwnPropertyDescriptors(obj)
);
}
2、实现mixin(混入)
function mix(object) {
return {
with(...mixins) {
return mixins.reduce(function(prev, mixin) {
return Object.create(
prev,
Object.getOwnPropertyDescriptors(mixin)
);
}, object);
}
};
}
let a = { a: 'a' };
let b = { b: 'b' };
let c = { c: 'c' };
let d = mix(a).with(b, c);
console.log(d);
/*
{
c: 'c',
__proto__:
b: 'b',
__proto__:
a: 'a'
}
*/
八、__proto__
属性,Object.setPrototypeOf()
、Object.getPrototypeOf()
1、__proto__
属性
__proto__
属性用来读取或者设置当前对象的prototype
对象,目前几乎所有现代浏览器(包括IE11)都部署了这个属性。
// ES6的写法
const obj = {
method: function(){ /* ... */ }
}
obj.__proto__ = someOtherObj;
// ES5的写法
const obj = Object.create(someOtherObj);
obj.method = function(){ /* ... */ }
虽然浏览器广泛支持这个属性,ES6里并没有写入正文,而是写在附录中,标准明确规定,只有浏览器必须部署这个属性,而其他运行环境则不一定需要部署。所以,推荐使用以下的方法代替:
Object.setPrototypeOf()
写操作Object.getPrototypeOf()
读操作Object.create()
生成操作
2、Object.setPrototypeOf()
Object.setPrototypeOf()
方法用来设置一个对象的prototype
对象,它是ES6证书推荐的原型设置方法:
Object.setPrototypeOf(obj, prototype);
// 相当于:
function(obj, prototype) {
obj.__proto__ = prototype;
return obj
}
如果第一个参数不是对象,则会自动转化为对象,但是由于返回的还是第一个参数,所以操作不会产生任何效果。但是需要注意的是,undefined
和null
无法转为对象,所以第一个参数如果是undefined
和null
时,则会报错
3、Object.getPrototypeOf()
用来读取一个对象的原型对象,相当于读取__proto__
属性,但是当参数不是对象时,就会读取并返回该对象,而是undefined和
null`时,就会报错
九、Object.keys()
、Object.values()
、Object.entries()
1、Object.keys()
返回自身的、可枚举的属性的key值数组
2、Object.values()
ES8引入的方法,返回自身的、可枚举的属性的value值数组(不含key为Symbol的键值)
如果参数是一个字符串,会返回各个字符串组成的数组:
Object.values('abc');
// ['a', 'b', 'c']
3、Object.entries()
返回自身的、可枚举的[key: value]
对数组(同样跳过key为Symbol类型的属性),常用用法
const obj = {
one: 1,
two: 2
}
for (let [key, value] of Object.entries(obj)) {
console.log(`${key}=${value}`);
}
/*
输出:
one=1
two=2
*/
还可以将对象转为Map
结构:
const obj = { foo: 'foo', num: 123 }
const map = new Map(Object.entries(obj));
map; // Map { foo: 'foo', num: 123 }
十、对象的扩展运算符
ES8将扩展运算符引入了对象
1、展开的解构赋值
展开的解构赋值,可以将所有可遍历的、但尚未读取的属性,分配到指定对象上,如:
const {x, y, ...z} = {
x: 1,
y: 2,
a: 3,
b: 4
};
x; // 1
y; // 2
z; // { a: 3, b: 4 }
解构赋值的右边,应该是一个可以被转成对象的值,如果不能转为对象(如undefined
和null
),那么解构就会报错,此外还需要注意:
1)展开的解构部分必须是最后一个参数,否则会报错:
const { x, y, ...z } = someObj; // 合法
const { ...x, y, z } = someObj; // 非法
const { x, ...y, ...z } = someObj; // 非法
2)展开的解构赋值的拷贝,是浅拷贝,所以对于复杂数据类型,拷贝的是引用
3)展开的解构赋值不会拷贝继承自原型对象的属性,如:
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3; // { b: 2 }
o3.a; // undefined
4)展开的解构赋值只能读取 自身的 属性,而不能读取原型对象上的属性,如:
const obj = Object.create({ x: 1, y: 2 });
obj.z = 3;
const { x, ...{ y, z } } = obj;
x; // 1
y; // undefined
z; // 3
/*
obj的结构为:
{
z: 3,
__proto__: {
x: 1,
y: 2
}
}
*/
这是因为,x是单纯的解构赋值,会沿着原型链查找,所以可以在原型链上找到,而...{y, z}
部分是扩展的解构赋值,我们可以先视为:...resObj
,所以得到的resObj
为:
{
z: 3
}
然后...{ y, z }
就相当于:const { y, z } = { z: 3 }
,从而y为undefined
,z为3
2、扩展运算符
使用扩展运算符,可以取出参数对象的所有可遍历属性,拷贝到当前对象中:
let z = { a:3, b:4 }
let n = { ...z };
n; // { a:3, b:4 }
这相当于:
let n = Object.assign({}, z);
其他用法:
1)合并两个对象
let n = { ...x, ...y }
// 相当于
let n = Object.assign({}, x, y);
2)覆盖原有属性
扩展运算符内部的属性,如果在列表中后面存在同名属性,则会被覆盖掉:
let old = { a: 123, b: 456 };
let newObj = { ...old, b: 789 }; // { a: 123, b: 789 }
newObj = { b: 789, ...old }; // { b: 456, a: 123 }
注意:
1)扩展运算符可以接一个表达式,如果表达式的值为null
或者undefined
,会忽略不会报错
2)如果表达式对象是一个get()
函数,则函数会执行:
let obj = {
... {
get name() {
return 'RuphiLau';
}
}
};
obj; // { name: 'RuphiLau' }
十一、Null传导运算符
在业务开发中,经常会有拿到一个JSON,然后取出message.body.user.firstName
这种数据的情况,如果其中有个部分是null
,则会有问题,所以通常比较安全的写法是:
const firstName = (
message &&
message.body &&
message.body.user &&
message.body.user.firstName
) || 'default';
为了避免这种层层判断,现在新增了一个提案,引入了Null传导运算符?.
简化了写法,当只要其中一部分返回了null
或者undefined
,整个表达式就返回undefined
,所以上例可以改为:
const firstName = message?.body?.user?.firstName || 'default';
用法总结如下:
obj?.prop
obj?.[expr]
func?.(...args)
new C?.(...args)