zoukankan      html  css  js  c++  java
  • ES6之路第七篇:对象的扩展

    属性的简洁表示法

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

    1 var foo = 'bar';
    2 var baz = {foo};
    3 baz // {foo: "bar"}
    4 
    5 // 等同于
    6 var baz = {foo: foo};

    上面代码表明,ES6 允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值。下面是另一个例子。

     1 function f(x, y) {
     2   return {x, y};
     3 }
     4 
     5 // 等同于
     6 
     7 function f(x, y) {
     8   return {x: x, y: y};
     9 }
    10 
    11 f(1, 2) // Object {x: 1, y: 2}

    除了属性简写,方法也可以简写。

     1 var o = {
     2   method() {
     3     return "Hello!";
     4   }
     5 };
     6 
     7 // 等同于
     8 
     9 var o = {
    10   method: function() {
    11     return "Hello!";
    12   }
    13 };

    下面是一个实际的例子。

     1 var birth = '2000/01/01';
     2 
     3 var Person = {
     4 
     5   name: '张三',
     6 
     7   //等同于birth: birth
     8   birth,
     9 
    10   // 等同于hello: function ()...
    11   hello() { console.log('我的名字是', this.name); }
    12 
    13 };

    这种写法用于函数的返回值,将会非常方便。

    1 function getPoint() {
    2   var x = 1;
    3   var y = 10;
    4   return {x, y};
    5 }
    6 
    7 getPoint()
    8 // {x:1, y:10}

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

     1 var ms = {};
     2 
     3 function getItem (key) {
     4   return key in ms ? ms[key] : null;
     5 }
     6 
     7 function setItem (key, value) {
     8   ms[key] = value;
     9 }
    10 
    11 function clear () {
    12   ms = {};
    13 }
    14 
    15 module.exports = { getItem, setItem, clear };
    16 // 等同于
    17 module.exports = {
    18   getItem: getItem,
    19   setItem: setItem,
    20   clear: clear
    21 };

    属性的赋值器(setter)和取值器(getter),事实上也是采用这种写法。

     1 var cart = {
     2   _wheels: 4,
     3 
     4   get wheels () {
     5     return this._wheels;
     6   },
     7 
     8   set wheels (value) {
     9     if (value < this._wheels) {
    10       throw new Error('数值太小了!');
    11     }
    12     this._wheels = value;
    13   }
    14 }

    属性名表达式

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

    1 // 方法一
    2 obj.foo = true;
    3 
    4 // 方法二
    5 obj['a' + 'bc'] = 123;

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

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

    1 var obj = {
    2   foo: true,
    3   abc: 123
    4 };

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

    1 let propKey = 'foo';
    2 
    3 let obj = {
    4   [propKey]: true,
    5   ['a' + 'bc']: 123
    6 };

    下面是另一个例子。

     1 var lastWord = 'last word';
     2 
     3 var a = {
     4   'first word': 'hello',
     5   [lastWord]: 'world'
     6 };
     7 
     8 a['first word'] // "hello"
     9 a[lastWord] // "world"
    10 a['last word'] // "world"

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

    1 let obj = {
    2   ['h' + 'ello']() {
    3     return 'hi';
    4   }
    5 };
    6 
    7 obj.hello() // hi

    注意,属性名表达式与简洁表示法,不能同时使用,会报错。

    1 // 报错
    2 var foo = 'bar';
    3 var bar = 'abc';
    4 var baz = { [foo] };
    5 
    6 // 正确
    7 var foo = 'bar';
    8 var baz = { [foo]: 'abc'};

    注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。

    1 const keyA = {a: 1};
    2 const keyB = {b: 2};
    3 
    4 const myObject = {
    5   [keyA]: 'valueA',
    6   [keyB]: 'valueB'
    7 };
    8 
    9 myObject // Object {[object Object]: "valueB"}

    上面代码中,[keyA][keyB]得到的都是[object Object],所以[keyB]会把[keyA]覆盖掉,而myObject最后只有一个[object Object]属性。

    方法的name属性

    函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。

    1 const person = {
    2   sayName() {
    3     console.log('hello!');
    4   },
    5 };
    6 
    7 person.sayName.name   // "sayName"

    上面代码中,方法的name属性返回函数名(即方法名)。

    如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的getset属性上面,返回值是方法名前加上getset

     1 const obj = {
     2   get foo() {},
     3   set foo(x) {}
     4 };
     5 
     6 obj.foo.name
     7 // TypeError: Cannot read property 'name' of undefined
     8 
     9 const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
    10 
    11 descriptor.get.name // "get foo"
    12 descriptor.set.name // "set foo"

     有两种特殊情况:bind方法创造的函数,name属性返回bound加上原函数的名字;Function构造函数创造的函数,name属性返回anonymous

    1 (new Function()).name // "anonymous"
    2 
    3 var doSomething = function() {
    4   // ...
    5 };
    6 doSomething.bind().name // "bound doSomething"

    如果对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。

    1 const key1 = Symbol('description');
    2 const key2 = Symbol();
    3 let obj = {
    4   [key1]() {},
    5   [key2]() {},
    6 };
    7 obj[key1].name // "[description]"
    8 obj[key2].name // ""

    上面代码中,key1对应的 Symbol 值有描述,key2没有。

    Object.is()

    ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。

    ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

    1 Object.is('foo', 'foo')
    2 // true
    3 Object.is({}, {})
    4 // false

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

    1 +0 === -0 //true
    2 NaN === NaN // false
    3 
    4 Object.is(+0, -0) // false
    5 Object.is(NaN, NaN) // true

    Object.assign()

    Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

    1 const target = { a: 1 };
    2 
    3 const source1 = { b: 2 };
    4 const source2 = { c: 3 };
    5 
    6 Object.assign(target, source1, source2);
    7 target // {a:1, b:2, c:3}

    Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。

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

    1 const target = { a: 1, b: 1 };
    2 
    3 const source1 = { b: 2, c: 2 };
    4 const source2 = { c: 3 };
    5 
    6 Object.assign(target, source1, source2);
    7 target // {a:1, b:2, c:3}

    如果只有一个参数,Object.assign会直接返回该参数。

    1 const obj = {a: 1};
    2 Object.assign(obj) === obj // true

    如果该参数不是对象,则会先转成对象,然后返回。

    1 typeof Object.assign(2) // "object"

    由于undefinednull无法转成对象,所以如果它们作为参数,就会报错。

    1 Object.assign(undefined) // 报错
    2 Object.assign(null) // 报错

    1.浅拷贝

    Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

    1 const obj1 = {a: {b: 1}};
    2 const obj2 = Object.assign({}, obj1);
    3 
    4 obj1.a.b = 2;
    5 obj2.a.b // 2

    上面代码中,源对象obj1a属性的值是一个对象,Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

    2.同名属性的替换

    对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。

    1 const target = { a: { b: 'c', d: 'e' } }
    2 const source = { a: { b: 'hello' } }
    3 Object.assign(target, source)
    4 // { a: { b: 'hello' } }

    上面代码中,target对象的a属性被source对象的a属性整个替换掉了,而不会得到{ a: { b: 'hello', d: 'e' } }的结果。这通常不是开发者想要的,需要特别小心。

    一些函数库提供Object.assign的定制版本(比如 Lodash 的_.defaultsDeep方法),可以得到深拷贝的合并。

    3.数组的处理

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

    1 Object.assign([1, 2, 3], [4, 5])
    2 // [4, 5, 3]

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

    4.取值函数的处理

    Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

    1 const source = {
    2   get foo() { return 1 }
    3 };
    4 const target = {};
    5 
    6 Object.assign(target, source)
    7 // { foo: 1 }

    上面代码中,source对象的foo属性是一个取值函数,Object.assign不会复制这个取值函数,只会拿到值以后,将这个值复制过去。

    5.Object.assign()为对象添加属性

    1 class Point {
    2   constructor(x, y) {
    3     Object.assign(this, {x, y});
    4   }
    5 }

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

    6.Object.assign()为对象添加方法

     1 Object.assign(SomeClass.prototype, {
     2   someMethod(arg1, arg2) {
     3     ···
     4   },
     5   anotherMethod() {
     6     ···
     7   }
     8 });
     9 
    10 // 等同于下面的写法
    11 SomeClass.prototype.someMethod = function (arg1, arg2) {
    12   ···
    13 };
    14 SomeClass.prototype.anotherMethod = function () {
    15   ···
    16 };

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

    7.Object.assign()克隆对象

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

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

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

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

    8.Object.assign()合并多个对象

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

    1 const merge =
    2   (target, ...sources) => Object.assign(target, ...sources);

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

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

    9.Object.assign()为属性指定默认值

     1 const DEFAULTS = {
     2   logLevel: 0,
     3   outputFormat: 'html'
     4 };
     5 
     6 function processContent(options) {
     7   options = Object.assign({}, DEFAULTS, options);
     8   console.log(options);
     9   // ...
    10 }

    上面代码中,DEFAULTS对象是默认值,options对象是用户提供的参数。Object.assign方法将DEFAULTSoptions合并成一个新对象,如果两者有同名属性,则option的属性值会覆盖DEFAULTS的属性值。

    注意,由于存在浅拷贝的问题,DEFAULTS对象和options对象的所有属性的值,最好都是简单类型,不要指向另一个对象。否则,DEFAULTS对象的该属性很可能不起作用。

     1 const DEFAULTS = {
     2   url: {
     3     host: 'example.com',
     4     port: 7070
     5   },
     6 };
     7 
     8 processContent({ url: {port: 8000} })
     9 // {
    10 //   url: {port: 8000}
    11 // }

    上面代码的原意是将url.port改成 8000,url.host不变。实际结果却是options.url覆盖掉DEFAULTS.url,所以url.host就不存在了。

    属性的可枚举性和遍历

    1.可枚举性

    对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

    1 let obj = { foo: 123 };
    2 Object.getOwnPropertyDescriptor(obj, 'foo')
    3 //  {
    4 //    value: 123,
    5 //    writable: true,
    6 //    enumerable: true,
    7 //    configurable: true
    8 //  }

    描述对象的enumerable属性,称为”可枚举性“,如果该属性为false,就表示某些操作会忽略当前属性。

    2.属性的遍历

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

    (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 或字符串,也不管是否可枚举。

    以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

    • 首先遍历所有数值键,按照数值升序排列。
    • 其次遍历所有字符串键,按照加入时间升序排列。
    • 最后遍历所有 Symbol 键,按照加入时间升序排列。
    1 Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
    2 // ['2', '10', 'b', 'a', Symbol()]

    上面代码中,Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性210,其次是字符串属性ba,最后是 Symbol 属性。

    Object.getOwnPropertyDescriptors()

    前面说过,Object.getOwnPropertyDescriptor方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors方法,返回指定对象所有自身属性(非继承属性)的描述对象。

     1 const obj = {
     2   foo: 123,
     3   get bar() { return 'abc' }
     4 };
     5 
     6 Object.getOwnPropertyDescriptors(obj)
     7 // { foo:
     8 //    { value: 123,
     9 //      writable: true,
    10 //      enumerable: true,
    11 //      configurable: true },
    12 //   bar:
    13 //    { get: [Function: get bar],
    14 //      set: undefined,
    15 //      enumerable: true,
    16 //      configurable: true } }

    上面代码中,Object.getOwnPropertyDescriptors方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。

    __proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()

    1.__proto__属性

    __proto__属性(前后各两个下划线),用来读取或设置当前对象的prototype对象。目前,所有浏览器(包括 IE11)都部署了这个属性。

    1 // es5 的写法
    2 const obj = {
    3   method: function() { ... }
    4 };
    5 obj.__proto__ = someOtherObj;
    6 
    7 // es6 的写法
    8 var obj = Object.create(someOtherObj);
    9 obj.method = function() { ... };

    2.Object.setPrototypeOf()

    Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。

    1 // 格式
    2 Object.setPrototypeOf(object, prototype)
    3 
    4 // 用法
    5 const o = Object.setPrototypeOf({}, null);

    3.Object.getPrototypeOf()

    该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。

    1 Object.getPrototypeOf(obj);

    super关键字

    我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。

     1 const proto = {
     2   foo: 'hello'
     3 };
     4 
     5 const obj = {
     6   foo: 'world',
     7   find() {
     8     return super.foo;
     9   }
    10 };
    11 
    12 Object.setPrototypeOf(obj, proto);
    13 obj.find() // "hello"

    上面代码中,对象objfind方法之中,通过super.foo引用了原型对象protofoo属性。

    对象的扩展运算符

    1.解构赋值

    对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。

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

    上面代码中,变量z是解构赋值所在的对象。它获取等号右边的所有尚未读取的键(ab),将它们连同值一起拷贝过来。

    由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefinednull,就会报错,因为它们无法转为对象。

    1 let { x, y, ...z } = null; // 运行时错误
    2 let { x, y, ...z } = undefined; // 运行时错误

    解构赋值必须是最后一个参数,否则会报错。

    1 let { ...x, y, z } = obj; // 句法错误
    2 let { x, ...y, ...z } = obj; // 句法错误

    2.扩展运算符

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

    1 let z = { a: 3, b: 4 };
    2 let n = { ...z };
    3 n // { a: 3, b: 4 }
  • 相关阅读:
    UVa 1349 (二分图最小权完美匹配) Optimal Bus Route Design
    UVa 1658 (拆点法 最小费用流) Admiral
    UVa 11082 (网络流建模) Matrix Decompressing
    UVa 753 (二分图最大匹配) A Plug for UNIX
    UVa 1451 (数形结合 单调栈) Average
    UVa 1471 (LIS变形) Defense Lines
    UVa 11572 (滑动窗口) Unique Snowflakes
    UVa 1606 (极角排序) Amphiphilic Carbon Molecules
    UVa 11054 Wine trading in Gergovia
    UVa 140 (枚举排列) Bandwidth
  • 原文地址:https://www.cnblogs.com/wanghao123/p/9309602.html
Copyright © 2011-2022 走看看