type
status
date
slug
summary
tags
category
icon
password
ES6
介绍
ECMAScript 和 JavaScript 的关系
要讲清楚这个问题,需要回顾历史。1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。
ECMAScript 是 JavaScript 的关系是 前者是后者的规格/标准 后者是前者的实现
ES6 与 ES2015 的关系
ECMAScript 2015(简称 ES2015)这个词,也是经常可以看到的。它与 ES6 是什么关系呢?
2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。
但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布 6.0 版,过一段时间再发 6.1 版,然后是 6.2 版、6.3 版等等。
标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。
ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的
includes
方法和指数运算符),基本上是同一个标准。根据计划,2017 年 6 月发布 ES2017 标准。ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。
Let
ES6 中新增了
let
命令去声明变量 类似var
但所声明的变量只在let
命令所在代码里才有效那是不是可以把它用在 for 循环里呢?
首先来看看
var
用在 for 里可能会有什么问题:上面代码中,变量
i
是var
命令声明的,在全局范围内都有效,所以全局只有一个变量i
。每一次循环,变量i
的值都会发生改变,而循环内被赋给数组a
的函数内部的console.log(i)
,里面的i
指向的就是全局的i
。也就是说,所有数组a
的成员里面的i
,指向的都是同一个i
,导致运行时输出的是最后一轮的i
的值,也就是 10。如果使用
let
,声明的变量仅在块级作用域内有效,最后输出的是 6。上面代码中,变量
i
是let
声明的,当前的i
只在本轮循环有效,所以每一次循环的i
其实都是一个新的变量,所以最后输出的是6
。你可能会问,如果每一轮循环的变量i
都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i
时,就在上一轮循环的基础上进行计算。另外,
for
循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。两个互相独立 互不干扰上面代码正确运行,输出了 3 次
abc
。这表明函数内部的变量i
与循环变量i
不在同一个作用域,有各自单独的作用域。不存在变量提升
var
命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined
。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。为了纠正这种现象,
let
命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。上面代码中,变量
foo
用var
命令声明,会发生变量提升,即脚本开始运行时,变量foo
已经存在了,但是没有值,所以会输出undefined
。变量bar
用let
命令声明,不会发生变量提升。这表示在声明它之前,变量bar
是不存在的,这时如果用到它,就会抛出一个错误。暂时性死区
只要块级作用域内存在
let
命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。上面代码中,存在全局变量
tmp
,但是块级作用域内let
又声明了一个局部变量tmp
,导致后者绑定这个块级作用域,所以在let
声明变量前,对tmp
赋值会报错。ES6 明确规定,如果区块中存在
let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。总之,在代码块内,使用
let
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。上面代码中,在
let
命令声明变量tmp
之前,都属于变量tmp
的“死区”。“暂时性死区”也意味着
typeof
不再是一个百分之百安全的操作。上面代码中,变量
x
使用let
命令声明,所以在声明之前,都属于x
的“死区”,只要用到该变量就会报错。因此,typeof
运行时就会抛出一个ReferenceError
。作为比较,如果一个变量根本没有被声明,使用
typeof
反而不会报错。上面代码中,
undeclared_variable
是一个不存在的变量名,结果返回“undefined”。所以,在没有let
之前,typeof
运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。有些“死区”比较隐蔽,不太容易发现。
上面代码中,调用
bar
函数之所以报错(某些实现可能不报错),是因为参数x
默认值等于另一个参数y
,而此时y
还没有声明,属于“死区”。如果y
的默认值是x
,就不会报错,因为此时x
已经声明了。另外,下面的代码也会报错,与
var
的行为不同。ES6 规定暂时性死区和
let
、const
语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
不允许重复声明
let
不允许在相同作用域内,重复声明同一个变量。因此,不能在函数内部重新声明参数。
ES6 块级作用域
let
实际上为 JavaScript 新增了块级作用域。上面的函数有两个代码块,都声明了变量
n
,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用var
定义变量n
,最后输出的值才是 10。ES6 允许块级作用域的任意嵌套。
上面代码使用了一个五层的块级作用域,每一层都是一个单独的作用域。第四层作用域无法读取第五层作用域的内部变量。
内层作用域可以定义外层作用域的同名变量。
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。防止变量污染
块级作用域与函数声明
ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于
let
,在块级作用域之外不可引用。上面代码在 ES5 中运行,会得到“I am inside!”,因为在
if
内声明的函数f
会被提升到函数头部,实际运行的代码如下。ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于
let
,对作用域之外没有影响。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?上面的代码在 ES6 浏览器中,都会报错。
- 允许在块级作用域内声明函数。
- 函数声明类似于
var
,即会提升到全局作用域或函数作用域的头部。
- 同时,函数声明还会提升到所在的块级作用域的头部。
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作
let
处理。根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于
var
声明的变量。上面的例子实际运行的代码如下。考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
另外,还有一个需要注意的地方。ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
上面代码中,第一种写法没有大括号,所以不存在块级作用域,而
let
只能出现在当前作用域的顶层,所以报错。第二种写法有大括号,所以块级作用域成立。函数声明也是如此,严格模式下,函数只能声明在当前作用域的顶层。
Const
基本用法
const
声明一个只读的常量。一旦声明,常量的值就不能改变。上面代码表明改变常量的值会报错。
const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。上面代码表示,对于
const
来说,只声明不赋值,就会报错。const
的作用域与let
命令相同:只在声明所在的块级作用域内有效。const
命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。上面代码在常量
MAX
声明之前就调用,结果报错。const
声明的常量,也与let
一样不可重复声明。本质
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。上面代码中,常量
foo
储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo
指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。下面是另一个例子。
上面代码中,常量
a
是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a
,就会报错。如果真的想将对象冻结(变成常量),应该使用
Object.freeze
方法。上面代码中,常量
foo
指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。顶层对象的属性
顶层对象,在浏览器环境指的是
window
对象,在 Node 指的是global
对象。ES5 之中,顶层对象的属性与全局变量是等价的。上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。(那很糟糕啊)
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,
window
对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。ES6 为了改变这一点,一方面规定,为了保持兼容性,
var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。上面代码中,全局变量
a
由var
命令声明,所以它是顶层对象的属性;全局变量b
由let
命令声明,所以它不是顶层对象的属性,返回undefined
。globalThis 对象
JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。
- 浏览器里面,顶层对象是
window
,但 Node 和 Web Worker 没有window
。
- 浏览器和 Web Worker 里面,
self
也指向顶层对象,但是 Node 没有self
。
- Node 里面,顶层对象是
global
,但其他环境都不支持。
同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用
this
变量,但是有局限性。- 全局环境中,
this
会返回顶层对象。但是,Node 模块和 ES6 模块中,this
返回的是当前模块。
- 函数里面的
this
,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this
会指向顶层对象。但是,严格模式下,这时this
会返回undefined
。
- 不管是严格模式,还是普通模式,
new Function('return this')()
,总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval
、new Function
这些方法都可能无法使用。
综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。
垫片库
global-this
模拟了这个提案,可以在所有环境拿到globalThis
。变量的解构赋值
(descructing)
什么是边框的结构赋值? 就是从数组和对象中提取值,再对变量进行赋值
以前,为变量赋值,只能直接指定值。
ES6 允许写成下面这样。
上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。
如果解构不成功,变量的值就等于
undefined
。以上两种情况都属于解构不成功,
foo
的值都会等于undefined
。另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。
上面两个例子,都属于不完全解构,但是可以成功。
如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。
上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。
对于 Set 结构,也可以使用数组的解构赋值。
事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
上面代码中,
fibs
是一个 Generator 函数(参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。默认值
解构赋值允许指定默认值
注意,ES6 内部使用严格相等运算符(
===
),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined
,默认值才会生效。上面代码中,如果一个数组成员是
null
,默认值就不会生效,因为null
不严格等于undefined
。如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
上面代码中,因为
x
能取到值,所以函数f
根本不会执行。上面的代码其实等价于下面的代码。默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
上面最后一个表达式之所以会报错,是因为
x
用y
做默认值时,y
还没有声明。对象的解构赋值
解构不仅可以用于数组,还可以用于对象。
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于
undefined
。如果解构失败,变量的值等于
undefined
。上面代码中,等号右边的对象没有
foo
属性,所以变量foo
取不到值,所以等于undefined
。对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。
上面代码的例一将
Math
对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。例二将console.log
赋值到log
变量。如果变量名与属性名不一致,必须写成下面这样。
这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。
也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
上面代码中,
foo
是匹配的模式,baz
才是变量。真正被赋值的是变量baz
,而不是模式foo
。与数组一样,解构也可以用于嵌套结构的对象。
注意,这时
p
是模式,不是变量,因此不会被赋值。如果p
也要作为变量赋值,可以写成下面这样。下面是另一个例子。
上面代码有三次解构赋值,分别是对
loc
、start
、line
三个属性的解构赋值。注意,最后一次对line
属性的解构赋值之中,只有line
是变量,loc
和start
都是模式,不是变量。下面是嵌套赋值的例子。
如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。
上面代码中,等号左边对象的
foo
属性,对应一个子对象。该子对象的bar
属性,解构时会报错。原因很简单,因为foo
这时等于undefined
,再取子属性就会报错。注意,对象的解构赋值可以取到继承的属性。
上面代码中,对象
obj1
的原型对象是obj2
。foo
属性不是obj1
自身的属性,而是继承自obj2
的属性,解构赋值可以取到这个属性。默认值
对象的解构也可以指定默认值。
默认值生效的条件是,对象的属性值严格等于
undefined
。上面代码中,属性
x
等于null
,因为null
与undefined
不严格相等,所以是个有效的赋值,导致默认值3
不会生效。字符串的解构赋值
字符串被转换成了一个类似数组的对象。
类似数组的对象都有一个
length
属性,因此还可以对这个属性解构赋值。数值和布尔值得解构赋值
如果等号右边是数值或布尔值, 会先转为对象
上面代码中,数值和布尔值的包装对象都有
toString
属性,因此变量s
都能取到值。解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于
undefined
和null
无法转为对象,所以对它们进行解构赋值,都会报错。函数参数的解构赋值
函数的参数也可以使用解构赋值。
上面代码中,函数
add
的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x
和y
。对于函数内部的代码来说,它们能感受到的参数就是x
和y
。下面是另一个例子。
函数参数的解构也可以使用默认值。
用途
(1)交换变量的值
上面代码交换变量
x
和y
的值,这样的写法不仅简洁,而且易读,语义非常清晰。(2)从函数返回多个值
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。
(3)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。
(4)提取 JSON 数据
解构赋值对提取 JSON 对象中的数据,尤其有用。
上面代码可以快速提取 JSON 数据的值。
(5)函数参数的默认值
指定参数的默认值,就避免了在函数体内部再写
var foo = config.foo || 'default foo';
这样的语句。(6)遍历 Map 结构
任何部署了 Iterator 接口的对象,都可以用
for...of
循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。如果只想获取键名,或者只想获取键值,可以写成下面这样。
(7)输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。
字符串的扩展
字符的 Unicode 表示法
ES6 允许采用\uxxxx 形式表示一个字符, 其中 xxxx 表示 unicode 码点
字符串的遍历器接口
ES6 为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被
for...of
循环遍历。除了遍历字符串,这个遍历器最大的优点是可以识别大于
0xFFFF
的码点,传统的for
循环无法识别这样的码点。上面代码中,字符串
text
只有一个字符,但是for
循环会认为它包含两个字符(都不可打印),而for...of
循环会正确识别出这一个字符。模板字符串
传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。
上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。
如果需要引用模板字符串本身,在需要时执行,可以写成函数。
字符串新增方法
String.fromCodePoint()
ES5 提供
String.fromCharCode()
方法,用于从 Unicode 码点返回对应字符,但是这个方法不能识别码点大于0xFFFF
的字符。上面代码中,
String.fromCharCode()
不能识别大于0xFFFF
的码点,所以0x20BB7
就发生了溢出,最高位2
被舍弃了,最后返回码点U+0BB7
对应的字符,而不是码点U+20BB7
对应的字符。ES6 提供了
String.fromCodePoint()
方法,可以识别大于0xFFFF
的字符,弥补了String.fromCharCode()
方法的不足。在作用上,正好与下面的codePointAt()
方法相反。上面代码中,如果
String.fromCodePoint
方法有多个参数,则它们会被合并成一个字符串返回。注意,
fromCodePoint
方法定义在String
对象上,而codePointAt
方法定义在字符串的实例对象上。String.raw()
正则扩展
函数拓展
1.函数参数默认值
ES6 前 函数形参不能指定默认值 只能 用 x = x|| "world" 方法
但是这种方法有种弊端 就是如果 x 被赋值但其值被转成 boolean 值为 false 那么还赋值就不起作用
比如
如何避免?
优势:
除了简洁,ES6 的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
参数变量是默认声明的,所以不能用
let
或const
再次声明。上面代码中,参数变量
x
是默认声明的,在函数体中,不能用let
或const
再次声明,否则会报错。使用参数默认值时,函数不能有同名参数。
默认值也可以是个表达式: 每次都会重新求值
2.参数默认值加解构赋值
上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。
3.函数的 length 属性
4.作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
上面代码中,参数
y
的默认值等于变量x
。调用函数f
时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x
指向第一个参数x
,而不是全局变量x
,所以输出是2
。再看下面的例子
上面代码中,函数
f
调用时,参数y = x
形成一个单独的作用域。这个作用域里面,变量x
本身没有定义,所以指向外层的全局变量x
。函数调用时,函数体内部的局部变量x
影响不到默认值变量x
。如果此时,全局变量
x
不存在,就会报错。下面这样写,也会报错。
上面代码中,参数
x = x
形成一个单独作用域。实际执行的是let x = x
,由于暂时性死区的原因,这行代码会报错”x 未定义“。如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。
上面代码中,函数
bar
的参数func
的默认值是一个匿名函数,返回值为变量foo
。函数参数形成的单独作用域里面,并没有定义变量foo
,所以foo
指向外层的全局变量foo
,因此输出outer
。5.应用
如果有必要的参数可以将其默认值设为一个会抛出错误的函数
箭头函数
ES6 允许使用'箭头'定义函数:
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用
return
语句返回。由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
下面是一种特殊情况,虽然可以运行,但会得到错误的结果。
上面代码中,原始意图是返回一个对象
{ a: 1 }
,但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1
。这时,a
可以被解释为语句的标签,因此实际执行的语句是1;
,然后函数就结束了,没有返回值。· 简化回调函数:
箭头函数注意点
- 函数体内 this 对象就是定义时所在的对象 而不是使用时所在对象
- 不可当做构造函数,不可以使用 new 关键字 否则会抛出一个错误
- 不可以使用
arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
- 不可以使用
yield
命令 因此箭头函数不能用作 Generator 函数。
上面四点中,第一点尤其值得注意。
this
对象的指向是可变的,但是在箭头函数中,它是固定的。setTimeout 的参数是一个箭头函数, 定义生效是在 foo 函数生成时 而真正执行要到 100ms 后
若是普通函数执行时 this 应该指向全局对想 window 输出 21 而现在箭头函数导致 this 总是指向函数定义生效时所在的对象 {id:42} 所以输出 42
箭头函数可以让 setTimeout 里的 this 绑定定义时所在的作用域, 而不是运行是所在作用域。下面是一个例子
上面代码中,
Timer
函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this
绑定定义时所在的作用域(即Timer
函数),后者的this
指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1
被更新了 3 次,而timer.s2
一次都没更新。箭头函数可以让 this 指向固定化,这种特性很利于封装回调函数。下面是一个例子
上面代码的
init
方法中,使用了箭头函数,这导致这个箭头函数里面的this
,总是指向handler
对象。否则,回调函数运行时,this.doSomething
这一行会报错,因为此时this
指向document
对象。this
指向的固定化,并不是因为箭头函数内部有绑定this
的机制,实际原因是箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。所以,箭头函数转成 ES5 的代码如下。
由于箭头函数没有自己的 this 它就不可能作为构造函数, 其次除了没有自己的
this
他还没有以下三个变量:指向外层函数的对应变量 arguments super new.target
- 由于没有自己的 this 指向所以无法使用 call() apply() bind() 这些方法去改变 this 得指向
箭头函数不适用场合:
第一个场合是定义对象的方法,且该方法内部包括
this
。上面代码中,
cat.jumps()
方法是一个箭头函数,这是错误的。调用cat.jumps()
时,如果是普通函数,该方法内部的this
指向cat
;如果写成上面那样的箭头函数,使得this
指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps
箭头函数定义时的作用域就是全局作用域。第二个场合是需要动态
this
的时候,也不应使用箭头函数。上面代码运行时,点击按钮会报错,因为
button
的监听函数是一个箭头函数,导致里面的this
就是全局对象。如果改成普通函数,this
就会动态指向被点击的按钮对象。箭头函数嵌套(套娃)
箭头函数内部,还可以再使用箭头函数。下面是一个 ES5 语法的多重嵌套函数。
上面这个函数,可以使用箭头函数改写。
- 作者:NotionNext
- 链接:http://zhanghaoyu.top/article/example-3
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。