ECMAScript 6 – 新对象和现有对象更新
本文是对发布于 DNC 杂志一月版 上的 ES6 系列文章的一个继续。在第一部分中,我们看见了在 ES6 中即将来临的对 JavaScript 语言的一些改进。本文将着眼于新的对象类型,以及对该语言中已经存在的对象的 API 的更新。
就如在 第一篇文章 中已经提及的,ES6 致力于匹配 JavaScript 来使之更适合于编写大规模的应用程序。为了实现该目标,该语言的设计者们已经对其添加了不少的新特性,这些特性的灵感源于那些“有类型(typed)”的 JavaScript 的替代版以及一些其它的库,包括一些服务端的库。以下是关于那些新的和更新过的对象的一瞥:
-
新的数据结构,用于更简单的储存唯一性的值(Set),或,有唯一性的键的键-值对(Map)
-
已经存在的对象,比如 Math 和 Number 会获得新的功能来执行更多的操作,和用更优的方式来执行已存在的操作。
-
String 类型将会获得一些使得 “解析(parsing)” 操作更方便的特性。
-
Object 类型将会获得一些函数来 “分配(assign)” 一个对象以及在两个对象之间进行比较。
-
在 Array 类型上的新函数集现在可以更方便的来查找到一个元素,一个元素的索引,以及在数组内部对元素进行拷贝操作。
-
新的 Proxy 对象来对以存在的对象或者函数扩展(extend)功能集。
为 ES6 API 提供平台支持
这些 API 目前并不完全支持所有的平台。Chrome 和 Opera 最新的版本不支持一些在 String 上的新功能并且根本不支持 Proxy 对象。类似于 traceur 和 6to5 这样的编译器没有提供polyfill相关的API。我是用Firefox nightly去测试所有的这些例子的。一些脚本为了arrow功能和short hand功能使用ES6的新语法,你最好使用traceur编译脚本。
现在你得到了一个关于ES6 API更新的结论,让我们开始探索他们吧。
Set 和 WeakSet
Set
Set 是一个包含互不相同的值的集合,这些值可以是任何 JavaScript 类型(即Number,Boolean,String,Object等)。它会忽略任何对其插入重复值的尝试。Set 是可迭代的;意思是我们可以在 Set 上用 for…of 循环来遍历。
我们可以通过调用 Set 类型的构造器来创建一个新的 Set 实例,就像这样:
var mySet = new Set(listOfItems);
..其中 listOfItems
是可选参数,它包含一个将要被插入到 Set 中的可迭代的元素列表。如果没有传递这个参数,将会创建一个空的 Set。
下面是一个 Set 对象的例子:
var setOfObjects = new Set([17, 19, 38, 82, 17]);
我们可以在 Set 对象上用 for…of 循环来遍历它,因为 Set 是一个可迭代的对象。下面的代码会打印 Set 内部存储的值:
for (let item of setOfObjects) { console.log(item); }
注意检查这个循环的输出;重复添加到这个 Set 的值是不会显示出来的。在我们这个例子中,“17”只被存储了一次。Set 内部通过使用 SameValueZero(x,y) 来忽略重复的值。
接下来让我们看看 Set 在 API 中提供了哪些方法。
添加元素
元素可以通过 set.add() 方法添加到 Set 中。
setOfObjects.add(4); setOfObjects.add(45); setOfObjects.add(18); setOfObjects.add(45);
第二次尝试将 45 插入到 Set 中的代码无法执行成功,因为 Set 中已经包含了那个值。
验证一个对象的存在性
Set 上的 has() 方法能检查 Set 中是否包含传入的对象。对象是按引用而非值比较的。下面的例子说明了这点:
var obj = {value: 100}; setOfObjects.add(obj); console.log(setOfObjects.has(obj)); // true console.log(setOfObjects.has({prop: 100})); // false
删除对象
存储在Set中的对象,可以通过它们的引用,使用delete() 方法删除,或者使用clear() 方法清除。 下面是一些例子:
setOfObjects.delete(obj); setOfObjects.clear(); |
Set的大小
Set的size属性包含了当前的对象数目。
console.log(setOfObjects.size); |
遍历Set
之前提到过,Set 可以通过常规的 for…of 循环来遍历。除此之外,对于Set,还有一些其它的迭代或者循环方式。如下所示: (* 表明方法返回迭代器)
*entries(): 返回一个包含key-value组合对象的迭代器。由于键值(key值)和数值(value值)在Set中是一样的,每个入口都是一个不断重复关联数值的数组。
for(let item of setOfObjects.entries()){ console.log(item); } |
*values(): 返回遍历Set中数值(value值)的迭代器。
for(let item of setOfObjects.values()){ console.log(item); } |
*keys():返回遍历Set中键值(Key值)的迭代器. 由于键值(key值)和数值(value值)在Set中是一样的,所以keys方法和values方法返回一样的结果
for(let item of setOfObjects.keys()){ console.log(item.); } |
forEach(callback): 这是Set中遍历入口的另一种方法。Set中的每个入口都会调用回调函数
setOfObjects.forEach(item => console.log(item)); |
WeakSet
WeakSet 是 Set 对应的弱引用版本。WeakSet 不会阻止插入其中的值被垃圾收集。它的工作方式和 Set 类似,但也有以下例外:
-
只能包含对象。属于 Number、String、Boolean、null 和 undefined 的值都不能被添加进 WeakSet
-
无法迭代或遍历 WeakSet 中包含的值,也就是说,WeakSet 不支持像 values()、entries() 或 forEach() 那样的方法
-
WeakSet 支持以下一组操作:add、has 和 delete。这些方法工作起来和用 Set 时一样
之所以取名叫 WeakSet,是因为它们不会阻止存储在它们内部的值被垃圾回收。
由于 WeakSet 天生存在上述限制,只有少数情况下会用到它。
Map和WeakMap
Map
Map是键值对(key-value pair)对象;键(key)和值(value)可以是任意的JavaScript对象或值。键(key)在给定的Map中必须是唯一的。像Set一样,Map是可迭代的。
新的Map对象可以通过如下的 Map 构造函数来创建:
varmyDictionary =newMap(...arguments); |
Map 构造函数参数是可选的。 如果传递参数,这些参数将会用来创建Map;否则,Map 对象将不会包含任何内容。
下面的代码段显示了如何使用对象集合来创建Map :
varmyDictionary =newMap([["key1","value1"], ["key2","value2"]]); |
由于字典是可迭代的,我们可以使用 for…of 循环遍历每个子项。
for(let dictionaryEntry of myDictionary){ console.log(dictionaryEntry); } |
Map 提供了一系列方法与之交互。让我们来看一看。
增加项目
新的项目可以通过set()方法加入到Map之中。 这个方法会检查传递到Map的键是否已经存在,如果键不存在,则将其添加到Map之中;否则就放弃。
下面的代码段增加了更多的项目到之前创建的Map之中:
myDictionary.set("key3","value4"); myDictionary.set("key2","value5"); varobj = {id:"1"}; myDictionary.set(obj, 1000); myDictionary.set(obj, 1900);
以key2 作为键,同时以obj 作为键,尝试插入myDictionary的时候,由于它们在myDictionary中已经存在,所以会被丢弃。
键通过引用校验,而不是值。 所以,下面的代码段会增加一个项目到myDictionary之中。
myDictionary.set({id:"1"}, 826);
通过 key 获取 value
如果 key 是已知的,那么可以使用 get() 方法从一个 Map 中提取 value。如果无法在 Map 中找到 key,那么方法会返回 undefined。
检查一个 key 是否存在
我们可以用 has() 方法检查一个 key 是否已经添加到 Map 中。和 Set 的情况类似,has() 方法按引用检查 key 的匹配项。
console.log(myDictionary.has("key2")); // true console.log(myDictionary.has(obj)); // true console.log(myDictionary.has({id: "1"})); // false
通过 key 得到 value
如果 key 是已知的,那么可以使用 get 方法从一个 Map 中提取 value。如果无法在 Map 中找到 key,则方法将返回 undefined。
console.log(myDictionary.get("key2")); // value2 console.log(myDictionary.get("key2ii")); // undefined
移除对象
Map 中的对象可以通过 delete 方法一个个的移除,也可以通过 clear 方法一下子全部移除。delete 方法接收 key 值,如果找到这个 key 值所对应的条目并成功删除,返回 'true',否则返回 'false'。
下面是调用delete 和 clear 方法的一些例子:
console.log(myDictionary.delete({prop: 2000})); //false console.log(myDictionary.delete(obj)); //true console.log(myDictionary.delete("key1")); //true myDictionary.clear(); |
Map 的大小
Map 对象的 size 属性保存了 Map 中条目的个数。
console.log(myDictionary.size); |
遍历 Map
在前面曾提到过,Maps 可以通过常规的 for...of 语句进行遍历。另外,还有一些方法可以遍历或循环 Maps 中 key 或 value 的值。下面是这些方法的示例(*表示返回值为迭代器)
*entries(): 返回一个包含 key-value pair 对象的迭代器。迭代器的每个条目是一个长度为 2 的数组,其中第一个值为 key,第二个值为 value。
for(let item of myDictionary.entries()){ console.log(item); } |
*values(): 返回一个包含 Map 中所有 value 的迭代器
for(let item of myDictionary.values()){ console.log(item); } |
*keys():返回一个包含 Map 中所有 key 的迭代器
for(let item of myDictionary.keys()){ console.log(item); } |
forEach(callback): 另外一种循环 Map 中 key 值的方法。对于每一个 key,都会调用一次 Callback 函数。
myDictionary.forEach(item => console.log(item)); |
WeakMap
WeakMap 的工作方式和 Map 类似,但也有一些例外。这些例外和用 WeakSet 时的一样。WeakMap 不会限制被用于 key 的对象遭到垃圾收集。以下是 WeakMap 的特性列表:
-
key 只能是对象;key 不能是值类型。value 可以是任何类型
-
不支持对其元素进行遍历。因此,for…of 循环不能被用于遍历 WeakMap 的元素。entries、values 和 keys 方法是不被支持的
被支持的操作是:set、get、has 和 delete。这些操作的行为和它们在 Map 上的行为是一致的。
Numbers
一些用于处理数字的全局函数如 parseInt, parseFloat 被移到了 Number 对象中,而且在语言层面对不同的数字表示系统做了更好的支持。让我们看一看这些变化。
数字系统
ES6 定义了显示表示8进制和2进制数字系统的方法。现在,你可以很方便的使用这些表示方法并与10进制数字进行转换。
8进制数字的表示方法是在前面加上前缀 “0o”. 通过 Number 对象,可以将带有这种格式的字符串转化为对应的数字类型。例如:
varoctal = 0o16; console.log(octal);//output: 14 varoctalFromString = Number("0o20"); console.log(octalFromString); //output: 16 |
类似的,2进制数字的表示方法为在前面加上“0b”. 你同样可以将这种格式的字符串转化为对应的数字类型。
varbinary = 0b1100; console.log(binary); //output: 12 varbinaryFromString = Number("0b11010"); console.log(binaryFromString); //output: 26 |
parseInt 和 parseFloat
现在 parseInt 和 parseFloat 函数可通过 Number 对象调用,这样会更加明确。它们的工作方式和之前一样。
console.log(Number.parseInt(
"182"
));
console.log(Number.parseFloat(
"817.12"
));
isNaN
现在我们可以通过 Number 对象的 isNaN 函数来检测表达式是否是合法的数字值(number)。全局 isNaN 函数和 Number.isNaN 的不同之处在于,该方法会在检测是否是数字值前先将值转换为数字值。下面是一些示例:
console.log(Number.isNaN(
"10"
));
//false as “10” is converted to the number 10 which is not NaN
console.log(Number.isNaN(10));
//false
“NaN” 表示 “该值是IEEE-754标准中的非数字值”
另外一种判断是否是数字值类型的方法是使用typeof()。
isFinite
该函数用来检测值是否是有限的数字值(number)。该函数会在检测前试图将值转换成数字值。下面是该函数的一些示例:
console.log(Number.isFinite("10")); //false console.log(Number.isFinite("x19")); //false
isInteger
该函数用来检测值是否是合法的整数。该函数不会在检测前将值转换成数字值。下面是该函数的一些示例:
console.log(Number.isInteger("10")); //false console.log(Number.isInteger(19)); //true
常量
Number API 现在包含了两个常量:
-
EPSILON (分数可能的最小数字值)。其值是 2.220446049250313e-16
-
MAX_INTEGER (数值可能的最大值)。其值是 1.7976931348623157e+308
Math
Math 对象上新添加了一些方法,包括对数函数、双曲函数以及其他一些实用函数。下面罗列了添加到 Math 的方法:
对数函数
-
log10:计算传入值的以10为底数的对数
-
log2:计算传入值的以2为底数的对数
-
log1p:将传入值自增1,然后计算其自然对数
-
expm1:实现前一个函数的逆运算。以传入值为指数对自然对数的底数求幂,所得结果再减去1
双曲函数
-
sinh, cosh, tanh:分别是双曲正弦、双曲余弦和双曲正切函数
-
asinh, acosh, atanh:分别是反双曲正弦、反双曲余弦和反双曲正切函数
杂项函数
-
hypot:接受两个数值作为直角三角形的两条直角边长度,返回斜边的长度
-
trunc:截断传入值的小数部分
-
sign:返回传入值的正负号。如果传入 NaN 则返回 NaN,传 -0 返回 -0,传 +0 返回 +0,任何负数返回 -1,任何正数返回 +1
-
cbrt:返回传入值的立方根
String
字符串模板化
在每一个JavaScript程序中的很多情况下, 我们会使用大量的字符串, 我们需要使用字符串来拼接出变量的值. 以前, 我们通常使用连接(+)操作符完成拼接. 有时, 这种方式会让人抓狂. ES6提供了字符串的模板化特性来解决这个问题.
如果我们使用模板, 就不需要手动将字符串分割开来与各种值拼接在一起. 我们可以一口气写出完整的字符串. 通过这个特性, 我们不需要使用单引号或者双引号; 我们要用的是反引号(`). 下面的示例显示了使用模板的语法. 它将一个变量值和一个字符串组装成一个REST的API地址:
varemployeeId ='E1001'; vargetDepartmentApiPath = `/api/department/${employeeId}`; console.log(getDepartmentApiPath);
模板里支持使用任意数值. 下面的例子显示了使用两个变量组成一个API地址:
varprojectId ='P2001'; varemployeeProjectDetailsApiPath = `/api/project/${projectId}/${employeeId}`; console.log(employeeProjectDetailsApiPath);
我们可以在模板里进行一些简单的算术运算. 下面的代码片段中显示了使用方式:
varx=20, y=10; console.log(`${x} + ${y} = ${x+y}`); console.log(`${x} - ${y} = ${x-y}`); console.log(`${x} * ${y} = ${x*y}`); console.log(`${x} / ${y} = ${x/y}`);
实用函数
在 ES6 中,String 添加了一个 repeat 实用函数。这个函数将字符串重复指定的次数并将其返回。任何字符串都能调用它。
var thisIsCool = "Cool! "; var repeatedString = thisIsCool.repeat(4); console.log(repeatedString);
子串匹配函数
ES6 在 String 的原型上添加了startsWith、endsWith 和 includes 函数,它们被用来检查某个子串是否分别在给定字符串的开头、末尾或任何位置出现过。所有这些函数都返回布尔值。includes 函数还被用来检查在给定的 index 处是否出现了子串。下面的例子演示了这些函数的用法:
startsWith():
console.log(repeatedString.startsWith("Cool! ")); console.log(repeatedString.startsWith("cool! "));
endsWith():
console.log(repeatedString.endsWith("Cool! ")); console.log(repeatedString.endsWith("Cool!"));
includes():
console.log(repeatedString.includes("Cool! ")); console.log(repeatedString.includes("Cool! ", 6)); console.log(repeatedString.includes("Cool! ", 10));
Unicode 函数
ES6 中提供了一些函数可以将 Unicode 编码转化为相应的字符,将字符转化为相应的 Unicode 编码,或使用不同的合字方式标准化 Unicode 字符串。
codePointAt(): 返回字符串中某个指定位置的字符的 Unicode 编码。
console.log(repeatedString.codePointAt(0)); |
fromCodePoint(): 是 string 对象的静态方法。传入 Unicode 编码,返回对应的字符。
console.log(String.fromCodePoint(200)); |
normalize(): 返回经过 Unicode 标准化的字符串。传入标准化格式作为参数。如果该格式是一个错误的格式,则使用 NFC 格式。请查看MDN 中的文档 ,了解这个函数的详细信息。
"c\u067e".normalize("NFKC"); //"cıe" |
数组
数组在任意编程语言中都是最常用的数据结构。ES6为数组类型的对象增加了一些新的实用函数,同时,也为数组增加了一些静态方法,用来查找元素,拷贝元素,遍历元素,以及将非数组类型转换为数组类型。
遍历数组
像Map一样, Array提供了entries() 和keys() 方法,用来遍历所有的元素。
*entries(): 从entries 函数返回的每个项目,都是一个包含键和其对应值的数组。 is an array of two elements containing a key and its corresponding value. 对数组来说,键就和索引一样。
varcitiesList = ["Delhi","Mumbai","Kolkata","Chennai","Hyderabad","Bangalore"]; for(let entry of citiesList.entries()){ console.log(entry); }
*keys(): 键和索引一样;所以这个函数返回数组中每个项目的索引。
for(let key of citiesList.keys()){ console.log(key); }
查找
数组 有两个方法,即 find 和 findIndex,它们通过谓词来匹配,返回满足条件的项目。对于谓词,我们可以传递箭头函数。
find(): 接受谓词参数,并返回数组中第一个满足条件的项目。
console.log(citiesList.find( city => city.startsWith("M") ));
findIndex():接受谓词参数,并返回数组中第一个满足条件的项目的索引。
console.log(citiesList.findIndex( city => city.startsWith("M") ));
填充和复制
填充整个 Array,或者用一个元素填充 Array 的一部分,又或将部分 Array 元素填充至其余部分,这些都将变得简单。
fill():下面是 fill 函数的调用语法:
arrayObject.fill(objectToFill, startIndex, endIndex);
只有第一个参数是必需的。当只传一个参数就调用它时,它将传入的值填充至整个数组。
citiesList.fill("Pune"); citiesList.fill("Hyderabad", 2); citiesList.fill("Bangalore", 3, 5);
copyWithin():将数组中的一个或多个元素复制到数组的其他位置。
citiesList.copyWithin(0, 3); // elements at 0 to 2 into elements from 3 onwards citiesList.copyWithin(0, 3, 5); // elements at 0 to 2 into elements from 3 to 5 citiesList.copyWithin(0, -3); // negative index starts from end of the array
转换成数组
ES6 为 Array 添加了两个静态方法,用于将数据集合和数据流转换成 Array。
of():这个函数传入一个对象列表,返回一个包含这些对象的 Array。
var citiesInUS = Array.of("New York", "Chicago", "Los Angeles", "Seattle");
from():用于将形如 Array 的数据(即函数的参数)转换成数组。
function convertToArray() { return Array.from(arguments); } var numbers = convertToArray(19, 72, 18, 71, 37, 91);
对象
Object 在 ES6 中获得了两个新的静态函数——用来比较两个对象,以及用来将多个对象上的可枚举属性赋值到一个对象上。
is():接受两个对象,返回一个用来表示对象是否相等的布尔值
var obj = {employeeId: 100}; var obj2 = obj; console.log(Object.is(obj, {employeeId: 100})); // false console.log(Object.is(obj, obj2)); // true
assign():下面是这个函数的调用语法:
Object.assign(target, source1, source2, …)
将所有来源对象上的可枚举属性赋值到目标对象上。
var obj3 = {departmentName: "Accounts"}; var obj4 = {}; Object.assign(obj4, obj, obj3); // contents of obj4: {employeeId: 100, departmentName: "Accounts"}
代理(Proxy)
正如其名,Proxy 对象被用来在对象和方法周围创建代理。Proxy 对象对于完成某些任务很有帮助,例如在调用一个函数前进行校验,当访问一个属性的值时对其进行格式化。在我看来,在 JavaScript 中,代理定义了一种新的装饰(decorate)对象的途径。让我们来实战演练一下。
假设有这样一个对象:
var employee = { employeeId: 'E10101', name: 'Hari', city: 'Hyderabad', age: 28, salary: 10000, calculateBonus() { return this.salary * 0.1; } };
代理 Getters
当这个员工(employee)的工资(salary)被访问时,我们来格式化它的值。为此我们需要在对象属性的 getter 上定义一个代理来格式化数据。为了完成这个任务,让我们来定义一个 Proxy 对象:
var employeeProxy = new Proxy(employee, { get(target, property) { if (property === "salary") { return `$ ${target[property]}`; } return target[property]; } }); console.log(employeeProxy.salary);
正如你所见,代理对象的 get 方法带有两个参数:
· target:被重新定义 getter 的那个对象
· property:被访问的那个属性的名称
再看一遍这个代码段,你会发现我在编写时用到了两个 ES6 的特性:定义方法的简写形式以及模版字符串。
代理 Setters
对于一个员工(employee)的 EmployeeId 而言只能被赋值一次,接下来任何对其赋值的尝试都将被阻止。为了做到这点,我们可以在对象的 setter 上创建代理,比如以下代码片段:
var employeeProxy = new Proxy(employee, { set(target, property, value) { if (property === "employeeId") { console.error("employeeId cannot be modified"); } else { target[property] = value; } } }); employeeProxy.employeeId = "E0102"; // Logs an error in the console
代理函数调用
假定当员工(employee)的工资(salary)在 $16,000 以上时,要为其计算奖金(bonus)。但是上述对象的 calculateBonus
方法没有检查这个条件。让我们通过定义一个代理来检查这个条件。
employee.calculateBonus = new Proxy(employee.calculateBonus, { apply(target, context, args) { if (context.salary < 16000) { return 0; } return target.apply(context, args); } }); console.log(employee.calculateBonus()); // Output: 0 employee.salary = 16000; console.log(employee.calculateBonus()); // Output: 1600
总结
正如我们所见,ES6 为已有的对象带来了一些新的 API,同时带来了一些新的对象类型和数据结构,从而简化了很多工作。正如所提到的,截止到发稿时,其中的部分 API 还未在所有平台上得到支持。希望它们在不久的将来能得到支持。在以后的文章里,我们将探索有关 promise 和 ES6 模块化的内容。
下载本文的完整源代码(GitHub)