Home > Web Front-end > JS Tutorial > body text

Modern JavaScript usage skills: shorthand syntax in ES6

coldplay.xixi
Release: 2020-06-20 17:02:19
forward
1978 people have browsed it

Modern JavaScript usage skills: shorthand syntax in ES6

ES6 provides non-destructive updates for some existing functions. Most of these updates can be understood as syntactic sugar, which is called syntactic sugar. It means that this What the new syntax can do can actually be done with ES5, but it will be a little more complicated. In this chapter, we will focus on these syntactic sugars. After reading this, you may have a different understanding of some of the new ES6 syntax that you are familiar with.

Object literal

Object literal refers to an object directly expressed in the form of {}, such as the following:

var book = {
  title: 'Modular ES6',
  author: 'Nicolas',
  publisher: 'O´Reilly'
}
Copy after login

ES6 brings some improvements to the syntax of object literals: including concise representation of properties/methods, computable property names, etc. Let’s look at them one by one:

Concise representation of properties

Have you ever encountered this scenario? An object we declare contains several attributes, whose attribute values ​​are represented by variables, and the variable names are the same as the attribute names. For example, as shown below, we want to assign an array named listeners to the listeners property in the events object. Using ES5 we will do this:

var listeners = []
function listen() {}
var events = {
  listeners: listeners,
  listen: listen
}
Copy after login

ES6 allows us to abbreviate it in the following form: How about

var listeners = []
function listen() {}
var events = { listeners, listen }
Copy after login

, does it feel much simpler? The concise writing method of using object literals allows us to reduce the number of words without affecting the semantics. Duplicate code.

This is one of the benefits of ES6. It provides many simpler and clearer semantic syntaxes, which greatly improves the readability and maintainability of our code.

Computable property names

Another important update to object literals is to allow you to use computable property names. In ES5 we can also add properties named variables to objects. , Generally speaking, we have to do this as follows, first declare a variable named expertise, and then add the variable as an object## in the form person[expertise] Attributes of #person:

var expertise = 'journalism'
var person = {
  name: 'Sharon',
  age: 27
}
person[expertise] = {
  years: 5,
  interests: ['international', 'politics', 'internet']
}
Copy after login

In ES6, object literals can use calculated property names. Put any expression in square brackets, and the result of the expression will be the corresponding property name. , the above code can be written like this in ES6:

var expertise = 'journalism'
var person = {
  name: 'Sharon',
  age: 27,
  [expertise]: {
    years: 5,
    interests: ['international', 'politics', 'internet']
  }
}
Copy after login

However, it should be noted that

abbreviated attributes and calculated attribute names cannot be used at the same time . This is because the abbreviated property is a syntactic sugar that takes effect at the compile stage, while the calculated property name takes effect at runtime. If you mix the two, the code will report an error. Moreover, mixing the two often reduces the readability of the code, so it is also a good thing for JavaScript to restrict the mixing of the two at the language level.

var expertise = 'journalism'
var journalism = {
  years: 5,
  interests: ['international', 'politics', 'internet']
}
var person = {
  name: 'Sharon',
  age: 27,
  [expertise] // 这里会报语法错误
}
Copy after login

Computable property names will make our code more concise when encountering the following scenarios:

    The properties of a new object are referenced from another object:
  1. var grocery = {
      id: 'bananas',
      name: 'Bananas',
      units: 6,
      price: 10,
      currency: 'USD'
    }
    var groceries = {
      [grocery.id]: grocery
    }
    Copy after login
    The attribute name of the object to be constructed comes from the function parameters. If we use ES5 to deal with this problem, we need to declare an object literal first, then dynamically add attributes, and then return the object. In the following example, we create a function that responds to Ajax requests. The function of this function is that when the request fails, the returned object has an attribute named
  1. error and a corresponding description. When the request succeeds, the returned object The object has a property named success and a corresponding description.
  2. // ES5 写法
    function getEnvelope(type, description) {
      var envelope = {
        data: {}
      }
      envelope[type] = description
      return envelope
    }
    Copy after login
Using the calculated property names provided by ES6, a more concise implementation is as follows:

// ES6 写法
function getEnvelope(type, description) {
  return {
    data: {},
    [type]: description
  }
}
Copy after login

The properties of object literals can be abbreviated, and the method is actually also possible.

Method definition

Let’s first look at how to define object methods traditionally. In the following code, we build an event generator, in which the

on method is used Register an event, and the emit method is used to execute the event:

var emitter = {
  events: {},
  on: function (type, fn) {
    if (this.events[type] === undefined) {
      this.events[type] = []
    }
    this.events[type].push(fn)
  },
  emit: function (type, event) {
    if (this.events[type] === undefined) {
      return
    }
    this.events[type].forEach(function (fn) {
      fn(event)
    })
  }
}
Copy after login

ES6's object literal method abbreviation allows us to omit the

function keyword of the object method and the colon after it, The rewritten code is as follows:

var emitter = {
  events: {},
  on(type, fn) {
    if (this.events[type] === undefined) {
      this.events[type] = []
    }
    this.events[type].push(fn)
  },
  emit(type, event) {
    if (this.events[type] === undefined) {
      return
    }
    this.events[type].forEach(function (fn) {
      fn(event)
    })
  }
}
Copy after login

The arrow function in ES6 is famous. It has some special advantages (about

this). Maybe you, like me, have been using arrow functions for a long time. , but there are some details that I have never understood before, such as several abbreviations of arrow functions and precautions for their use.

Arrow function

Ordinary functions declared in JS generally have a function name, a series of parameters and a function body, as follows:

function name(parameters) {
  // function body
}
Copy after login

Ordinary anonymous functions do not have a function name. Anonymous functions are usually assigned to a variable/property, and sometimes are called directly:

var example = function (parameters) {
  // function body
}
Copy after login

ES6 provides us with a new way to write anonymous functions, namely arrow functions. Arrow functions do not need to use the

function keyword, and their parameters and function bodies are connected with =>:

var example = (parameters) => {
  // function body
}
Copy after login
Copy after login

Although arrow functions look similar to traditional anonymous functions, but they are fundamentally different:

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

最后一点是箭头函数最大的特点,我们来仔细看看。

词法作用域

我们在箭头函数的函数体内使用的this,arguments,super等都指向包含箭头函数的上下文,箭头函数本身不产生新的上下文。下述代码中,我们创建了一个名为timer的对象,它的属性seconds用以计时,方法start用以开始计时,若我们在若干秒后调用start方法,将打印出当前的seconds值。

// ES5
var timer = {
  seconds: 0,
  start() {
    setInterval(function(){
      this.seconds++
    }, 1000)
  }
}

timer.start()
setTimeout(function () {
  console.log(timer.seconds)
}, 3500)

> 0
Copy after login
// ES6
var timer = {
  seconds: 0,
  start() {
    setInterval(() => {
      this.seconds++
    }, 1000)
  }
}

timer.start()
setTimeout(function () {
  console.log(timer.seconds)
}, 3500)
// <- 3
Copy after login

第一段代码中start方法使用的是常规的匿名函数定义,在调用时this将指向了windowconsole出的结果为undefined,想要让代码正常工作,我们需要在start方法开头处插入var self = this,然后替换匿名函数函数体中的thisself,第二段代码中,我们使用了箭头函数,就不会发生这种情况了。

还需要说明的是,箭头函数的作用域也不能通过.call,.apply,.bind等语法来改变,这使得箭头函数的上下文将永久不变。

我们再来看另外一个箭头函数与普通匿名函数的不同之处,你猜猜,下面的代码最终打印出的结果会是什么:

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

答案是1,2,3,原因是对常规匿名函数而言,arguments指向匿名函数本身。

作为对比,我们看看下面这个例子,再猜猜,打印结果会是什么?

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

答案是a,b,c,箭头函数的特殊性决定其本身没有arguments对象,这里的arguments其实是其父函数puzzle的。

前面我们提到过,箭头函数还可以简写,接下来我们一起看看。

简写的箭头函数

完整的箭头函数是这样的:

var example = (parameters) => {
  // function body
}
Copy after login
Copy after login

简写1:

当只有一个参数时,我们可以省略箭头函数参数两侧的括号:

var double = value => {
  return value * 2
}
Copy after login

简写2:

对只有单行表达式且,该表达式的值为返回值的箭头函数来说,表征函数体的{},可以省略,return 关键字可以省略,会静默返回该单一表达式的值。

var double = (value) => value * 2
Copy after login

简写3:
上述两种形式可以合并使用,而得到更加简洁的形式

var double = value => value * 2
Copy after login

现在,你肯定学会了箭头函数的基本使用方法,接下来我们再看几个使用示例。

简写箭头函数带来的一些问题

当你的简写箭头函数返回值为一个对象时,你需要用小括号括起你想返回的对象。否则,浏览器会把对象的{}解析为箭头函数函数体的开始和结束标记。

// 正确的使用形式
var objectFactory = () => ({ modular: 'es6' })
Copy after login

下面的代码会报错,箭头函数会把本想返回的对象的花括号解析为函数体,number被解析为label,value解释为没有做任何事情表达式,我们又没有显式使用return,返回值默认是undefined

[1, 2, 3].map(value => { number: value })
// <- [undefined, undefined, undefined]
Copy after login

当我们返回的对象字面量不止一个属性时,浏览器编译器不能正确解析第二个属性,这时会抛出语法错误。

[1, 2, 3].map(value => { number: value, verified: true })
// <- SyntaxError
Copy after login

解决方案是把返回的对象字面量包裹在小括号中,以助于浏览器正确解析:

[1, 2, 3].map(value => ({ number: value, verified: true }))
/* <- [
  { number: 1, verified: true },
  { number: 2, verified: true },
  { number: 3, verified: true }]
*/
Copy after login

该何时使用箭头函数

其实我们并不应该盲目的在一切地方使用ES6,ES6也不是一定比ES5要好,是否使用主要看其能否改善代码的可读性和可维护性。

箭头函数也并非适用于所有的情况,比如说,对于一个行数很多的复杂函数,使用=>代替function关键字带来的简洁性并不明显。不过不得不说,对于简单函数,箭头函数确实能让我们的代码更简洁。

给函数以合理的命名,有助于增强程序的可读性。箭头函数并不能直接命名,但是却可以通过赋值给变量的形式实现间接命名,如下代码中,我们把箭头函数赋值给变量 throwError,当函数被调用时,会抛出错误,我们可以追溯到是箭头函数throwError报的错。

var throwError = message => {
  throw new Error(message)
}
throwError('this is a warning')
<- Uncaught Error: this is a warning
  at throwError
Copy after login

如果你想完全控制你的函数中的this,使用箭头函数是简洁高效的,采用函数式编程尤其如此。

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

解构赋值

ES6提供的最灵活和富于表现性的新特性莫过于解构了。一旦你熟悉了,它用起来也很简单,某种程度上解构可以看做是变量赋值的语法糖,可应用于对象,数组甚至函数的参数。

对象解构

为了更好的描述对象解构如何使用,我们先构建下面这样一个对象(漫威迷一定知道这个对象描述的是谁):

// 描述Bruce Wayne的对象
var character = {
  name: 'Bruce',
  pseudonym: 'Batman',
  metadata: {
    age: 34,
    gender: 'male'
  },
  batarang: ['gas pellet', 'bat-mobile control', 'bat-cuffs']
}
Copy after login

假如现有有一个名为 pseudonym 的变量,我们想让其变量值指向character.pseudonym,使用ES5,你往往会按下面这样做:

var pseudonym = character.pseudonym
Copy after login

ES6致力于让我们的代码更简洁,通过ES6我们可以用下面的代码实现一样的功能:

var { pseudonym } = character
Copy after login

如同你可以使用var加逗号在一行中同时声明多个变量,解构的花括号内使用逗号可以做一样的事情。

var { pseudonym, name } = character
Copy after login

我们还可以混用解构和常规的自定义变量,这也是解构语法灵活性的表现之一。

var { pseudonym } = character, two = 2
Copy after login

解构还允许我们使用别名,比如我们想把character.pseudonym赋值给变量 alias,可以按下面的语句这样做,只需要在pseudonym后面加上:即可:

var { pseudonym: alias } = character
console.log(alias)
// <- 'Batman'
Copy after login

解构还有另外一个强大的功能,解构值还可以是对象:

var { metadata: { gender } } = character
Copy after login

当然,对于多层解构,我们同样可以赋予别名,这样我们可以通过非常简洁的方法修改子属性的名称:

var { metadata: { gender: characterGender } } = character
Copy after login

在ES5 中,当你调用一个未曾声明的值时,你会得到undefined:

console.log(character.boots)
// <- undefined
console.log(character['boots'])
// <- undefined
Copy after login

使用解构,情况也是类似的,如果你在左边声明了一个右边对象中不存在的属性,你也会得到undefined.

var { boots } = character
console.log(boots)
// <- undefined
Copy after login

对于多层解构,如下述代码中,boots并不存在于character中,这时程序会抛出异常,这就好比你你调用undefined或者null的属性时会出现异常。

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

解构其实就是一种语法糖,看以下代码,你肯定就能很快理解为什么会抛出异常了。

var nothing = null
var missing = nothing.missing
// <- Exception
Copy after login

解构也可以添加默认值,如果右侧不存在对应的值,默认值就会生效,添加的默认值可以是数值,字符串,函数,对象,也可以是某一个已经存在的变量:

var { boots = { size: 10 } } = character
console.log(boots)
// <- { size: 10 }
Copy after login

对于多层的解构,同样可以使用默认值

var { metadata: { enemy = 'Satan' } } = character
console.log(enemy)
// <- 'Satan'
Copy after login

默认值和别名也可以一起使用,不过需要注意的是别名要放在前面,默认值添加给别名:

var { boots: footwear = { size: 10 } } = character
Copy after login

对象解构同样支持计算属性名,但是这时候你必须要添加别名,这是因为计算属性名允许任何类似的表达式,不添加别名,浏览器解析时会有问题,使用如下:

var { ['boo' + 'ts']: characterBoots } = character
console.log(characterBoots)
// <- true
Copy after login

还是那句话,我们也不是任何情况下都应该使用解构,语句characterBoots = character[type]看起来比{ [type]: characterBoots } = character语义更清晰,但是当你需要提取对象中的子对象时,解构就很简洁方便了。

我们再看看在数组中该如何使用解构。

数组解构

数组解构的语法和对象解构是类似的。区别在于,数组解构我们使用中括号而非花括号,下面的代码中,通过结构,我们在数组coordinates中提出了变量 x,y 。 你不需要使用x = coordinates[0]这样的语法了,数组解构不使用索引值,但却让你的代码更加清晰。

var coordinates = [12, -7]
var [x, y] = coordinates
console.log(x)
// <- 12
Copy after login

数组解构也允许你跳过你不想用到的值,在对应地方留白即可:

var names = ['James', 'L.', 'Howlett']
var [ firstName, , lastName ] = names
console.log(lastName)
// <- 'Howlett'
Copy after login

和对象解构一样,数组解构也允许你添加默认值:

var names = ['James', 'L.']
var [ firstName = 'John', , lastName = 'Doe' ] = names
console.log(lastName)
// <- 'Doe'
Copy after login

在ES5中,你需要借助第三个变量,才能完成两个变量值的交换,如下:

var left = 5, right = 7;
var aux = left
left = right
right = aux
Copy after login

使用解构,一切就简单多了:

var left = 5, right = 7;
[left, right] = [right, left]
Copy after login

我们再看看函数解构。

函数默认参数

在ES6中,我们可以给函数的参数添加默认值了,下例中我们就给参数 exponent 分配了一个默认值:

function powerOf(base, exponent = 2) {
  return Math.pow(base, exponent)
}
Copy after login

箭头函数同样支持使用默认值,需要注意的是,就算只有一个参数,如果要给参数添加默认值,参数部分一定要用小括号括起来。

var double = (input = 0) => input * 2
Copy after login

我们可以给任何位置的任何参数添加默认值。

function sumOf(a = 1, b = 2, c = 3) {
  return a + b + c
}
console.log(sumOf(undefined, undefined, 4))
// <- 1 + 2 + 4 = 7
Copy after login

在JS中,给一个函数提供一个包含若干属性的对象字面量做为参数的情况并不常见,不过你依旧可以按下面方法这样做:

var defaultOptions = { brand: 'Volkswagen', make: 1999 }
function carFactory(options = defaultOptions) {
  console.log(options.brand)
  console.log(options.make)
}
carFactory()
// <- 'Volkswagen'
// <- 1999
Copy after login

不过这样做存在一定的问题,当你调用该函数时,如果传入的参数对象只包含一个属性,另一个属性的默认值会自动失效:

carFactory({ make: 2000 })
// <- undefined
// <- 2000
Copy after login

函数参数解构就可以解决这个问题。

函数参数解构

通过函数参数解构,可以解决上面的问题,这里我们为每一个属性都提供了默认值,单独改变其中一个并不会影响其它的值:

function carFactory({ brand = 'Volkswagen', make = 1999 }) {
  console.log(brand)
  console.log(make)
}
carFactory({ make: 2000 })
// <- 'Volkswagen'
// <- 2000
Copy after login

不过这种情况下,函数调用时,如果参数为空即carFactory()函数将抛出异常。这种问题可以通过下面的方法来修复,下述代码中我们添加了一个空对象作为options的默认值,这样当函数被调用时,如果参数为空,会自动以{}作为参数。

function carFactory({
  brand = 'Volkswagen',
  make = 1999
} = {}) {
  console.log(brand)
  console.log(make)
}
carFactory()
// <- 'Volkswagen'
// <- 1999
Copy after login

除此之外,使用函数参数解构,还可以让你的函数自行匹配对应的参数,看接下来的例子,你就能明白这一点了,我们定义一个名为car的对象,这个对象拥有很多属性:owner,brand,make,model,preferences等等。

var car = {
  owner: {
    id: 'e2c3503a4181968c',
    name: 'Donald Draper'
  },
  brand: 'Peugeot',
  make: 2015,
  model: '208',
  preferences: {
    airbags: true,
    airconditioning: false,
    color: 'red'
  }
}
Copy after login

解构能让我们的函数方便的只使用里面的部分数据,下面代码中的函数getCarProductModel说明了具体该如何使用:

var getCarProductModel = ({ brand, make, model }) => ({
  sku: brand + ':' + make + ':' + model,
  brand,
  make,
  model
})
getCarProductModel(car)
Copy after login

解构使用示例

当一个函数的返回值为对象或者数组时,使用解构,我们可以非常简洁的获取返回对象中某个属性的值(返回数组中某一项的值)。比如说,函数getCoordinates()返回了一系列的值,但是我们只想用其中的x,y,我们可以这样写,解构帮助我们避免了很多中间变量的使用,也使得我们代码的可读性更高。

function getCoordinates() {
  return { x: 10, y: 22, z: -1, type: '3d' }
}
var { x, y } = getCoordinates()
Copy after login

通过使用默认值,可以减少重复,比如你想写一个random函数,这个函数将返回一个位于minmax之间的值。我们可以分辨设置min默认值为1,max默认值为10,在需要的时候还可以单独改变其中的某一个值:

function random({ min = 1, max = 10 } = {}) {
  return Math.floor(Math.random() * (max - min)) + min
}
console.log(random())
// <- 7
console.log(random({ max: 24 }))
// <- 18
Copy after login

解构还可以配合正则表达式使用。看下面这个例子:

function splitDate(date) {
  var rdate = /(\d+).(\d+).(\d+)/
  return rdate.exec(date)
}
var [ , year, month, day] = splitDate('2015-11-06')
Copy after login

不过当.exec不比配时会返回null,因此我们需要修改上述代码如下:

var matches = splitDate('2015-11-06')
if (matches === null) {
  return
}
var [, year, month, day] = matches
Copy after login

下面我们继续来讲讲spreadrest操作符。

剩余参数和拓展符

ES6之前,对于不确定数量参数的函数。你需要使用伪数组arguments,它拥有length属性,却又不具备很多一般数组有的特性。需要通过Array#slice.call转换arguments对象真数组后才能进行下一步的操作:

function join() {
  var list = Array.prototype.slice.call(arguments)
  return list.join(', ')
}
join('first', 'second', 'third')
// <- 'first, second, third'
Copy after login

对于这种情况,ES6提供了一种更好的解决方案:rest

剩余参数rest

使用rest, 你只需要在任意JavaScript函数的最后一个参数前添加三个点...即可。当rest参数是函数的唯一参数时,它就代表了传递给这个函数的所有参数。它起到和前面说的.slice一样的作用,把参数转换为了数组,不需要你再对arguments进行额外的转换了。

function join(...list) {
  return list.join(', ')
}
join('first', 'second', 'third')
// <- 'first, second, third'
Copy after login

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

function join(separator, ...list) {
  return list.join(separator)
}
join('; ', 'first', 'second', 'third')
// <- 'first; second; third'
Copy after login

在箭头函数中使用rest参数时,即使只有这一个参数,也需要使用圆括号把它围起来,不然就会报错SyntaxError,使用示例如下:

var sumAll = (...numbers) => numbers.reduce(
  (total, next) => total + next
)
console.log(sumAll(1, 2, 5))
// <- 8
Copy after login

上述代码的ES5实现如下:

// ES5的写法
function sumAll() {
  var numbers = Array.prototype.slice.call(arguments)
  return numbers.reduce(function (total, next) {
    return total + next
  })
}
console.log(sumAll(1, 2, 5))
// <- 8
Copy after login

拓展运算符

拓展运算符可以把任意可枚举对象转换为数组,使用拓展运算符可以高效处理目标对象,在拓展目前前添加...就可以使用拓展运算符了。下例中...arguments就把函数的参数转换为了数组字面量。

function cast() {
  return [...arguments]
}
cast('a', 'b', 'c')
// <- ['a', 'b', 'c']
Copy after login

使用拓展运算符,我们也可以把字符串转换为由每一个字母组成的数组:

[...'show me']
// <- ['s', 'h', 'o', 'w', ' ', 'm', 'e']
Copy after login

使用拓展运算符,还可以拼合数组:

function cast() {
  return ['left', ...arguments, 'right']
}
cast('a', 'b', 'c')
// <- ['left', 'a', 'b', 'c', 'right']
Copy after login
var all = [1, ...[2, 3], 4, ...[5], 6, 7]
console.log(all)
// <- [1, 2, 3, 4, 5, 6, 7]
Copy after login

这里我还想再强调一下,拓展运算符不仅仅适用于数组和arguments对象,对任意可迭代的对象都可以使用。迭代也是ES6新提出的一个概念,在 Iteration and Flow Control这一章,我们将详细叙述迭代。

Shifting和Spreading

当你想要抽出一个数组的前一个或者两个元素时,常用的解决方案是使用.shift.尽管是函数式的,下述代码在第一次看到的时候却不好理解,我们使用了两次.slicelist中抽离出两个不同的元素。

var list = ['a', 'b', 'c', 'd', 'e']
var first = list.shift()
var second = list.shift()
console.log(first)
// <- 'a'
Copy after login

在ES6中,结合使用拓展和解构,可以让代码的可读性更好:

var [first, second, ...other] = ['a', 'b', 'c', 'd', 'e']
console.log(other)
// <- ['c', 'd', 'e']
Copy after login

除了对数组进行拓展,你同样可以对函数参数使用拓展,下例展示了如何添加任意数量的参数到multiply函数中。

function multiply(left, right) {
  return left * right
}
var result = multiply(...[2, 3])
console.log(result)
// <- 6
Copy after login

向在数组中一样,函数参数中的拓展运算符同样可以结合常规参数一起使用。下例中,print函数结合使用了rest,普通参数,和拓展运算符:

function print(...list) {
  console.log(list)
}
print(1, ...[2, 3], 4, ...[5])
// <- [1, 2, 3, 4, 5]
Copy after login

下表总结了,拓展运算符的常见使用方法:

使用示例ES5ES6
Concatenation[1, 2].concat(more)[1, 2, ...more]
Push an array onto listlist.push.apply(list, items)list.push(...items)
Destructuringa = list[0], other = list.slice(1) [a, ...other] = list
new and applynew (Date.bind.apply(Date, [null,2015,31,8]))new Date(...[2015,31,8])

模板字符串

模板字符串是对常规JavaScript字符串的重大改进,不同于在普通字符串中使用单引号或者双引号,模板字符串的声明需要使用反撇号,如下所示:

var text = `This is my first template literal`
Copy after login

因为使用的是反撇号,你可以在模板字符串中随意使用单双引号了,使用时不再需要考虑转义,如下:

var text = `I'm "amazed" at these opportunities!`
Copy after login

模板字符串具有很多强大的功能,可在其中插入JavaScript表达式就是其一。

在字符串中插值

通过模板字符串,你可以在模板中插入任何JavaScript表达式了。当解析到表达式时,表达式会被执行,该处将渲染表达式的值,下例中,我们在字符串中插入了变量name

var name = 'Shannon'
var text = `Hello, ${ name }!`
console.log(text)
// <- 'Hello, Shannon!'
Copy after login

模板字符串是支持任何表达式的。使用模板字符串,代码将更容易维护,你无须再手动连接字符串和JavaScript表达式了。

看下面插入日期的例子,是不是又直观又方便:

`The time and date is ${ new Date().toLocaleString() }.`
// <- 'the time and date is 8/26/2015, 3:15:20 PM'
Copy after login

表达式中还可以包含数学运算符:

`The result of 2+3 equals ${ 2 + 3 }`
// <- 'The result of 2+3 equals 5'
Copy after login

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

`This template literal ${ `is ${ 'nested' }` }!`
// <- 'This template literal is nested!'
Copy after login

模板字符串的另外一个优点是支持多行字符串;

多行文本模板

在ES6之前,如果你想表现多行字符串,你需要使用转义,数组拼合,甚至使用使用注释符做复杂的hacks.如下所示:

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

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

var joined = [
'The first line',
'A second line',
'Then a third line'
].join('\n')
Copy after login

应用ES6,这种处理就简单多了,模板字符串默认支持多行:

var multiline =
`The first line
A second line
Then a third line`
Copy after login

当你需要返回的字符串基于html和数据生成,使用模板字符串是很简洁高效的,如下所示:

var book = {
  title: 'Modular ES6',
  excerpt: 'Here goes some properly sanitized HTML',
  tags: ['es6', 'template-literals', 'es6-in-depth']
}
var html = `

${ book.title }

${ book.excerpt }
    ${ book.tags .map(tag => `
  • ${ tag }
  • `) .join('\n ') }
`
Copy after login

上述代码将得到下面这样的结果。空格得以保留,多个li也按我们的预期被合适的渲染:

Modular ES6

Here goes some properly sanitized HTML
  • es6
  • template-literals
  • es6-in-depth
Copy after login

不过有时候我们并不希望空格被保留,下例中我们在函数中使用包含缩进的模板字符串,我们希望结果没有缩进,但是实际的结果却有四格的缩进。

function getParagraph() {
  return `
    Dear Rod,

    This is a template literal string that's indented
    four spaces. However, you may have expected for it
    to be not indented at all.

    Nico
  `
}
Copy after login

我们可以用下面这个功能函数对生成的字符串进行处理已得到我们想要的结果:

function unindent(text) {
  return text
    .split('\n')
    .map(line => line.slice(4))
    .join('\n')
    .trim()
}
Copy after login

不过,使用被称为标记模板的模板字符串新特性处理这种情况可能会更好。

标记模板

默认情况下,JavaScript会把`解析为转义符号,对浏览器来说,以`开头的字符一般具有特殊的含义。比如说\n意味着新行,\u00f1表示ñ等等。如果你不想浏览器执行这种特殊解析,你也可以使用String.raw来标记模板。下面的代码就是这样做的,这里我们使用了String.row来处理模板字符串,相应的这里面的\n没有被解析为新行。

var text = String.raw`"\n" is taken literally.
It'll be escaped instead of interpreted.`
console.log(text)
// "\n" is taken literally.
// It'll be escaped instead of interpreted.
Copy after login

我们添加在模板字符串之前的String.raw前缀,这就是标记模板,这样的模板字符串在被渲染前被该标记代表的函数预处理。

一个典型的标记模板字符串如下:

tag`Hello, ${ name }. I am ${ emotion } to meet you!`
Copy after login

实际上,上面标记模板可以用以下函数形式表示:

tag(
  ['Hello, ', '. I am ', ' to meet you!'],
  'Maurice',
  'thrilled'
)
Copy after login

我们还是用代码来说明这个概念,下述代码中,我们先定义一个名为tag函数:

function tag(parts, ...values) {
  return parts.reduce(
    (all, part, index) => all + values[index - 1] + part
  )
}
Copy after login

然后我们调用使用使用标记模板,不过此时的结果和不使用标记模板是一样的,这是因为我们定义的tag函数实际上并未对字符串进行额外的处理。

var name = 'Maurice'
var emotion = 'thrilled'
var text = tag`Hello, ${ name }. I am ${ emotion } to meet you!`
console.log(text)
// <- 'Hello Maurice, I am thrilled to meet you!'
Copy after login

我们看一个进行额外处理的例子,比如转换所有用户输入的值为大写(假设用户只会输入英语),这里我们定义标记函数upper来做这件事:

function upper(parts, ...values) {
  return parts.reduce((all, part, index) =>
    all + values[index - 1].toUpperCase() + part
  )
}
var name = 'Maurice'
var emotion = 'thrilled'
upper`Hello, ${ name }. I am ${ emotion } to meet you!`
// <- 'Hello MAURICE, I am THRILLED to meet you!'
Copy after login

既然可以转换输入为大写,那我们再进一步想想,如果提供合适的标记模板函数,使用标记模板,我们还可以对模板中的表达式进行各种过滤处理,比如有这么一个场景,假设表达式的值都来自用户输入,假设有一个名为sanitize的库可用于去除用户输入中的html标签,那通过使用标记模板,就可以有效的防止XSS攻击了,使用方法如下。

function sanitized(parts, ...values) {
  return parts.reduce((all, part, index) =>
    all + sanitize(values[index - 1]) + part
  )
}
var comment = 'Evil comment'
var html = sanitized`

${ comment }

` console.log(html) // <- '

Evil comment

'
Copy after login

ES6中的另外一个大的改变是提供了新的变量声明方式:letconst声明,下面我们一起来学习。

let & const 声明

可能很早之前你就听说过 let 了,它用起来像 var 但是,却有不同的作用域规则。

JavaScript的作用域有一套复杂的规则,变量提升的存在常常让新手忐忑不安。变量提升,意味着无论你在那里声明的变量,在浏览器解析时,实际上都被提升到了当前作用域的顶部被声明。看下面的这个例子:

function isItTwo(value) {
  if (value === 2) {
    var two = true
  }
  return two
}
isItTwo(2)
// <- true
isItTwo('two')
// <- undefined
Copy after login

尽管two是在代码分支中被声明,之后被外部分支引用,上述的JS代码还是可以工作的。var 声明的变量two实际是在isItTwo顶部被声明的。由于声明提升的存在,上述代码其实和下面代码的效果是一样的

function isItTwo(value) {
  var two
  if (value === 2) {
    two = true
  }
  return two
}
Copy after login

带来了灵活性的同事,变量提升也带来了更大的迷惑性,还好ES6 为我们提供了块作用域。

块作用域和let 声明

相比函数作用域,块作用域允许我们通过if,for,while声明创建新作用域,甚至任意创建{}块也能创建新的作用域:

{{{{{ var deep = 'This is available from outer scope.'; }}}}}
console.log(deep)
// <- 'This is available from outer scope.'
Copy after login

由于这里使用的是var,考虑到变量提升的存在,我们在外部依旧可以读取到深层中的deep变量,这里并不会报错。不过在以下情况下,我们可能希望这里会报错:

  • 访问内部变量会打破我们代码中的某种封装原则;
  • 父块中已有有一个一个同名变量,但是内部也需要用同名变量;

使用let就可以解决这个问题,let 创建的变量在块作用域内有效,在ES6提出let以前,想要创建深层作用域的唯一办法就是再新建一个函数。使用let,你只需添加另外一对{}

let topmost = {}
{
  let inner = {}
  {
    let innermost = {}
  }
  // attempts to access innermost here would throw
}
// attempts to access inner here would throw
// attempts to access innermost here would throw
Copy after login

for循环中使用let是一个很好的实践,这样定义的变量只会在当前块作用域内生效。

for (let i = 0; i < 2; i++) {
  console.log(i)
  // <- 0
  // <- 1
}
console.log(i)
// <- i is not defined
Copy after login

考虑到let声明的变量在每一次循环的过程中都重复声明,这在处理异步函数时就很有效,不会发生使用var时产生的诡异的结果,我们看一个具体的例子。

我们先看看 var 声明的变量是怎么工作的,下述代码中 i变量 被绑定在 printNumber 函数作用域中,当每个回调函数被调用时,它的值会逐步升到10,但是当每个回调函数运行时(每100us),此时的i的值已经是10了,因此每次打印的结果都是10.

function printNumbers() {
  for (var i = 0; i < 10; i++) {
    setTimeout(function () {
      console.log(i)
    }, i * 100)
  }
}
printNumbers()
Copy after login

使用let,则会把i绑定到每一个块作用域中。每一次循环 i 的值还是在增加,但是每次其实都是创建了一个新的 i ,不同的 i 之间不会相互影响 ,因此打印出的就是预想的0到9了。

function printNumbers() {
  for (let i = 0; i < 10; i++) {
    setTimeout(function () {
      console.log(i)
    }, i * 100)
  }
}
printNumbers()
Copy after login

为了细致的讲述let的工作原理, 我们还需要弄懂一个名为 Temporal Dead Zone 的概念。

Temporal Dead Zone

简言之,如果你的代码类似下面这样,就会报错。即在某个作用域中,在let声明之前调用了let声明的变量,导致的问题就是由于,Temporal Dead Zone(TDZ)的存在。

{
  console.log(name)
  // <- ReferenceError: name is not defined
  let name = 'Stephen Hawking'
}
Copy after login

如果定义的是一个函数,函数中引用了name变量则是可以的,但是这个函数并未在声明前执行则不会报错。如果let声明之前就调用了该函数,同样会导致TDZ。

// 不会报错
function readName() {
  return name
}
let name = 'Stephen Hawking'
console.log(readName())
// <- 'Stephen Hawking'
Copy after login
// 会报错
function readName() {
  return name
}
console.log(readName())
// ReferenceError: name is not defined
let name = 'Stephen Hawking'
Copy after login

即使像下面这样let定义的变量没有被赋值,下面的代码也会报错,原因依旧是它试图在声明前访问一个被let定义的变量

function readName() {
  return name
}
console.log(readName())
// ReferenceError: name is not defined
let name
Copy after login

下面的代码则是可行的:

function readName() {
  return name
}
let name
console.log(readName())
// <- undefined
Copy after login

TDZ的存在使得程序更容易报错,由于声明提升和不好的编码习惯常常会存在这样的问题。在ES6中则可以比较好的避免了这种问题了,需要注意的是let声明的变量同样存在声明提升。这意味着,变量会在我们进入块作用域时就会创建,TDZ也是在这时候创建的,它保证该变量不许被访问,只有在代码运行到let声明所在位置时,这时候TDZ才会消失,访问限制才会取消,变量才可以被访问。

Const 声明

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

下面的代码表明,const具有和let一致的块作用域:

const pi = 3.1415
{
  const pi = 6
  console.log(pi)
  // <- 6
}
console.log(pi)
// <- 3.1415
Copy after login

下面我们说说constlet的主要区别,首先const声明的变量在声明时必须赋值,否则会报错:

const pi = 3.1415
const e // SyntaxError, missing initializer
Copy after login

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

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

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

通过const声明的变量值并非不可改变

使用const只是意味着,变量将始终指向相同的对象或初始的值。这种引用是不可变的。但是值并非不可变。

下面的例子说明,虽然people的指向不可变,但是数组本身是可以被修改的。

const people = ['Tesla', 'Musk']
people.push('Berners-Lee')
console.log(people)
// <- ['Tesla', 'Musk', 'Berners-Lee']
Copy after login

const只是阻止变量引用另外一个值,下例中,尽管我们使用const声明了people,然后把它赋值给了humans,我们还是可以改变humans的指向,因为humans不是由const声明的,其引用可随意改变。people 是由 const 声明的,则不可改变。

const people = ['Tesla', 'Musk']
var humans = people
humans = 'evil'
console.log(humans)
// <- 'evil'
Copy after login

如果我们的目的是让值不可修改,我们需要借助函数的帮助,比如使用Object.freeze

const frozen = Object.freeze(
  ['Ice', 'Icicle', 'Ice cube']
)
frozen.push('Water')
// Uncaught TypeError: Can't add property 3
// object is not extensible
Copy after login

下面我们详细讨论一下constlet的优点

constlet的优点

新功能并不应该因为是新功能而被使用,ES6语法被使用的前提是它可以显著的提升我们代码的可读写和可维护性。let声明在大多数情况下,可以替换var以避免预期之外的问题。使用let你可以把声明在块的顶部进行而非函数的顶部进行。

有时,我们希望有些变量的引用不可变,这时候使用const就能防止很多问题的发生。下述代码中 在checklist函数外给items变量传递引用时就非常容易出错,它返回的todo API和items有了交互。当items变量被改为指向另外一个列表时,我们的代码就出问题了。todo API 用的还是items之前的值,items本身的指代则已经改变。

var items = ['a', 'b', 'c']
var todo = checklist(items)
todo.check()
console.log(items)
// <- ['b', 'c']
items = ['d', 'e']
todo.check()
console.log(items)
// <- ['d', 'e'], would be ['c'] if items had been constant
function checklist(items) {
  return {
    check: () => items.shift()
  }
}
Copy after login

这类问题很难debug,找到问题原因就会花费你很长一段时间。使用const运行时就会报错,可以帮助你可以避免这种问题。

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

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

推荐地址:《javascript基础教程

The above is the detailed content of Modern JavaScript usage skills: shorthand syntax in ES6. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:webhek.com
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!