本文是对发布于 DNC 杂志一月版 上的 ES6 系列文章的一个继续。在第一部分中,我们看见了在 ES6 中即将来临的对 JavaScript 语言的一些改进。本文将着眼于新的对象类型,以及对该语言中已经存在的对象的 API 的更新。
就如在 第一篇文章 中已经提及的,ES6 致力于匹配 JavaScript 来使之更适合于编写大规模的应用程序。为了实现该目标,该语言的设计者们已经对其添加了不少的新特性,这些特性的灵感源于那些“有类型(typed)”的 JavaScript 的替代版以及一些其它的库,包括一些服务端的库。以下是关于那些新的和更新过的对象的一瞥:
|
1 | var mySet = new Set(listOfItems); |
..其中 listOfItems
是可选参数,它包含一个将要被插入到 Set 中的可迭代的元素列表。如果没有传递这个参数,将会创建一个空的 Set。
下面是一个 Set 对象的例子:
1 | var setOfObjects = new Set([17, 19, 38, 82, 17]); |
我们可以在 Set 对象上用 for…of 循环来遍历它,因为 Set 是一个可迭代的对象。下面的代码会打印 Set 内部存储的值:
1 2 3 | for ( let item of setOfObjects) { console.log(item); } |
注意检查这个循环的输出;重复添加到这个 Set 的值是不会显示出来的。在我们这个例子中,“17”只被存储了一次。Set 内部通过使用 SameValueZero(x,y) 来忽略重复的值。
接下来让我们看看 Set 在 API 中提供了哪些方法。
现在你得到了一个关于ES6 API更新的结论,让我们开始探索他们吧。
添加元素
元素可以通过 set.add() 方法添加到 Set 中。
1 2 3 4 | setOfObjects.add(4); setOfObjects.add(45); setOfObjects.add(18); setOfObjects.add(45); |
第二次尝试将 45 插入到 Set 中的代码无法执行成功,因为 Set 中已经包含了那个值。
验证一个对象的存在性
Set 上的 has() 方法能检查 Set 中是否包含传入的对象。对象是按引用而非值比较的。下面的例子说明了这点:
1 2 3 4 | 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 是 Set 对应的弱引用版本。WeakSet 不会阻止插入其中的值被垃圾收集。它的工作方式和 Set 类似,但也有以下例外:
只能包含对象。属于 Number、String、Boolean、null 和 undefined 的值都不能被添加进 WeakSet
无法迭代或遍历 WeakSet 中包含的值,也就是说,WeakSet 不支持像 values()、entries() 或 forEach() 那样的方法
WeakSet 支持以下一组操作:add、has 和 delete。这些方法工作起来和用 Set 时一样
之所以取名叫 WeakSet,是因为它们不会阻止存储在它们内部的值被垃圾回收。
由于 WeakSet 天生存在上述限制,只有少数情况下会用到它。
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之中:
1 2 3 4 5 6 7 | myDictionary.set( "key3" , "value4" ); myDictionary.set( "key2" , "value5" ); varobj = {id: "1" }; myDictionary.set(obj, 1000); myDictionary.set(obj, 1900); |
以key2 作为键,同时以obj 作为键,尝试插入myDictionary的时候,由于它们在myDictionary中已经存在,所以会被丢弃。
键通过引用校验,而不是值。 所以,下面的代码段会增加一个项目到myDictionary之中。
1 | myDictionary.set({id: "1" }, 826); |
通过 key 获取 value
如果 key 是已知的,那么可以使用 get() 方法从一个 Map 中提取 value。如果无法在 Map 中找到 key,那么方法会返回 undefined。
检查一个 key 是否存在
我们可以用 has() 方法检查一个 key 是否已经添加到 Map 中。和 Set 的情况类似,has() 方法按引用检查 key 的匹配项。
1 2 3 | 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。
1 2 | 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 的工作方式和 Map 类似,但也有一些例外。这些例外和用 WeakSet 时的一样。WeakMap 不会限制被用于 key 的对象遭到垃圾收集。以下是 WeakMap 的特性列表:
key 只能是对象;key 不能是值类型。value 可以是任何类型
不支持对其元素进行遍历。因此,for…of 循环不能被用于遍历 WeakMap 的元素。entries、values 和 keys 方法是不被支持的
被支持的操作是:set、get、has 和 delete。这些操作的行为和它们在 Map 上的行为是一致的。
一些用于处理数字的全局函数如 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)。该函数会在检测前试图将值转换成数字值。下面是该函数的一些示例:
1 2 | console.log(Number.isFinite( "10" )); //false console.log(Number.isFinite( "x19" )); //false |
isInteger
该函数用来检测值是否是合法的整数。该函数不会在检测前将值转换成数字值。下面是该函数的一些示例:
1 2 | 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 的方法:
对数函数
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:返回传入值的立方根
字符串模板化
在每一个JavaScript程序中的很多情况下, 我们会使用大量的字符串, 我们需要使用字符串来拼接出变量的值. 以前, 我们通常使用连接(+)操作符完成拼接. 有时, 这种方式会让人抓狂. ES6提供了字符串的模板化特性来解决这个问题.
如果我们使用模板, 就不需要手动将字符串分割开来与各种值拼接在一起. 我们可以一口气写出完整的字符串. 通过这个特性, 我们不需要使用单引号或者双引号; 我们要用的是反引号(`). 下面的示例显示了使用模板的语法. 它将一个变量值和一个字符串组装成一个REST的API地址:
1 2 3 4 | varemployeeId = 'E1001' ; vargetDepartmentApiPath = `/api/department/${employeeId}`; console.log(getDepartmentApiPath); |
模板里支持使用任意数值. 下面的例子显示了使用两个变量组成一个API地址:
1 2 3 4 | varprojectId = 'P2001' ; varemployeeProjectDetailsApiPath = `/api/project/${projectId}/${employeeId}`; console.log(employeeProjectDetailsApiPath); |
我们可以在模板里进行一些简单的算术运算. 下面的代码片段中显示了使用方式:
1 2 3 4 5 6 | 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 实用函数。这个函数将字符串重复指定的次数并将其返回。任何字符串都能调用它。
1 2 3 | var thisIsCool = "Cool! " ; var repeatedString = thisIsCool.repeat(4); console.log(repeatedString); |
子串匹配函数
ES6 在 String 的原型上添加了startsWith、endsWith 和 includes 函数,它们被用来检查某个子串是否分别在给定字符串的开头、末尾或任何位置出现过。所有这些函数都返回布尔值。includes 函数还被用来检查在给定的 index 处是否出现了子串。下面的例子演示了这些函数的用法:
startsWith():
1 2 | console.log(repeatedString.startsWith( "Cool! " )); console.log(repeatedString.startsWith( "cool! " )); |
endsWith():
1 2 | console.log(repeatedString.endsWith( "Cool! " )); console.log(repeatedString.endsWith( "Cool!" )); |
includes():
1 2 3 | 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. 对数组来说,键就和索引一样。
1 2 3 4 5 | varcitiesList = [ "Delhi" , "Mumbai" , "Kolkata" , "Chennai" , "Hyderabad" , "Bangalore" ]; for ( let entry of citiesList.entries()){ console.log(entry); } |
*keys(): 键和索引一样;所以这个函数返回数组中每个项目的索引。
1 2 3 | for ( let key of citiesList.keys()){ console.log(key); } |
查找
数组 有两个方法,即 find 和 findIndex,它们通过谓词来匹配,返回满足条件的项目。对于谓词,我们可以传递箭头函数。
find(): 接受谓词参数,并返回数组中第一个满足条件的项目。
1 | console.log(citiesList.find( city => city.startsWith( "M" ) )); |
findIndex():接受谓词参数,并返回数组中第一个满足条件的项目的索引。
1 | console.log(citiesList.findIndex( city => city.startsWith( "M" ) )); |
填充和复制
填充整个 Array,或者用一个元素填充 Array 的一部分,又或将部分 Array 元素填充至其余部分,这些都将变得简单。
fill():下面是 fill 函数的调用语法:
arrayObject.fill(objectToFill, startIndex, endIndex);
只有第一个参数是必需的。当只传一个参数就调用它时,它将传入的值填充至整个数组。
1 2 3 | citiesList.fill( "Pune" ); citiesList.fill( "Hyderabad" , 2); citiesList.fill( "Bangalore" , 3, 5); |
copyWithin():将数组中的一个或多个元素复制到数组的其他位置。
1 2 3 | 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。
1 | var citiesInUS = Array.of( "New York" , "Chicago" , "Los Angeles" , "Seattle" ); |
from():用于将形如 Array 的数据(即函数的参数)转换成数组。
1 2 3 4 | function convertToArray() { return Array.from(arguments); } var numbers = convertToArray(19, 72, 18, 71, 37, 91); |
Object 在 ES6 中获得了两个新的静态函数——用来比较两个对象,以及用来将多个对象上的可枚举属性赋值到一个对象上。
is():接受两个对象,返回一个用来表示对象是否相等的布尔值
1 2 3 4 5 | 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, …)
将所有来源对象上的可枚举属性赋值到目标对象上。
1 2 3 4 | var obj3 = {departmentName: "Accounts" }; var obj4 = {}; Object.assign(obj4, obj, obj3); // contents of obj4: {employeeId: 100, departmentName: "Accounts"} |
正如其名,Proxy 对象被用来在对象和方法周围创建代理。Proxy 对象对于完成某些任务很有帮助,例如在调用一个函数前进行校验,当访问一个属性的值时对其进行格式化。在我看来,在 JavaScript 中,代理定义了一种新的装饰(decorate)对象的途径。让我们来实战演练一下。
假设有这样一个对象:
1 2 3 4 5 6 7 8 9 10 | var employee = { employeeId: 'E10101' , name: 'Hari' , city: 'Hyderabad' , age: 28, salary: 10000, calculateBonus() { return this .salary * 0.1; } }; |
代理 Getters
当这个员工(employee)的工资(salary)被访问时,我们来格式化它的值。为此我们需要在对象属性的 getter 上定义一个代理来格式化数据。为了完成这个任务,让我们来定义一个 Proxy 对象:
1 2 3 4 5 6 7 8 9 10 | 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 上创建代理,比如以下代码片段:
1 2 3 4 5 6 7 8 9 10 11 | 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
方法没有检查这个条件。让我们通过定义一个代理来检查这个条件。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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)