ES6入门 ___ 对象的扩展

一、属性的简介表示法

ES6允许直接写入变量和函数作为对象的属性和方法。允许在对象中只写属性名,不写属性值。这时,属性值等于属性名所代表的变量。

var foo = 'bar'
var baz = {foo}
baz
 // {foo: "bar"} 

// 等同于
var baz = {foo: foo}

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

var o = {
  method() {
    return 'Hello!'
  }
}

// 等同于
var o = {
  method: function() {
    return 'Hello!'
  }
}

这种写法用于函数的返回值会非常方便,CommonJS 模块输出变量也非常适合使用简介写法


属性的复制器(setter)和取值器(getter)事实上也采用了这种写法

var cart = {
  _wheels: 4,

  get wheels() {
    return this._wheels
  },
  set wheels(value) {
    if (value < this._wheels) {
      throw new Error('数值太小了')
    }
    this._wheels = value
  }
}

注意:简介写法中属性名总是字符串,这回导致一些看上去比较奇怪的结果。

var obj = {
  class() {}
}

// 等同于
var obj = {
  'class': function() {}
}

class 是字符串,所以不会因为它属于关键字而导致语法解析错误。如果是一个 Generator 函数,则需要在前面加上星号

二、属性名表达式

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

// 方法一
obj.foo = true

// 方法二
obj.['a' + 'bc'] = 123

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

var obj = {
  foo: true,
  abc: 123
}

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

let propKey = 'foo'

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
}
console.dir(obj)

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

let obj = {
  ['h' + 'ello']() {
    return 'hi'
  }
}

obj.hello()

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

// 报错
var foo = 'bar'
var bar = 'abc'
var baz = { [foo] }

// 正确写法
var foo = 'bar'
var baz = { [foo]: 'abc' }

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

const keyA = {a: 1}
const keyB = {b: 2}

const myObject = {
  [keyA]: 'valueA',
  [keyB]: 'valueB'
}

myObject
// {[object Object]: "valueB"}

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

三、方法的 name 属性

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

const person = {
  sayName() {
    console.log('Hello')
  }
}
person.sayName.name
// "sayName"

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

const obj = {
  get foo() {},
  set foo(x) {}
}
obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo')
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

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

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

const key1 = Symbol('description')
const key2 = Symbol()

let obj = {
  [key1]() {},
  [key2]() {}
}
obj[key1].name // "[description]"
obj[key2].name // ""

四、Object.is()

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

Object.is('foo', 'foo')
// true

Object.is({}, {})
// false

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

+0 === -0 // true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

五、Object.assign()

5.1、基本用法

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

var target = {a: 1}

var source1 = {b: 2}
var source2 = {c: 3}

Object.assign(target, source1, source2)
target // {a: 1, b: 2, c: 3}

Object.assign 方法的第一个参数是目标对象,后面的参数都是源对象。如果目标对象和源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

  • 如果只有一个参数,Object.assign 会直接返回该参数
  • 如果该参数不是对象,则会先转成对象,然后返回
  • 由于 undefined 和 null 无法转成对象,所以如果将他们作为参数,就会报错。
    如果非对象参数出现在源对象的位置(即非首参数),那么处理规则则将有所不同。首先,这些参数都会转成对象,如果无法转成对象便会跳过。这意味着,如果undefined 和 null 不在首参数便不会报错。

其他类型的值(即数值、字符串 和 布尔值)不在首参数也不会报错。但是,除了字符串会以数组形式复制到目标对象,其他值都不会产生效果。

var v1 = 'abc'
var v2 = true
var v3 = 10

var obj = Object.assign({}, v1, v2, v3)
obj // {0: "a", 1: "b", 2: "c"}

上面代码中,数值和布尔值被忽略。这是因为只有字符串的包装对象会产生可枚举属性

Object(true) // {[ [PrimitiveValue] ]: true}
Object(10) // {[[ PrimitiveValue]]: 10}
Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: 'abc'}

原始值都在包装对象的内部属性 [[ PrimitiveValue ]] 上面,这个属性是不会被 Object.assign 复制的。只有字符串的包装对象会产生可枚举的实义属性,那些属性则会被拷贝

Object.assign 复制的属性是有限制的,只复制源对象的自身属性(不复制继承属性)。也不复制不可枚举的属性(enumerable: false)

Object.assign({b: 'c'}, Object.defineProperty({}, 'invisible', {
  enumerable: false,
  value: 'hello'
}))
// {b: "c"}

属性名为 Symbol 值的属性也会被 Object.assign 复制。

Object.assign({a: 'b'}, { [Symbol('c')]: 'd' })
// {a: "b", Symbol(c): "d"}

5.2、注意点

  1. Object.assign 方法实现的是浅复制,而不是深复制。也就是说,如果源对象某个属性的值是对象,那么目标对象复制得到的是这个对象的引用。
  2. 一旦遇到同名属性,Object.assign 的处理方法是替换而不是添加。

5.3、常见用途

1. 为对象添加属性

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

2. 为对象添加方法

Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    // todo
  },
  anotherMethod() {

  }
})

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

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 = (target, ...sources) => Object.assign(target, ...sources)

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

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

5. 为属性指定默认值

const DEFAULTS = {
  logLevel: 0,
  outputFormat: 'html'
}

function processContent(options) {
  options = Object.assign({}, DEFAULTS, options)
  console.log(options)
 // ...
}

如果 DEFAULTS 和 options 对象中有同名属性,则 options 的属性会会覆盖 DEFAULTS 的属性值

  • 由于存在深复制的问题,DEFAULTS 对象和 options 对象的所有属性的值都只能是简单类型,而不能指向另一个对象,否则将导致 DEFAULTS 对象的该属性不起作用。

六、属性的可枚举性

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

let obj = {foo: 123}

Object.getOwnPropertyDescriptor(obj, 'foo')
/* {
     configurable: true
     enumerable: true
     value: 123  
     writable: true
  {
*/

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

ES5 有3个 操作会忽略 enumerable 为 false 的属性

  • for...in 循环:只遍历对象自身的和继承的可枚举属性
  • Object.keys():返回对象自身的所有可枚举属性的键名
  • JSON.stringify():只串行化对象自身的可枚举属性

ES6 新增了 1 个操作 Object.assign(),会忽略 enumerable 为 false 的属性,只复制自身的可枚举属性

引入 enumerable 的最初目的就是让某些属性可以规避掉 for...in 操作。比如,对象原型的 toString 方法以及数组的 length 属性

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

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

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

七、属性的遍历

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

以上五种方法遍历对象的属性时都遵守同样的属性遍历次序规则。

  • 首先遍历所有属性名为数值的属性,按照数字排序
  • 其次遍历所有属性名为字符串的属性,按照生成时间排序
  • 最后遍历所有的属性名为Symbol 值的属性,按照生成时间排序。

八、proto属性、Object.setPrototypeOf()、Object.getPrototypeOf()

8.1、proto属性

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

var obj = { /*...*/ }

obj.__proto__

该属性没有写入 ES6 的正文,而是写入了 附录,原因是 proto 前后的双下划线说明,它本质上是一个内部属性,而不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入 ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定要部署。

新的代码最好认为这个属性是不存在的。因为,无论从语义的角度,还是从兼容的角度,都不要使用这个属性,而是使用Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)或 Object.create()(生成操作)代替。

8.2、Object.setPrototypeOf()

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

// 格式
Object.setPrototypeOf(obj, prototype)

// 用法
var o = Object.setPrototypeOf({}, null)

如果第一个参数不是对象,则会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。

Object.setPrototypeOf(1, {}) === 1 // true
Object.setPrototypeOf('foo', {}) === 'foo' // true
Object.setPrototypeOf(true, {}) === true // true

由于 undefined 和 null 无法转为对象,所以如果第一个参数是 undefined 或 null 就会报错。

8.3、Object.getPrototypeOf()

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

Object.getPrototypeOf(obj)

与 Object.setPrototypeOf() 类似

  • 如果参数不是对象,则会被自动转为对象。
  • 如果参数是 undefined 或 null,他们无法转为对象就会报错。

九、Object.keys()、Object.values()、Object.entries()

9.1、Object.keys()

ES5 引进了 Object.keys 方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

var obj = {foo: 'bar', baz: 42}
Object.keys(obj)

ES2017 中有一个提案,其中引入了与 Object.keys 配套的Object.values 和 Object.entries 作为遍历一个对象的补充手段,供 for...of 循环使用。

9.2、Object.values()

Object.values 方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。

var obj = {foo: 'bar', baz: 42}
Object.values(obj)
// ['bar', 42]

下面是 Object.values 的注意点:

  • Object.values 只返回对象自身的可遍历属性
  • Object.values 会过滤属性名为 Symbol 值的属性
  • 如果 Object.values 方法的参数是一个字符串,则会返回各个字符组成的一个数组
  • 如果参数不是对象,Object.values 会先将其转为对象。由于数值和布尔值包装对象都不会为实例添加非继承的属性,所以Object.values会返回空数组。

9.3、Object.entries()

Object.entries 方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组

var obj = {foo: 'baz', baz: 42}
Object.entries(obj)
// [ ["foo", "baz"], ["baz", 42] ]

除了返回值不一样,该方法的行为与 Object.values 基本一致

如果该对象的属性名是一个 Symbol 值,该属性会被忽略

Object.entries({ [Symbol() ]: 123, foo: 'abc'})
// [ ["foo", "abc"] ]

Object.entries 的基本用途是遍历对象的属性

let obj = {one: 1, two: 2}
for(let [k, v] of Object.entries(obj)) {
  console.log(`${JSON.stringify(k)}: ${JSON.stringify(v)}`)
}
// "one": 1
// "two": 2

Object.entries 方法的另一个用处是将对象转为真正的 Map 结构

var obj = {foo: 'bar', baz: 42}
var map = new Map(Object.entries(obj))
map // Map(2) {"foo" => "bar", "baz" => 42}

十、对象的扩展运算符

ES2017 将扩展运算符引入了对象

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

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

以下是对象解构赋值的主要事项:

  • 由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined 或 null 就会报错,因为他们无法转为对象
  • 解构赋值必须是最后一个参数,否则会报错
  • 解构赋值的复制是浅复制,即如果一个键的值是复合类型的值(数组、对象、函数),那么解构值复制的是这个值的引用,而不是这个值的副本。
  • 解构赋值不会复制继承自原型对象的属性

解构赋值的一个用处是扩展某个函数的参数,引入其他操作

function baseFunction({a, b}) {
  // ...
}

function wrapperFunction({x, y, ...restConfig}) {
  // 使用 x 和 y参数进行操作
  // 其余参数传给原始函数
  return baseFunction(...restConfig)
}

函数 wrapperFunction 在 baseFunction 的基础上进行了扩展,能够接受多余的参数并且保留原始函数的行为。

扩展运算符

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

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

这等同于使用 Object.assign 方法

let aClone = { ...a }

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

如果想完整克隆一个对象,还要复制对象原型的属性,可以采用以下方法

// 方法一
const clone1 = {
  __proto__: Object.getPrototypeOf(obj),
  ...obj
}

// 方法二
const clone2 = Object.assign(
  Object.create(Object.getPrototypeOf(obj)),
  obj
)

推荐使用写法二,因为 __proto__属性在非浏览器的环境不一定部署

扩展运算符可用于合并两个对象

let ab = {...a, ...b}
// 等同于
let ab = Object.assign({}, a, b)

  • 如果用户自定义的属性放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖。
    这个特性试验用来修改现有对象的部分属性
let newVersion = {
  ...previousVersion,
  name: 'New Name' // Override the name prototype
}
  • 如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值。
  • 与数组的扩展运算符一样,对象的扩展运算符后面可以有表达式。
  • 如果扩展运算符后面是一个空对象,则没有任何效果
  • 如果扩展运算符的参数是 null 或 undefined,则这两个值会被忽略,不会报错

如果扩展运算符的参数对象之中有取值函数 get,这个函数将会执行。

let a = {}
let runtimeError = {
  ...a,
  ...{
    get x() {
      throw new Error('getter running')
    }
  }
}

以上代码会抛出错误,因为 x 属性被执行了

十一、Object.getOwnPropertyDescriptors()

ES5 的Object.getOwnPropertyDescriptor 方法用来返回某个对象属性的描述对象(descriptor)

var obj = {p: 'a'}

Object.getOwnPropertyDescriptor(obj, 'p')
// {value: "a", writable: true, enumerable: true, configurable: true}

ES2017 引入了 Object.getOwnPropertyDescriptors 方法,返回指定对象所有自身属性(非继承属性)的描述对象

const obj = {
  foo: 123,
  get bar() {
    return 'abc'
  }
}

Object.getOwnPropertyDescriptors(obj)
/*
{
  bar: {get: ƒ, set: undefined, enumerable: true, configurable: true},
  foo: {value: 123, writable: true, enumerable: true, configurable: true}
}
*/

该方法的引入主要是为了解决 Object.assign() 无法正确复制 get 属性 和 set 属性的问题。

const source = {
  set foo(value) {
    console.log(value)
  }
}

const target1 = {}
Object.assign(target1, source)

Object.getOwnPropertyDescriptor(target1, 'foo')
// {value: undefined, writable: true, enumerable: true, configurable: true}

Object.getOwnPropertyDescriptors 方法配合 Object.defineProperties 方法就可以实现正确复制。

const source = {
  set foo(value) {
    console.log(value)
  }
}

const target2 = {}
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source))
Object.getOwnPropertyDescriptor(target2, 'foo')
// {get: undefined, set: ƒ, enumerable: true, configurable: true}

Object.getOwnPropertyDescriptors 方法的另一个用处是,配合 Object.create 方法将对象属性克隆岛一个新对象。这属于浅复制

const clone = Object.create(
  Object.getPrototypeOf(obj), 
  Object.getOwnPropertyDescriptors(obj)
)

// 或者
const shallowClone = obj => Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
)

另外,Object.getOwnPropertyDescriptors 方法可以实现一个对象继承另一个对象。

const obj = Object.create(
  port,
  Object.getOwnPropertyDescriptors({
    foo: 123
  })
)

Object.getOwnPropertyDescriptors 也可以用来实现 Mixin(混入)模式

let mix = object => ({
  with: (...mixins) => mixins.reduce(
    (c, mixin) => Object.create(
      c,
      Object.getOwnPropertyDescriptors(mixin)
    ),
    object
  )
})

let a = {a: 'a'}
let b = {b: 'b'}
let c = {c: 'c'}

let d = mix(c).with(a, b)
d.c // "c"
d.b // "b"
d.a // "a"

上面的代码返回一个新的对象 d,代表了对象 a 和 b 被混入了对象 c 的操作

十二、Null 传导运算符

编程业务中,如果读取对象内部的某个属性,往往需要判断该对象是否存在。这样的一些列操作需要用到大量的短路运算符,现在有一个提案,其中引入了“Null传导运算符”(null propagation operator)?.

“Null 传导运算符” 有 4 种用法

  • obj?.prop:读取对象属性
  • obj?.[expr]:同上
  • func?.(...args):函数或对象方法的调用
  • new C?.(...args):构造函数的调用

传导运算符之所以写成 obj?.prop,而不是 obj?prop,是为了方便编译器能够区分三元运算符 ?:

// 如果 a 是null 或 undefined,返回 undefined
// 否则 返回 a.b.c() .d
a?.b.c().d

// 如果a是 null 或 undefined,下面的语句不产生任何效果
// 否则执行 a.b = 42
a?.b = 42

// 如果 a 是 null 或 undefined,下面的语句不产生任何效果
delete a?.b

你可能感兴趣的:(ES6入门 ___ 对象的扩展)