ES6

75次阅读

共计 13564 个字符,预计需要花费 34 分钟才能阅读完成。

这是 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

正文完
 0

ES6

75次阅读

共计 2649 个字符,预计需要花费 7 分钟才能阅读完成。

Symbol

概述

ES6 引进了一种新的原始数据类型 Symbol 表示独一无二的值。它是 JavaScript 语言的第七种类型。
Symbol 值是通过 Symbol 函数生成。这就是说,对象的属性名吸纳在可以有两种类型,一种是原来的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名冲突。

let s = Symbol();
alert(typeof s)  //Symbol

注意:

1.symbol 函数不能使用 new 命令,否则会报错
2.Symnol 是一个原始类型的值,不是对象 (不能添加属性)
3. 它是一种类似于字符串的数据类型

Symbol 函数可以接收一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时比较容易区分。
Symbol 函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的 Symbol 函数的返回值是不相等的。

// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false

// 有参数的情况
let s1 = Symbol('foo')
let s1 = Symbol('bar')
s1 === s2 // false 
  
// 如果参数是一个对象,就会调用改对象的 toString 方法,将其转为字符串,然后才生成一个 Symbol 值
const obj = {toString() {return 'abc';}
};
const sym = Symbol(obj);
sym // Symbol(abc)

作为属性名的 Symbol

let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
let a = {[mySymbol]: 'Hello!'
};

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!'});

// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
a.mySymbol  // undefined  // 作为对象属性名时,不能用点运算符。a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
// 因为点运算符后面是字符串,所以不会读取 mySymbol 作为标识名所指代的那个值,导致 a 的属性名实际上是一个字符串,而不是一个 Symbol 值。

属性名的遍历

Symbol 作为属性名,该属性不会出现在 for…in、for…of 也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()
返回。但是它也不是私有属性。有一个 Object。getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。

Object.getOwnPropertySymbol 方法返回的是一个数组, 成员是当前对象中所有用作属性名的 Symbol 值。

const obj = {};

let foo = Symbol("foo");

Object.defineProperty(obj, foo, {value: "foobar",});

for (let i in obj) {console.log(i); // 无输出
}

Object.getOwnPropertyNames(obj)
// []

Object.getOwnPropertySymbols(obj)
// [Symbol(foo)]

另一个新的 API,Reflect.ownKeys 方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

let obj = {[Symbol('my_key')]: 1,
  enum: 2,
  nonEnum: 3
};

Reflect.ownKeys(obj)
//  ["enum", "nonEnum", Symbol(my_key)]

由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的,但又希望只用于内部的方法。

let size = Symbol('size');
class Collection{constructor(){this[size] = 0;
    }
}
add(item){this[this[size]] = item;
    this[size]++;
}
static sizeOf(instance){return instance[size];
}

let x = new Collection();
Collection.sizeOf(x); //0

x.add('foo');
Collection.sizeOf(x) // 1

Object.keys(x) // ['0']
Object.getOwnPropertyNames(x) // ['0']
Object.getOwnPropertySymbols(x) // [Symbol(size)]
// 对象 x 的 size 属性是一个 Symbol 值,所有 Object.key(x)、object.getOwnPropertyName(x)

symbol.for(),symbol.keyFor()

有时,我们希望重新使用同一个 Symbol 值,symbol.for() 方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2  //true   

Symbol.for() 与 Symbol() 这两种写法,都会生成新的 Symbol,它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for() 不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的 key 是否已经存在,如果不存在才会新建一个值。

Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false

Symbol.keyFor 方法返回一个已登记的 symbol 类型的 key

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) //"foo"

let 上 = Symbol("foo");
Symbol.keyFor(s2);  //undefined


正文完
 0

ES6

75次阅读

共计 1489 个字符,预计需要花费 4 分钟才能阅读完成。

原型继承和 Class 继承
原型如何实现继承?Class 如何实现继承?Class 本质是什么?
首先先来讲下 class,其实在 JS 中并不存在类,class 只是语法糖,本质还是函数。
那让我们来实现一下继承
组合继承
组合继承是最常用的继承方式,
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true
以上继承的方式核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。
寄生组合继承
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}

function Child(value) {
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true
以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
Class 继承
class Parent {
constructor(value) {
this.val = value
}
getValue() {
console.log(this.val)
}
}
class Child extends Parent {
constructor(value) {
super(value)
this.val = value
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true
class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。
模块化
为什么要使用模块化?都有哪几种方式可以实现模块化,各有什么特点?

解决命名冲突
提高代码的可复用性
提高代码的可维护性

立即执行函数
以作用域的方式来解决命名冲突的问题
Module
// 引入模块 API
import XXX from ‘./a.js’
import {XXX} from ‘./a.js’
// 导出模块 API
export function a() {}
export default function() {}
Proxy
Proxy 可以实现什么功能?
Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。

正文完
 0