【07】ES6:对象的扩展

一、对象字面量语法扩展

1、属性简写

当属性名称和属性值的变量名称相同时,可以省略冒号的变量名称。

const foo = 'bar'

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

baz // { foo: 'bar' }
function f(x, y) {
	return { x, y }
}
// 等同于
function f(x, y) {
	return { x: x, y: y }
}

f(1, 2) // Object { x: 1, y: 2 }

2、方法简写

es6 可以省略冒号和函数关键字

// es5
const o = {
	method: function() {
		return 'Hello!'
	}
}

// es6
const o = {
	method() {
		return 'Hello!'
	}
}
let birth = '2000/01/01'

const Person = {
	name: '张三',
	// 等同于 birth: birth
	birth,
	// 等同于 hello: function ()...
	hello() { console.log('我的名字是', this.name) }
}

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

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
}

3、计算属性名

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

// 方法一:标识符作为属性名
obj.foo = true

// 方法二:表达式作为属性名
obj['a' + 'bc'] = 123

ES6 允许字面量定义对象时,属性名可以计算,用方括号表达式法进行计算,方括号中的值为表达式,最终结果为表达式计算的结果。

let lastWord = 'last word'

const obj = {
	'first word': 'hello',
	[lastWord]: 'world',
	['h' + 'ello']() {
		return 'hi'
	}
}

obj['first word'] // 'hello'
obj[lastWord] // 'world'
obj['last word'] // 'world'
obj.hello() // hi

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

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

// 正确
const foo = 'bar'
const baz = { [foo]: 'abc'}

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

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

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

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

二、super 关键字

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

const proto = {
	foo: 'hello'
}

const obj = {
	foo: 'world',
	find() {
		return super.foo
	}
}

Object.setPrototypeOf(obj, proto)
obj.find() // 'hello'

注意,super 关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

// 报错:super 用在属性里面
const obj = {
	foo: super.foo
}

// 报错:super 用在一个函数里面,然后赋值给 foo 属性
const obj = {
	foo: () => super.foo
}

// 报错:super 用在一个函数里面,然后赋值给 foo 属性
const obj = {
	foo: function () {
		return super.foo
	}
}

上面三种 super 的用法都会报错,因为对于 JavaScript 引擎来说,这里的 supe r都没有用在对象的方法之中。第一种写法是 super 用在属性里面,第二种和第三种写法是 super 用在一个函数里面,然后赋值给 foo 属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。

JavaScript 引擎内部,super.foo 等同于 Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。

const proto = {
	x: 'hello',
	foo() {
		console.log(this.x)
	}
}

const obj = {
	x: 'world',
	foo() {
		super.foo()
	}
}

Object.setPrototypeOf(obj, proto)

obj.foo() // 'world'

上面代码中,super.foo 指向原型对象 proto 的 foo 方法,但是绑定的 this 却还是当前对象 obj,因此输出的就是 world 。

三、对象的扩展运算符

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

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

1、基本语法

对象不能直接展开,必须在 {} 中展开。

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

数组是特殊的对象,所以对象的扩展运算符也可以用于数组。

let foo = { ...['a', 'b', 'c'] }
foo // { 0: 'a', 1: 'b', 2: 'c' }

如果扩展运算符后面是一个空对象,则没有任何效果。

{ ...{}, a: 1 } // { a: 1 }

如果扩展运算符后面不是对象,则会自动将其转为对象。

// 等同于 { ...Object(1) }
{ ...1 } // {}  

// 等同于 { ...Object(true) }
{ ...true } // {}

// 等同于 { ...Object(undefined) }
{ ...undefined } // {}

// 等同于 { ...Object(null) }
{ ...null } // {}

如果扩展运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象。

{ ...'hello' } // { 0: 'h', 1: 'e', 2: 'l', 3: 'l', 4: '0' }

对象的扩展运算符,只会返回参数对象自身的、可枚举的属性,这一点要特别小心,尤其是用于类的实例对象时。

// m() 方法定义在 C 的原型对象上,对实例 c 进行扩展时,不会返回 c 的方法 c.m()
class C {
	p = 12
	m() {}
}

let c = new C()
let clone = { ...c }

clone.p // ok
clone.m() // 报错

如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法。

// 写法一:不推荐,__proto__ 属性在非浏览器的环境不一定部署。
const clone1 = {
	__proto__: Object.getPrototypeOf(obj)
	...obj
}

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

// 写法三
const clone3 = Object.create(
	Object.getPrototypeOf(obj),
	Object.getOwnPropertyDescriptors(obj)
)

对象的扩展运算符等同于使用 Object.assign() 方法。

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

2、用途

(1)复制对象

const a = { x: 1, y: 2 }
const c = { ...a }
console.log(c, c === a) // { x: 1, y: 2 } false 浅拷贝

(2)合并对象

const apple = {
	color: '红色',
    shape: '球形',
    taste: '甜'
}

const pen = {
    color: '黑色',
    shape: '圆柱形',
    use: '写字'
}

// 新对象拥有全部属性,相同属性,后者覆盖前者
{ ...apple, ...pen } // { color: '黑色', shape: '圆柱形', taste: '甜', use: '写字' }
{ ...pen, ...apple } // { color: '红色', shape: '球形', use: '写字', taste: '甜' }

// 等同于
Object.assign({}, apple, pen)

(3)与解构赋值结合

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

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

// 浅拷贝
let obj = { a: { b: 1 } }
let { ...x } = obj
obj.a.b = 2
x.a.b // 2

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

let { ...z } = null // 运行时错误
let { ...z } = undefined // 运行时错误

扩展运算必须是最后一个参数,否则会报错。

let { ...x, y, z } = someObject // 句法错误
let { x, ...y, ...z } = someObject // 句法错误

另外,扩展运算符的解构赋值,不能复制继承自原型对象的属性。

// 对象 o3 复制了 o2,但是只复制了 o2 自身的属性,没有复制它的原型对象 o1 的属性。
let o1 = { a: 1 }
let o2 = { b: 2 }
o2.__proto__ = o1
let { ...o3 } = o2
o3 // { b: 2 }
o3.a // undefined

变量 x 是单纯的解构赋值,所以可以读取对象 o 继承的属性;变量 y 和 z 是扩展运算符的解构赋值,只能读取对象 o 自身的属性,所以变量 z 可以赋值成功,变量y取不到值。

const o = Object.create({ x: 1, y: 2 })
o.z = 3

let { x, ...newObj } = o
let { y, z } = newObj
x // 1
y // undefined
z // 3

// ES6 规定,变量声明语句之中,如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式。以下写法会报错。
let { x, ...{ y, z } } = o
// SyntaxError: ... must be followed by an identifier in declaration contexts

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

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

上面代码中,原始函数 baseFunction 接受 a 和 b 作为参数,函数 wrapperFunction 在 baseFunction 的基础上进行了扩展,能够接受多余的参数,并且保留原始函数的行为。

四、对象的新增方法

1、Object.is()

ES5 中比较两个值是否相等,只有两个运算符:

运算符 说明
相等运算符(==) 弱类型相等运算符,它比较两个值是否相等,如果两个值类型不同,会自动进行类型转换后再比较。

缺点:会自动转换数据类型。
严格相等运算符(===) 严格类型相等运算符,它比较两个值的类型和值是否都相等,如果类型不同或值不同,返回 false。

缺点:NaN 不等于自身,以及 +0 等于 -0。

ES 新增的 Object.is 方法,与严格相等运算符的行为基本一致,并解决了严格相等运算符存在的问题( +0 不等于 -0、NaN 等于自身)。

Object.is('foo', 'foo')// true
// ==、===、Object.is() 在比较引用类型比较时,比较的是内存的地址,不是内容。
Object.is({}, {}) // false 

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

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

了解:ES5 可以通过下面的代码,实现 Object.is 方法。

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

2、Object.assign()

Object.assign() 方法用于对象的合并,用于将一个或多个源对象的所有属性复制到目标对象。

语法说明:

Object.assign(target, ...sources)
参数 说明
target 目标对象,接收复制的属性。
sources 一个或多个源对象,从中复制属性到目标对象。
/* 基本用法 */
const target = { a: 1 }
const source1 = { b: 2 }
const source2 = { c: 3 }
Object.assign(target, source1, source2)
target // {a:1, b:2, c:3}

/* 同名属性覆盖 */
const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source) // { a: { b: 'hello' } }

/* 返回值是目标对象(如果该参数不是对象,则会先转成对象,然后返回。)*/
const obj = { a: 1 }
Object.assign(obj) === obj // true
typeof Object.assign(2) // 'object'

/* 基本数据类型作为源对象:先转换成对象,再合并*/
Object.assign({}, 'str') // { '0': 's', '1': 't', '2': 'r' }
Object.assign({}, 1) // {}
Object.assign({}, true) // {}
Object.assign({}, undefined) // {}
Object.assign({}, null) // {}

/* 数组的处理:将数组视为对象 */
Object.assign([1, 2, 3], [4, 5]) // [4, 5, 3] (把数组视为属性名为 0、1、2 的对象,源数组的 0 号属性 4 覆盖了目标数组的 0 号属性 1。)

/* 浅拷贝 */
const obj1 = { a: { b: 1 } }
const obj2 = Object.assign({}, obj1)
obj1.a.b = 2
obj2.a.b // 2

/* 不拷贝不可枚举的属性 */
const v1 = 'abc'
const v2 = true
const v3 = 10
const obj = Object.assign({}, v1, v2, v3)
obj // { '0': 'a', '1': 'b', '2': 'c' }

/* 不拷贝源对象的原型链上的属性 */
const parent = { parentProp: 'I am from the parent' }
// 定义一个子对象,并将父对象作为其原型
const child = Object.create(parent, { 
  childProp: 'I am from the child'
})
Object.assign({}, child) // { childProp: 'I am from the child' }

注意点:

Object.assign() 只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。

如果目标对象已经具有与源对象相同的属性名,则会覆盖目标对象的属性值。

Object.assign() 是浅拷贝,复制的是引用而不是对象本身。

返回值是目标对象。

常见用途:

为对象添加属性

// 通过 Object.assign() 方法,将 x 属性和 y 属性添加到 Point 类的对象实例
class Point {
	constructor(x, y) {
		Object.assign(this, {x, y})
	}
}

为对象添加方法

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

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

克隆对象

// 只能克隆原始对象自身的值,不能克隆它继承的值
function clone(origin) {
	return Object.assign({}, origin)
}

合并多个对象

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

const merge = (...sources) => Object.assign({}, ...sources) // 合并后返回一个新对象

为属性指定默认值

// 默认值(由于存在浅拷贝的问题,DEFAULTS 对象和 options 对象的所有属性的值,最好都是简单类型,不要指向另一个对象。)
const DEFAULTS = {
	logLevel: 0,
	outputFormat: 'html'
}

function processContent(options) {
	options = Object.assign({}, DEFAULTS, options) // options 对象是用户提供的参数
	// ...
}

3、Object.getOwnPropertyDescriptors()

语法说明:

Object.getOwnPropertyDescriptor() 方法

ES5 引入,获取单个属性的描述符,会返回某个对象属性的描述对象。

Object.getOwnPropertyDescriptors() 方法

ES 2017 引入,获取所有属性的数据描述符 ,返回指定对象所有自身属性(非继承属性)的描述对象。

// 返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。
const obj = {
	foo: 123,
	get bar() { return 'abc' }
}

Object.getOwnPropertyDescriptors(obj)
/* {
    foo: {
        value: 123, // 属性的值
        writable: true, // 属性的值是否可被改变
        enumerable: true, // 属性的值是否可被枚举
        configurable: true // 描述符本身是否可被修改,属性是否可被删除
    },
    bar: {
        get: [Function: get bar], // 获取该属性的访问器函数(getter)。如果没有访问器, 该值为undefined。
        set: undefined, // 获取该属性的设置器函数(setter)。 如果没有设置器, 该值为undefined
        enumerable: true,
        configurable: true
    }
} */

使用 ES5 的 Object.getOwnPropertyDescriptor() 方法实现 Object.getOwnPropertyDescriptors() 方法。

function getOwnPropertyDescriptors(obj) {
	const result = {}
	for (let key of Reflect.ownKeys(obj)) {
		result[key] = Object.getOwnPropertyDescriptor(obj, key)
	}
	return result
}

常见用途:

拷贝 get 属性和 set 属性

Object.assigns方法无法正确拷贝get属性和set属性。

const source = {
	set foo(value) { // 赋值函数
		console.log(value)
	}
}

const target1 = {}
// Object.assign 方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法。
Object.assign(target1, source)

Object.getOwnPropertyDescriptor(target1, 'foo')
/* {
    value: undefined, // Object.assign 方法拷贝后属性值变为 undefined
    writable: true,
    enumerable: true,
    configurable: true
} */

Object.getOwnPropertyDescriptors() 方法配合 Object.defineProperties() 方法,就可以实现正确拷贝。

const source = {
	set foo(value) { // 赋值函数
		console.log(value)
	}
}

const target2 = {}
// Object.defineProperties() 方法将直接在对象上定义一个或多个新的属性或修改现有属性,并返回该对象。
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source))
Object.getOwnPropertyDescriptor(target2, 'foo')
/* {
    get: undefined,
    set: [Function: set foo],
    enumerable: true,
    configurable: true
} */

// 拷贝方法封装
const shallowMerge = (target, source) => Object.defineProperties(
	target,
	Object.getOwnPropertyDescriptors(source)
)

配合 Object.create() 方法,将对象属性克隆到一个新对象(浅拷贝)

/* Object.create(proto, propertiesObject)
	说明:以一个现有对象作为原型,创建一个新对象。
	参数:
		1. proto,新创建对象的原型对象。
		2. propertiesObject 可选,如果该参数被指定且不为 undefined,则该传入对象可枚举的自有属性将为新创建的对象添加具有对应属性名称的属性描述符。这些属性对应于 Object.defineProperties() 的第二个参数。
	返回值:根据指定的原型对象和属性创建的新对象。
*/

/* Object.getPrototypeOf()
	说明:返回指定对象的原型(即内部 [[Prototype]] 属性的值)。
	返回值:要返回其原型的对象。
*/
const clone = Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))

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

实现一个对象继承另一个对象

// 1. __proto__写法
const obj = {
	__proto__: prot,
	foo: 123,
}

// 2.  Object.create 写法
const obj = Object.create(prot)
obj.foo = 123

// 3. Object.assign + Object.create 写法
const obj = Object.assign(
	Object.create(prot),
	{ foo: 123 }
)

// 4. Object.create + Object.getOwnPropertyDescriptors 写法
const obj = Object.create(
	prot,
	Object.getOwnPropertyDescriptors({
		foo: 123
	})
)

4、proto 属性,Object.setPrototypeOf(),Object.getPrototypeOf()

名称 说明
proto 属性 用来读取或设置当前对象的原型对象(prototype)。内部属性,不推荐使用。
Object.setPrototypeOf() 用来设置一个对象的原型对象(prototype),返回参数对象本身。
Object.setPrototypeOf(object, prototype)
Object.setPrototypeOf() 用来读取一个对象的原型对象(prototype)。

__proto__本质上是一个内部属性,而不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入了 ES6。推荐使用 Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。

// __proto__写法
const obj = {
	method: function() { ... }
}
obj.__proto__ = someOtherObj

// Object.create 写法
var obj = Object.create(someOtherObj)
obj.method = function() { ... }

// Object.setPrototypeOf 写法
Object.setPrototypeOf(obj, someOtherObj)

5、Object.keys(),Object.values(),Object.entries()

方法 说明
Object.keys 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。
Object.values 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。
Object.entries 返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。
// 基本用法
const person = {
	name: 'Alex',
	age: 18
}
// 返回键数组
console.log(Object.keys(person)) // [ 'name', 'age' ]
// 返回值数组
console.log(Object.values(person)) // [ 'Alex', 18 ]
// 返回键值二维数组
console.log(Object.entries(person)) // [ [ 'name', 'Alex' ], [ 'age', 18 ] ]


// 与数组类似方法的区别
console.log([1, 2].keys()) // Object [Array Iterator] {}
console.log([1, 2].values()) // Object [Array Iterator] {}
console.log([1, 2].entries()) // Object [Array Iterator] {}
// 数组的 keys()、values()、entries() 等方法是实例方法,返回的都是 Iterator
// 对象的 Object.keys()、Object.values()、Object.entries() 等方法是构造函数方法,返回的是数组


// 应用(使用 for...of 循环遍历对象)
const person = {
	name: 'Alex',
	age: 18
}
for (const key of Object.keys(person)) {
	console.log(key)
}
// name
// age
for (const value of Object.values(person)) {
    console.log(value)
}
// Alex
// 18
for (const entries of Object.entries(person)) {
    console.log(entries)
}
// [ 'name', 'Alex' ]
// [ 'age', 18 ]
for (const [key, value] of Object.entries(person)) {
    console.log(key, value)
}
// name Alex
// age 18

// Object.keys()/values()/entires() 并不能保证顺序一定是你看到的样子,这一点和 for in 是一样的
// 如果对遍历顺序有要求那么不能用 for in 以及这种方法,而要用其他方法

6、Object.hasOwn()

JavaScript 对象的属性分成两种:自身的属性和继承的属性。

对象实例有一个 hasOwnProperty() 方法,可以判断某个属性是否为原生属性。

ES2022 在 Object 对象上面新增了一个静态方法 Object.hasOwn(),也可以判断是否为自身的属性。

const foo = Object.create({ a: 123 })
foo.b = 456

Object.hasOwn(foo, 'a') // false
Object.hasOwn(foo, 'b') // true

Object.hasOwn() 的一个好处是,对于不继承 Object.prototype 的对象不会报错,而 hasOwnProperty() 是会报错的。

// Object.create(null)返回的对象obj是没有原型的,不继承任何属性,
const obj = Object.create(null)

obj.hasOwnProperty('foo') // 报错
Object.hasOwn(obj, 'foo') // false

你可能感兴趣的:(ES,es6,前端,ecmascript)