1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 18:32:22 +00:00
es6tutorial/docs/object.md
2015-12-22 16:28:25 +08:00

784 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 对象的扩展
## 属性的简洁表示法
ES6允许直接写入变量和函数作为对象的属性和方法。这样的书写更加简洁。
```javascript
var foo = 'bar';
var baz = {foo};
baz // {foo: "bar"}
// 等同于
var baz = {foo: foo};
```
上面代码表明ES6允许在对象之中只写属性名不写属性值。这时属性值等于属性名所代表的变量。下面是另一个例子。
```javascript
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
f(1, 2) // Object {x: 1, y: 2}
```
除了属性简写,方法也可以简写。
```javascript
var o = {
method() {
return "Hello!";
}
};
// 等同于
var o = {
method: function() {
return "Hello!";
}
};
```
下面是一个实际的例子。
```javascript
var Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
```
这种写法用于函数的返回值,将会非常方便。
```javascript
function getPoint() {
var x = 1;
var y = 10;
return {x, y};
}
getPoint()
// {x:1, y:10}
```
CommonJS模块输出变量就非常合适使用简洁写法。
```javascript
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 };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};
```
属性的赋值器setter和取值器getter事实上也是采用这种写法。
```javascript
var cart = {
_wheels: 4,
get wheels () {
return this._wheels;
},
set wheels (value) {
if (value < this._wheels) {
throw new Error('数值太小了!');
}
this._wheels = value;
}
}
```
注意,简洁写法的属性名总是字符串,这会导致一些看上去比较奇怪的结果。
```javascript
var obj = {
class () {}
};
// 等同于
var obj = {
'class': function() {}
};
```
上面代码中,`class`是字符串,所以不会因为它属于关键字,而导致语法解析报错。
如果某个方法的值是一个Generator函数前面需要加上星号。
```javascript
var obj = {
* m(){
yield 'hello world';
}
}
```
## 属性名表达式
JavaScript语言定义对象的属性有两种方法。
```javascript
// 方法一
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;
```
上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。
但是如果使用字面量方式定义对象使用大括号在ES5中只能使用方法一标识符定义属性。
```javascript
var obj = {
foo: true,
abc: 123
};
```
ES6允许字面量定义对象时用方法二表达式作为对象的属性名即把表达式放在方括号内。
```javascript
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
```
下面是另一个例子。
```javascript
var lastWord = 'last word';
var a = {
'first word': 'hello',
[lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
```
表达式还可以用于定义方法名。
```javascript
let obj = {
['h'+'ello']() {
return 'hi';
}
};
obj.hello() // hi
```
注意,属性名表达式与简洁表示法,不能同时使用,会报错。
```javascript
// 报错
var foo = 'bar';
var bar = 'abc';
var baz = { [foo] };
// 正确
var foo = 'bar';
var baz = { [foo]: 'abc'};
```
## 方法的name属性
函数的`name`属性,返回函数名。对象方法也是函数,因此也有`name`属性。
```javascript
var person = {
sayName() {
console.log(this.name);
},
get firstName() {
return "Nicholas"
}
}
person.sayName.name // "sayName"
person.firstName.name // "get firstName"
```
上面代码中,方法的`name`属性返回函数名(即方法名)。如果使用了取值函数,则会在方法名前加上`get`。如果是存值函数,方法名的前面会加上`set`
有两种特殊情况:`bind`方法创造的函数,`name`属性返回“bound”加上原函数的名字`Function`构造函数创造的函数,`name`属性返回“anonymous”。
```javascript
(new Function()).name // "anonymous"
var doSomething = function() {
// ...
};
doSomething.bind().name // "bound doSomething"
```
如果对象的方法是一个Symbol值那么`name`属性返回的是这个Symbol值的描述。
```javascript
const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
[key1]() {},
[key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""
```
上面代码中,`key1`对应的Symbol值有描述`key2`没有。
## Object.is()
`Object.is`用来比较两个值是否严格相等。它与严格比较运算符(===)的行为基本一致。
```javascript
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
```
不同之处只有两个:一是`+0`不等于`-0`,二是`NaN`等于自身。
```javascript
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
```
ES5可以通过下面的代码部署`Object.is`
```javascript
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错误。
```javascript
var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
```
注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
```javascript
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和继承的属性不会被拷贝。
```javascript
Object.assign({b: 'c'},
Object.defineProperty({}, 'invisible', {
enumerable: false,
value: 'hello'
})
)
// { b: 'c' }
```
上面代码中,`Object.assign`要拷贝的对象只有一个不可枚举属性`invisible`,这个属性并没有被拷贝进去。
属性名为Symbol值的属性也会被`Object.assign`拷贝。
```javascript
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }
```
对于嵌套的对象,`Object.assign`的处理方法是替换,而不是添加。
```javascript
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`方法),可以解决深拷贝的问题。
注意,`Object.assign`可以用来处理数组,但是会把数组视为对象。
```javascript
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
```
上面代码中,`Object.assign`把数组视为属性名为0、1、2的对象因此目标数组的0号属性`4`覆盖了原数组的0号属性`1`
`Object.assign`方法有很多用处。
**1为对象添加属性**
```javascript
class Point {
constructor(x, y) {
Object.assign(this, {x, y});
}
}
```
上面方法通过assign方法将x属性和y属性添加到Point类的对象实例。
**2为对象添加方法**
```javascript
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
```
上面代码使用了对象属性的简洁表示法直接将两个函数放在大括号中再使用assign方法添加到SomeClass.prototype之中。
**3克隆对象**
```javascript
function clone(origin) {
return Object.assign({}, origin);
}
```
上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。
不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。
```javascript
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);
}
```
**4合并多个对象**
将多个对象合并到某个对象。
```javascript
const merge =
(target, ...sources) => Object.assign(target, ...sources);
```
如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。
```javascript
const merge =
(...sources) => Object.assign({}, ...sources);
```
**5为属性指定默认值**
```javascript
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`对象的该属性不起作用。
## 属性的可枚举性
对象的每个属性都有一个描述对象Descriptor用来控制该属性的行为。`Object.getOwnPropertyDescriptor`方法可以获取该属性的描述对象。
```javascript
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// { value: 123,
// writable: true,
// enumerable: true,
// configurable: true }
```
描述对象的`enumerable`属性,称为”可枚举性“,如果该属性为`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`遍历到。
```javascript
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false
Object.getOwnPropertyDescriptor([], 'length').enumerable
// false
```
另外ES6规定所有Class的原型的方法都是不可枚举的。
```javascript
Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false
```
总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用`for...in`循环,而用`Object.keys()`代替。
## 属性的遍历
ES6一共有6种方法可以遍历对象的属性。
**1for...in**
`for...in`循环遍历对象自身的和继承的可枚举属性不含Symbol属性
**2Object.keys(obj)**
`Object.keys`返回一个数组包括对象自身的不含继承的所有可枚举属性不含Symbol属性
**3Object.getOwnPropertyNames(obj)**
`Object.getOwnPropertyNames`返回一个数组包含对象自身的所有属性不含Symbol属性但是包括不可枚举属性
**4Object.getOwnPropertySymbols(obj)**
`Object.getOwnPropertySymbols`返回一个数组包含对象自身的所有Symbol属性。
**5Reflect.ownKeys(obj)**
`Reflect.ownKeys`返回一个数组包含对象自身的所有属性不管是属性名是Symbol或字符串也不管是否可枚举。
**6Reflect.enumerate(obj)**
`Reflect.enumerate`返回一个Iterator对象遍历对象自身的和继承的所有可枚举属性不含Symbol属性`for...in`循环相同。
以上的6种方法遍历对象的属性都遵守同样的属性遍历的次序规则。
- 首先遍历所有属性名为数值的属性,按照数字排序。
- 其次遍历所有属性名为字符串的属性,按照生成时间排序。
- 最后遍历所有属性名为Symbol值的属性按照生成时间排序。
```javascript
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
```
上面代码中,`Reflect.ownKeys`方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性`2``10`,其次是字符串属性`b``a`最后是Symbol属性。
## `__proto__`属性Object.setPrototypeOf()Object.getPrototypeOf()
**1`__proto__`属性**
`__proto__`属性(前后各两个下划线),用来读取或设置当前对象的`prototype`对象。目前所有浏览器包括IE11都部署了这个属性。
```javascript
// es6的写法
var obj = {
method: function() { ... }
}
obj.__proto__ = someOtherObj;
// es5的写法
var obj = Object.create(someOtherObj);
obj.method = function() { ... }
```
该属性没有写入ES6的正文而是写入了附录原因是`__proto__`前后的双下划线说明它本质上是一个内部属性而不是一个正式的对外的API只是由于浏览器广泛支持才被加入了ES6。标准明确规定只有浏览器必须部署这个属性其他运行环境不一定需要部署而且新的代码最好认为这个属性是不存在的。因此无论从语义的角度还是从兼容性的角度都不要使用这个属性而是使用下面的`Object.setPrototypeOf()`(写操作)、`Object.getPrototypeOf()`(读操作)、`Object.create()`(生成操作)代替。
在实现上,`__proto__`调用的是`Object.prototype.__proto__`,具体实现如下。
```javascript
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__`属性,则该属性的值就是对象的原型。
```javascript
Object.getPrototypeOf({ __proto__: null })
// null
```
**2Object.setPrototypeOf()**
`Object.setPrototypeOf`方法的作用与`__proto__`相同,用来设置一个对象的`prototype`对象。它是ES6正式推荐的设置原型对象的方法。
```javascript
// 格式
Object.setPrototypeOf(object, prototype)
// 用法
var o = Object.setPrototypeOf({}, null);
```
该方法等同于下面的函数。
```javascript
function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
```
下面是一个例子。
```javascript
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
proto.y = 20;
proto.z = 40;
obj.x // 10
obj.y // 20
obj.z // 40
```
上面代码将proto对象设为obj对象的原型所以从obj对象可以读取proto对象的属性。
**3Object.getPrototypeOf()**
该方法与setPrototypeOf方法配套用于读取一个对象的prototype对象。
```javascript
Object.getPrototypeOf(obj);
```
下面是一个例子。
```javascript
function Rectangle() {
}
var rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype
// true
Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false
```
## 对象的扩展运算符
目前ES7有一个[提案](https://github.com/sebmarkbage/ecmascript-rest-spread)将rest参数/扩展运算符(...引入对象。Babel转码器已经支持这项功能。
**1Rest参数**
Rest参数用于从一个对象取值相当于将所有可遍历的、但尚未被读取的属性分配到指定的对象上面。所有的键和它们的值都会拷贝到新对象上面。
```javascript
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
```
上面代码中变量z是Rest参数所在的对象。它获取等号右边的所有尚未读取的键a和b将它们和它们的值拷贝过来。
注意Rest参数的拷贝是浅拷贝即如果一个键的值是复合类型的值数组、对象、函数、那么Rest参数拷贝的是这个值的引用而不是这个值的副本。
```javascript
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
```
上面代码中x是Rest参数拷贝了对象obj的a属性。a属性引用了一个对象修改这个对象的值会影响到Rest参数对它的引用。
另外Rest参数不会拷贝继承自原型对象的属性。
```javascript
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let o3 = { ...o2 };
o3 // { b: 2 }
```
上面代码中对象o3是o2的复制但是只复制了o2自身的属性没有复制它的原型对象o1的属性。
**2扩展运算符**
扩展运算符用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
```javascript
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
```
这等同于使用`Object.assign`方法。
```javascript
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
```
扩展运算符可以用于合并两个对象。
```javascript
let ab = { ...a, ...b };
```
扩展运算符还可以用自定义属性,会在新对象之中,覆盖掉原有参数。
```javascript
let aWithOverrides = { ...a, x: 1, y: 2 };
// 等同于
let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
// 等同于
let x = 1, y = 2, aWithOverrides = { ...a, x, y };
// 等同于
let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
```
上面代码中a对象的x属性和y属性拷贝到新对象后会被覆盖掉。
如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值。
```javascript
let aWithDefaults = { x: 1, y: 2, ...a };
// 等同于
let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
// 等同于
let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);
```
扩展运算符的参数对象之中,如果有取值函数`get`,这个函数是会执行的。
```javascript
// 并不会抛出错误因为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这个两个值会被忽略不会报错。
```javascript
let emptyObject = { ...null, ...undefined }; // 不报错
```