你不懂JS:ES6与未来

第六章:新增API

从值的转换到数学计算,ES6给各种内建原生类型和对象增加了许多静态属性和方法来辅助这些常见任务。另外,一些原生类型的实例通过各种新的原型方法获得了新的能力。

注意: 大多数这些特性都可以被忠实地填补。我们不会在这里深入这样的细节,但是关于兼容标准的shim/填补,你可以看一下“ES6 Shim”(https://github.com/paulmillr/es6-shim/)。

Array

在JS中被各种用户库扩展得最多的特性之一就是数组类型。ES6在数组上增加许多静态的和原型(实例)的帮助功能应当并不令人惊讶。

Array.of(..) 静态函数

Array(..)的构造器有一个尽人皆知的坑:如果仅有一个参数值被传递,而且这个参数值是一个数字的话,它并不会制造一个含有一个带有该数值元素的数组,而是构建一个长度等于这个数字的空数组。这种操作造成了不幸的和怪异的“空值槽”行为,而这正是JS数组为人诟病的地方。

Array.of(..)作为数组首选的函数型构造器取代了Array(..),因为Array.of(..)没有那种单数字参数值的情况。考虑如下代码:

var a = Array( 3 );
a.length;						// 3
a[0];							// undefined

var b = Array.of( 3 );
b.length;						// 1
b[0];							// 3

var c = Array.of( 1, 2, 3 );
c.length;						// 3
c;								// [1,2,3]

在什么样的环境下,你才会想要是使用Array.of(..)来创建一个数组,而不是使用像c = [1,2,3]这样的字面语法呢?有两种可能的情况。

如果你有一个回调,传递给它的参数值本应当被包装在一个数组中时,Array.of(..)就完美地符合条件。这可能不是那么常见,但是它可以为你的痒处挠上一把。

另一种场景是如果你扩展Array构成它的子类,而且希望能够在一个你的子类的实例中创建和初始化元素,比如:

class MyCoolArray extends Array {
	sum() {
		return this.reduce( function reducer(acc,curr){
			return acc + curr;
		}, 0 );
	}
}

var x = new MyCoolArray( 3 );
x.length;						// 3 -- 噢!
x.sum();						// 0 -- 噢!

var y = [3];					// Array,不是 MyCoolArray
y.length;						// 1
y.sum();						// `sum` is not a function

var z = MyCoolArray.of( 3 );
z.length;						// 1
z.sum();						// 3

你不能(简单地)只创建一个MyCoolArray的构造器,让它覆盖Array父构造器的行为,因为这个父构造器对于实际创建一个规范的数组值(初始化this)是必要的。在MyCoolArray子类上“被继承”的静态of(..)方法提供了一个不错的解决方案。

Array.from(..) 静态函数

在JavaScript中一个“类数组对象”是一个拥有length属性的对象,这个属性明确地带有0或更高的整数值。

在JS中处理这些值出了名地让人沮丧;将它们变形为真正的数组曾经是十分常见的做法,这样各种Array.property方法(map(..)indexOf(..)等等)才能与它一起使用。这种处理通常看起来像:

// 类数组对象
var arrLike = {
	length: 3,
	0: "foo",
	1: "bar"
};

var arr = Array.prototype.slice.call( arrLike );

另一种slice(..)经常被使用的常见任务是,复制一个真正的数组:

var arr2 = arr.slice();

在这两种情况下,新的ES6Array.from(..)方法是一种更易懂而且更优雅的方式 —— 也不那么冗长:

var arr = Array.from( arrLike );

var arrCopy = Array.from( arr );

Array.from(..)会查看第一个参数值是否是一个可迭代对象(参见第三章的“迭代器”),如果是,它就使用迭代器来产生值,并将这些值“拷贝”到将要被返回的数组中。因为真正的数组拥有一个可以产生这些值的迭代器,所以这个迭代器会被自动地使用。

但是如果你传递一个类数组对象作为Array.from(..)的第一个参数值,它的行为基本上是和slice()(不带参数值的!)或apply()相同的,它简单地循环所有的值,访问从0开始到length值的由数字命名的属性。

考虑如下代码:

var arrLike = {
	length: 4,
	2: "foo"
};

Array.from( arrLike );
// [ undefined, undefined, "foo", undefined ]

因为在arrLike上不存在位置01,和3,所以对这些值槽中的每一个,结果都是undefined值。

你也可以这样产生类似的结果:

var emptySlotsArr = [];
emptySlotsArr.length = 4;
emptySlotsArr[2] = "foo";

Array.from( emptySlotsArr );
// [ undefined, undefined, "foo", undefined ]

避免空值槽

前面的代码段中,在emptySlotsArrArray.from(..)调用的结果有一个微妙但重要的不同。Array.from(..)从不产生空值槽。

在ES6之前,如果你想要制造一个被初始化为在每个值槽中使用实际undefined值(不是空值槽!)的特定长数组,你不得不做一些额外的工作:

var a = Array( 4 );								// 四个空值槽!

var b = Array.apply( null, { length: 4 } );		// 四个 `undefined` 值

但现在Array.from(..)使这件事简单了些:

var c = Array.from( { length: 4 } );			// 四个 `undefined` 值

警告: 使用一个像前面代码段中的a那样的空值槽数组可以与一些数组函数工作,但是另一些函数会忽略空值槽(比如map(..)等)。你永远不应该刻意地使用空值槽,因为它几乎肯定会在你的程序中导致奇怪/不可预料的行为。

映射

Array.from(..)工具还有另外一个绝技。第二个参数值,如果被提供的话,是一个映射函数(和普通的Array#map(..)几乎相同),它在将每个源值映射/变形为返回的目标值时调用。考虑如下代码:

var arrLike = {
	length: 4,
	2: "foo"
};

Array.from( arrLike, function mapper(val,idx){
	if (typeof val == "string") {
		return val.toUpperCase();
	}
	else {
		return idx;
	}
} );
// [ 0, 1, "FOO", 3 ]

注意: 就像其他接收回调的数组方法一样,Array.from(..)接收可选的第三个参数值,它将被指定为作为第二个参数传递的回调的this绑定。否则,this将是undefined

一个使用Array.from(..)将一个8位值数组翻译为16位值数组的例子,参见第五章的“类型化数组”。

创建 Arrays 和子类型

在前面几节中,我们讨论了Array.of(..)Array.from(..),它们都用与构造器相似的方法创建一个新数组。但是在子类中它们会怎么做?它们是创建基本Array的实例,还是创建衍生的子类的实例?

class MyCoolArray extends Array {
	..
}

MyCoolArray.from( [1, 2] ) instanceof MyCoolArray;	// true

Array.from(
	MyCoolArray.from( [1, 2] )
) instanceof MyCoolArray;							// false

of(..)from(..)都使用它们被访问时的构造器来构建数组。所以如果你使用基本的Array.of(..)你将得到Array实例,但如果你使用MyCoolArray.of(..),你将得到一个MyCoolArray实例。

在第三章的“类”中,我们讲解了在所有内建类(比如Array)中定义好的@@species设定,它被用于任何创建新实例的原型方法。slice(..)是一个很棒的例子:

var x = new MyCoolArray( 1, 2, 3 );

x.slice( 1 ) instanceof MyCoolArray;				// true

一般来说,这种默认行为将可能是你想要的,但是正如我们在第三章中讨论过的,如果你想的话你 可以 覆盖它:

class MyCoolArray extends Array {
	// 强制 `species` 为父类构造器
	static get [Symbol.species]() { return Array; }
}

var x = new MyCoolArray( 1, 2, 3 );

x.slice( 1 ) instanceof MyCoolArray;				// false
x.slice( 1 ) instanceof Array;						// true

要注意的是,@@species设定仅适用于原型方法,比如slice(..)of(..)from(..)不使用它;它们俩都只使用this绑定(哪个构造器被用于发起引用)。考虑如下代码:

class MyCoolArray extends Array {
	// 强制 `species` 为父类构造器
	static get [Symbol.species]() { return Array; }
}

var x = new MyCoolArray( 1, 2, 3 );

MyCoolArray.from( x ) instanceof MyCoolArray;		// true
MyCoolArray.of( [2, 3] ) instanceof MyCoolArray;	// true

copyWithin(..) 原型方法

Array#copyWithin(..)是一个对所有数组可用的新修改器方法(包括类型化数组;参加第五章)。copyWithin(..)将数组的一部分拷贝到同一个数组的其他位置,覆盖之前存在在那里的任何东西。

它的参数值是 目标(要被拷贝到的索引位置),开始(拷贝开始的索引位置(含)),和可选的 结束(拷贝结束的索引位置(不含))。如果这些参数值中存在任何负数,那么它们就被认为是相对于数组的末尾。

考虑如下代码:

[1,2,3,4,5].copyWithin( 3, 0 );			// [1,2,3,1,2]

[1,2,3,4,5].copyWithin( 3, 0, 1 );		// [1,2,3,1,5]

[1,2,3,4,5].copyWithin( 0, -2 );		// [4,5,3,4,5]

[1,2,3,4,5].copyWithin( 0, -2, -1 );	// [4,2,3,4,5]

copyWithin(..)方法不会扩张数组的长度,就像前面代码段中的第一个例子展示的。当到达数组的末尾时拷贝就会停止。

与你可能想象的不同,拷贝的顺序并不总是从左到右的。如果起始位置与目标为重叠的话,它有可能造成已经被拷贝过的值被重复拷贝,这大概不是你期望的行为。

所以在这种情况下,算法内部通过相反的拷贝顺序来避免这个坑。考虑如下代码:

[1,2,3,4,5].copyWithin( 2, 1 );		// ???

如果算法是严格的从左到右,那么2应当被拷贝来覆盖3,然后这个被拷贝的2应当被拷贝来覆盖4,然后这个被拷贝的2应当被拷贝来覆盖5,而你最终会得到[1,2,2,2,2]

与此不同的是,拷贝算法把方向反转过来,拷贝4来覆盖5,然后拷贝3来覆盖4,然后拷贝2来覆盖3,而最后的结果是[1,2,2,3,4]。就期待的结果而言这可能更“正确”,但是如果你仅以单纯的从左到右的方式考虑拷贝算法的话,它就可能让人糊涂。

fill(..) 原型方法

ES6中的Array#fill(..)方法原生地支持使用一个指定的值来完全地(或部分地)填充一个既存的数组:

var a = Array( 4 ).fill( undefined );
a;
// [undefined,undefined,undefined,undefined]

fill(..)可选地接收 开始结束 参数,它们指示要被填充的数组的一部分,比如:

var a = [ null, null, null, null ].fill( 42, 1, 3 );

a;									// [null,42,42,null]

find(..) 原型方法

一般来说,在一个数组中搜索一个值的最常见方法曾经是indexOf(..)方法,如果值被找到的话它返回值的位置索引,没有找到的话返回-1

var a = [1,2,3,4,5];

(a.indexOf( 3 ) != -1);				// true
(a.indexOf( 7 ) != -1);				// false

(a.indexOf( "2" ) != -1);			// false

indexOf(..)比较要求一个严格===匹配,所以搜索"2"找不到值2,反之亦然。没有办法覆盖indexOf(..)的匹配算法。不得不手动与值-1进行比较也很不幸/不优雅。

提示: 一个使用~操作符来绕过难看的-1的有趣(而且争议性地令人糊涂)技术,参见本系列的 类型与文法

从ES5开始,控制匹配逻辑的最常见的迂回方法是some(..)。它的工作方式是为每一个元素调用一个回调函数,直到这些调用中的一个返回true/truthy值,然后它就会停止。因为是由你来定义这个回调函数,所以你就拥有了如何做出匹配的完全控制权:

var a = [1,2,3,4,5];

a.some( function matcher(v){
	return v == "2";
} );								// true

a.some( function matcher(v){
	return v == 7;
} );								// false

但这种方式的缺陷是你只能使用true/false来指示是否找到了合适的匹配值,而不是实际被匹配的值。

ES6的find(..)解决了这个问题。它的工作方式基本上与some(..)相同,除了一旦回调返回一个true/truthy值,实际的数组值就会被返回:

var a = [1,2,3,4,5];

a.find( function matcher(v){
	return v == "2";
} );								// 2

a.find( function matcher(v){
	return v == 7;					// undefined
});

使用一个自定义的matcher(..)函数还允许你与对象这样的复杂值进行匹配:

var points = [
	{ x: 10, y: 20 },
	{ x: 20, y: 30 },
	{ x: 30, y: 40 },
	{ x: 40, y: 50 },
	{ x: 50, y: 60 }
];

points.find( function matcher(point) {
	return (
		point.x % 3 == 0 &&
		point.y % 4 == 0
	);
} );								// { x: 30, y: 40 }

注意: 和其他接收回调的数组方法一样,find(..)接收一个可选的第二参数。如果它被设置了的话,就将被指定为作为第一个参数传递的回调的this绑定。否则,this将是undefined

findIndex(..) 原型方法

虽然前一节展示了some(..)如何在一个数组检索给出一个Boolean结果,和find(..)如何从数组检索中给出匹配的值,但是还有一种需求是寻找匹配的值的位置索引。

indexOf(..)可以完成这个任务,但是没有办法控制它的匹配逻辑;它总是使用===严格等价。所以ES6的findIndex(..)才是答案:

var points = [
	{ x: 10, y: 20 },
	{ x: 20, y: 30 },
	{ x: 30, y: 40 },
	{ x: 40, y: 50 },
	{ x: 50, y: 60 }
];

points.findIndex( function matcher(point) {
	return (
		point.x % 3 == 0 &&
		point.y % 4 == 0
	);
} );								// 2

points.findIndex( function matcher(point) {
	return (
		point.x % 6 == 0 &&
		point.y % 7 == 0
	);
} );								// -1

不要使用findIndex(..) != -1(在indexOf(..)中经常这么干)来从检索中取得一个boolean,因为some(..)已经给出了你想要的true/false了。而且也不要用a[ a.findIndex(..) ]来取得一个匹配的值,因为这是find(..)完成的任务。最后,如果你需要严格匹配的索引,就使用indexOf(..),如果你需要一个更加定制化的匹配,就使用findIndex(..)

注意: 和其他接收回调的数组方法一样,findIndex(..)接收一个可选的第二参数。如果它被设置了的话,就将被指定为作为第一个参数传递的回调的this绑定。否则,this将是undefined

entries(), values(), keys() 原型方法

在第三章中,我们展示了数据结构如何通过一个迭代器来提供一种模拟逐个值的迭代。然后我们在第五章探索新的ES6集合(Map,Set,等)如何为了产生不同种类的迭代器而提供几种方法时阐述了这种方式。

因为Array并不是ES6的新东西,所以它可能不被认为是一个传统意义上的“集合”,但是在它提供了相同的迭代器方法:entries()values(),和keys()的意义上,它是的。考虑如下代码:

var a = [1,2,3];

[...a.values()];					// [1,2,3]
[...a.keys()];						// [0,1,2]
[...a.entries()];					// [ [0,1], [1,2], [2,3] ]

[...a[Symbol.iterator]()];			// [1,2,3]

就像Set一样,默认的Array迭代器与values()放回的东西相同。

在本章早先的“避免空值槽”一节中,我们展示了Array.from(..)如何将一个数组中的空值槽看作带有undefined的存在值槽。其实际的原因是,在底层数组迭代器就是以这种方式动作的:

var a = [];
a.length = 3;
a[1] = 2;

[...a.values()];		// [undefined,2,undefined]
[...a.keys()];			// [0,1,2]
[...a.entries()];		// [ [0,undefined], [1,2], [2,undefined] ]

Object

几个额外的静态帮助方法已经被加入Object。从传统意义上讲,这种种类的函数是关注于对象值的行为/能力的。

但是,从ES6开始,Object静态函数还用于任意种类的通用全局API —— 那些还没有更自然地存在于其他的某些位置的API(例如,Array.from(..))。

Object.is(..) 静态函数

Object.is(..)静态函数进行值的比较,它的风格甚至要比===比较还要严格。

Object(..)调用底层的SameValue算法(ES6语言规范,第7.2.9节)。SameValue算法基本上与===严格等价比较算法相同(ES6语言规范,第7.2.13节),但是带有两个重要的例外。

考虑如下代码:

var x = NaN, y = 0, z = -0;

x === x;							// false
y === z;							// true

Object.is( x, x );					// true
Object.is( y, z );					// false

你应当为严格等价性比较继续使用===Object.is(..)不应当被认为是这个操作符的替代品。但是,在你想要严格地识别NaN-0值的情况下,Object.is(..)是现在的首选方式。

注意: ES6还增加了一个Number.isNaN(..)工具(在本章稍后讨论),它可能是一个稍稍方便一些的测试;比起Object.is(x, NaN)你可能更偏好Number.isNaN(x)。你 可以 使用笨拙的x == 0 && 1 / x === -Infinity来准确地测试-0,但在这种情况下Object.is(x,-0)要好得多。

Object.getOwnPropertySymbols(..) 静态函数

第二章中的“Symbol”一节讨论了ES6中的新Symbol基本值类型。

Symbol可能将是在对象上最经常被使用的特殊(元)属性。所以引入了Object.getOwnPropertySymbols(..),它仅取回直接存在于对象上的symbol属性:

var o = {
	foo: 42,
	[ Symbol( "bar" ) ]: "hello world",
	baz: true
};

Object.getOwnPropertySymbols( o );	// [ Symbol(bar) ]

Object.setPrototypeOf(..) 静态函数

还是在第二章中,我们提到了Object.setPrototypeOf(..)工具,它为了 行为委托 的目的(意料之中地)设置一个对象的[[Prototype]](参见本系列的 this与对象原型)。考虑如下代码:

var o1 = {
	foo() { console.log( "foo" ); }
};
var o2 = {
	// .. o2 的定义 ..
};

Object.setPrototypeOf( o2, o1 );

// 委托至 `o1.foo()`
o2.foo();							// foo

另一种方式:

var o1 = {
	foo() { console.log( "foo" ); }
};

var o2 = Object.setPrototypeOf( {
	// .. o2 的定义 ..
}, o1 );

// 委托至 `o1.foo()`
o2.foo();							// foo

在前面两个代码段中,o2o1之间的关系都出现在o2定义的末尾。更常见的是,o2o1之间的关系在o2定义的上面被指定,就像在类中,而且在对象字面量的__proto__中也是这样(参见第二章的“设置[[Prototype]]”)。

警告: 正如展示的那样,在对象创建之后立即设置[[Prototype]]是合理的。但是在很久之后才改变它一般不是一个好主意,而且经常会导致困惑而非清晰。

Object.assign(..) 静态函数

许多JavaScript库/框架都提供将一个对象的属性拷贝/混合到另一个对象中的工具(例如,jQuery的extend(..))。在这些不同的工具中存在着各种微妙的区别,比如一个拥有undefined值的属性是否被忽略。

ES6增加了Object.assign(..),它是这些算法的一个简化版本。第一个参数是 目标对象 而所有其他的参数是 源对象,它们会按照罗列的顺序被处理。对每一个源对象,它自己的(也就是,不是“继承的”)可枚举键,包括symbol,将会好像通过普通=赋值那样拷贝。Object.assign(..)返回目标对象。

考虑这种对象构成:

var target = {},
	o1 = { a: 1 }, o2 = { b: 2 },
	o3 = { c: 3 }, o4 = { d: 4 };

// 设置只读属性
Object.defineProperty( o3, "e", {
	value: 5,
	enumerable: true,
	writable: false,
	configurable: false
} );

// 设置不可枚举属性
Object.defineProperty( o3, "f", {
	value: 6,
	enumerable: false
} );

o3[ Symbol( "g" ) ] = 7;

// 设置不可枚举 symbol
Object.defineProperty( o3, Symbol( "h" ), {
	value: 8,
	enumerable: false
} );

Object.setPrototypeOf( o3, o4 );

仅有属性abce,和Symbol("g")将被拷贝到target

Object.assign( target, o1, o2, o3 );

target.a;							// 1
target.b;							// 2
target.c;							// 3

Object.getOwnPropertyDescriptor( target, "e" );
// { value: 5, writable: true, enumerable: true,
//   configurable: true }

Object.getOwnPropertySymbols( target );
// [Symbol("g")]

属性df,和Symbol("h")在拷贝中被忽略了;非枚举属性和非自身属性将会被排除在赋值之外。另外,e作为一个普通属性赋值被拷贝,而不是作为一个只读属性被复制。

在早先一节中,我们展示了使用setPrototypeOf(..)来在对象o2o1之间建立一个[[Prototype]]关系。这是利用Object.assign(..)的另外一种形式:

var o1 = {
	foo() { console.log( "foo" ); }
};

var o2 = Object.assign(
	Object.create( o1 ),
	{
		// .. o2 的定义 ..
	}
);

// 委托至 `o1.foo()`
o2.foo();							// foo

注意: Object.create(..)是一个ES5标准工具,它创建一个[[Prototype]]链接好的空对象。更多信息参见本系列的 this与对象原型

Math

ES6增加了几种新的数学工具,它们协助或填补了常见操作的空白。所有这些操作都可以被手动计算,但是它们中的大多数现在都被原生地定义,这样JS引擎就可以优化计算的性能,或者进行与手动计算比起来小数精度更高的计算。

与直接的开发者相比,asm.js/转译的JS代码(参见本系列的 异步与性能)更可能是这些工具的使用者。

三角函数:

  • cosh(..) - 双曲余弦
  • acosh(..) - 双曲反余弦
  • sinh(..) - 双曲正弦
  • asinh(..) - 双曲反正弦
  • tanh(..) - 双曲正切
  • atanh(..) - 双曲反正切
  • hypot(..) - 平方和的平方根(也就是,广义勾股定理)

算数函数:

  • cbrt(..) - 立方根
  • clz32(..) - 计数32位二进制表达中前缀的零
  • expm1(..) - 与exp(x) - 1相同
  • log2(..) - 二进制对数(以2为底的对数)
  • log10(..) - 以10为底的对数
  • log1p(..) - 与log(x + 1)相同
  • imul(..) - 两个数字的32为整数乘法

元函数:

  • sign(..) - 返回数字的符号
  • trunc(..) - 仅返回一个数字的整数部分
  • fround(..) - 舍入到最接近的32位(单精度)浮点数值

Number

重要的是,为了你的程序能够正常工作,它必须准确地处理数字。ES6增加了一些额外的属性和函数来辅助常见的数字操作。

两个在Number上新增的功能只是既存全局函数的引用:Number.parseInt(..)Number.parseFloat(..)

静态属性

ES6以静态属性的形式增加了一些有用的数字常数:

  • Number.EPSILON - 在任意两个数字之间的最小值:2^-52(关于为了应对浮点算数运算不精确的问题而将这个值用做容差的讲解,参见本系列的 类型与文法 的第二章)
  • Number.MAX_SAFE_INTEGER - 可以用一个JS数字值明确且“安全地”表示的最大整数:2^53 - 1
  • Number.MIN_SAFE_INTEGER - 可以用一个JS数字值明确且“安全地”表示的最小整数:-(2^53 - 1)(-2)^53 + 1.

注意: 关于“安全”整数的更多信息,参见本系列的 类型与文法 的第二章。

Number.isNaN(..) 静态函数

标准的全局isNaN(..)工具从一开始就坏掉了,因为不仅对实际的NaN值返回true,而且对不是数字的东西也返回true。其原因是它会将参数值强制转换为数字类型(这可能失败而导致一个NaN)。ES6增加了一个修复过的工具Number.isNaN(..),它可以正确工作:

var a = NaN, b = "NaN", c = 42;

isNaN( a );							// true
isNaN( b );							// true —— 噢!
isNaN( c );							// false

Number.isNaN( a );					// true
Number.isNaN( b );					// false —— 修好了!
Number.isNaN( c );					// false

Number.isFinite(..) 静态函数

看到像isFinite(..)这样的函数名会诱使人们认为它单纯地意味着“不是无限”。但这不十分正确。这个新的ES6工具有更多的微妙之处。考虑如下代码:

var a = NaN, b = Infinity, c = 42;

Number.isFinite( a );				// false
Number.isFinite( b );				// false

Number.isFinite( c );				// true

标准的全局isFinite(..)会强制转换它收到的参数值,但是Number.isFinite(..)会省略强制转换的行为:

var a = "42";

isFinite( a );						// true
Number.isFinite( a );				// false

你可能依然偏好强制转换,这时使用全局isFinite(..)是一个合法的选择。或者,并且可能是更明智的选择,你可以使用Number.isFinite(+x),它在将x传递前明确地将它强制转换为数字(参见本系列的 类型与文法 的第四章)。

整数相关的静态函数

JavaScript数字值总是浮点数(IEEE-754)。所以判定一个数字是否是“整数”的概念与检查它的类型无关,因为JS没有这样的区分。

取而代之的是,你需要检查这个值是否拥有非零的小数部分。这样做的最简单的方法通常是:

x === Math.floor( x );

ES6增加了一个Number.isInteger(..)帮助工具,它可以潜在地判定这种性质,而且效率稍微高一些:

Number.isInteger( 4 );				// true
Number.isInteger( 4.2 );			// false

注意: 在JavaScript中,44.4.0,或4.0000之间没有区别。它们都将被认为是一个“整数”,因此都会从Number.isInteger(..)中给出true

另外,Number.isInteger(..)过滤了一些明显的非整数值,它们在x === Math.floor(x)中可能会被混淆:

Number.isInteger( NaN );			// false
Number.isInteger( Infinity );		// false

有时候处理“整数”是信息的重点,它可以简化特定的算法。由于为了仅留下整数而进行过滤,JS代码本身不会运行得更快,但是当仅有整数被使用时引擎可以采取几种优化技术(例如,asm.js)。

因为Number.isInteger(..)NanInfinity值的处理,定义一个isFloat(..)工具并不像!Number.isInteger(..)一样简单。你需要这么做:

function isFloat(x) {
	return Number.isFinite( x ) && !Number.isInteger( x );
}

isFloat( 4.2 );						// true
isFloat( 4 );						// false

isFloat( NaN );						// false
isFloat( Infinity );				// false

注意: 这看起来可能很奇怪,但是无穷即不应当被认为是整数也不应当被认为是浮点数。

ES6还定义了一个Number.isSafeInteger(..)工具,它检查一个值以确保它是一个整数并且在Number.MIN_SAFE_INTEGER-Number.MAX_SAFE_INTEGER的范围内(包含两端)。

var x = Math.pow( 2, 53 ),
	y = Math.pow( -2, 53 );

Number.isSafeInteger( x - 1 );		// true
Number.isSafeInteger( y + 1 );		// true

Number.isSafeInteger( x );			// false
Number.isSafeInteger( y );			// false

String

在ES6之前字符串就已经拥有好几种帮助函数了,但是有更多的内容被加入了进来。

Unicode 函数

在第二章的“Unicode敏感的字符串操作”中详细讨论了String.fromCodePoint(..)String#codePointAt(..)String#normalize(..)。它们被用来改进JS字符串值对Unicode的支持。

String.fromCodePoint( 0x1d49e );			// "𝒞"

"ab𝒞d".codePointAt( 2 ).toString( 16 );		// "1d49e"

normalize(..)字符串原型方法用来进行Unicode规范化,它将字符与相邻的“组合标志”进行组合,或者将组合好的字符拆开。

一般来说,规范化不会对字符串的内容产生视觉上的影响,但是会改变字符串的内容,这可能会影响length属性报告的结果,以及用位置访问字符的行为:

var s1 = "e\u0301";
s1.length;							// 2

var s2 = s1.normalize();
s2.length;							// 1
s2 === "\xE9";						// true

normalize(..)接受一个可选参数值,它用于指定使用的规范化形式。这个参数值必须是下面四个值中的一个:"NFC"(默认),"NFD""NFKC",或者"NFKD"

注意: 规范化形式和它们在字符串上的效果超出了我们要在这里讨论的范围。更多细节参见“Unicode规范化形式”(http://www.unicode.org/reports/tr15/)。

String.raw(..) 静态函数

String.raw(..)工具被作为一个内建的标签函数来与字符串字面模板(参见第二章)一起使用,取得不带有任何转译序列处理的未加工的字符串值。

这个函数几乎永远不会被手动调用,但是将与被标记的模板字面量一起使用:

var str = "bc";

String.raw`\ta${str}d\xE9`;
// "\tabcd\xE9", not "	abcdé"

在结果字符串中,\t是分离的未被加工过的字符,而不是一个转译字符序列\t。这对Unicode转译序列也是一样。

repeat(..) 原型函数

在Python和Ruby那样的语言中,你可以这样重复一个字符串:

"foo" * 3;							// "foofoofoo"

在JS中这不能工作,因为*乘法是仅对数字定义的,因此"foo"会被强制转换为NaN数字。

但是,ES6定义了一个字符串原型方法repeat(..)来完成这个任务:

"foo".repeat( 3 );					// "foofoofoo"

字符串检验函数

作为对ES6以前的String#indexOf(..)String#lastIndexOf(..)的补充,增加了三个新的搜索/检验函数:startsWith(..)endsWith(..),和includes(..)

var palindrome = "step on no pets";

palindrome.startsWith( "step on" );	// true
palindrome.startsWith( "on", 5 );	// true

palindrome.endsWith( "no pets" );	// true
palindrome.endsWith( "no", 10 );	// true

palindrome.includes( "on" );		// true
palindrome.includes( "on", 6 );		// false

对于所有这些字符串搜索/检验方法,如果你查询一个空字符串"",那么它将要么在字符串的开头被找到,要么就在字符串的末尾被找到。

警告: 这些方法默认不接受正则表达式作为检索字符串。关于关闭实施在第一个参数值上的isRegExp检查的信息,参见第七章的“正则表达式Symbol”。

复习

ES6在各种内建原生对象上增加了许多额外的API帮助函数:

  • Array增加了of(..)from(..)之类的静态函数,以及copyWithin(..)fill(..)之类的原型函数。
  • Object增加了is(..)assign(..)之类的静态函数。
  • Math增加了acosh(..)clz32(..)之类的静态函数。
  • Number增加了Number.EPSILON之类的静态属性,以及Number.isFinite(..)之类的静态函数。
  • String增加了String.fromCodePoint(..)String.raw(..)之类的静态函数,以及repeat(..)includes(..)之类的原型函数。

这些新增函数中的绝大多数都可以被填补(参见ES6 Shim),它们都是受常见的JS库/框架中的工具启发的。


书籍推荐