ES6 - JS的语法糖

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

 学习ES6,并不应该因为是新功能而被使用,使用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

猜你喜欢

转载自blog.csdn.net/weixin_34137799/article/details/87229834