ES6(八: 对象扩展)

来源:互联网 发布:apache 无法访问 编辑:程序博客网 时间:2024/04/30 10:40

前言

很多事情趁着年轻时候做,因为年轻时候不做,所失去的心理满足的成本,实在太大了,而且一旦失去,就永远失去了。

(一)属性的简洁表示法

ES6允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

const foo = 'bar';let baz = {foo};console.log(baz); // {foo: "bar"}

上面代码表明ES6允许在对象之中,只写属性名,不写属性值。这时,属性值等于属性名所代表的变量。

方法简写为

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

恩,其实class语法多用上面那种,ts也是上面那种。可是为什么觉得加上箭头函数逼格高些呢,哈哈。

CommonJS模块输出变量,就非常合适使用属性简写。

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

上面代码提供简写模块输出。问题是,我们一般写都将方法写在
module.exports 里面呢。不过这也提供另外一种引申

// 在react中import * as WelcomeComponent from './Users';export {  WelcomeComponent,};

上面代码,提供将组件默认输出到index.js,然后在容器组件中调用就不需要写多层结构。当然Angular2中也支持类似集中输出的方式。

(二)属性名表达式

JavaScript语言定义对象的属性,有两种方法。

// 方法一obj.foo = true;// 方法二obj['a' + 'bc'] = 123;

上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。

但是,如果使用字面量方式定义对象(使用大括号),在ES5中只能使用方法一(标识符)定义属性。

ES6允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。

let propKey = 'foo';let obj = {    [propKey]: true,    ['a' + 'bc']: 123};

没什么说的,一看就懂的。

(三)Object.is()

Object.is 用来比较两个值是否严格相等。它与严格比较运算符(===)的行为基本一致。

Object.is('foo', 'foo')// trueObject.is({}, {})// false

不同之处只有两个:一是+0 不等于-0 ,二是NaN 等于自身。

+0 === -0 //trueNaN === NaN // false  这里用isNaN()Object.is(+0, -0) // falseObject.is(NaN, NaN) // true

(四)Object.assign()

Object.assign 方法用来将源对象(source)的所有可枚举属性,复制到目标对象(target)。它至少需要两个对象作为参数,第一个参数是目标对象,后面的参数都是源对象

只要有一个参数不是对象,就会抛出TypeError错误。

const source1 = { b: 2 };const source2 = { c: 3, b: '第二个' };const target= Object.assign({}, source1, source2);console.log(target);// { b: '第二个', c: 3 }

注意:

如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

Object.assign的浅拷贝

(1)Object.assign 只拷贝自身属性,不可枚举的属性(enumerable 为false)和继承的属性不会被拷贝。

const c= Object.assign({},{b: 'c'},    Object.defineProperty({}, 'invisible', {        enumerable: false,        value: 'hello'    }))console.log(c);// { b: 'c' }

上面代码中, Object.assign 要拷贝的对象只有一个不可枚举属性invisible ,这个属性并没有被拷贝进去。

(2)属性名为Symbol值的属性,也会被Object.assign 拷贝(实际代码不是这样)。

const a= Object.assign({},{ a: 'b' }, { [Symbol('c')]: 'd' });console.log(a);// { a: 'b' }

书上说可以拷贝,实际上是不能的。不知道是不是我拿错教材,n次都这样。

(3)对于嵌套的对象, Object.assign 的处理方法是替换,而不是添加。

const target = { a: { b: 'c', d: 'e' } }const source = { a: { b: 'hello' } }const c= Object.assign({},target, source);console.log(c);// { a: { b: 'hello' } }// 解决方案,对应结构嵌套const c2= Object.assign({},source,{    a:{        b: target.a.b,        d: target.a.d    }});console.log(c2);// { a: { b: 'c', d: 'e' } }

上面代码中, target 对象的a 属性被source 对象的a 属性整个替换掉了,而不会得到{ a: { b: ‘hello’, d:’e’ } } 的结果。

这通常不是开发者想要的,需要特别小心。有一些函数库提供Object.assign 的定制版本(比如Lodash的_.defaultsDeep 方法),可以解决深拷贝的问题。

当然,如果你也可以使用对应的属性嵌套实现。

(4)Object.assign 可以用来处理数组,但是会把数组视为对象。

const a= [1,2,3,4];const b= [5,6];const c= Object.assign(a, b);console.log(a); // [ 5, 6, 3, 4 ]console.log(a===c); // true

上面代码中, Object.assign 把数组视为属性名为0、 1、 2的对象,
因此目标数组的0号属性4 覆盖了原数组的0号属性1 。

Object.assign()的应用

(1)为对象添加属性

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

上面方法通过assign方法,将x属性和y属性添加到Point类的对象实例。

(2)为对象添加方法

Object.assign(SomeClass.prototype, {    someMethod(arg1, arg2) {        ···    },    anotherMethod() {        ···    }});// 等同于下面的写法SomeClass.prototype.someMethod = function (arg1, arg2) {    ···};SomeClass.prototype.anotherMethod = function () {    ···}

上面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用assign方法添加到SomeClass.prototype之中。

(3)克隆对象

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

上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。

不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。

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

(4)合并多个对象

将多个对象合并到某个对象。

const merge =(...sources) => Object.assign({}, ...sources);

合并后返回一个新对象,可以改写上面函数,对一个空对象合并。

(5)为属性指定默认值

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 方法可以获取该属性的描述对象。

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 遍历到。

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

另外, ES6规定,所有Class的原型的方法都是不可枚举的。

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

总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不
要用for…in 循环,而用Object.keys() 代替。

(六)属性的遍历

ES6一共有6种方法可以遍历对象的属性。

(1) for…in
for…in 循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。
(2) Object.keys(obj)

Object.keys 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。

(3) Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames 返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚
举属性)。

(4) Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols 返回一个数组,包含对象自身的所有Symbol属性。

(5) Reflect.ownKeys(obj)
Reflect.ownKeys 返回一个数组,包含对象自身的所有属性,不管是属性名是Symbol或字符串,也不管是否
可枚举。

(6) Reflect.enumerate(obj)
Reflect.enumerate 返回一个Iterator对象,遍历对象自身的和继承的所有可枚举属性(不含Symbol属性),
与for…in 循环相同。

以上的6种方法遍历对象的属性,都遵守同样的属性遍历的次序规则。
首先遍历所有属性名为数值的属性,按照数字排序。
其次遍历所有属性名为字符串的属性,按照生成时间排序。
最后遍历所有属性名为Symbol值的属性,按照生成时间排序。

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)都部署了这个属性。

// 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 ,具体实现如下。

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

下面是一个列子

let proto = {};let obj = { x: 10 };Object.setPrototypeOf(obj, proto);proto.y = 20;proto.z = 40;obj.x // 10obj.y // 20obj.z // 40

上面代码将proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。
(3) Object.getPrototypeOf()

该方法与setPrototypeOf方法配套,用于读取一个对象的prototype对象。

Object.getPrototypeOf(obj);
下面是一个例子。

function Rectangle() {}var rec = new Rectangle();Object.getPrototypeOf(rec) === Rectangle.prototype// trueObject.setPrototypeOf(rec, Object.prototype);Object.getPrototypeOf(rec) === Rectangle.prototype// false

(八)对象的扩展运算符

目前, ES7有一个提案,将rest参数/扩展运算符(…)引入对象。 Babel转码器已经支持这项功能。

(1) Rest参数
Rest参数用于从一个对象取值,相当于将所有可遍历的、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面(redux中操作store基本抛弃了Object.assign(),采用rest参数,让代码更加优雅 )。

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };x // 1y // 2z // { a: 3, b: 4 }

上面代码中,变量z是Rest参数所在的对象。它获取等号右边的所有尚未读取的键(a和b),将它们和它们的
值拷贝过来。

注意, Rest参数的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么Rest参数拷
贝的是这个值的引用,而不是这个值的副本

let obj = { a: { b: 1 } };let { ...x } = obj;obj.a.b = 2;x.a.b // 2

上面代码中, x是Rest参数,拷贝了对象obj的a属性。 a属性引用了一个对象,修改这个对象的值,会影响到
Rest参数对它的引用。

另外, Rest参数不会拷贝继承自原型对象的属性。

let o1 = { a: 1 };let o2 = { b: 2 };o2.__proto__ = o1;let o3 = { ...o2 };o3 // { b: 2 }

上面代码中,对象o3是o2的复制,但是只复制了o2自身的属性,没有复制它的原型对象o1的属性。

(2)扩展运算符

扩展运算符用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

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

这等同于使用Object.assign 方法。

let aClone = { ...a };// 等同于let aClone = Object.assign({}, a);

扩展运算符还可以用自定义属性,会在新对象之中,覆盖掉原有参数

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属性,拷贝到新对象后会被覆盖掉。
如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值。

如果扩展运算符的参数是null或undefined,这个两个值会被忽略,不会报错。

let emptyObject = { ...null, ...undefined }; // 不报错
原创粉丝点击