ES6学习笔记:对象

编辑推荐: 掘金是一个高质量的技术社区,从 CSS 到 Vue.js,性能优化到开源类库,让你不错过前端开发的每一个技术干货。 点击链接查看最新前端内容,或到各大应用市场搜索「 掘金」下载APP,技术干货尽在掌握中。

在JavaScript中,几乎每一个值都是某种特定类型的对象,于是ES6也着重提升了对象的功能性。上周花了一周的时间了解了JavaScript中的对象相关的知识,对于ES6中有关于对象的扩展功能并不太了解。今天开始就来简单的了解和学习有关于ES6中对象的扩展功能。

ES6通过多种方式来加强对象的使用,通过简单的语法扩展,提供了更多操作对象及对象交互的方法。接下来的内容就是有关于这些知识的整理。

对象类别

在浏览器这样的执行环境中,对象没有统一的标准,在标准中又使用不同的术语描述对象,ES6规范清晰定义了每一个类别的对象。总而言之,理解这些术语对象这门语言来说非常重要。在ES6中的对象类别主要分为:

  • 普通对象(Ordinary):具有JavaScript对象所有的默认内部行为
  • 特殊对象(Exotic):具有某些与默认行为不符的内部行为
  • 标准对象(Standard):ES6规范中定义的对象,例如ArrayDate等,标准对象既可以是普通对象,也可以是特殊对象
  • 内建对象:脚本开始执行时存在于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对象,动态给这个对象添加了三个属性nameagejob,另外添加了一个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()函数创建了一个对象,这个对象的属性名和函数的参数相同,在返回的结果中nameagejob分别重复了两遍,只是其中一个是对象属性的名称,另外一个是对象属性赋值的变量。

在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 namelast 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');

在这个例子中,myObjectEventTarget.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

由于undefinednull无法转成对象,所以如果它们做为Object.assign()的参数(只有一个参数),就会报错:

Object.assign(undefined);
Object.assign(null);

如果undefinednull不是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);

上面的代码中,v1v2v3v4分别是字符串、布尔值、数字和数组。结果数组和字符串并入目标对象,而且数组中的替换掉了字符串的(具体这个是为什么?我也没整明白)。对于数值和布尔值都被忽略了。这是因为只有字符串的包装对象会产生可枚举属性。

布尔值、数值、字符串分别转成对应的包装对象,可以看到它们的原始值都在包装对象的内部属性[[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有三个操作会忽略enumerablefalse的属性。

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

ES6新增了一个操作Object.assign(),会忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。这四个操作之中,只有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

这段代码中,我们有两个基本对象:persondog,两个对象都有一个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中对象的相关知识点。可能理解有误,如果发现有不对之处,还请指正。

大漠

常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《图解CSS3:核心技术与案例实战》。

如需转载,烦请注明出处:https://www.w3cplus.com/javascript/ES6-Objects.html

返回顶部