ES6的箭头函数和块级作用域

    最近要好好学下ES6,下文主要是对箭头函数和const,let的稍微详细点的理解。里面我主要的学习方法是写小例子,然后去分析ES6通过babel转码后有什么区别和和两者的对比。babel的使用可以看下:bable install,这里不涉及babel的使用。


    箭头函数(Arrow Functions)

    箭头函数最好的一点当然是箭头函数preserve the context of this from its lexical scope,保存我们上下文作用域中的this的指向。因为一般我们可能会遇到下面这些问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Person(name){
    this.name = name
    }
    Person.prototype.add = function(arr){
    return arr.map(function(item){
    return this.name+item
    })
    }
    var arr=['cld','seven']
    var person = new Person('n6303-')
    console.log(person.add(arr))

    [ 'undefinedcld', 'undefinedseven' ],最后结果是这个。因为当我们使用nested functions的时候,this的指向会发生变化。所以,我们没有箭头函数以前,是这样解决的:

    1
    2
    3
    4
    5
    Person.prototype.add = function(arr){
    return arr.map(function(item){
    return this.name + item
    },this)
    }

    或者是通过bind,这样解决的:

    1
    2
    3
    4
    5
    Person.prototype.add = function(arr){
    return arr.map(function(item){
    return this.name + item
    }.bind(this))
    }

    或者是这样解决的,通过:that=this:

    1
    2
    3
    4
    5
    6
    Person.prototype.add = function(arr){
    var that = this
    return arr.map(function(item){
    return that.name + item
    })
    }

    以上三种方法都能够解决。但是当我们有了箭头函数,就更加容易了:

    1
    2
    3
    Person.prototype.add = function(arr){
    return arr.map((item)=>this.name+item)
    }

    而且最好的就是箭头函数大部分浏览器都支持啦。
    而且对于返回值只有一个的时候,箭头函数非常简洁,比如:

    1
    2
    3
    var arr = [1,3,4]
    console.log(arr.map(function(item){return item*item}))
    console.log(arr.map((item)=>item*item))

    通过babel转码后,箭头函数是这样的,跟自己写的一样:

    1
    console.log(arr.map(function(item){return item*item}))


    let and const

    let与var之变量提升的区别

    ES6新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。相当于增加了块级作用域

    先看下下面的输出结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var global = "global"
    function test1(para){
    if(para){
    var global = "inner"
    return global
    }
    return global
    }
    function test2(para){
    if(para){
    let global = "inner"
    return global
    }
    return global
    }
    console.log(test1(false))
    console.log(test2(false))

    结果分别是:undefined,global。第一个是因为var 有变量提升,而let没有变量提升。我们可以看一下当我们通过babel编译以后会得到什么结果:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    "use strict";
    var global = "global";
    function test1(para) {
    if (para) {
    var global = "inner";
    return global;
    }
    return global;
    }
    function test2(para) {
    if (para) {
    var _global = "inner";
    return _global;
    }
    return global;
    }
    console.log(test1(false));
    console.log(test2(false));

    babel把我们的let变成了var,并且把变量名称换了。正是因为let不存在变量提升,所以我们要先声明再使用。否则会报错,而不是输出undefined。

    再来看下面这个例子:

    1
    2
    3
    4
    {
    var a = 3;
    let b =4;
    }

    会被编译成下面这样。所以最后会报错。因为我们在外部打印b的时候,是ReferenceError: b is not defined

    1
    2
    3
    4
    5
    6
    {
    var a = 3;
    var _b = 4;
    }
    console.log(a)
    console.log(b)


    let和var在闭包中的作用

    由于i是var声明的。所以是全局变量。全局范围内都有效。所以当我们不管是调用哪一个a,都是输出最后一个i,也就是3。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var a = []
    for(var i=0;i<4;i++){
    a[i] = function(){
    console.log(i)
    }
    }
    a[2]()
    var b = []
    for(let i=0;i<4;i++){
    b[i] = function(){
    console.log(i)
    }
    }
    b[2]()

    我看了下babel编译后的结果。编译后它处理的方法是把函数声明提到了外面。我们通常也可以用里面(function(i){})(i)这中方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var a = [];
    for (var i = 0; i < 4; i++) {
    a[i] = function () {
    console.log(i);
    };
    }
    var b = [];
    var _loop = function _loop(_i) {
    b[_i] = function () {
    console.log(_i);
    };
    };
    for (var _i = 0; _i < 4; _i++) {
    _loop(_i);
    }

    可惜了,我现在用谷歌来测试还是不能用。期待那么一天。火狐还是很好的。


    let不允许重复声明

    我们用var 的时候,都可以一直就重新声明。想重新声明就重新声明。但是let不允许在相同作用域内,重复声明同一个变量。比如先var a=1,再let a=2,这是不行的。再比如let a=1;let a=2这也是不行的。那比如说,我在一个函数内传入了一个param,我们let param = other值,这也是不行的。但是注意这前提室相同作用域如果是不同作用域肯定就是可以了,比如先let a=1,然后{let a=1},这样开辟了一个新的作用域。肯定是可以重复声明的。内层作用域可以定义外层作用域的同名变量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function f() {
    {
    let x;
    {
    // okay, block scoped name
    const x = "sneaky";
    // error, const
    x = "foo";
    }
    // okay, declared with let
    x = "bar";
    // error, already declared in block
    let x = "inner";
    }
    }

    需要块级作用域let的原因

    1. 由于var是可以随时重新声明赋值的,又因为有变量提升这么回事,所以经常就会出现undefined的错误。内部变量覆盖了外部变量。
    2. 消除刚才我们提到的,由于循环计数的原因,导致了变量泄露成了全局变量。

    我在想是不是可以这么理解,当我们使用了{}这个的时候,{}内部的和{}外部的元素就形成了两个不同的块级作用域。无论是我们直接{}还是通过if(){}或者function(){}这样。我不知道我这样总结的是不是太绝对了。

    我们先前的是:内部作用域可以访问外部作用域。因为作用域链的存在。外部作用域不能访问内部作用域。因为作用域链只能向上查找。let的出现并没有影响这一点。

    而且正是因为let的出现,以后估计我们的匿名函数(用于保护全局变量)用的也就会少很多了。

    1
    2
    3
    4
    (function () {
    var food = 'meat';
    }());
    console.log(food); // Reference Error
    1
    2
    3
    4
    {
    let food = 'vegetables;
    }
    console.log(food); // Reference Error

    const是single-assignment

    const与我们先前学习别的语言一样,代表常量,一旦赋值就不可以改变。这也意味着必须在声明的时候赋值,否则后面不管再怎么赋值,都是错误的了。

    这又包括两种情况,一种是普通模式,也就是没有用use strict,这种情况是重新赋值不会报错,但是值是不会变的,一直都是你最开始赋的初值。第二种情况是用了use strick,这种情况,是会报错的。

    还有一点要注意,就是const的作用域是跟let一样,在当前的块级作用域里面的。外部作用域无法访问当前定义的const的作用域。

    并且const也是不存在变量提升。这是个好事,让我们形成先定义,再使用的习惯,并且也可以有效的避免undefined这种情况发生。

    还有一点,是我以前没有注意到的。感谢阮一峰老师的这个书。真棒。就是如果const定义的常量指向的是一个对象。这个时候,它实际上指向的是当前对象的地址。这个地址是在栈里面的,而这个真实的对象是在堆里面的。所以,我们使用const定义这个对象后,是可以改变对象的内容的。但是这个地址是不可以变的。意思也就是不可以给这个对象重新赋值,比如const foo={},foo = {},即使是这样foo好像什么都没有改变,但还是错误的。(然而我在普通模式下,并没有报错。。。)而foo.username=’cld’,这是完全可以的。这跟javascript存储引用对象的值的方式有密切的关系。

    1
    2
    3
    4
    5
    const obj = Object.freeze({})
    const b = {}
    obj.name = "cld"
    b.name = "seven"

    这段代码会在babel下转成这样:

    1
    2
    3
    4
    5
    var obj = Object.freeze({});
    var b = {};
    obj.name = "cld";
    b.name = "seven";

    事实是不管我们是以const 然后freeze object,或者是不freeze,babel都是会把它转成var这种形势。因为babel本身的作用就是transform我们的es6的代码的。当然我们运行的话,会报错:TypeError: Can't add property name, object is not extensible

    在阮一峰老师的书里,我看到了他写的这样一块代码:

    1
    2
    3
    4
    5
    6
    7
    8
    var constantize = (obj) => {
    Object.freeze(obj);
    Object.keys(obj).forEach( (key, value) => {
    if ( typeof obj[key] === 'object' ) {
    constantize( obj[key] );
    }
    });
    };

    看到了以后,眼睛亮了,因为记得前1,2个月面试的时候,有个面试官问我,知不知道有一个函数,可以直接获得所有obj的key值。我确实没用过js里的这个,只知道php里面有类似的函数是array_keys(),后来下去查,也没查到。就一直记在心里。今天一看这段代码就知道了是Object.keys(),真好。比如下面这个测试:

    1
    2
    3
    4
    5
    6
    7
    8
    var obj = {'username':['name1','name2'],'password':'seven'}
    console.log(Object.keys(obj))
    Object.keys(obj).forEach(function(key,value){
    if(typeof obj[key] == 'object'){
    console.log(obj[key])
    }
    })

    打印结果是:
    [ ‘username’, ‘password’ ]
    [ ‘name1’, ‘name2’ ]

    有点偏题了。上面我的那段阮一峰老师的代码是为了让除了将对象本身冻结,对象的属性也冻结。

    然后看下babel转码后,会得到啥样的结果:

    1
    2
    3
    4
    5
    6
    7
    8
    var constanize = function constanize(obj) {
    Object.freeze(obj);
    Object.keys(obj).forEach(function (key, value) {
    if (_typeof(obj[key]) == 'object') {
    constanize(obj[key]);
    }
    });
    };

    babel把箭头函数给转了,然后加了引号。但是typeof前面加_下划线是什么呢?我知道一般来说加下划线的变量为私有变量。。??

    还有就是加入let后,就算let是在window环境下声明的。通过let定义的属性,也不能通过window获得。var声明的可以通过window对象获得。