【前端学习】ES6(一)简介、let/const、解构、字符串、正则、数值、函数

建议希望学习的同学直接看:阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版

一、简介

1、es和js的关系
ES全称ECMA(European Computer Manufacturers Association)Script,是一种标准。
js除了标准的还有JScript 和 ActionScript。总的来说前者是后者的规格,后者是前者的一种实现。
2、ES6和ES2015的关系
ES6泛指5.1版本后的js的标准,也有ES2016、ES2017等,ES2015是ES6的第一个版本,有时候ES6也特指ES2015。
3、ES历史
ES3就是js的基础,1、2都没有得到广泛的支持。ES4比较激进没有通过,没什么名气,ES5比较经典。
4、babel
Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码。

二、let与const

let的特性:
1、块级作用域(var会暴露)

// var
var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10
// let
var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

for里面是父作用域,花括号内是块级作用域。
2、没有变量提升(提前使用直接报错)
3、暂时性死区(块级作用域顶部到声明的那段区域)
typeof 一个未声明过的变量为undefined,但是不能提前对一个用let声明的变量
4、不允许重复声明

块级作用域:
1、ES5只有全局作用域和函数作用域(实际上try catch是块级作用域 ,闭包也可以模仿块级作用域),可能导致:
1.1、内层变量覆盖外层变量

var tmp = new Date();
function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}
f(); // undefined

if块中的变量被提升到函数作用域顶端以后覆盖了全局作用域的tmp
1.2、用来计数的循环变量泄露为全局变量

var s = 'hello';
for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}
console.log(i); // 5

for声明的i暴露出来

2、块级作用域和函数声明
ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。
下面相对ES5都是非法的,但是浏览器支持这样写

// 情况一
if (true) {
  function f() {}
}
// 情况二
try {
  function f() {}
} catch(e) {
  // ...
}

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。但这个没有被浏览器遵守。以及重新说明函数的提升和声明:
允许在块级作用域内声明函数。
函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
同时,函数声明还会提升到所在的块级作用域的头部。

一般来讲,尽量避免在块级作用域中声明函数,即使需要也最好使用函数表达式。

ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

// 第一种写法,报错
if (true) let x = 1;
// 第二种写法,不报错
if (true) {
  let x = 1;
}

const特性:
1、const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
2、继承了let的特性(比如块级作用域、暂时性死区等等)
3、本质:const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。因此对于复杂类型(对象、数组等)只能保证指向不可变,内部数据结构的变化不能控制。如果需要冻结对象,使用Object.freeze方法

ES5 只有两种声明变量的方法:var命令和function命令。
ES6 除了添加let和const命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。

顶层对象属性
顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

globalThis 对象
self指向的是顶层对象(浏览器指window,node指global)

var a = 0
console.log(self === window) // true
{
    console.log(self === window) // true
}

一般用this取顶层对象,但也有局限性:
a.全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
b.函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined。
c.不管是严格模式,还是普通模式,new Function(‘return this’)(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval、new Function这些方法都可能无法使用。

取顶层对象推荐的方法

// 方法一
(typeof window !== 'undefined'
   ? window
   : (typeof process === 'object' &&
      typeof require === 'function' &&
      typeof global === 'object')
     ? global
     : this);
// 方法二
var getGlobal = function () {
  if (typeof self !== 'undefined') { return self; }
  if (typeof window !== 'undefined') { return window; }
  if (typeof global !== 'undefined') { return global; }
  throw new Error('unable to locate global object');
};

ES2020 在语言标准的层面,引入globalThis作为顶层对象。也就是说,任何环境下,globalThis都是存在的,都可以从它拿到顶层对象,指向全局环境下的this。

console.log(globalThis === self) // true
console.log(self === window) // true

三、解构赋值

数组解构
1、普通的解构

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 [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

如果解构不成功,变量的值就等于undefined。

2、不完全解构
不完全解构:等号左边的模式,只匹配一部分的等号右边的数组,这种也算解构成功。

let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

3、set

let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"

从这里可以看出来,只要这个数据结构能够被迭代,就可以被解构。

function* fibs() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}
let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5

fibs是一个 Generator 函数,原生有iterator接口。

4、默认值

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,默认值才会生效。如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined。

let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

function f() {
  console.log('aaa');
}
let [x = f()] = [1];

//等价于
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
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError: y is not defined

对象解构

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
1、普通解构

let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined

用在库函数会非常容易取得值

// 例一
let { log, sin, cos } = Math;
// 例二
const { log } = console;
log('hello') // hello

2、重命名
对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

//实际上的赋值
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

注意,这时p是模式,不是变量,因此不会被赋值。如果p也要作为变量赋值,可以写成下面这样。

3、嵌套赋值

let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
obj // {prop:123}
arr // [true]

4、错误的解构方式
如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。

// 报错
let {foo: {bar}} = {baz: 'baz'};

上面代码中,等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,再取子属性就会报错。
5、继承
对象的解构赋值可以取到继承的属性。

const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);
const { foo } = obj1;
foo // "bar"

6、默认值
注意msg变量

var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
var {x: y = 3} = {};
y // 3
var {x: y = 3} = {x: 5};
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

7、特殊情况
7.1、代码块问题

// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。 {x} = {x: 1};报错的原因不是没有使用var或者let,而是因为js引擎的错误解释。

// 正确的写法
let x;
({x} = {x: 1});

7.2空白解构问题
不报错但没有什么意义

({} = [true, false]);
({} = 'abc');
({} = []);

7.3对数组进行对象解构
索引相当于属性名

let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

字符串解构
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。(相当于‘hello’字符串本身的length赋给左侧的len)

let {length : len} = 'hello';
len // 5

let {length} = ""
console.log(length) // 0

数值和布尔值的解构
解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true

上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

函数参数的解构
1、普通解构

function add([x, y]){
  return x + y;
}
add([1, 2]); // 3
//感觉这里官网给的不是很清楚
add([1,2,3])//答案也是3
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]

2、对参数的解构(不是参数列表,而是整个arguments对象)

function move({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]

上面代码中,函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。
undefined就会触发函数参数的默认值。
注意区分null和undefined,undefined会触发,null会直接传入。

function loss(a,b=2){
    console.log(a,b)
}
loss(1,undefined) // 1,2
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]

圆括号的问题
1、不可以使用圆括号的情况
变量声明

// 全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };

函数参数

// 报错
function f([(z)]) { return z; }
// 报错
function f([z,(x)]) { return x; }

赋值语句

// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];

2、可以使用圆括号的情况
可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。

[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确

进阶用法
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对象数据

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

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

7、输入模块指定方法

const { SourceMapConsumer, SourceNode } = require("source-map");

四、字符串扩展(全是些编码问题,不看了)

unicode表示法
允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点。但是,这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

"\uD842\uDFB7"
// "𠮷"
"\u20BB7"
// " 7"

上面代码表示,如果直接在\u后面跟上超过0xFFFF的数值(比如**\u20BB7**),JavaScript 会理解成**\u20BB+7**。由于\u20BB是一个不可打印字符,所以只会显示一个空格,后面跟着一个7。
ES6加入了大括号,用于正确解读字符

"\u{20BB7}"
// "𠮷"
"\u{41}\u{42}\u{43}"
// "ABC"
let hello = 123;
hell\u{6F} // 123
'\u{1F680}' === '\uD83D\uDE80'
// true

表示字符的六种方法
这个需要以后再查

'\z' === 'z'  // true 
'\172' === 'z' // true 
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true

字符串迭代器接口
for of,主要是可以识别大于0xFFFF的码点,这是普通for循环和for in无法做到的。

let text = String.fromCodePoint(0x20BB7);
for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " " 有的浏览器也会打出一个菱形里面有问号的图标
// " "
for (let i of text) {
  console.log(i);
}
// "𠮷"
}

段分隔符和行分隔符
JavaScript 规定有5个字符,不能在字符串里面直接使用,只能使用转义形式。
U+005C:反斜杠(reverse solidus)
U+000D:回车(carriage return)
U+2028:行分隔符(line separator)
U+2029:段分隔符(paragraph separator)
U+000A:换行符(line feed)
举例来说,字符串里面不能直接包含反斜杠,一定要转义写成\或者\u005c。
这个规定本身没有问题,麻烦在于 JSON 格式允许字符串里面直接使用 U+2028(行分隔符)和 U+2029(段分隔符)。这样一来,服务器输出的 JSON 被JSON.parse解析,就有可能直接报错。

const json = '"\u2028"';
JSON.parse(json); // 可能报错

(这里没看懂啥意思)为了消除这个报错,ES2019 允许 JavaScript 字符串直接输入 U+2028(行分隔符)和 U+2029(段分隔符)。

const PS = eval("'\u2029'");

注意,模板字符串现在就允许直接输入这两个字符。另外,正则表达式依然不允许直接输入这两个字符,这是没有问题的,因为 JSON 本来就不允许直接包含正则表达式。

改造JSON.stringfy()
UTF-8 标准规定,0xD800到0xDFFF之间的码点,不能单独使用,必须配对使用。
为了确保返回的是合法的 UTF-8 字符,ES2019 改变了JSON.stringify()的行为。如果遇到0xD800到0xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。

JSON.stringify('\u{D834}') // ""\\uD834""
JSON.stringify('\uDF06\uD834') // ""\\udf06\\ud834""

模版字符串
这个用的比较多,就不写了。注意以下几点
1、上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

let greeting = `\`Yo\` World!`;

2、如果模板字符串中的变量没有声明,将报错。

// 变量place没有声明
let msg = `Hello, ${place}`;
// 报错

3、如果需要引用模板字符串本身,在需要时执行,可以写成函数。

let func = (name) => `Hello ${name}!`;
func('Jack') // "Hello Jack!"

4、嵌套使用模版字符串

const tmpl = addrs => `
  <table>
  ${addrs.map(addr => `
    <tr><td>${addr.first}</td></tr>
    <tr><td>${addr.last}</td></tr>
  `).join('')}
  </table>
`;

标签模版
模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。

alert`hello`
// 等同于
alert(['hello'])

标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。
但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。

let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

还有一些很强大的功能(比如在js中嵌入jsx或者java),但是比较繁琐,也不太实用。

模版字符串的限制

五、字符串新增方法

感觉都没什么用,挑几个还比较有用的记一下
1、String.raw()
ES6 还为原生的 String 对象,提供了一个raw()方法。该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。
大概意思就是raw的参数是什么,最后就显示什么,raw函数会帮助转义

String.raw`Hi\n${2+3}!`
// 实际返回 "Hi\\n5!",显示的是转义后的结果 "Hi\n5!"
String.raw`Hi\u000A!`;
// 实际返回 "Hi\\u000A!",显示的是转义后的结果 "Hi\u000A!"

2、includes(), startsWith(), endsWith()
传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。(感觉search可以实现这几个函数)
includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

这三个方法都支持第二个参数,表示开始搜索的位置。

let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false

3、实例方法 repeat()

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
'na'.repeat(2.9) // "nana" 向下取整
'na'.repeat(-0.9) // "" ?? 参数是 0 到-1 之间的小数,则等同于 0
'na'.repeat('na') // "" 字符串强制转换成数字,这个显然不能转换
'na'.repeat('3') // "nanana" 这个可以转换成数字3

4、实例方法 padStart() padEnd()
第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
//如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'
//如果省略第二个参数,默认使用空格补全长度。
'x'.padStart(4) // '   x'
'x'.padEnd(4) // 'x   '

padStart()的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。

'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"

另一个用途是提示字符串格式。

'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

5、实例方法trimStart()和trimEnd()
trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

const s = '  abc  ';
s.trim() // "abc"
s.trimStart() // "abc  "
s.trimEnd() // "  abc"

另外trimLeft()是trimStart()的别名,trimRight()是trimEnd()的别名。

6、实例方法 matchAll()
matchAll()方法返回一个正则表达式在当前字符串的所有匹配

六、正则的扩展

构造函数

// ES5允许
var regex =newRegExp(/xyz/i);
// 等价于
var regex =/xyz/i;
// ES5允许
var regex =newRegExp(/xyz/i);
// 等价于
var regex =/xyz/i;
// ES5不允许 ES6允许
var regex =newRegExp(/xyz/,'i');
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another
newRegExp(/abc/ig,'i').flags // ES6 "i",ig被i覆盖掉了

正则还没有学明白,暂时不看了

七、数值的扩展

二进制和八进制的表示扩展
扩展了二进制和八进制的写法

0b111110111===503// true
0o767===503// true

从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀0表示,ES6 进一步明确,要使用前缀0o表示。

如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法。

Number('0b111')// 7
Number('0o10')// 8

Number.isFinite()和Number.isNaN()
ES6 在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法。
Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity。
注意,如果参数类型不是数值,Number.isFinite一律返回false。
Number.isNaN()用来检查一个值是否为NaN。
如果参数类型不是NaN,Number.isNaN一律返回false。
注意:它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false。

Number.parseInt(), Number.parseFloat()
ES6 将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。
这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

Number.isInteger()
用来判断一个数值是否为整数。
如果参数不是数值,Number.isInteger返回false。
注意,由于 JavaScript 采用 IEEE 754 标准,数值存储为64位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第54位及后面的位就会被丢弃,这种情况下,Number.isInteger可能会误判。

Number.isInteger(3.0000000000000002) // true

总之,如果对数据精度的要求较高,不建议使用Number.isInteger()判断一个数值是否为整数。

Number.EPSILON
ES6 在Number对象上面,新增一个极小的常量Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。
引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。
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
1.1 + 1.3 === 2.4 // false
withinErrorMargin(1.1 + 1.3, 2.4) // true

安全整数和 Number.isSafeInteger()
超出 2 的 53 次方之后,一个数就不精确了

Math.pow(2, 53) === Math.pow(2, 53) + 1
// true

Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。

Math对象的扩展
1、Math.trunc()
Math.trunc方法用于去除一个数的小数部分,返回整数部分。

Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.trunc(-4.9) // -4
Math.trunc(-0.1234) // -0

如果是非数值,会先转换成Number,无法转换就返回NaN。
实现:(这个写法感觉很简洁,使用||判断一个函数是否存在)

Math.trunc = Math.trunc || function(x) {
  return x < 0 ? Math.ceil(x) : Math.floor(x);
};

2、Math.sign() 返回数值的符号(正、负、0)
参数为正数,返回+1;
参数为负数,返回-1;
参数为 0,返回0;
参数为-0,返回-0;
其他值,返回NaN。

3、Math.cbrt() Math.cbrt()方法用于计算一个数的立方根。
往下也看不明白,分不清64位数和32位数
4、Math.clz32() 好像没什么用 count leading zero bits in 32-bit binary representation of a number
这个方法和左移运算符相关
Math.clz32()方法将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。
对于小数,只考虑整数部分

Math.clz32(3.2) // 30
Math.clz32(3.9) // 30

5、Math.imul() Math.imul方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。

Math.imul(2, 4)   // 8
Math.imul(-1, 8)  // -8
Math.imul(-2, -2) // 4

6、Math.fround() Math.fround方法返回一个数的32位单精度浮点数形式。

**7、Math.hypot()**方法返回所有参数的平方和的平方根。

Math.hypot(3, 4);        // 5
Math.hypot(3, 4, 5);     // 7.0710678118654755
Math.hypot();            // 0
Math.hypot(NaN);         // NaN
Math.hypot(3, 4, 'foo'); // NaN
Math.hypot(3, 4, '5');   // 7.0710678118654755
Math.hypot(-3);          // 3

8、对数方法
Math.expm1(x)返回 ex - 1,即Math.exp(x) - 1。
Math.log1p(x)方法返回1 + x的自然对数,即Math.log(1 + x)。
Math.log10(x)返回以 10 为底的x的对数。
Math.log2(x)返回以 2 为底的x的对数。
对于所有x,如果x小于 0,则返回 NaN。

9、双曲函数
Math.sinh(x) 返回x的双曲正弦(hyperbolic sine)
Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine)
Math.tanh(x) 返回x的双曲正切(hyperbolic tangent)
Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine)
Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine)
Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent)

指数运算符:
注意指数运算符遵守右结合律

// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512

bigInt
ES2020 引入了一种新的数据类型 BigInt(大整数),BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。

八、函数的扩展

函数参数默认值:
ES6前,不能直接指定参数默认值,只能在函数内部模仿一下。

// ES5
function log(x, y) {
  y = y || 'World';
  console.log(x, y);
}
// ES6
function log(x, y = 'World') {
  console.log(x, y);
}

注意:
1、看代码

// 使用参数默认值时,函数不能有同名参数。
// 不报错
function foo(x, x, y) {
  // ...
}
// 报错
function foo(x, x, y = 1) {
  // ...
}// SyntaxError: Duplicate parameter name not allowed in this context
// 参数变量是默认声明的,所以不能用let或const再次声明。
function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}
// 参数默认值不是传值的,而是每次都重新计算默认值表达式的值。
let x = 99;
function foo(p = x + 1) {
  console.log(p);
}
foo() // 100
x = 100;
foo() // 101

2、定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
3、指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。这是因为length属性的含义是,该函数预期传入的参数个数。
4、作用域问题
调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2。

var x = 1;
function f(x, y = x) {
  console.log(y);
}
f(2) // 2

5、可以将参数默认值设为undefined,表明这个参数是可以省略的。
其他应用:当缺少缺省值时抛出一个错误

function throwIfMissing() {
  throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}
foo()
// Error: Missing parameter

rest参数
就是“…”,用于获取函数的多余参数。这样就省得去给arguments对象切片

function add(...values) {
  let sum = 0;
  for (var val of values) {
    sum += val;
  }
  return sum;
}

注意
1、rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
2、函数的length属性,不包括 rest 参数。

name属性
函数的name属性,返回该函数的函数名(ES5就已经支持了)。

var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"

函数声明和具名函数表达式都会ES5和ES6返回的没有差异,主要是上例匿名函数的返回。

// Function构造函数返回的函数实例,name属性的值为anonymous。
(new Function).name // "anonymous"
// bind返回的函数,name属性值会加上bound前缀。
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "

箭头函数

let foo = () => { a: 1 };
foo() // undefined

上面代码中,原始意图是返回一个对象{ a: 1 },但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1。这时,a可以被解释为语句的标签,因此实际执行的语句是1;,然后函数就结束了,没有返回值。

箭头函数有几个使用注意点。
(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。
this对象的指向是可变的,但是在箭头函数中,它是固定的。绑定定义时所在的作用域,而不是指向运行时所在的作用域。箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  setInterval(() => this.s1++, 1000);
  // 普通函数
  setInterval(function () {
    this.s2++;
  }, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

timer.s1一直绑定的是当时的Timer函数中的s1,而不是新实例中的s1。
箭头函数里面根本没有自己的this,而是引用外层的this,箭头函数使得this从“动态”变成“静态”。
除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。
另外,由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。

两个不适用的场景:
1、定义对象的方法,且该方法内部包括this

const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  }
}

上面代码中,cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。
2、动态使用this

尾调用优化(因为浏览器原因,没太大用处)
1、定义
尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

function f(x){return g(x);}

下面三种情况不属于尾调用

// 情况一
function f(x){
  let y = g(x);
  return y;
}
// 情况二
function f(x){
  return g(x) + 1;
}
// 情况三
function f(x){
  g(x);
}

尾调用不一定出现在函数尾部,只要是最后一步操作即可。

function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();
// 等同于
function f() {
  return g(3);
}
f();
// 等同于
g(3);

上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧。
这就叫做
“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧
如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。
2、尾递归

// 普通
function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}
factorial(5) // 120
//尾递归
function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
//尾递归斐波那契数列
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};
  return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

柯里化(见过面试题)

//方法1
function currying(fn, n) {
  return function (m) {
    return fn.call(this, m, n);
  };
}
function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}
const factorial = currying(tailFactorial, 1);
factorial(5) // 120
//方法2
function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}
factorial(5) // 120

递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。

函数参数的尾逗号
ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。

Function.prototype.toString()
toString()方法返回函数代码本身,以前会省略注释和空格。

catch参数省略
ES2019 做出了改变,允许catch语句省略参数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值