在JavaScript中,几乎每一个值都是某种特定类型的对象,于是ES6也着重提升了对象的功能性。上周花了一周的时间了解了JavaScript中的对象相关的知识,对于ES6中有关于对象的扩展功能并不太了解。今天开始就来简单的了解和学习有关于ES6中对象的扩展功能。
ES6通过多种方式来加强对象的使用,通过简单的语法扩展,提供了更多操作对象及对象交互的方法。接下来的内容就是有关于这些知识的整理。
对象类别
在浏览器这样的执行环境中,对象没有统一的标准,在标准中又使用不同的术语描述对象,ES6规范清晰定义了每一个类别的对象。总而言之,理解这些术语对象这门语言来说非常重要。在ES6中的对象类别主要分为:
- 普通对象(Ordinary):具有JavaScript对象所有的默认内部行为
- 特殊对象(Exotic):具有某些与默认行为不符的内部行为
- 标准对象(Standard):ES6规范中定义的对象,例如
Array
、Date
等,标准对象既可以是普通对象,也可以是特殊对象 - 内建对象:脚本开始执行时存在于JavaScript执行环境中的对象,所有标准对象都是内建对象
对象字面量语法扩展
JavaScript中的字面量模式更加简洁、有表现力,而且在定义对象时不容易出错。在JavaScript里面,字面量包括:
- 字符串字面量(String Literal):比如
hello world
- 数组字面量(Array Literal):比如
['w3cplus', 7, 'FE']
- 对象字面量(Object Literal):比如
{name:'w3cplus', age: 7}
- 函数字面量(Function Literal):比如
tell:function(){console.log(name)}
,其中function(){console.log(name)}
被认为是函数字面量
不过我们要聊的只是对象字面量。
对象字面量
我们可以将JavaScript中的对象简单地理解为名值对组成的散列表(Hash Table,也叫哈希表)。在其他编程语言中被称作关联数组。其中的值可以是原始值,也可以是对象。不管是什么类型,它们都是属性(Property),属性值同样可以是函数,这时属性就被称为方法(Method)。
JavaScript中自定义的对象(用户定义的本地对象)任何时候都是可变的。内置本地对象的属性也是可变的。你可以先创建一个空对象,然后在需要时给它添加功能。对象字面量写法是按需创建对象的一种理想方式。比如:
var person = {};
person.name = 'w3cplus';
person.age = 7;
person.job = 'FE';
person.sayName = function() {
console.log(this.name)
}
这里创建了一个person
对象,动态给这个对象添加了三个属性name
、age
和job
,另外添加了一个sayName()
方法。
但每次创建空对象并不是必须的,对象字面量模式可以直接在创建对象时添加功能。就像下面的示例:
var person = {
name: 'w3cplus',
age: 7,
job: 'FE,
sayName: function() {
console.log(this.name)
}
}
如果你从来没有接触过对象字面量的写法,可能会感觉怪怪的。但越到后来你就会越喜欢它。本质上讲,对象字面量语法包括:
- 将对象主体包含在一对大括号内
{}
- 对象内的属性或方法之间使用逗号分隔
- 属性名和值之间使用冒号分隔
上面是有关于ES5中对象字面量相关知识点,而在ES6中,通过下面的几种语法,让对象字面量变得更强大,更简洁。
属性初始值的简写
在ES5及更早的版本中,对象字面量只是简单的键值对集合,这意味着初始化属性值时会有一些重复:
function person(name, age, job) {
return {
name: name,
age: age,
job: job
}
}
person()
函数创建了一个对象,这个对象的属性名和函数的参数相同,在返回的结果中name
、age
和job
分别重复了两遍,只是其中一个是对象属性的名称,另外一个是对象属性赋值的变量。
在ES6中,当一个对象的属性名和本地变量同名时,不必再写冒号:
和值,简单地只写属性名即可:
function person(name, age, job) {
return {
name,
age,
job
}
}
当对象字面量里只有一个属性的名称时,JavaScript引擎会在可访问的作用域中查找其同名的变量;如果找到,则该变量的值就会被赋值给对象字面量里的同名属性。比如在上面的示例代码中,对象的字面量的属性name
被赋值为局部变量name
的值。
对象方法简写
在ES6中,除了对象的属性可以简写之外,对象中的方法也可以简写。在ES5中,如果为对象添加方法,必须通过指定名称并完整定义函数来实现,比如:
var person = {
name: 'w3cplus',
sayName: function() {
console.log(this.name)
}
}
在ES6中,通过省略冒号:
和function
关键词,使对象中的语法变得更加简洁。所以上面的示例可以修改成:
var person = {
name: 'w3cplus',
sayName() {
console.log(this.name)
}
}
在这个示例中,person
对象中创建一个sayName()
方法,该属性被赋值为一个匿名函数表达式,它拥有在ES5中定义的对象方法所具有的全部特性。二者唯一的区别是,简写方法可以使用super
关键词。
可计算属性名
在JavaScript中,可以通过.
和[]
两种方式设置和访问对象中的属性名:
// 设置对象的属性
var person = new Object();
person.name = 'w3cplus';
person['first name'] = 'damo';
console.log(person);
// 访问对象中的属性
var person = {
'first name': 'w3cplus',
age: 7
}
console.log(person['age']); // => 7
console.log(person['first name']); // => w3cplus
.
运算符具有很大的局限性,比如上面示例中first name
这种属性只能通过[]
方式来设置或者访问。中括号的方式允许我们使用变量或者在使用标识符时会导致语法错误的字符串直接变量来定义属性。
var person = {};
var lastName = 'last name';
person['first name'] = 'w3cplus';
person[lastName] = 'damo';
console.log(person['first name']); // => w3cplus
console.log(person[lastName]); // => damo
这两种方式只能通过中括号的方式来定义的。在ES5中,你可以在对象字面量中使用字符串字面量作为属性,如:
var person = {
'first name': w3cplus
}
console.log(person['first name']); // => w3cplus
这种模式仅适用于属性名提前已知或可被字符串字面量表示的情况。然而当一个属性名存在一个变量中或者需要计算时,在ES5中是无法使用对象字面量是无法定义属性的。但在ES6中,可在对象字面量中使用可计算属性名称,其语法与引用对象实例的可计算属性名相同,也是使用中括号[]
。比如:
let lastName = 'last name';
let person = {
'first name': 'w3cplus',
[lastName]: 'damo'
}
console.log(person['first name']); // => w3cplus
console.log(person[lastName]); // => damo
在对象字面量中使用方括号表示的该属性名称是可计算的,它的内容将被求值并被最终转化为一个字符串,因而同样可以使用表达式作为属性的可计算名称,如:
var suffix = 'name';
ver person = {
['first' + suffix]: 'w3cplus',
['last' + suffix]: 'damo'
}
console.log(person['first name']); // => w3cplus
console.log(person['last name']); // => damo
上面示例中[]
表达式计算了来的字符串分别是first name
和last name
,然后他们可以用于属性引用。任何可用于对象实例括号记法的属性名,也可以作为字面量中的计算属性名。
新增方法
在ES6中,全局Object
对象上引入了一些新方法。比如Object.is()
和Object.assign()
。
Object.is()
在JavaScript中常常喜欢使用==
或者===
来比较两个值,许多开发者更趋向于使用===
,从而避免在比较时触发强制类型转换的行为。但事实上,即使使用===
来给两个值做比较也并不完全准确。比如,在JavaScript中+0
和-0
表示为两个完全不同的实体,而如果使用===
进行比较,得到的结果是true
;而对于NaN === NaN
的返回值则是false
,此时需要使用isNaN()
方法才能检测NaN
。
在ES6中引入了Object.is()
方法来弥补===
不准确运算。这个方法接受两个参数,如果这两个参数类型相同且具有相同的值,则返回true
:
console.log(+0 == -0); // => true
console.log(+0 === -0); // => true
console.log(Object.is(+0, -0)); // => false
console.log(NaN == NaN); // => false
console.log(NaN === NaN); // => false
console.log(Object.is(NaN, NaN)); // => true
console.log(5 == 5); // => true
console.log(5 == '5'); // => true
console.log(5 === 5); // => true
console.log(5 === '5'); // => false
console.log(Object.is(5, 5)); // => true
console.log(Object.is(5,'5')); // => false
对于Object.is()
方法来说,其运行结果在大部分情况下与===
相同,唯一的区别在于:
+0
不等于-0
NaN
等于NaN
Object.assign()
在ES5或以下的一些版本,一个对象从另一个对象中接收属性和方法(类似复制对象)。在很多JavaScript库都有一个类似下面的mixin()
函数:
function mixin(receiver, supplier) {
Object.keys(supplier).forEach(function(key){
receiver[key] = supplier[key];
});
return receiver;
}
mixin()
函数遍历supplier
对象的自有属性,并将其拷贝到receiver
。这就使得receiver
没有通过继承就获得了新的行为。例如:
function EventTarget() {
// ...
}
EventTarget.prototype = {
constructor: EventTarget,
emit: function () {
// ...
},
on: function () {
// ...
}
}
var myObject = {};
mixin(myObject, EventTarget.prototype);
myObject.emit('somethingChanged');
在这个例子中,myObject
从EventTarget.prototype
接收了新的行为。
为此在ES6中添加了Object.assign()
,它和mixin()
的行为一样。但不同之处在于,mixin()
使用赋值运算符=
来拷贝,它不能拷贝访问属性accessor properties
到接受者作为访问属性。Object.assign()
是可以做到这点的。
我们可以使用Object.assign()
重写上面的mixin()
函数:
function EventTarget() {
// ...
}
EventTarget.prototype = {
constructor: EventTarget,
emit: function () {
// ...
},
on: function () {
// ...
}
}
var myObject = {}
Object.assign(myObject, EventTarget.prototype);
myObject.emit('somethingChanged');
Object.assign()
可以接受任意多个提供属性的对象,接收者则按顺序从提供者接收属性,这可能会导致第二个提供者会覆盖第一个提供者提供给接收者的属性。
var receiver = {};
Object.assign(receiver, {
type: "js",
name: "file.js"
}, {
type: "css"
}
);
console.log(receiver.type); // => "css"
console.log(receiver.name); // => "file.js"
下面再看看Object.assign()
用于访问属性的例子:
var receiver = {},
supplier = {
get name() {
return "file.js"
}
};
Object.assign(receiver, supplier);
var descriptor = Object.getOwnPropertyDescriptor(receiver, "name");
console.log(descriptor.value); // => "file.js"
console.log(descriptor.get); // => undefined
接下来来看看Object.assign()
方法一些常用的场景。
Object.assign()
方法用于对象的合并,将源对象的所有可枚举属性复制到目标对象:
var targetObj = {
a: 1
}
var sourceObj1 = {
b: 2
}
var sourceObj2 = {
c: 3
}
Object.assign(targetObj, sourceObj1, sourceObj2);
console.log(targetObj); // => {a: 1, b: 2, c: 3}
注意:如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属笥会覆盖前面的属性。
var targetObj = {
a: 1,
b: 1
}
var sourceObj1 = {
b: 2,
c: 2
}
var sourceObj2 = {
c: 3
}
Object.assign(targetObj, sourceObj1, sourceObj2);
console.log(targetObj); // => {a: 1, b: 2, c: 3}
如果Object.assign()
只有一个参数,那么它会直接返回该参数:
var obj = {
a: 1
}
Object.assign(obj) === obj; // => true
如果Object.assign()
的参数不是对象,则会先转换成对象,然后返回:
typeof Object.assign(2); // => object
由于undefined
和null
无法转成对象,所以如果它们做为Object.assign()
的参数(只有一个参数),就会报错:
Object.assign(undefined);
Object.assign(null);
如果undefined
和null
不是Object.assign()
的首参,就不会报错:
let obj = {
a: 1
}
Object.assign(obj, undefined);
Object.assign(obj, null);
Object.assign(obj, undefined) === obj; // => true
Object.assign(obj, null) === obj; // => true
其他类型的值,即数值、字符串和布尔值,不在首参数,也不会报错。但是,除了字符串会以数组形式拷贝到目标对象外,其他的值都不会产生效果
var v1 = 'w3cplus';
var v2 = true;
var v3 = 123;
var v4 = ['a', '2', false];
var obj = Object.assign({}, v1, v2, v3, v4);
console.log(obj);
上面的代码中,v1
、v2
、v3
和v4
分别是字符串、布尔值、数字和数组。结果数组和字符串并入目标对象,而且数组中的替换掉了字符串的(具体这个是为什么?我也没整明白)。对于数值和布尔值都被忽略了。这是因为只有字符串的包装对象会产生可枚举属性。
布尔值、数值、字符串分别转成对应的包装对象,可以看到它们的原始值都在包装对象的内部属性[[PrimitiveValue]]
上面,这个属性是不会被Object.assign()
拷贝的。只有字符串的包装对象,会产生可枚举的实义属性,那些属性则会被拷贝。
Object.assign()
拷贝的属性是有限制的,只拷贝源对象的自身属性(不会拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false
)。
属性名为Symbol
值的属性,也会被Object.assign()
拷贝:
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' }); // => { a: 'b', Symbol(c): 'd' }
重复的对象字面量属性
在ES5的非严格模式下,对象中同时有相同的属性名存在时,后面的会覆盖前面的:
var person = {
name: 'w3cplus',
name: 'damo'
}
但在ES5严格模式下加入了对象字面量重复属性的校验。当同时存在多个同名属性时会抛出错误。
'use strict';
var person = {
name: 'w3cplus',
name: 'damo' // => syntax error in ES5 strict mode
}
但是在ES6中,重复属性检查已经被移除了。不管是strict和nostrict模式都不会取检查重复属性,它会取给定名称的最后一个属性作为实际值:
var person = {
name: "w3cplus",
name: "damo" // => not an error in ES6
};
console.log(person.name); // => damo
在这个例子中,person.name
的值为damo
,因为它是赋给该属性的最后一个值。
属性的可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDesciptor
方法可以获取该属性的描述对象。
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新增了一个操作Object.assign()
,会忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。这四个操作之中,只有for...in
会返回继承的属性
另外,ES6规定,所有Class
的原型的方法都是不可枚举的。
除些之外,在ES6中严格规定了对象的自有属性被枚举时的返回顺序,这会影响到Object.getOwnPropertyNames()
方法及Reflect.ownKeys
返回属性的方式,Object.assign()
方法处理属性的顺序也将随之改变。
对象自有属性枚举顺序的基本原则是:
- 所有数字键按升序排序
- 所有字符串键按照它们被加入对象的顺序排序
- 所有
symbol
键按照它们被加入对象的顺序排序
比如下面这个示例:
var obj = {
a: 1,
0: 2,
c: 3,
2: 4,
b: 5,
1: 7
}
obj.d = 8;
Object.getOwnPropertyNames(obj);
var str = Object.getOwnPropertyNames(obj).join('');
console.log(str); // => 012acbd
改变原型
正常情况下,无论是通过构造函数还是Object.create()
方法创建对象,其原型是在对象被创建时指定的。而在JavaScript中,原型是JavaScript继承时的基础。在ES5中添加了Object.getPrototypeOf()
方法来检索任何给定对象的原型。但在ES6中提供了一个相反的操作方法Object.setPrototypeOf()
,用来改变任何给定对象的原型。
在ES6之前,是无法在对象创建后来改变其原型,但是ES6的Object.setPrototypeOf()
打破了这一情况。Object.setPrototypeOf()
接收两个参数,第一个参数为要改变原型的对象,第二个参数为被设置为第一个对象原型的对象。
let person = {
getGreeting() {
return "Hello";
}
};
let dog = {
getGreeting() {
return "Woof";
}
};
// 以 person 对象为原型
let friend = Object.create(person);
console.log(friend.getGreeting()); // => "Hello"
console.log(Object.getPrototypeOf(friend) === person); // => true
// 将原型设置为dog
Object.setPrototypeOf(friend, dog);
console.log(friend.getGreeting()); // => "Woof"
console.log(Object.getPrototypeOf(friend) === dog); // => true
这段代码中,我们有两个基本对象:person
和dog
,两个对象都有一个getGreeting()
的方法,对象friend
首先从person
中继承,意味着调用getGreeting()
会输出Hello
。当我们改变friend
的原型为dog
时,此时getGreeting()
输出Woof
。
一个对象的原型的实际值是存储在一个内部属性[[Prototype]]
中。Object.getPrototypeOf()
方法返回存储在[[Prototype]]
的值,而Object.setPrototypeOf()
改变存储在[[Prototype]]
上的值。
在JavaScript中,__proto__
属性,用来读取或设置当前对象的prototype
对象
// ES6
var obj = {
method: function () {
// ...
}
}
obj.__proto__ = someOtherObj;
// ES5
var obj = Object.create(someOtherObj);
obj.method = function() {
// ...
}
ES6中的Object.setPrototypeOf()
方法的作用与__proto__
相同,用来设置一个对象的prototype
对象。它是ES6正式推荐的设置原型对象的方法:
// 格式
Object.setPrototypeOf(object, prototype)
// 用法
var obj = Object.setPrototypeOf({}, null)
来看一个简单的示例:
let proto = {}
let obj = {
x: 10
};
Object.setPrototypeOf(obj, proto);
proto.y = 20;
proto.z = 40;
obj.x; // => 10
obj.__proto__.y; // => 20
obj.__proto__.z; // => 40
obj.y; // => 20
obj.z; // => 40
上面代码将proto
对象设为obj
对象的原型,所以从obj
对象可以读取proto
对象的属性。
属性的遍历
ES6一共有五种方法可以遍历对象的属性。
for ... in
:循环遍历对象自身的和继承的可枚举的属性(不包含Symbol
属性)Object.keys(obj)
: 返回一个数组,包括对象自身的所有可枚举的属性(不包含继承,不包含Symbol
属性)Object.getOwnPropertyNames(obj)
:返回一个数组,包含对象自身的所有属性(含继承、不可枚举属性,不含Symbol
属性)Object.getOwnPropertySymbols(obj)
:返回一个数组,包含对象自身的所有Symbol
属性Reflect.ownKeys(obj)
:返回一个数组,包含对象自身的所有属性,不管是属性名是Symbol
或字符串,也不管是否可枚举
以上的5种方法遍历对象的属性,都遵守同样的属性遍历的次序规则。
- 首先遍历所有属性名为数值的属性,按照数字排序
- 其次遍历所有属性名为字符串的属性,按照生成时间排序
- 最后遍历所有属性名为
Symbol
值的属性,按照生成时间排序
例如:
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// => ['2', '10', 'b', 'a', Symbol()]
Object其他方法
在JavaScript中,除了ES6给Object
新增的Object.is()
和Object.assign()
方法之外,还有一些其他的方法。
Object.keys()
ES5 引入了Object.keys()
方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable
)属性的键名
var obj = {
foo: 'bar',
baz: 42
};
Object.keys(obj); // => ["foo", "baz"]
Object.values()
Object.values()
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值
var obj = {
foo: 'bar',
baz: 42
};
Object.values(obj); // => ["bar", 42]
返回数组的顺序:
var obj = {
100: 'a',
2: 'b',
7: 'c'
};
Object.values(obj); // => ["b", "c", "a"]
上面代码中,属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是b、c、a
。
Object.values()
只返回对象自身的可遍历属性。
var obj = Object.create({}, {p: {value: 42}});
Object.values(obj); // => []
上面代码中,Object.create()
方法的第二个参数添加的对象属性(属性p
),如果不显式声明,默认是不可遍历的,因为p是继承的属性,而不是对象自身的属性。Object.values()
不会返回这个属性。
Object.values()
会过滤属性名为 Symbol
值的属性。
Object.values({ [Symbol()]: 123, foo: 'abc' }); // => ['abc']
如果Object.values()
方法的参数是一个字符串,会返回各个字符组成的一个数组。
Object.values('foo'); // => ['f', 'o', 'o']
如果参数不是对象,Object.values()
会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values()
会返回空数组。
Object.values(42); // => []
Object.values(true); // => []
Object.entries()
Object.entries()
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组
var obj = {
foo: 'bar',
baz: 42
};
Object.entries(obj); // => [ ["foo", "bar"], ["baz", 42] ]
除了返回值不一样,该方法的行为与Object.values()
基本一致。
总结
在JavaScript中的对象一文中整理了有关于对象方面的知识点。在这篇文章中整理和学习了有关于ES6中对象的相关知识点。可能理解有误,如果发现有不对之处,还请指正。
如需转载,烦请注明出处:https://www.w3cplus.com/javascript/ES6-Objects.html