阅读 113

ES6学习文档(更新至第7节)

参考文章: es6.ruanyifeng.com/


前言和ES6简介

ES6是JavaScript的统一规范。

node.js 是能让js代码配置服务器的技术,一般配置在3000端口。npm是管理js包的工具,其脚本命令npm script可以用来运行测试等功能。


Let和Const命令

  1. let

    var可以在声明前被调用,输出undefined,这种现象称为变量提升。 在代码块中,let声明前的变量都不能使用,这种现象称为暂时性死区 DTZ

    let作用的代码块,称为块级作用域

    所以let只能作用于对应代码块中。var是全局引用,let则在循环中表现比较正常。

for (var i = 0; i < 10; i++) {
    a[i]=i;
    
    } // 数组内元素均为10。
复制代码
  1. const

    const表示常量,声明后不能改变。但其实是变量对应的地址不能改变,所以对于一个数组,仍然可以push/pop元素来改变它的值。(使用Object.freeze()来冻结对象)

    顶层对象问题:顶层对象在不同js环境下,获取方式不同。ES6加入let/const后对顶层对象设计问题,有所帮助。


变量的解构赋值

类似python一样的多变量赋值

  1. 数组的解构赋值

    let [a,b,c] = [1,2,3] 未被成功解构的为undefined;等号右边只要是Iterator就可以。

    let [a=1] = [] 这种方式赋默认值。

  2. 对象的解构赋值

    let { foo, bar } = { bar: "a", foo: "b" } 无需严格按照顺序,因为左侧其实是 let { foo:foo, bar:bar }的形式。

    解构赋值的特殊情况

    let x; 
    {x} = {x:1};
    // 这里会报错,行首为大括号会被视为代码块
    // 所以第二行需要整行用圆括号括起来
复制代码
  1. 函数的解构赋值
function add([x,y]) {
    return x + y;
} // 这里x,y都被解构了
复制代码
  1. 圆括号问题

    圆括号帮助解析赋值,使用一句话总结:声明不能用圆括号,赋值可以。


字符串的扩展

  1. Unicode

    "\u0061" === "a", 用"\u{xxxxxxxx}"可以表示UTF-16的字符。

  2. codePointAt()

    codePointAt()能够正确地返回UTF-16的码点,对应而言的charAt和charCodeAt都不行。但是codePointAt仍有bug,结合for of可以正确读取。

for (let ch of a){
    console.log(ch.codePointAt(0).toString(16));
}
复制代码
  1. fromCodePoint()

    将Unicode转化为字符,与codePointAt方法相反。fromCodePoint可以接受多个参数,它会将这些参数先拼接为字符串。

  2. 字符串的遍历接口

    for of 就是字符串的遍历接口,这个接口支持UTF-16格式。对应的传统遍历模式:for (let i=0;i<str.length;i++)只能在UTF-8组成的字符串上正确运行。

  3. includes(),startsWith(),endsWith()

    indexOf()判断一个字符串是否包含在另一个字符串中。

    • startsWith 判断参数字符串是否在原字符串的头部;
    • endsWith 判断参数字符串是否在原字符串的尾部;
    • includes 表示是否找到了参数字符串。
  4. repeat()

    repeat方法接受非负数为参数,小数参数向上取整。表示对字符串重复多少次,并返回结果。

  5. padStart(), padEnd()

    这两个函数接受两个参数,一个是填充后长度,另一个是填充字符串。padStart表示头部填充,padEnd表示尾部填充。

'x'.padStart(4,'ab')
// 'abax'
复制代码
  1. 模板字符串

    非常重要的概念,反引号(`)内表示模板字符串。 一个例子:

name = 'rick';
age = 22;
s = `My name is ${name}, aged ${age}`;
复制代码

当然也可以在${}内加入任何JS表达式,调用函数等等。

  1. 标签模板

    在一个函数后面,可以直接跟一个模板字符串。

a = 5;
b = 10;

tag`Hello ${a+b} world ${a*b}`; 
//等价于
tag(['Hello ',' world ',''],15,50)
复制代码

使用标签模板有两个好处,一个是防止用户恶意输入</ script>脚本,因为这时候模板字符串的参数已经被抽取出来了;另外一个是用于国际化,你可以只国际化参数或非参数。


正则的扩展

  1. RegExp构造函数

ES6允许RegExp包含第二个参数作为正则表达式修饰符。多个修饰符可以叠加。

修饰符是影响整个正则规则的特殊符号。

  • i: 大小写不敏感;
  • g: 全局查找;
  • m: 检测换行符。
  • u: ES6新增,Unicode匹配。
var regex = new RegExp('xyz','i');
var regex = new RegExp(/xyz/i);
var regex = /xyz/i;
复制代码
  1. 字符串的正则方法

字符串对象可以直接调用对应的正则对象的方法,包括match/replace/search/split。

  1. u修饰符

JS中,正则表达式的test()方法,表示是否匹配括号内参数是否匹配正则对象的模式。

ES6中加入了对Unicode的正则。关键语法就是u。

  • u. 表示任意Unicode字符;
  • \u{xx}表示模式中的具体unicode;
  • 详情见原文...
  1. y修饰符

JS中,正则表达式的exec()方法,如果匹配到了模式,则返回匹配结果。

y修饰符表示粘连(sticky),加入了该修饰符进行匹配后,下一次会从上一次匹配的后一个字符开始匹配,这和g修饰符相同。不同的是,g只要剩下的字符串含有模式就会返回,但是y必须严格从下一个字符开始匹配。

var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;

r1.exec(s) // ["aaa"] 剩余“_aa_a”
r2.exec(s) // ["aaa"] 剩余“_aa_a”

r1.exec(s) // ["aa"]
r2.exec(s) // null
复制代码

y修饰符的设计本意,就是让头部匹配的标志^在全局匹配中都有效。

y修饰符的一个应用,是从字符串提取token(词元),y修饰符确保了匹配之间不会有漏掉的字符。

  1. s修饰符

s修饰符作用是让.可以匹配任意字符,包括换行符。

  1. 断言

/x(?=reg_pattern)/ 正前向断言,只有当字符串右侧出现匹配reg_pattern的字符时才匹配正则表达式。 /x(?!reg_pattern)/ 负前向断言。

JS原来只支持先行断言,而不支持后行断言。ES6加入了后行断言,一个例子是 /(?<=reg_pattern)x//(?<!reg_pattern)x/.

  1. 具名组匹配和解构赋值

ES6支持给正则表达式中括号内的字表达式赋予具体的名称,有了这个名称过后就可以进行解构赋值。注意exec后的对象,在调用groups即可获得具名组的对象了。

// 旧定义
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
// 加入具名组
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
// 调用
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31

// 例子:解构替换
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;

'2015-01-02'.replace(re, '$<day>/$<month>/$<year>');

复制代码

数值的扩展

Number 对象是原始数值的包装对象。

  1. 二进制和八进制

二进制前面带0b,八进制前面带0o。

  1. parseInt和parseFloat

现在这两个方法可以通过Number对象调用,而不只是全局函数。

  1. Number.isInteger()

这个方法可以判断一个Number是否为整数,但要注意JS采用IEEE 754标准,小数点后16个10进制位,对应超过了52个有效位,就会被丢弃。那么判断结果就会发生错误。

  1. Math对象扩展
  • Math.trunc() 去除一个数的小数部分。
  • Math.sign() 返回一个数的符号,可以是1,0,-1,-0,NaN
  • Math.cbrt() 返回一个数的立方根。
  • Math.clz32() 返回一个数的无符号32位表示,有多少个前置0.
  • Math.log2() 返回2为底的对数。
  • ... 其他函数不太常用

函数的扩展

  1. 函数的默认值

ES6之前不能指定函数的默认值,现在指定方式类似于python。参数默认值是惰性求值的,可以看做运行函数时,先触发计算默认值。

  1. 函数默认值结合解构赋值

难点:请问下面两种写法有什么差别?

// 写法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

复制代码

分析:写法一定义了函数的默认值为{},并且设置了解构赋值的默认值。但写法二定义了函数的默认值为一个对象,但没有设置解构赋值的默认值

  1. 参数默认值的位置

将含有默认值的参数放到前面(x=1,y),明显不能直接省略。但是显式地写(undefined,y)却可以触发默认值。当然这种写法很蠢...

  1. 作用域

函数默认值的作用域是单独的作用域,如f(x,y=x)。作用域就为x,y=x,赋值完了就销毁了。

  1. rest参数

...变量名用于获取函数的多余参数,本质是一个数组。原来要获取多余参数,需要使用arguments变量。但是arguments是一个对象,实际使用时要转换成数组。rest参数就不需要这样。

  • rest参数必须是最后一个参数。
  • 函数的length属性不包括rest参数。
  1. name属性

foo.name返回函数名称(foo)。匿名函数在ES5返回"",但在ES6返回该匿名函数被赋值的变量名。

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"
复制代码
  1. 箭头函数

箭头:=>可以用来定义函数。

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

复制代码

多于一条语句就要用大括号括起来,并且使用return返回。

箭头函数注意事项:

  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

箭头函数体内的this和普通函数function的this不同,后者的this指的是全局对象window。

this在普通函数function中不是绑定的,也就是说this指定的对象是不确定的。如果this的函数被父对象调用了,那么this指代的就是父对象,否则就是window。

由于箭头函数内不存在this对象,那么this指代的就是父对象,而不会是window,从而实现了绑定。 除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target

  1. 尾调用和优化

尾调用指某个函数的最后一步是调用另一个函数。 其实道理是,如果函数体内有保存局部变量,那么即使最后一步调用另一个函数,也要保存这些局部变量和调用的位置(虽然是最后一行,也看成任意位置处理)。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120
复制代码

这个递归函数就不是尾递归,因为保存了n作为局部变量。算法复杂度O(n)。

function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120
复制代码

上面这个例子就是尾递归,不保存任何调用记录,算法复杂度为O(1)。

凭什么呢?联系本节第4点——作用域,函数的参数有单独作用域。如果把参数放到函数体中,那么这个参数是参数列表的变量的拷贝,成为了局部变量,占用调用栈。

  1. 尾递归函数的改写

上面尾递归函数明显很丑陋,有两种方法改写。一个是加入辅助函数,原函数就能使用正常的参数列表了。

function tailFactorial(n, total) {
  if (n === 1) return total;
  return tailFactorial(n - 1, n * total);
}

function factorial(n) {
  return tailFactorial(n, 1);
}

factorial(5) // 120
复制代码

第二个方法是柯里化(Currying),简而言之就是把原函数从接受多个参数变成接受一个参数的过程,是函数式编程的一个重要思想。

// 自己的例子
function foo(a1, a2, a3) {
    return a1*a2+a3;
}

function currying(fun,num1,num2) {
    return function (num3) {
        fun(num1,num2,num3);
    }
}

function foo2 = currying(fun,a1,a2);

foo2(a3);

复制代码

可惜尾递归优化只存在于严格模式下,正常函数的优化(因为自带两个局部变量arguments,caller)还是需要递归变循环...