本文是对发布于 DNC 杂志一月版 上的 ES6 系列文章的一个继续。在第一部分中,我们看见了在 ES6 中即将来临的对 JavaScript 语言的一些改进。本文将着眼于新的对象类型,以及对该语言中已经存在的对象的 API 的更新。
就如在 第一篇文章 中已经提及的,ES6 致力于匹配 JavaScript 来使之更适合于编写大规模的应用程序。为了实现该目标,该语言的设计者们已经对其添加了不少的新特性,这些特性的灵感源于那些“有类型(typed)”的 JavaScript 的替代版以及一些其它的库,包括一些服务端的库。以下是关于那些新的和更新过的对象的一瞥:
新的数据结构,用于更简单的储存唯一性的值(Set),或,有唯一性的键的键-值对(Map)
已经存在的对象,比如 Math 和 Number 会获得新的功能来执行更多的操作,和用更优的方式来执行已存在的操作。
String 类型将会获得一些使得 “解析(parsing)” 操作更方便的特性。
Object 类型将会获得一些函数来 “分配(assign)” 一个对象以及在两个对象之间进行比较。
在 Array 类型上的新函数集现在可以更方便的来查找到一个元素,一个元素的索引,以及在数组内部对元素进行拷贝操作。
新的 Proxy 对象来对以存在的对象或者函数扩展(extend)功能集。
这些 API 目前并不完全支持所有的平台。Chrome 和 Opera 最新的版本不支持一些在 String 上的新功能并且根本不支持 Proxy 对象。类似于 traceur 和 6to5 这样的编译器没有提供polyfill相关的API。我是用 Firefox nightly 去测试所有的这些例子的。一些脚本为了arrow功能和short hand功能使用ES6的新语法,你最好使用 traceur 编译脚本。
现在你得到了一个关于ES6 API更新的结论,让我们开始探索他们吧。
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的 size 属性包含了当前的对象数目。
console.log(setOfObjects.size);
之前提到过,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之中:
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 是已知的,那么可以使用 get() 方法从一个 Map 中提取 value。如果无法在 Map 中找到 key,那么方法会返回 undefined。
我们可以用 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 是已知的,那么可以使用 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 s 可以通过常规的 for...of 语句进行遍历。另外,还有一些方法可以遍历或循环 Map s 中 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 函数可通过 Number 对象调用,这样会更加明确。它们的工作方式和之前一样。
console.log(Number.parseInt(
"182"
));
console.log(Number.parseFloat(
"817.12"
));
现在我们可以通过 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()。
该函数用来检测值是否是有限的数字值(number)。该函数会在检测前试图将值转换成数字值。下面是该函数的一些示例:
console.log(Number.isFinite("10")); //false console.log(Number.isFinite("x19")); //false
该函数用来检测值是否是合法的整数。该函数不会在检测前将值转换成数字值。下面是该函数的一些示例 :
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地址:
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 对象对于完成某些任务很有帮助,例如在调用一个函数前进行校验,当访问一个属性的值时对其进行格式化。在我看来,在 JavaScript 中,代理定义了一种新的装饰(decorate)对象的途径。让我们来实战演练一下。
假设有这样一个对象:
var employee = { employeeId: 'E10101', name: 'Hari', city: 'Hyderabad', age: 28, salary: 10000, calculateBonus() { return this.salary * 0.1; } };
当这个员工(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 的特性:定义方法的简写形式以及模版字符串。
对于一个员工(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)