1. 属性的简介表示法
ES6
允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
复制 const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
变量foo
直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。
函数也可以简写
复制 let birth = '2000/01/01';
const Person = {
name: '张三',
//等同于birth: birth
birth,
// 等同于hello: function ()...
hello() { console.log('我的名字是', this.name); }
};
属性的赋值器(setter)和取值器(getter),事实上也是采用这种写法。
复制 const cart = {
_wheels: 4,
get wheels () {
return this._wheels;
},
set wheels (value) {
if (value < this._wheels) {
throw new Error('数值太小了!');
}
this._wheels = value;
}
}
简洁写法在打印对象时也很有用。
复制 let user = {
name: 'test'
};
let foo = {
bar: 'baz'
};
console.log(user, foo)
// {name: "test"} {bar: "baz"}
console.log({user, foo})
// {user: {name: "test"}, foo: {bar: "baz"}}
上面代码中,console.log
直接输出user
和foo
两个对象时,就是两组键值对,可能会混淆。把它们放在大括号里面输出,就变成了对象的简洁表示法,每组键值对前面会打印对象名,这样就比较清晰了。
注意,简写的对象方法不能用作构造函数,会报错。
复制 const obj = {
f() {
this.foo = 'bar';
}
};
new obj.f() // 报错
2. 属性名表达式
1)定义属性名
JavaScript 定义对象的属性,有两种方法。
复制 // 方法一
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;
上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。
ES6
允许字面量定义对象时,把表达式放在方括号内。
复制 let propKey = 'foo';
let obj = {
};
// {foo: true, abc: 123}
复制 let lastWord = 'last word';
const a = {
'first word': 'hello',
};
a['first word'] // "hello"
a['firstWord'] // undefined
a[lastWord] // "world"
a['last word'] // "world"
属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object]
,这一点要特别小心。
复制 const keyA = {a: 1};
const keyB = {b: 2};
const myObject = {
};
myObject // Object {[object Object]: "valueB"}
[keyA]
和[keyB]
得到的都是[object Object]
,所以[keyB]
会把[keyA]
覆盖掉,而myObject
最后只有一个[object Object]
属性。
2)定义方法名
复制 let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
3. 方法的 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
。
复制 (new Function()).name // "anonymous"
var doSomething = function() {
// ...
};
doSomething.bind().name // "bound doSomething"
4. 属性的可没举性和遍历
1)可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。
复制 let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
引入“可枚举”(enumerable
)这个概念的最初目的,就是让某些属性可以规避掉for...in
操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的toString
方法,以及数组的length
属性,就通过“可枚举性”,从而避免被for...in
遍历到。
复制 Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable
// false
Object.getOwnPropertyDescriptor([], 'length').enumerable
// false
ES6
规定,所有 Class
的原型的方法都是不可枚举的。
复制 Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false
操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in
循环,而用Object.keys()
代替。
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 或字符串,也不管是否可枚举。
都遵守同样的属性遍历的次序规则。
最后遍历所有 Symbol 键,按照加入时间升序排列。
复制 Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
5. super 关键字
this
关键字总是指向函数所在的当前对象,ES6
又新增了另一个类似的关键字super
,指向当前对象的原型对象。
复制 const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
上面代码中,对象obj.find()
方法之中,通过super.foo
引用了原型对象proto
的foo
属性。
注意,super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
复制 // 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
上面三种super
的用法都会报错,因为对于 JavaScript 引擎来说,这里的super
都没有用在对象的方法之中。第一种写法是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"
6. 对象的扩展运算符
1)解构赋值
对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。
复制 let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined
或null
,就会报错,因为它们无法转为对象。
复制 let { ...z } = null; // 运行时错误
let { ...z } = undefined; // 运行时错误
解构赋值必须是最后一个参数,否则会报错。
复制 let { ...x, y, z } = someObject; // 句法错误
let { x, ...y, ...z } = someObject; // 句法错误
注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
复制 let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
扩展运算符的解构赋值,不能复制继承自原型对象的属性。
复制 let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined
2)扩展运算符
对象的扩展运算符(...
)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
复制 let z = { a: 3, b: 4 };
let n = { ...z };
n // { 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: "o"}
对象的扩展运算符等同于使用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
);
// 写法三
const clone3 = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
)
上面代码中,写法一的__proto__
属性在非浏览器的环境不一定部署,因此推荐使用写法二和写法三。
扩展运算符可以用于合并两个对象。
复制 let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);
如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。这用来修改现有对象部分的属性就很方便了。
复制 let newVersion = {
...previousVersion,
name: 'New Name' // Override the name property
};
如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值。
复制 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);
与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式。
复制 const obj = {
...(x > 1 ? {a: 1} : {}),
b: 2,
};
扩展运算符的参数对象之中,如果有取值函数get
,这个函数是会执行的。
复制 // 并不会抛出错误,因为 x 属性只是被定义,但没执行
let aWithXGetter = {
...a,
get x() {
throw new Error('not throw yet');
}
};
// 会抛出错误,因为 x 属性被执行了
let runtimeError = {
...a,
...{
get x() {
throw new Error('throw now');
}
}
};
7. 链判断运算符 ?. *
编程实务中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。
比如,要读取message.body.user.firstName
,安全的写法是写成下面这样。
复制 const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
复制 const firstName = message?.body?.user?.firstName || 'default';
上面代码使用了?.
运算符,直接在链式调用的时候判断,左侧的对象是否为null
或undefined
。如果是的,就不再往下运算,而是返回undefined
。
链判断运算符有三种用法。
func?.(...args)
// 函数或对象方法的调用
下面是判断对象方法是否存在,如果存在就立即执行的例子。
上面代码中,obj.fn如果有定义,就会调用该方法,否则直接返回
undefined`。
对于那些可能没有实现的方法,这个运算符尤其有用。
复制 if (myForm.checkValidity?.() === false) {
// 表单校验失败
return;
}
上面代码中,老式浏览器的表单可能没有checkValidity
这个方法,这时?.
运算符就会返回undefined
,判断语句就变成了undefined === false
,所以就会跳过下面的代码。
下面是这个运算符常见的使用形式,以及不使用该运算符时的等价形式。
复制 a?.b
// 等同于
a == null ? undefined : a.b
a?.[x]
// 等同于
a == null ? undefined : a[x]
a?.b()
// 等同于
a == null ? undefined : a.b()
a?.()
// 等同于
a == null ? undefined : a()
使用这个运算符,有几个注意点。
1)断路机制
复制 a?.[++x]
// 等同于
a == null ? undefined : a[++x]
上面代码中,如果a
是undefined
或null
,那么x
不会进行递增运算。也就是说,链判断运算符一旦为真,右侧的表达式就不再求值。
2)delete 运算符
复制 delete a?.b
// 等同于
a == null ? undefined : delete a.b
上面代码中,如果a
是undefined
或null
,会直接返回undefined
,而不会进行delete
运算。
3)括号的影响
如果属性链有圆括号,链判断运算符只对圆括号内部有影响。
复制 (a?.b).c
// 等价于
(a == null ? undefined : a.b).c
一般来说,使用?.
运算符的场合,不应该使用圆括号。
4)报错场合
以下写法是禁止的,会报错。
复制 // 构造函数
new a?.()
new a?.b()
// 链判断运算符的右侧有模板字符串
a?.`{b}`
a?.b`{c}`
// 链判断运算符的左侧是 super
super?.()
super?.foo
// 链运算符用于赋值运算符左侧
a?.b = c
5)右侧不得为十进制数值
为了保证兼容以前的代码,允许foo?.3:0
被解析成foo ? .3 : 0
,因此规定如果?.
后面紧跟一个十进制数字,那么?.
不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。
8. Null 判断运算符 ?? *
读取对象属性的时候,如果某个属性的值是null
或undefined
,有时候需要为它们指定默认值。常见做法是通过||
运算符指定默认值。
复制 const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;
上面的三行代码都通过||
运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为null
或undefined
,默认值就会生效,但是属性的值如果为空字符串或false
或0
,默认值也会生效。
复制 const headerText = response.settings.headerText ?? 'Hello, world!';
const animationDuration = response.settings.animationDuration ?? 300;
const showSplashScreen = response.settings.showSplashScreen ?? true;
这个运算符的一个目的,就是跟链判断运算符?.
配合使用,为null
或undefined
的值设置默认值。
复制 const animationDuration = response.settings?.animationDuration ?? 300;
这个运算符很适合判断函数参数是否赋值。
复制 function Component(props) {
const enable = props.enabled ?? true;
// …
}
//等同于
function Component(props) {
const {
enabled: enable = true,
} = props;
// …
}
??
有一个运算优先级问题,它与&&
和||
的优先级孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。
复制 // 报错
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs
必须加入表明优先级的括号。
复制 (lhs && middle) ?? rhs;
lhs && (middle ?? rhs);
(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);
(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);
(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);
9. 操作符反回值 && ||
1)&&
(1)真真反后
复制 let a = 1 && 2
// a = 2
(2)假假反前
复制 let a = 0 && null
// a = 0
(3)真假反假
复制 let a = 0 && 1
// a = 0
let a = 1 && 0
// a = 0
2)||
(1)真真反前
复制 let a = 1 || 2
// a = 1
(2)假假反后
复制 let a = 0 || null
// a = null
(3)真假反真
复制 let a = 0 && 1
// a = 1
let a = 1 && 0
// a = 1