何为ES6语法糖?即这些事情ES5也可以做,只是稍微复杂一些,而ES6提供了非破坏性的更新, 目的是提供更简洁,语义更清晰的语法,从而提高代码的可读性和可维护性
本文几点提炼:
- 对象字面量的简写属性和计算的属性名不可同时使用,原因是简写属性是一种在编译阶段的就会生效的语法糖,而计算的属性名则在运行时才生效
- 箭头函数本身已经很简洁,但是还可以进一步简写
- 解构也许确实可以理解为变量声明的一种语法糖,当涉及到多层解构时,其使用非常灵活
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
,然后替换匿名函数体中的this
为self
。而第二段代码中,使用了箭头函数则没有这个问题了。
箭头函数的作用域不能通过
.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
对象中的name
,nick
已经可以轻松解构了,那metadata
中的age
和gender
又如何解构?于是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.brand
和 null.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 声明
let
和 var
比较像,但他们有不同的作用域
在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
【码农解释】为何会打印10
个10
?为什么不是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
的存在使得程序更容易报错,由于声明提升和不好的编码习惯常常会导致这样的问题。其实let
与var
一样, 也存在声明提升,提升到块级作用域顶部,但TDZ
的存在限制了let
定义的变量的访问,TDZ
在let
声明变量的位置才消失,访问限制才被取消,这就造成了let
定义的变量和var
定义的变量在这一方面上表现不一致的原因
@ const 声明
const
声明也具有类似let
的块作用域,它同样具有TDZ
机制。实际上,TDZ
机制是因为const
才被创建,随后才被应用到let
声明中。const
需要TDZ
的原因是为了防止由于变量提升,在程序解析到const
语句之前,对const
声明的变量进行了赋值操作,这样是有问题的。
const
具有和let
一致的块作用域。他们的主要区别是:
-
首先
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
即:抛出异常,对象是不可被拓展的。
@ const
和let
的优点
let
声明在大多数情况下,可以替换var
以避免预期之外的问题。使用let
你可以把声明在块的顶部进行而非函数的顶部进行。
如果我们默认只使用cosnt
和let
声明变量,所有的变量都会有一样的作用域规则,这让代码更易理解,由于const
造成的影响最小,它还曾被提议作为默认的变量声明。
总的来说,const
不允许重新指定值,使用的是块作用域,存在TDZ
。let
则允许重新指定值,其它方面和const
类似,而var
声明使用函数作用域,可以重新指定值,可以在未声明前调用,考虑到这些,推荐尽量不要使用var
声明了。