这是 ES6 的入门篇教程的笔记,网址:链接描述,以下内容中 粗体 + 斜体 表示大标题,粗体 是小标题,还有一些重点;斜体 表示对于自身,还需要下功夫学习的内容。这里面有一些自己的见解,所以若是发现问题,欢迎指出~
上一篇 es5 的到最后令人崩溃,看来深层的东西还是不太熟,希望这次不要这样了!!!
ECMAScript 6 简介
Babel 转码器
以前构建 Vue-cli 的时候一直不明白为什么要添加 babel 的依赖,现在才知道。。。。
Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在现有环境执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。
let 和 const 命令
1.let 命令
基本用法
ES6 新增了 let 命令,用来声明变量。它的用法类似于 var,但是所声明的变量,只在 let 命令所在的代码块内有效。
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
// so for 循环的计数器,就很合适使用 let 命令
for (let i = 0; i < 10; i++) {// ...}
console.log(i); // ReferenceError: i is not defined
有一个重大发现!for 循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。这是以前没注意的!!
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc 输出 3 次 abc,这表明函数内部的变量 i 与循环变量 i 不在同一个作用域,有各自单独的作用域。
不存在变量提升
var 命令会发生“变量提升”现象,即变量可以在声明之前使用,值为 undefined;而 let 命令纠正了这种现象,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出 undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错 ReferenceError
let bar = 2;
暂时性死区
只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
ES6 明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了 封闭作用域。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp; // 在块级作用域内又声明了一个局部变量 tmp,tmp 绑定这个块级作用域,凡是在声明之前就使用这些变量,就会报错。}
总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的,这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// TDZ 开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ 结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
不允许重复声明
let 不允许在相同作用域内,重复声明同一个变量。
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
2. 块级作用域
为什么需要块级作用域?
ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。
第一种场景,内层变量可能会覆盖外层变量。
var tmp = new Date();
function f() {console.log(tmp); // 原意是调用外层的 tmp 变量
if (false) {var tmp = 'hello world';}
}
f(); // undefined 变量提升,导致内层的 tmp 变量覆盖了外层的 tmp 变量
第二种场景,用来计数的循环变量泄露为全局变量。
var s = 'hello';
for (var i = 0; i < s.length; i++) {console.log(s[i]);
}
console.log(i); // 5 变脸 i 只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
ES6 的块级作用域
let 实际上为 JavaScript 新增了块级作用域。
function f1() {
let n = 5;
if (true) {let n = 10;}
console.log(n); // 5
}
块级作用域与函数声明
函数能不能在块级作用域之中声明?这是一个相当令人混淆的问题。
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
// 情况一
if (true) {function f() {}}
// 情况二
try {function f() {}} catch(e) {// ...}
// 这两种情况,根据 ES5 的规定都是非法的。
3.const 命令
基本用法
const 声明一个只读的常量。一旦声明,常量的值就不能改变。
const 声明的变量不得改变值,这意味着,const 一旦声明变量,就必须立即初始化,不能留到以后赋值。
const foo; // SyntaxError: Missing initializer in const declaration 对于 const 来说,只声明不赋值,就会报错。
const 的作用域与 let 命令相同:只在声明所在的块级作用域内有效。
const 实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合雷公的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const 只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {}; // foo 储存的是一个地址,这个地址指向一个对象,不可以变得只是这个地址,即不能把 foo 指向另一个地址,但对象本身是可变得,所以可以为其添加新属性。// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is ready-only
如果真的想将对象冻结,应该使用 Object.freeze 方法。
const foo = Object.freeze({}); // 常量 foo 指向一个冻结得对象,所以下面得添加新属性不起作用,严格模式时还会报错。// 常规模式时,下面一行不起作用;// 严格模式时,该行会报错
foo.prop = 123;
foo.prop // undefined
ES6 声明变量得六种方法
ES5 只有两种声明变量得方法:var 命令和 function 命令。ES6 除了添加 let 和 const 命令,还有另外两种声明变量的方法:import 命令和 class 命令。所以 ES6 一共有 6 种声明变量的方法。
4. 顶层对象的属性
顶层对象,在浏览器环境指的是 window 对象,在 Node 指的是 global 对象。ES5 之中,顶层对象的属性与全局变量时等价的。
顶层对象的属性与全局变量挂钩,被认为时 JavaScript 语言最大的设计败笔之一。
window.a = 1;
a // 1
a = 2;
window.a // 2
// 以上代码表示顶层对象的属性赋值与全局变量的赋值,是同一件事。会带来以下三个问题:// 1)没法再编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层兑现的属性创造的,而属性的创造是动态的)。// 2)程序员很容易不知不觉地就创建了全局变量
// 3)顶层对象的属性是到处可以读写的,这非常不利于模块化编程
// 而且 window 对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有尸体含义的对象。
ES6 为了保持兼容性,规定,var 命令和 function 命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let 命令、const 命令、class 命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的熟悉那个脱钩。
var a = 1;
window.a // 1
let b = 1;
window.b // undefined
变量的解构赋值
1. 数组的解构赋值
基本用法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被成为解构(Destructuring)。
let a = 1;
let b = 2;
let c = 3;
// 这是以前为变量赋值,只能直接指定值
// ES6 允许写成下面这样
let [a, b, c] = [1, 2, 3]; // 表示可以从数组中提取值,按照对应位置,对变量赋值。
上面这种写法,本质上,是属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [,, third] = ["foo", "bar", "baz"];
third // "baz"
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
// 如果解构不成功,变量的值就等于 undefined
let [foo] = [];
let [bar, foo] = [1];
// 两种情况都属于解构不成功,foo 的值都会等于 undefined
默认值
解构赋值允许指定默认值。
let [foo = true] = [];
foo // true
let [x, y = 'b'] = ['a']; // x='a'', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
ES6 内部使用严格相等运算符(===),判断一个位置是否有值,所以,只有当一个数组成员严格等于 undefined。默认值才会生效,如下:
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null 默认值没有生效,因为 null 不严格等于 undefined
如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
function f() {console.log('aaa');
}
let [x = f()] = [1]; // 因为 x 能取到值,所以函数 f 根本不会执行,等价于下面的代码
let x;
if ([1][0] === undefined) {x = f();
}
else {x = [1][0];
}
默认值可以应用解构赋值的其他变量,但该变量必须已经声明。
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2 x= 2 是因为 [2][0] 不是 undefined
let [x = 1, y = x] = [1, 2]; // x=1;y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined 因为 x 用 y 做默认值时,y 还没有声明
2. 对象的解构赋值
简介
解构不仅可以用于数组,还可以用于对象。
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的 位置 决定;而对象的属性没有次序,变量必须与属性 同名,才能取到正确的值。
let {bar, foo} = {foo: 'aaa', bar: 'bbb'};
foo // "aaa"
bar // "bbb"
let {baz} = {foo: 'aaa', bar: 'bbb'};
baz // undefined
对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。
let {log, sin, cos} = Math; // 将 Math 对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多
const {log} = console; // 将 console.log 赋值到 log 变量
log('hello') // hello
// 如果变量名与属性名不一致,必须写成下面这样
let {foo: baz} = {foo: 'aaa', bar: 'bbb'};
baz // "aaa"
// 以上说明,对象的解构赋值是下面形式的简写。也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。** 真正被赋值的是后者,而不是前者。**
let {foo: foo, bar: bar} = {foo: 'aaa', bar: 'bbb'};
let {foo: baz} = {foo: 'aaa', bar: 'bbb'};
baz // "aaa" foo 是匹配的模式,baz 才是变量。真正被赋值的是变量 baz,而不是模式 foo。foo // error: foo is not defined
// 与数组一样,解构也可以用于嵌套解构的对象。let obj = {
p: [
'Hello',
{y: 'World'}
]
};
let {p: [x, { y}] } = obj;
x // "Hello"
y // "World"
p // error: p is not defined 因为 p 是模式,不是变量,不会被赋值
let obj = {
p: [
'Hello',
{y: 'World'}
]
};
let {p} = obj; // 这时 p 是变量
p // ['Hello', {y: 'World'}]
x // error: x is not defined
y // error: y is not defined
// 如果想要 p、x、y 同时赋值
let obj = {
p: [
'Hello',
{y: 'World'}
]
};
let {p, p: [x, { y}] } = obj; // 第一个 p 是变量,可以将 p 赋值;第二个 p 是模式,将 x、y 赋值,因为 p 已经有值了,所以从 p 中获取(一般情况下,冒号‘:’表示模式)
如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。
// 报错
let {foo: {bar}} = {baz: 'baz'} // 等号左边对象的 foo 属性,对应一个子对象。该子对象的 bar 属性,解构时会报错。这是因为 foo 此时等于 undefined,再取子属性就会报错
注意点
(1)如果要将一个已经声明的变量用于解构赋值,必须非常小心。
// 错误的写法
let x;
{x} = {x: 1} // SyntaxError: syntex error
代码的写法报错,时因为 JavaScript 引擎会将 {x} 理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。
// 正确的写法
let x;
({x} = {x: 1});
(2)解构赋值允许等号左边的模式之中,不防止任何变量名。因此,可以写出非常古怪的赋值表达式。
({} = [true, false]);
({} = 'abc');
({} = []);
// 上面的表达式虽然毫无意义,但是语法是合法的,可以执行。
3. 字符串的解构赋值
字符串也可以解构赋值,这是因为此时,字符串被转换成了一个类似数组的对象。
const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个 length 属性,因此还可以对这个属性解构赋值。
let {length: len} = 'hello';
len // 5
4. 数值和布尔值的解构赋值
解构赋值时,如果等号右边事数值和布尔值,则会先转为对象。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其传为对象。由于 undefined 和 null 无法转为对象,所以对它们进行解构赋值,都会报错。
let {toString: s} = 123;
s === Number.prototype.toString // true * 那 s 可以干什么吗?表示疑惑 *
let {prop: x} = undefined; // TypeError
let {prop: y} = null; // TypeError
5. 函数参数的解构赋值
函数的参数也可以使用解构赋值。
function add([x, y]) { // 表面上,该函数的参数是一个数组
return x + y;
}
add([1, 2]); // 3 传入参数的那一刻,数组参数就被解构成变量 x 和 y
// so on
[[1, 2], [3, 4]].map(([a, b]) => a + b); // [3, 7]
// 函数参数的解构也可以使用默认值
function move({x = 0, y = 0} = {}) {// 后面加 '={}',就是防止什么都没传时,默认为{}
return [x, y];
}
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
// 不信看下面的,哈哈哈
function move({x, y} = {x: 0, y: 0}) { // 这是给 move 的参数指定默认值,而不是为变量 x 和 y 指定默认值
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
// undefined 会触发函数参数的默认值
[1, undefined, 3].map((x = 'yes') => x); // [1, 'yes', 3]
6. 圆括号问题
解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是 模式 ,还是 表达式 ,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。
由此带来的问题市,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。
但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。
不能使用圆括号的情况
1)变量声明语句
// 全部报错 它们都是变量声明语句,模式不能使用圆括号。let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
2)函数参数
函数参数也属于变量声明,因此不能带有圆括号。
// 报错
function f([(z)]) {return z;}
function f([z, (x)]) {return x;}
3)赋值语句的模式
// 报错
({p: a}) = {p: 42};
([a]) = [5];
[({p: a}), {x: c}] = [{}, {}];
// 但是这样是可以的
({p: a} = {p: 42});
可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。
下面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不熟模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是 p,而不是 d;第三行语句与第一行语句的性质一致。
[(b)] = [3]; // 正确
({p: (d)} = {}); // 正确
({(parseInt.prop)] = [3]; // 正确
7. 用途
变量的解构赋值用途很多。
1)交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x]; // 这个是数组赋值,不像对象赋值一样去找对应的名字
2)从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。
// 返回一个数组
function example() {return [1, 2, 3];
}
let [a, b , c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
let {foo, bar} = example();
3)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来
// 参数是一组有次序的值
function f([x, y, z]) {...}
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) {...}
f({z: 3, y: 2, x: 1});
4)提取 JSON 数据
解构赋值对提取 JSON 对象中的数据,尤其有用。
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let {id, status, data: number} = jsonData;
console.log(id, status, number); // 42, "OK", [867, 5309]
5)函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {// ... do stuff};
6)遍历 Map 结构
之前知道有 map 遍历,现在又出来了 Map 结构,要好好区分,不然就晕了。
任何部署了 Interator 接口的对象,都可以用 for…of 循环遍历。Map 结构原生支持 Interator 接口,配合变量的解构赋值,获取键名和键值就非常方便。
百度了一下,map 与其他键值对集合的区别,发现 map 的“key”范围不仅限于字符串,而是各种类型的值都可以当作 key。也就是说,object 提供了“字符串 - 值”的对应结构,map 则提供的是“值 - 值”的对应,是一种更加完善的 hash 结构。
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {console.log(key + "is" + value);
}
// first is hello
// second is world
// 如果只想获取键名,或者只想获取键值,可以写成下面这样
// 获取键名
for (let [key] of map) {// ...}
for (let [, value] of map) {// ...}
7)输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。结构赋值使得输入语句非常清晰。
const {SourceMapConsumer, SourceNode} = require("source-map");
字符串的扩展
1. 字符串的 Unicode 表示法
emm 感觉用 Unicode 表示一个字符的用处不大,所以跳过这节吧~
2. 字符串的遍历器接口
字符串可以被 for…of 循环遍历,这个遍历器最大的优点是可以识别大于 0xFFFF 的码点,传统的 for 循环无法识别这样的码点。
for (let codePoint of 'foo') {console.log(codePoint)
}
// "f"
// "o"
// "o"
5. 模板字符串
模板字符串(template string)是增强版的字符串,用反引号(`)表示。它可以当作普通字符串使用,也 可以用来定义多行字符串,或者在字符串中嵌入变量 。模板字符串中嵌入变量,需要将变量名写在 ${} 之中。
大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。模板字符串之中还能调用函数。
let x = 1;
let y = 2;
`${x} + ${y} = ${x + y}` // "1 + 2 = 3"
let obj = {x: 1, y: 2};
`${obj.x + obj.y}` // "3"
function fn() {return "Hello World";}
`foo ${fn()} bar` // foo Hello World bar
如果需要引用模板字符串本身,在需要时执行,可以写成函数。模板字符串写成了一个函数的返回值。执行这个函数,就相当于执行这个模板字符串了。
let func = (name) => `Hello ${name}!`;
func('Jack') // "Hello Jack!"
emmm 下面的看不懂了,也没在代码中看到过,看来后期的学习还有很长一段路呢。
字符串的新增方法
前面的那几种方法感觉并不常用,只是粗略看了一遍,并没有细看。
6. 实例方法:repeat()
repeat 方法返回一个新字符串,表示将原字符串重复 n 次。
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""'na'.repeat(2.9) // "nana" 小数会被取整,向下取整
正则的扩展
1、RegExp 构造函数
在 ES5 中,RegExp 构造函数的参数有两种情况。
第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。
第二种情况是,参数是一个正则表达式,这时会返回一个原有正则表达式的拷贝。
// 第一种
let regex = new RegExp('xyz', 'i');
// 等价于
let regex = /xyz/i;
// 第二种
let regex = new RegExp(/xyz/i);
// 等价于
let regex = /xyz/i;
// 但是不允许此时使用第二个参数添加修饰符,否则会报错。let regex = new RegExp(/xyz/, 'i'); // Uncaught TypeError: Cannot supply flags when constructing one RegExp from another.
2、字符串的正则方法
字符串对象共有 4 个方法,可以使用正则表达式:match()、replace()、search()、split()。
ES6 将这 4 个方法,在语言内部全部调用 RegExp 的实例方法,从而做到所有与正则相关的方法,全部定义在 RegExp 对象上。
- String.prototype.match 调用 RegExp.prototype[Symbol.match]
- String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
- String.prototype.search 调用 RegExp.prototype[Symbol.search]
- String.prototype.split 调用 RegExp.prototype[Symbol.split]
数值的扩展
2、Number.isFinite(),Number.isNaN()
ES6 在 Number 对象上,新提供了 Number.isFinite()和 Number.isNaN()两个方法。
Number.isFinite()用来检查一个数值是否为有限的(finite),即不是 Infinity。
注:如果参数类型不是数值,Number.isFinite 一律返回 false。
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false
Number.isNaN()用来检查一个值是否为 NaN。
注:如果参数类型不是数值,Number.isFinite 一律返回 false。
Number.isNaN(NaN); // true
Number.isNaN(15); // true
Number.isNaN('15'); // false
Number.isNaN(true); // false
Number.isNaN(9 / NaN); // true
Number.isNaN('true' / 0); // true
Number.isNaN('true' / 'true'); // true
它们与传统的全局方法 isFinite()和 isNaN()的区别在于,传统方法先调用 Number()将非数值的值转为数值,在进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回 false,Number.isNaN()只有对于 NaN 才返回 true,非 NaN 一律返回 false。
isFinite('25'); // true
Number.isFinite('25'); // false
isNaN('NaN'); // false
Number.isNaN('NaN'); // true
3、Number.parseInt(),Number.parseFloat()
ES6 将全局方法 parseInt()和 parseFloat(),移植到 Number 对象上面,行为完全保持不变。
这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。
// ES5 的写法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45
// ES6 的写法
Number.parseInt('12.34') // 12
Number.parseFloat('12.345#') // 123.45
4、Number.isInteger()
Number.isInteger()用来判断一个数值是否为整数,也就是说,是直接判断的。
Number.isInteger(25); // true
Number.isInteger(25.1); // false
Number.isInteger(); // false
Number.isInteger(null); // false
Number.isInteger('25'); // false
Number.isInteger(true); // false
5、Number.EPSILON
ES6 在 Number 对象上面,新增一个极小的常量 Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。(相当于 2 的 -52 次方。)
Number.EPSILON 实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
Number.EPSILON 可以用来设置“能够接受的误差范围”。比如。误差范围设为 2 的 -50 次方(即 Number.EPSILON * Math.pow(2, 2)),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。
function withinErrorMargin (left, right) {return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
}
0.1 + 0.2 === 0.3; // false
withinErrorMargin(0.1 + 0.2, 0.3); // true
7、Math 对象的扩展
ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都静态方法,只能在 Math 对象上调用。
Math.trunc()
Math.trunc 方法用于去除一个数的小数部分,返回整数部分。
对于非数值,Math.trunc 内部使用 Number 方法将其先转为数值。
Math.trunc(4.1); // 4
Math.trunc(4.9); // 4
Math.trunc(-4.9); // -4
Math.trunc('123.456'); // 123
Math.trunc(true); // 1
Math.trunc(null); // 0
Math.trunc(NaN); // NaN
Math.trunc('foo'); // NaN
Math.trunc(undefined); // NaN
// 实际意义
Math.trunc = Math.trunc || function(x) {return x < 0 ? Math.ceil(x) : Math.floor(x);
}
Math.sign()
Math.sign()方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。
它会返回五种值:
- 参数为正数,返回 +1;Math.sign(5) // +1
- 参数为负数,返回 -1;Math.sign(-5) // -1
- 参数为 0,返回 0;Math.sign(0) // +0
- 参数为 -0,返回 -0;Math.sign(-0) // -0
- 其他值,返回 NaN。Math.sign(NaN) // NaN
如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回 NaN。
Math.sign('') // 0
Math.sign(true) // +1
Math.sign(false) // 0
Math.sign(null) // 0
Math.sign('foo') // NaN
Math.sign('9') // +1
Math.sign() // NaN
Math.sign(undefined) // NaN
Math.cbrt()
Math.cbrt 方法用于计算一个数的立方根。
对于非数值,Math.cbrt 方法内部也是先使用 Number 方法将其转为数值。
Math.cbrt(-1) // -1
Math.cbrt(2) // 1.2599210498948734
Math.cbrt('8') // 2
Math.cbrt('hello') // NaN
// 实质
Math.cbrt = Math.cbrt || function(x) {let y = Math.power(Math.abs(x), 1/3);
return x < 0 ? -y : y;
}
8、指数运算符
ES2016 新增了一个指数运算符(**)。该运算符是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。
2 ** 3 // 8
2 ** 3 ** 2 // 512 相当于 2**(3**2)
指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。
a **= 2 // 等同于 a = a * a
b **= 3 // 等同于 b = b * b * b