《ES6标准入门》读书笔记

@(StuRep)

图片发自简书App

let和const命令

  1. ES6新增let命令,用于声明变量,是块级作用域。
  2. let声明的变量不会像var声明的变量发生“变量提升”现象,所以,变量一定要在声明后使用,不然就会报错。
  3. 暂时性死区:只要块级作用域内存在let命令,它所声明的变量就会“绑定”在这个区域,不再受外部的影响。即在代码块内,使用let命令声明变量之前,这个变量都是不可用的,这在语法上称为“暂时性死区”。
  4. ES6规定暂时性死区和不存在变量提升,主要是为了减少运行时的错误,防止在变量声明前就使用这个变量,导致意外,这样的错误在ES5中很常见。
  5. let不允许在相同作用域内重复声明同一个变量。
  6. const命令用来声明常量,声明了之后就不能再改变,所以在声明的时候就必须赋值,这个命令同样是块级作用域,同样存在暂时性死区。
  7. 对于用const声明的对象,变量名不会指向对象的数据,而是指向对象所在的地址,所以用const声明的复合类型变量中的数据是可以改变的,这点需要当心!

变量的解构赋值

  1. 解构:ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被成为解构。如下:

    //ES5
    var a = 1;
    var b = 2;
    var c = 3;
    
    //ES6
    var [a, b, c] = [1, 2, 3];
    
    //嵌套
    let [foo, [[bar], baz]] = [1, [[2], 3]];
    foo;//1
    bar;//2
    baz;//3
    
    //如果等号右边不是可以遍历的结构,就无法匹配就会报错。
  2. 解构赋值是允许设置默认值的,在ES6内部使用'==='来判断一个位置是否有值。所以,如果一个数组成员不严格等于undefined,默认值是不会生效的。例如:

    [x, y = 'b'] = ['a', undefined];//x='a',y='b'
    [x = 1] = [null];//x=null
  3. 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。例如:

    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] = []; //报错,因为x在使用y作为其默认值的时候y还没有被声明
  4. 对象的解构赋值:

    var {foo, bar} = {foo: "aaa", bar:"bbb"};
    foo //"aaa"
    bar //"bbb"
对象的解构赋值和数组有一个重要的不同:数组的元素是按次序排列的,变量的取值由它的位置来决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
  1. 对象的解构赋值可以很方便地将现有对象的方法赋值到某个变量。例如:

    let {log, sin, cos} = Math;
    //这样就可以把取对数、正弦、余弦3个方法赋值到对应的变量上面,用起来很方便;
  2. 字符串也可以解构赋值,因为字符串会被转换成一个类似数组的对象。例如:

    const [a, b, c, d, e] = 'hello';
    a // 'h'
    b // 'e'
    c // 'l'
    d // 'l'
    e // 'o'
    
    let {length : len} = 'hello';
    len //5
  3. 数值和布尔值的解构赋值:解构赋值时,如果等号右边是数值或布尔值,则会先转为对象。
  4. 函数参数也可以解构赋值,例如:

    function add([x,y]){
        return x + y;
    }
    
    add([1, 2]) //3
  5. 变量的解构赋值的用途很多,简洁易读:

    • 交换变量的值[x, y] = [y, x]
    • 从函数返回多个值;
    function example(){
        return [1, 2, 3];
    }
    var [a, b, c] = example();
    • 函数参数的定义;

      //有序
      function f([x, y, z]){...};
      f([1, 2, 3]);
      
      //无序
      function f({x, y, z}){...};
      f({z:3, y:2, x:1});
    • 提取JSON数据,可以快速提取json对象中的数据;

字符串的扩展

ES6加强了对Unicode的支持,并且扩展了字符串对象。
  1. ES5对字符串对象提供了CharAt方法,返回字符串给定位置的字符。但是该方法不能识别码点大于0xFFFF的字符。于是在ES7中提供了一个at方法,可以识别Unicode编号大于0xFFFF的字符。
  2. includes(),startsWith(),endsWith()方法。JS中只有indexOf方法可以用来确定一个字符串是否包含在另一个字符串中,ES6又提供了三种方法:

    • includes():返回布尔值,表示是否找到了参数字符串;
    • startsWith():返回布尔值,表示参数字符串是否在源字符串的头部;
    • endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部;
  3. repeat(),repeat方法返回一个新字符串,表示将原字符串重复n次。
  4. padStart(),padEnd():ES7推出了字符串补全长度的功能。如果某个字符串长度未达指定长度,会在头部或尾部补全。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'

正则的扩展

  1. 在ES5中,RegExp构造函数只能接受字符串作为参数var regex = new RegExp("xyz", "i");。在ES6中允许RegExp构造函数接受正则表达式作为参数,这时会返回一个原有正则表达式的拷贝。
  2. ES6新增了使用大括号表示Unicode字符的表示法,这种表示法在正则表达式中必须加上u修饰符才能识别。例如:

    /\u{61}/.test('a'); //false
    /\u{61}/u.test('a'); //true
  3. ES6为正则表达式新增了flags属性,会返回正则表达式的修饰符。ES5的source属性会返回表达式的正文。

数值的扩展

  1. ES6提供了二进制和八进制数值的新写法,分别用前缀0b(或0B)和0o(或0O)来表示。从ES5开始,在严格模式中,八进制数值就不再允许使用前缀0表示,ES6进一步明确,要使用0o前缀表示。
  2. Number.isFinite(),Number.isNaN():ES6在Number对象上面新提供了这两个方法,分别用于检查Infinite(是否非无穷)和NaN这两个特殊值。
  3. Number.parseInt(),Number.parseFloat():ES6将全局方法parseInt()和parseFloat()移植到了Number对象上。这样是为了逐步减少全局性的方法,使语言逐步模块化。

    //ES5
    parseInt('');
    
    //ES6
    Number.parseInt('');
    
    Number.parseInt === parseInt; //true
  4. Number.isInteger():该方法用来判断一个值是否为整数。
  5. 新增了一个极小的常量Number.EPSILON,当我们做计算的时候,如果误差可以小于这个常量,那么就可以认为计算的结果是正确的。
  6. Number.isSafeInteger():JavaScript能够准确表示的整数范围在-2{53}到2{53}之间,超出的就不能精确表示了,该函数用来判断一个数是否落在这个范围之内。
  7. Math对象的扩展,ES6在Math对象上新增了17个与数学相关的方法:

    • Math.trunc():用于去除小数部分,返回整数部分;
    • Math.sign():用于判断一个数到底是正数、负数还是0,整数返回1,负数返回-1,0返回0,-0返回-0,其他返回NaN;
    • Math.cbrt():计算一个数的立方根;
    • Math.clz32():返回一个数的32位无符号数有多少个前导0;
    • Math.imul():返回两个数以32位带符号整数形式相乘的结果,返回的也是一个带符号整数,例如:Math.imul(-1, 8); //-8
    • Math.fround():返回一个数的单精度浮点数形式;
    • Math.hypot():返回所有参数平方和的平方根,例如:Math.hypot(3, 4);//5
    • 还有一些和对数运算、三角函数运算、指数运算相关的方法。

数组的扩展

  1. Array.from():将类似数组的对象和可遍历的对象转为真正的数组;
  2. Array.of():将一组数值转换为数组,例如:Array.of(3, 11, 8) //[3,11,8]
  3. fill()方法,使用给定值填充数组,例如:new Array(3).fill(7) //[7,7,7]
  4. 数组实例的entries()、keys()、和values()方法,主要用来遍历数组,keys()是对键名的遍历,values()是对键值的遍历,entries()是对键值对的遍历;

函数的扩展

  1. ES6之前不能直接为函数的参数指定默认值,所以经常有x = x || "XXX"这样的写法,ES6允许为函数的参数设置默认值,就可以这样写function test(x, y = "xxx"){};,这样的设计还有一个好处就是开发人员阅读别人的代码一眼就能看出来在调用这个接口哪些参数是可省的。此外,这种写法还可以和解构赋值结合使用,非常灵活。
  2. 函数的length属性修改,如果函数中的参数有指定默认值,那么length就不会把这个参数计算进去,例如:(function(a=5){}).length;//0
  3. 作用域问题,如果一个参数的默认值是一个变量,那么这个变量所处的作用域与其他变量的作用域规则是一样的,先是当前函数的作用域,然后才是全局作用域;
  4. ES6引入了rest参数(形式为"...变量名"),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入其中,例如:

    function add(...values){
        let sum = 0;
        for(var val of values){
            sum += val;
        }
        return sum;
    }
    add(2, 5, 3); //10
    //add函数是一个求和函数,利用rest参数可以向该函数传入任意数目的参数。
  5. 扩展运算符,三个点(...),作用是把一个数组转为用逗号隔开的参数序列。例如:console.log(1,...[2,3,4],5);//1 2 3 4 5;
  6. 扩展运算符替代数组的apply方法,扩展运算符可以直接把数组拆开,例如:

    //ES5
    function f(x,y,z){};
    var args = [0,1,2];
    f.apply(null, args);
    
    //ES6
    function f(x,y,z){};
    var args = [0,1,2];
    f(...args);
  7. 扩展运算符提供了数组合并的新方法:

    //ES5
    [1,2].concat(more)
    //ES6
    [1,2, ...more]
  8. 扩展运算符还可以与解构赋值结合;
  9. ES6还写入了函数的name属性,可以返回函数名,虽然这个属性很早就被各个浏览器支持了,但是在ES6才正式写入;
  10. 箭头函数:ES6允许使用"箭头"(=>)定义函数,例如:

    var sum = (num1, num2) => num1 + num2;
    //等价于
    var sum = function(num1, num2){
        return num1 + num2;
    }
  11. 使用箭头函数有几个注意点:

    • 函数体内的this对象就是定义时所在的对象,而不是使用时所在的对象。在js中this的指向是可以改变的,但是在箭头函数中this的指向是不变的;
    • 不可以当作构造函数。也就是说,不可以使用new命令;
    • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用ES6中的rest参数代替;
    • 不可以使用yield命令,因此箭头函数不能用作Generator函数;
  12. 函数绑定:在ES6之后的ES7版本中有一个提案是函数绑定运算符(::),双冒号左边是一个对象,右边是一个函数。这个运算符会自动将左边的对象作为this绑定到右边的函数上面,例如:foo::bar(...arguments)等价于bar.apply(foo,arguments;)。感觉函数绑定这个设计非常的便捷,不需要在显式的去绑定一下上下文,期待该提案的通过(目前babel已经支持这个写法了);
  13. 尾调用:就是指某个函数的最后一步是调用另一个函数;
  14. 尾调用优化:尾调用之所以与其他调用不同,就在于其特殊的调用位置。函数调用的时候会在内存形成一个‘调用记录’,又称为‘调用帧’,保存调用位置和内部变量等信息。如果在函数A内部调用函数B,那么在A的调用帧上方还会形成一个B的调用帧。等到B执行结束再返回给A,B的调用帧才消失。如果B的内部调用了C,那么还会产生一个调用帧,以此类推,所有调用帧会形成一个‘调用栈’。然而尾调用是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,直接用内层函数的调用帧取代外层函数的即可;
  15. 尾递归:函数调用自身称为递归,如果尾调用自身就称为尾递归。递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易stackoverflow。但对于尾递归来说,只存在一个调用帧,所以永远不会发生“栈溢出”错误。例如:

    //这是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度为O(n)。
    function factorial(n){
        if(n === 1) return 1;
        return n * factorial(n - 1);
    }
    //改写成尾递归,只保用一个调用记录,则复杂度为O(1);
    function factorial(n, totla){
        if(n === 1) return total;
        return factorial(n - 1, n * total);
    }

    由此可见‘尾调用优化’对于递归操作的意义非常重大,所以一些函数式编程语言将其写入了语言规格。ES6也是如此,第一次明确规定,所有ECMAScript的实现,都必须部署‘尾调用优化’。这就是说,在ES6中,只要使用尾递归,就不会栈溢出,节省内存。

(未完待续......)


Gideon
354 声望23 粉丝