美文网首页
ES6 - 入门篇

ES6 - 入门篇

作者: 果汁凉茶丶 | 来源:发表于2018-08-08 20:33 被阅读0次

      何为ES6语法糖?即这些事情ES5也可以做,只是稍微复杂一些,而ES6提供了非破坏性的更新, 目的是提供更简洁,语义更清晰的语法,从而提高代码的可读性和可维护性

      本文几点提炼:

    1. 对象字面量的简写属性和计算的属性名不可同时使用,原因是简写属性是一种在编译阶段的就会生效的语法糖,而计算的属性名则在运行时才生效
    2. 箭头函数本身已经很简洁,但是还可以进一步简写
    3. 解构也许确实可以理解为变量声明的一种语法糖,当涉及到多层解构时,其使用非常灵活
    4. let, const声明的变量同样存在变量提升,理解TDZ机制

    $ 对象字面量

      对象字面量是以{}表示的对象,在JS中表现如下

    var person = {
        name: 'zhangfs',
        sex: 'men'
    }
    

    1. 属性/方法的简洁表示
      当属性名和变量名一致时, ES5中表现如下

    var books = []
    function read () {}
    var events = {
        books: books,
        read: read
    }
    

    ES6下可以如此表示

    var books = []
    function read () {}
    var event = { books, read }
    

    2. 可计算的属性名
      当我们需要用拼接的字符串来作为对象某个新的属性并进行赋值时,ES6表现如下

    var newAttr = 'feature';
    var person = {
        name: 'zhangfs',
        sex: 'men'
        [newAttr]: {
            age: '25',
            hobby: ['swim', 'basketball', 'travel']
        }
    }
    

    feature将会被正确解析到person对象中,取值时

    person.feature = {
        age: '25',
        hobby: ['swim', 'basketball', 'travel']
    }
    

    简写属性和计算属性名不可重用。因为简写属性是一种在编译阶段就生效的语法糖,而计算属性名则是在运行时才生效。作用时期不一致,混用它们代码将直接报错。

    var newAttr= 'feature'
    var feature= {
        age: 25,
        hobby: ['swim', 'basketball', 'travel']
    }
    var person = {
        name: 'zhangfs',
        sex: men,
        [newAttr]   // 这里无法被正确解析,报错
    }
    

    $ 方法定义

      如下我们构建一个事件发生器,在ES5中的表现形式如下:

    var emitter = {
        events: {},
        on: function (type, fn) {
            (!this.events[type]) && this.events[type] = [];
            this.events[type].push(fn)
        },
        emit: function (type, event) {
            if (this.events[type]) {
                this.events[type].forEach(function(fn) {
                    fn(event)
                })
            }
        }
    }
    

      ES6中可以省略冒号function关键字

    var emitter = {
        events: {},
        on(type, fn)  {
            (!this.events[type]) && this.events[type] = [];
            this.events[type].push(fn)
        },
        emit(type, event) {
            if (this.events[type]) {
                this.events[type].forEach(function(fn) {
                    fn(event)
                })
            }
        }
    }
    

    $ 箭头函数

      ES5及之前,我们这么声明普通函数

    function doIt() {
        // TODO..
    }
    

      或者使用匿名函数,通常将匿名函数赋值给一个变量或属性,或直接被调用

    var example = function (p) {
        // TODO..
    }
    

      ES6在匿名函数上做了发散,提供了箭头函数,同样没有函数名,并用=>连接参数和函数体

    var example = (p) => {
        // TODO...
    }
    

      值得注意的是,箭头函数和匿名函数是有本质区别的,

    • 箭头函数不能被直接命名,不过允许赋值给一个变量
    • 箭头函数不能被用作构造函数,不能对它使用new关键字
    • 箭头函数没有prototype属性
    • 箭头函数绑定了词法作用域,不会修改this的指向

    @ 词法作用域 【难点

      有一个点特别需要注意的是:

    在箭头函数内部使用的this,arguments,super等,都是指向了包含箭头函数的上下文箭头函数本身不产生上下文

      为对比差异,我们以timer为例,ES5方式编写代码如下

    var timer = {
        seconds: 0,
        start: function() {
            setInterval(function(){
                this.seconds++  // this.second 为 undefined
            }, 1000)
        }
    }
    
    timer.start()
    setTimeout(function() {
        console.log(timer.seconds)   // 0
    },3500)
    

    ES6编写代码如下

    var timer = {
        seconds: 0,
        start() {
            setInterval(() => {
                this.seconds++
            }, 1000)
        }
    }
    timer.start()
    setTimeout(function () {
        console.log(timer.seconds)  // 3
    })
    

      从执行结果上来看有很大的差异,为什么呢?第一段代码中的start采用了常规匿名函数定义,它的this指向了window, 因此答应结果为undefined。当然我么也有解决方案,在start方法开头处插入var self = this,然后替换匿名函数体中的thisself。而第二段代码中,使用了箭头函数则没有这个问题了。

    箭头函数的作用域不能通过.call, .apply, .bind等语法来改变。也就是说,箭头函数的上下文永久不变l

      箭头函数与普通函数的另一个区别

    function puzzle() {
      return function () {
        console.log(arguments)   //  1,2,3
      }
    }
    puzzle('a', 'b', 'c')(1,2,3)
    

    结果打印1,2,3,对于匿名函数而言,arguments指向匿名函数本身。

    function puzzle() {
      return () => {
        console.log(arguments) 
      }
    }
    puzzle('a','b','c')(1,2,3)
    

    结果打印a,b,c

      原因很简单:箭头函数本身不产生上下文,也就是说箭头函数没有argument对象。而这里打印的argument其实是指向父函数puzzle的。

    @ 箭头函数简写

      一个完整的箭头函数

    var doIt = (p) => {
        // TODO..
    }
    

    简写1:当只有一个参数时,参数可以省略括号

    var doIt = p => {
        // TODO..
    }
    

    简写2:只有单行表达式,且该表达式为返回值时,表征函数体的{},可以省略,return 关键字可以省略,会静默返回该单一表达式的值。

    var doIt = (value) => value * 2;
    

    简写3:以上条件均符合时两种简写可以并用

    var doIt = value => value * 2
    

    @ 简写的注意事项

      当采用简写2时,如果返回值是一个对象,则需要用(),否则对象的{}将会被识别成函数体的开始和结束标记

    // 对象返回值要加小括号
    var objectFactory = () => ({ modular: 'es6' })
    

      箭头函数可以被直接调用,同样也要注意返回值为对象的问题

    // 箭头函数被map直接调用
    [1,2,3].map(value => { key: value }) 
    // 没加小括号,输出 [undefined, undefined, undefined]
    

      上例只是输出值不对,但返回的对象字面量不止一个属性时,浏览器将无法正确解析后面的属性,直接报错

    [1,2,3].map(value => { id: value, verify: true })  // SyntaxError
    // 正确的用法,给返回值加小括号
    [1,2,3].map(value => ({ id: value, verify: true }))
    

    @ 何时使用箭头函数

      并不见得使用箭头函数就一定好,对比较复杂的函数逻辑,箭头函数所带来的简洁就不那么明显了。合理的定义函数名对于代码的可读性非常重要。虽然箭头函数不可直接命名,但可以通过赋值给变量的方法间接命名,实现调用

    var throwError = message => {
      throw new Error(message)
    }
    throwError('this is a warning')
    

      以上也提到过,this在箭头函数中的意义与普通函数的区别,如果你想完全控制this(避免出现var self = this现象),箭头函数是个不错的选择。

    [1,2,3,4]
      .map(value => value * 2)
      .filter(value => value > 2)
      .forEach(value => console.log(value))
    // 4, 6, 8
    

    $ 解构赋值

    @ 对象解构

      ES6中的对象解构允许我们利用大括号将对象属性赋值给同名变量。ES6在该过程中,会去获取对象中的某个属性值,再定义一个同命变量将该值赋值给它。现有对象如下

    var character = {
      name: 'Bruce',
      nick: 'Batman',
      metadata: {
        age: 34,
        gender: 'male'
      },
      friends: ['July', 'Condy', 'Amy']
    }
    

    在ES5中,如果要获取其中属性值,我们会这么定义变量

    var nick = character.nick
    

    而在ES6中,利用对象解构特性,可以简化代码如下

    var { nick } = character
    

    如果需要多个变量时,用逗号隔开

    var { name, nick } = character
    

    因为对象解构赋值本质上也是表达式,因此,它并不影响常规的自定义变量

    var { nick } = character,  home = 'china';
    

    还可以使用别称(暂时不知道有啥用)

    var { name: mingzi } = character;
    alert(mingzi)  // Bruce
    

    对象解构的另一个强大功能,解构值还可以是对象(多层解构),如下

    var { metadata: { gender } } = character;
    

    码农解释】ES6为什么要提供这种看似难以理解的表达式?其实就是为了解决当对象中存在对象嵌套的问题。本例中,character对象中的namenick已经可以轻松解构了,那metadata中的agegender又如何解构?于是ES6就提出了这种方案,这样我们能很方便的获取对象中每一层次中的每一个属性。明白了这点,我们就能很清楚这个表达式最终该语法糖要给我们什么了,它其实等价于:

    // ES5 等价表达式
    var gender = character.metadata.gender
    

    或许你有疑问,如果对象中没有的属性,利用对象解构的方式定义变量,会有什么结果?

    var { name, boots} = character
    alert(boots);   // undefined
    

    如果对象解构属性名不存在于对象中,多层解构将抛出异常

    var { boots: { size } } = character
    // <- Exception
    var { missing } = null
    // <- Exception
    

    原因很容易理解,看看ES5等价形式

    // 示例1
    var boots = null
    var size = boots.size
    // 示例2
    var nothing = null
    var missing = nothing.missing
    

    对象中没有的属性,除了ES5单独定义外,ES6解构同样提供另一个办法防止抛出错误。为解构添加默认值,默认值可以是数值,字符串,函数,对象,也可以是已存在的变量

    var { boots = { size: 10 } } = character
    console.log(boots);  // {size: 10}
    

    多层解构的默认值。假设接收的请求返回字段的存在无确定性,为为避免抛错可以如下使用

    var { metadata: { weight = 65 } } = character
    console.log(weight)   //65
    

    存在的属性也可以定义默认值(不过似乎没啥用)

    var { name = 'xx' } = character
    console.log(name);  // Bruce 
    

    @ 数组解构

    对象解构采用的是花括号{},数组解构采用中括号[]
    ES5中,要获取数组中的某一项,通常我们这么做

    var arr = [12, -7]
    var a = arr [0];
    

    在ES6的数组解构中,允许我们不使用索引值

    var arr = [12, -7];
    var [x, y] = arr;
    console.log(y);   // -7
    

    允许我们调过不想要的值

    var names= ['James', 'L.', 'Howlett'];
    var [firstname, ,lastname] = names;
    console.log(firstname, lastname);   // 'James', 'Howlett'
    

    允许添加默认值

    var names = ['Jane', 'Li'];
    var [ firstName = 'Mary', , lastName = 'Doe' ] = names;
    console.log(firstName, lastName);   // Jane, Doe
    

    简化了数据交换操作,不需要辅助变量

    var left = 5, right = 7;
    [left, right] = [right, left]
    console.log(left, right)  // 7, 5
    

    @ 函数解构

    允许我们给函数参数添加默认值

    function power(base, exponent = 2) {
      return Math.pow(base, exponent)
    }
    

    箭头函数同样可以添加默认值,注意此时就不能省略参数的括号了

    var double = (input = 0) => input * 2;
    

    也可以结合对象解构的办法给函数传参

    var defaultOption = { brand: 'volov', make: '2015' }
    function carFactory(option = defaultOption) {
        console.log(option.brand);  // 'volov'
        conosle.log(option.make);  // '2015'
    }
    carFactory();
    

    结合上例,思考以下输出

    carFactory({ brand: 'BMW' });
    // 'BMW'
    // undefined
    

    码农解释】看函数carFactory本身其实很容易理解,并不是make参数属性失效了,而是option参数值由原来的defaultOption变成了对象{ brand: 'BMW' },新的参数值对象并没有make属性,因此为undefined

      如果我们想让make保持生效呢?需要做如下改动

    function carFactory( { brand: 'volov', make: '2015' } ) {
      console.log(brand);
      console.log(make);
    }
    carFactory({brand: 'BMW'});
    // 'BMW'
    // '2015'
    

    在该案例下,假如我们传入参数为空,你猜会有什么结果?

    carFactory(); 
    // <-  Exception
    

    码农解释】为什么抛出了异常?其实很容易理解。上例中,我们给carFactory函数传递了一个对象做为形参,形参中包含两个属性,我们给这两个属性设置了默认值。当实参为null时,函数在调用形参中的两个属性时相当于调用null.brandnull.make,这就回归到原生JS的知识,抛出异常就可以理解。

    解决办法】 设置默认参数

    function carFactory( { brand: 'volov', make: '2015' } = {}) {
      console.log(brand);
      console.log(make);
    }
    carFactory(); 
    // undefined
    // undefined
    

    还能只使用实参的部分属性,这使得定义实参变量时具有更高的可扩展性

    var car = {
      owner: {
        id: 'e2c3503a4181968c',
        name: 'Donald Draper'
      },
      brand: 'Peugeot',
      make: 2015,
      model: '208',
      preferences: {
        airbags: true,
        airconditioning: false,
        color: 'red'
      }
    }
    
    var getCarProductModel = ({ brand, make, model }) => ({
      sku: brand + ':' + make + ':' + model,
      brand,  // 字面量简写办法,属性名与变量名一致时使用
      make,
      model
    })
    var desc = getCarProductModel(car)
    console.log(desc)  
    // { sku: Peugeot:2015:208, brand: Peugeot, make: 2015, model: 208}
    

    什么时候使用解构? 在任何需要的时候。
      请求返回值常为JSON或数组格式,通过解构可以很快捷的截取出想要的字段

    ajaxFunc () {
      return {x: 19, y: 33, z: -5, type: '3d'}
    }
    var { x, z} = ajaxFunc();
    // 19, -5
    

    $ 拓展运算符Rest

      拓展运算符可以获取等号右边的所有尚未读取的键,将他们拷贝过来。 只需要在任意函数的最后一个参数前添加三个点...即可。

    Rest参数是函数的唯一参数时,它就代表了传递给这个函数的所有参数。

    function join(...list) {
      return list.join(', ')
    }
    join('first', 'second', 'third')
    //  'first, second, third'
    

    rest参数之前的命名参数不会被包含在rest中,

    function join(separator, ...list) {
      return list.join(separator)
    }
    join('- ', 'first', 'second', 'third')
    //  'first-second-third'
    

    rest可以把任意可枚举对象转换为数组

    function cast() {
      return [...arguments]
    }
    cast('a', 'b', 'c')
    // ['a', 'b', 'c']
    

    rest运用在数组解构赋值中

    var [first, second, ...other] = ['a', 'b', 'c', 'd', 'e'];
    console.log(other);  // ['c', 'd', 'e']
    

    rest运用在对象解构赋值中

    let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    console.log(z);  // {a: 3, b: 4}
    

    $ 模板字符串

      ES6中的模板字符串是JS中对字符串的重大改进,在表示上也有所区别,不同于普通的单引号和双引号,采用的是反撇号表示,在模板中,我们可以随意的使用单引号和双引号。

    var text = `I'm first string “template”`;
    console.log(text)  // I'm first string “template”
    

    @ 在字符串中插值

      模板字符串支持使用变量插值,使用${}嵌入变量或所要执行的表达式

    var name = `world`;
    var greet = `hello ${name}`;
    console.log(greet);  // hello world
    

    输出当前时间与日期

    `The time and date is ${ new Date().toLocaleString() }`
    

    包含计算表达式

    The result of 2 + 3 equals ${2+3}
    

    鉴于模板字符串本身也是Javascript表达式,我们在模板字符串中还可以嵌套模板字符串

    `This template literal ${ `is ${ 'nested' }` }!`
    

    ES5及之前,要使用多行文本,需要添加一些hack如下

    var escaped =
    'The first line\n\
    A second line\n\
    Then a third line'
    

    模板字符串支持多行文本

    var escape = `The first line
    The second line
    The third line`
    

    模板字符串甚至可以拼接HTML

    var book = {
      title: 'Modular ES6',
      excerpt: 'Here goes some properly sanitized HTML',
      tags: ['es6', 'template-literals', 'es6-in-depth']
    }
    var html = `<article>
      <header>
        <h1>${ book.title }</h1>
      </header>
      <section>${ book.excerpt }</section>
      <footer>
        <ul>
          ${
            book.tags
              .map(tag => `<li>${ tag }</li>`)
              .join('\n      ')
          }
        </ul>
      </footer>
    </article>`
    

    上述代码执行结果如下,html片段被渲染,li列表也被渲染

    <article>
      <header>
        <h1>Modular ES6</h1>
      </header>
      <section>Here goes some properly sanitized HTML</section>
      <footer>
        <ul>
          <li>es6</li>
          <li>template-literals</li>
          <li>es6-in-depth</li>
        </ul>
      </footer>
    </article>
    

    $ let 和 const 声明

    letvar比较像,但他们有不同的作用域

      在JS中,作用域具有一套复杂的规则,这也是写代码时常出现错误的地方。变量提升的存在更让人摸不着头脑。所谓变量提升,即无论在哪里声明的变量,在浏览器解析时,实际上被提升到了当前作用域顶部被声明

    function check(val) {
        if (val=== 2) {
            var result = true;
        }
        return result;
    }
    check(2);  // true
    check('two');  // undefined   不会抛出异常
    

    利用var定义的变量会被提升到函数作用域的顶部,及等效于如下

    function check(val) {
        var result;
        if (val === 2) {
          result = true;
        }
        return result
    }
    

    ES6为了更好的控制作用域及变量的作用范围,引入了let

    function check(val) {
        if (val=== 2) {
            let result = true;
        }
        return result;
    }
    check('two');  // Excpetion
    

    为什么会抛错?let作用域又是什么?叫块作用域,这并不是ES6引入的概念,只是之前因为var的原因很少被提及。

    @ 块作用域 与 let 声明

      与函数作用域不同的是,块作用域允许我们用if, for, while声明创建新的作用域,甚至,任意的{}也能创建

    for (let i = 0; i < 2; i++) {
      console.log(i)  // 0, 1
    }
    console.log(i)
    // Exception:i is not defined
    

    看一个经典的 for + setTimeout 案例

    function printNumbers() {
      for (var i = 0; i < 10; i++) {
        setTimeout(function () {
          console.log(i)
        }, i * 100) 
      }
    }
    printNumbers();  // 打印10个10
    

    码农解释】为何会打印1010?为什么不是1 - 10?原因是该例中,var定义的i被绑定在printNumber函数作用域中, setTimeout()是JS中实现异步的手段之一,每一次执行for循环,延时函数的回调函数被调用,但未被运行(延时执行了),变量i逐步递增到10,然后再运行console.log()因为此时函数作用域的i已经是10了,因此,打印出来的为十个10

    改用块级作用域使用let定义

    function printNumbers() {
      for (let i = 0; i < 10; i++) {
        setTimeout(function () {
          console.log(i)
        }, i * 100) 
      }
    }
    printNumbers();  // 0,1,2,3,4,5,6,7,8,9
    

    码农解释】为什么现在又是0 - 9了呢?原因是 使用let定义的i,被绑定到每一个块级作用域中,每一次循环i还是在增加,但是每一次for执行完成后上一次的i就已经销毁了,每次都创建一个新的i。不同的i之间不会相互影响。保存在各个回调函数arguments中的i都保留了原有的值,因此,打印出来的值是0 - 9

    @ 暂时死区 - TDZ Temporal Dead Zone

      看一个通俗的代码实例

    'use strict';
        
    {   // enter new scope, TDZ starts
        tmp = 'abc'; // Uncaught ReferenceError: tmp is not defined
        console.log(tmp); // Uncaught ReferenceError: tmp is not defined
        let tmp; // TDZ ends, `tmp` = `undefined`
        console.log(tmp); // undefined
        tmp = 123;
        console.log(tmp); // 123
    }
    

      规范中的意思就是,用let/const声明的变量,在声明之前访问时,会抛出ReferenceError。而用var声明的变量,声明之前访问它的时候,值会默认为undefined。在示例中,我们可以看到 TDZ存在周期为用新的块作用域之后,到let声明该变量时。

      再看一个例子,这个例子说明了TDZ是一个动态的问题,就是真正访问这个变量时才会进行这种检查。

    
    { // enter new scope, TDZ starts
        const func = function () {
            console.log(myVar); // OK!
        };
        //这之后
        //访问myVar都会报ReferenceError
        //这之前
        let myVar = 3; // TDZ ends
        func(); 
    }
    

      以下代码执行抛出异常

    function readName() {
      return name
    }
    console.log(readName());  // ReferenceError: name is not defined
    let name = 'steven';
    

    TDZ的存在使得程序更容易报错,由于声明提升和不好的编码习惯常常会导致这样的问题。其实letvar一样, 也存在声明提升,提升到块级作用域顶部,但TDZ的存在限制了let定义的变量的访问,TDZlet声明变量的位置才消失,访问限制才被取消,这就造成了let定义的变量和var定义的变量在这一方面上表现不一致的原因

    @ const 声明

    const声明也具有类似let的块作用域,它同样具有TDZ机制。实际上,TDZ机制是因为const才被创建,随后才被应用到let声明中。const需要TDZ的原因是为了防止由于变量提升,在程序解析到const语句之前,对const声明的变量进行了赋值操作,这样是有问题的。

    const具有和let一致的块作用域。他们的主要区别是:

    1. 首先const声明的变量在声明时必须赋值初始化,否则直接报错
    cosnt pi = 3.14159
    const c  // SyntaxError, missing initializer
    

    2. 除了必须初始化,被const声明的变量不能再被赋予别的值。在严格模式下,试图改变const声明的变量会直接报错,在非严格模式下,改变被忽略,依旧保留原始值。

    const people = ['Tesla', 'Musk']
    people = []
    console.log(people)
    // <- ['Tesla', 'Musk']
    

      请注意,const声明的变量并非意味着,其对应的值是不可变的。真正不能变的是对该值的引用,下面我们具体说明这一点。

    】 通过const声明的变量值并非不可改变,只是阻止变量引用另外一个值

      使用const只是意味着,变量始终指向相同的对象(引用类型)或初始的值(值类型)。这种引用是不可改变的,并非值就一定不能改变,当然,对于值类型的变量,值就不可改变了。

    const a = 5;
    a = 6;  // Uncaught TypeError: Assignment to constant variable.
    console.log(a); 
    
    // 只要不修改引用类型的变量,可以修改该变量的值
    const arr = ['x','y'];
    arr.push('z');  
    console.log(arr);   // 能正确将 z 添加到数组中
    

    拓展】如果我们想让值也不可改变呢?可以借助函数Object.freeze:

    const frozen = Object.freeze(['Ice', 'Icicle']);  // 将const替换成var也一样
    frozen.push('Icer')
    // Uncaught TypeError: Cannot add property 2, object is not extensible
    

    即:抛出异常,对象是不可被拓展的。

    @ constlet的优点

    let声明在大多数情况下,可以替换var以避免预期之外的问题。使用let你可以把声明在块的顶部进行而非函数的顶部进行。

      如果我们默认只使用cosntlet声明变量,所有的变量都会有一样的作用域规则,这让代码更易理解,由于const造成的影响最小,它还曾被提议作为默认的变量声明。

      总的来说,const不允许重新指定值,使用的是块作用域,存在TDZlet则允许重新指定值,其它方面和const类似,而var声明使用函数作用域,可以重新指定值,可以在未声明前调用,考虑到这些,推荐尽量不要使用var声明了。

    $ 几个链接

    1. ES6 概要
    2. Practical Modern JavaScript

    相关文章

      网友评论

          本文标题:ES6 - 入门篇

          本文链接:https://www.haomeiwen.com/subject/yrxfbftx.html