ES6 知识梳理
ES6 —— 从一脸懵逼到灵活运用
本文为笔者学习 ES6 时自用的知识梳理笔记,如有错误,欢迎指出~
var let const
var let const 的比较
声明与赋值
var
声明的变量是可以重新赋值的,也可以重复声明let
和const
声明的变量都是不可以重复声明的 在不同作用域内可以出现同名变量,但并不相同,只能在各自的作用域中使用
不同的是,
let
声明的变量是可以重新赋值的,但const
不行 注意:用
const
声明的变量并不是完全不可以改变的 如果用
const
来声明一个对象,虽然无法给这个对象重新赋值,但是我们可以改变对象的属性值(对象是引 用类型变量,只改变对象的属性并不会影响指针指向) 如果你也不希望改变属性值的话可以使用
Object.freeze()
方法
变量作用域
var
为 function scope 即函数作用域在函数中声明的变量只能在函数中使用
如果在在
if
和for
等语句里定义的变量你只希望在内部使用,var
就无法满足,因为它没有在函数里声明,所以会变成一个全局变量,污染全局作用域let
和const
是 block scope 即块级作用域一对大括号 { } 所包裹的内容即为一个块级作用域,声明的变量只能在块内使用,在块级作用域外调用则会报错
let 和 const 的使用场景
用
let
和const
代替 IIFEIIFE 即 立即执行函数 ,应用之一就用来生成一个私有变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14// 例如我们在window对象中有一个name属性,如果直接这样声明一个name变量的话会覆盖属性name的值
// var name = 'Tom';
// 我们通常会用一个立即执行函数来使变量私有化
(function () {
var name = 'Tom';
// do something
})();
// 如果使用 let 或者 const 就可以简单实现,只需要用一对大括号包裹起来,就形成了一个块级作用域
{
let name = 'Tom';
// do something
}for
循环1
2
3
4
5
6
7
8
9
10
11
12
13
14for (var i = 0; i < 10; i++) {
console.log(i); // 输出:0 1 2 3 ... 9
// 我们这里用setTimeout来模拟异步请求
setTimeout(function() {
console.log('i:' + i);
// 这里输出了10个 i:10
// 因为函数是延迟一秒执行的 此时 for 循环已经结束
}, 1000)
}
// 将 var 改成 let 即可得到想要的结果
// 注意:不能使用 const !…
临时性死区 Temporal Dead Zone
变量提升是指 JavaScript 将变量的声明移至作用域的顶部
1 | console.log(color); // 不会报错 而是输出 undefined |
在 ES6 中, let 也会将变量提升到块级作用域顶部,但你想在块级作用域中变量的声明之前引用的话就会报 ReferenceError, 因为它是在临时性死区中的, const 亦是如此
需要注意的是,因为 const 定义的是一个常量,所以声明的同时必须赋初始值,否则会报错
使用建议
- 默认使用 const
- 当变量需要重新赋值时使用 let
- 尽量不使用 var
Arrow Function 箭头函数
优点:
- 简明的语法
- 可以隐式返回
- 不绑定
this
简明的语法
例如我们要用 map
遍历一个数组使其中的数乘以二返回
1 | const nums = [1, 3, 0, 5]; |
箭头函数写法:去掉function
关键字,加上 =>
如果箭头函数只有一个参数的话,()
可以省略,没有参数或者有多个参数则必须使用括号并且参数之间用,
隔开
隐式返回
显式返回即return
关键字加上返回的内容。
箭头函数中的隐式返回:
去掉return
关键字 , 去掉 {}
, 将表达式写到一行中
用于我们只想简单返回一些内容时使用,使代码更加简洁
1 | // like this |
注: 因为箭头函数都是匿名函数,匿名函数在递归或者作为回调函数等场景时非常好用,但如果你只想作为一个简单函数的话我们一般把它赋值给一个变量来使用
this 问题
在使用箭头函数以前我们经常遇到这样的问题
1 | const Tom = { |
这里的showHobbies()
是由 对象Tom
调用的,所以this
指向的是对象Tom
,因此 this.hobbies
可以正常取到;
而forEach()
方法里的回调函数他不是作为对象的方法调用,也没有使用apply
、call
等方法来改变this
指向,所以这里的this
指向的是 Window 或者说全局(严格模式下为 undefined)
以前我们通常的做法是在这之前var self = this;
,然后用self
代替this
来使用
在 ES6 中我们可以借助箭头函数来代替这种 hack 写法,因为箭头函数没有自己的this
,它的this
值是继承它的父级作用域的(词法作用域,由上下文确定)
箭头函数不适用的场景
- 作为构造函数,向原型对象中添加方法
- 当你真的需要
this
的时候,例如事件绑定 - 需要使用
arguments
对象时
参数默认值
1 | // 直接定义在函数的形参后面 |
模板字符串
在过去我们要组合 变量 和 字符串 的时候需要不停地用+
进行连接,这样既繁琐又容易出错而且不易检查。
有了 ES6 的模板字符串就变得容易多了
模板字符串 允许我们用一对反引号 ` 来定义字符串
${}
里面可以是任意的 JS 表达式,包括对象的属性,甚至是一个函数
1 | const name = 'Tom'; |
New String Methods
1 | const id = 'adcd123456x'; |
解构
对象解构
1 | const Tom = { |
数组解构
1 | const numbers = [1, 2, 3, 4, 5] |
以前我们要交换两个变量的值的时候,经常会定义一个中间变量,有了解构赋值我们就可以方便的进行交换了
1 | let a = 10 |
for of 循环
for of 是 ES6 新增的一种循环方式
1 | const fruits = ['apple', 'banana', 'orange', 'mango'] |
New Array Methods
.from()
& .of()
这两个方法并不是原型上的方法,需要通过 Array
对象来调用,即Array.from()
和 Array.of()
Array.from()
用于把一个类数组或者可遍历对象转换为一个真正的数组,同时它还支持传入回调函数作为第二个参数 ,来对转换成的数组每一项执行一定的方法进行处理
Array.of()
主要解决Array()
构造函数传入不同数量的参数时行为不一致的问题: 当你传入一个参数(例如:2)时,它会返回长度为 2 的数组(undefined x 2),当你传入多个参数(例如:1,2,3),这时它又会返回由这些参数组成的数组
而
Array.of()
无论你传入多少个参数,它都会返回由这些参数组成的数组
其他方法
.find()
.findIndex()
.some()
.every()
find()
方法用于返回数组中满足条件(函数内判断)的第一个元素的值- 当数组中的元素在符合条件时返回,之后的值不会再调用执行函数
- 如果没有符合条件的元素则返回 undefined
语法:
array.find(function(element, index, arr))
参数:测试函数 function(element, index, arr)
element - 当前元素
index - 当前元素索引
arr - 正在执行该方法的数组
1
2
3
4
5
6// 例如我们想要从ages数组中得到年龄大于等于18的第一个元素
const ages = [3, 10, 17, 20]
const res = ages.find((age) => {
return age >= 18
})
console.log(res) // 18findIndex()
方法与find()
方法类似,唯一不同就是它返回的是满足条件的元素的索引值some()
和every()
用法类似,只不过返回的是一个布尔值 前者表示至少有一个满足,即找到一个满足条件的元素时返回
true
后者表示每一个都满足,即当所有元素都满足条件时才会返回
true
,当找到一个不满足条件的元素时就会立即返回false
剩余参数
剩余参数语法允许我们将一个不定数量的参数表示为一个数组
当我们定义一个函数的时候,如果在最后一个参数前面加...
前缀,就会将剩余参数存到一个数组中
语法:
1 | function(a, b, ...theArgs) { |
1 | // 比如我们定义一个函数来计算商品的折后价 |
前面我们提到过,剩余参数还以用于变量的解构
1 | // 我们定义一个变量来记录玩家的 name id 和 scores |
扩展运算符(…)
扩展运算符用法与剩余参数相反,它用于把可遍历对象的元素扩展为一个新的参数序列
1 | // 把一个字符串中的每一个字符存到一个数组中 |
1 | // 把两个数组进行拼接可以用 concat 方法,但如果我们还要在两个数组的元素中间加入一个元素,就不太方便了 |
对象字面量的扩展
当我们在声明一个对象的时候,如果属性名和属性值所指向的变量名一致,可以简化书写
1 | const name = 'Tom' |
ES6 还提供了计算属性,你可以在对象的属性名和属性值处写入 js 语句
1 | const keys = ['name', 'age', 'gender'] |
Promise
以下内容来自于 MDN Web 文档, 查看更多
因为大部分情况下我们只是使用已创建的 Promise 实例对象,所以此处只简单介绍用法,要想了解 Promise 构造函数,点击此处
Promise
是一个对象,它代表了一个异步操作的最终完成或者失败。本质上,Promise 是一个被某些函数传出的对象,我们附加回调函数(callback)使用它,而不是将回调函数传入那些函数内部。
约定
不同于“老式”的传入回调,在使用 Promise 时,会有以下约定:
在 本轮 Javascript event loop(事件循环)运行完成 之前,回调函数是不会被调用的。
通过
then()
添加的回调函数总会被调用,即便它是在异步操作完成之后才被添加的函数。通过多次调用
then()
,可以添加多个回调函数,它们会按照插入顺序一个接一个独立执行。因此,Promise 最直接的好处就是链式调用(chaining)。
链式调用
在过去,要想做多重的异步操作,会导致经典的回调地狱:
1 | doSomething(function(result) { |
通过新的功能方法,我们把回调绑定到被返回的 Promise 上代替以往的做法,形成一个 Promise 链:
1 | doSomething().then(function(result) { |
then 里的参数是可选的,catch(failureCallback)
是 then(null, failureCallback)
的缩略形式。如下所示,我们也可以用箭头函数来表示:
1 | doSomething() |
Symbol
symbol 是 ES6 新加的一种基本数据类型。
Symbol()
函数会返回symbol类型的值,但它并不是构造函数,因为它不支持语法:new Symbol()
。每个从
Symbol()
返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符,这是该数据类型仅有的目的。
语法: Symbol([description])
description 为可选参数,字符串类型,是对 symbol 的描述
1 | const sym1 = Symbol() |
上面的代码创建了三个新的 symbol 类型。 它们每一个都会是新的 symbol 类型,即使用了同样的描述:
1 | Symbol("foo") === Symbol("foo") // false |
使用 new
运算符的语法将抛出 TypeError
错误:
1 | const sym = new Symbol() // TypeError |
Symbols 在 for...in
循环中不可枚举。另外,Object.getOwnPropertyNames()
也不会返回 symbol 类型的属性,但是你能使用 Object.getOwnPropertySymbols()
得到它们
Modules (模块)
导出模块 export
命名导出 named exports
为了获得模块的功能要做的第一件事是把它们导出来。使用
export
语句来完成。最简单的方法是把它(指 export 语句)放到你想要导出的项前面,比如:
1
2
3
4
5export const name = 'square'
export function funcName() {
// ...
}你能够导出函数,
var
,let
,const
, 和类。export 要放在最外层;比如你不能够在函数内使用export
。一个更方便的方法导出所有你想要导出的模块的方法是在模块文件的末尾使用一个 export 语句, 用花括号括起来你想导出的模块并用逗号分割。比如:
export { name, age, func1, func2 }
此方式还支持你对导出的模块使用as
进行重命名:
export { name as n, age as a, func1, func2 }
这样在你导入的时候就需要使用as
后的变量名进行导入
默认导出 default export
上面方法导出的功能都是由 named exports 组成 — 每个项目(无论是函数,常量等)在导出时都由其名称引用,并且该名称也用于在导入时引用它。
还有一种导出类型叫做 default export
语法:
export default xxx
,xxx 为你要导出的项目的名字 我们还可以把
export default
放到函数或者类的前面:
export default function() {}
export default class {}
注意,不能使用
var
、let
或const
用于导出默认值export default
。关于两种导出方式
你能够在每一个模块中定义多个命名导出,但是只允许有一个默认导出。
在导出多个值时,命名导出非常有用。在导入期间,必须使用相应对象的相同名称。但是,你可以使用任何名称导入默认导出的模块。
导入模块 import
语法:
1 | import defaultExport from "module-name" |
defaultExport
导入默认导出时的的引用名
module-name
要导入的模块。通常是包含目标模块的.JavaScript
文件的相对或绝对路径名,可以不包括.JavaScript
扩展名。
name
导入模块对象整体的别名
export, export1, export2
被导入模块的导出接口的名称 ( 命名导出,导入时需使用相同的名称 )
alias
用于引用指定导入的名称 ( 同导出时,导入时也可以使用 as 进行重命名 )
Class
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过
class
关键字,可以定义类。基本上,ES6 的class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
基本语法
1 | // 传统方法 通过构造函数定义并生成一个新的对象 |
上面代码定义了一个“类”,可以看到里面有一个constructor
方法,这就是构造方法,而this
关键字则代表实例对象。也就是说,ES5 的构造函数Person
,对应 ES6 的Person
类的构造方法。
Person
类除了构造方法,还定义了一个sayName
方法。注意,定义“类”的方法的时候,前面不需要加上function
这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
ES6 的类,完全可以看作构造函数的另一种写法。
1 | class Person { |
上面代码表明,类的数据类型就是函数,类本身就指向构造函数。
使用的时候,也是直接对类使用new
命令,跟构造函数的用法完全一致。
构造函数的prototype
属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype
属性上面。在类的实例上面调用方法,其实就是调用原型上的方法。
由于类的方法都定义在prototype
对象上面,所以类的新方法可以添加在prototype
对象上面。Object.assign
方法可以很方便地一次向类添加多个方法。
1 | class Person { |
另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。
支持计算属性:类的属性名,可以采用表达式。
1 | let methodName = 'greet' |
constructor 方法
constructor
方法是类的默认方法,通过new
命令生成对象实例时,自动调用该方法。一个类必须有constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。
constructor
方法默认返回实例对象(即this
),完全可以指定返回另外一个对象。
1 | class Foo { |
类的实例对象
生成类的实例对象的写法,与 ES5 完全一样,也是使用new
命令。如果忘记加上new
,像函数那样调用Class
,将会报错。
与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this
对象上),否则都是定义在原型上(即定义在class
上)。
1 | //定义类 |
类的所有实例共享一个原型对象。
1 | var p1 = new Person('Tom',21); |
上面代码中,p1
和p2
都是 Point 的实例,它们的原型都是 Point.prototype,所以__proto__
属性是相等的。
这也意味着,可以通过实例的__proto__
属性为 Class 添加方法。
不存在变量提升
Class 不存在变量提升(hoist),这一点与 ES5 完全不同。
Class 表达式
与函数一样,类也可以使用表达式的形式定义。
1 | const MyClass = class Me { |
上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass
而不是Me
,Me
只在 Class 的内部代码可用,指代当前类。
1 | let inst = new MyClass() |
上面代码表示,Me
只在 Class 内部有定义。
如果类的内部没用到的话,可以省略Me
,也就是可以写成下面的形式。
1 | const MyClass = class { /* ... */ } |
采用 Class 表达式,可以写出立即执行的 Class。
1 | let person = new class { |
上面代码中,person
是一个立即执行的类的实例。
静态方法
静态方法就是直接定义在构造函数上的方法,例如 Array.from()
和 Array.of()
,只能通过构造函数 Array 来调用,而不能通过实例进行调用。
在 class 类中,我们通过static
关键字来定义一个静态方法
1 | class MyClass() { |
getter 和 setter
在 Class 内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
1 | class MyClass { |
class 的继承
Class 之间可以通过extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
1 | class Cat extends Pet {} |
上面代码定义了一个Cat
类,该类通过extends
关键字,继承了Pet
类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Pet
类。下面,我们在Cat
内部加上代码。
1 | class Pet { |
上面代码中,constructor
方法中出现了super
关键字,它在这里表示父类的构造函数,用来新建父类的this
对象。
子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类没有自己的this
对象,而是继承父类的this
对象,然后对其进行加工。如果不调用super
方法,子类就得不到this
对象。
子类Cat
的greet
方法覆盖了父类中的greet
方法
迭代器和生成器
处理集合中的每个项是很常见的操作。JavaScript 提供了许多迭代集合的方法,从简单的
for
循环到map()
和filter()
。迭代器和生成器将迭代的概念直接带入核心语言,并提供了一种机制来自定义for...of
循环的行为。
Proxy
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
语法
let p = new Proxy(target, handler);
参数:
target
用
Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。handler
一个对象,其属性是当执行一个操作时定义代理的行为的函数。
Reflect
-
Set
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set 本身是一个构造函数,用来生成 Set 数据结构。
语法
new Set([iterable]);
参数
iterable
如果传递一个可迭代对象,它的所有元素将不重复地被添加到新的 Set 中。如果不指定此参数或其值为
null
,则新的 Set 为空。返回值
一个新的
Set
对象。
值的相等
向 Set 加入值的时候,不会发生类型转换,所以5
和"5"
是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value equality”,它类似于精确相等运算符(===
)。NaN
和undefined
都可以被存储在 Set 中, NaN
之间被视为相同的值(不同于精确相等)。
由于对象为引用数据类型,两个空对象不相等,所以它们被视为两个值。
属性和方法
Set 的实例对象有以下属性:
Set.prototype.constructor
:构造函数,默认就是Set
函数。Set.prototype.size
:返回Set
实例的成员总数。
Set 的实例对象的方法:
add(value)
:向尾部添加某个值,返回 Set 结构本身。delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。has(value)
:返回一个布尔值,表示该值是否为Set
的成员。clear()
:清除所有成员,没有返回值。keys()
:返回键名的遍历器values()
:返回键值的遍历器entries()
:返回键值对的遍历器forEach()
:使用回调函数遍历每个成员
key
方法、value
方法、entries
方法返回的都是遍历器对象(详见Iterator
对象)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以key
方法和value
方法的行为完全一致。
entries
方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。
Set 的实例默认可遍历,它的默认遍历器生成函数就是它的values
方法。Set.prototype[Symbol.iterator] === Set.prototype.values
这意味着,可以省略values
方法,直接用for...of
循环遍历 Set。
forEach
方法,用于对每个成员执行某种操作,没有返回值。
参数是一个处理函数,该函数的参数依次为 键值、键名、集合自身。另外,forEach
方法还可以传入第二个参数,表示绑定的 this 对象。
扩展运算符 (...
) 可用于 Set 结构,两者结合使用,可以用来去除数组重复成员:
1 | let arr = [3, 5, 2, 2, 5, 5] |
WeakSet
语法
1 | new WeakSet([iterable]); |
参数
iterable
如果传入一个可迭代对象作为参数, 则该对象的所有迭代值都会被自动添加进生成的
WeakSet
对象中。null 被认为是 undefined。
描述
WeakSet
对象是一些对象值的集合, 并且其中的每个对象值都只能出现一次。在WeakSet
的集合中是唯一的
它和 Set
对象的区别有两点:
- 与
Set
相比,WeakSet
只能是对象的集合,而不能是任何类型的任意值。 WeakSet
持弱引用:集合中对象的引用为弱引用。 如果没有其他的对WeakSet
中对象的引用,那么这些对象会被当成垃圾回收掉。 这也意味着 WeakSet 中没有存储当前对象的列表。 正因为这样,WeakSet
是不可枚举的。
方法
add(value)
在该
WeakSet
对象中添加一个新元素value
.delete(value)
从该
WeakSet
对象中删除value
这个元素, 之后has(value)
方法便会返回false
.has(value)
返回一个布尔值, 表示给定的值
value
是否存在于这个WeakSet
中.
Map
Map
对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。
描述
键的相等
NaN
是与 NaN
相等的(虽然 NaN !== NaN
),剩下所有其它的值是根据 ===
运算符的结果判断是否相等。
与 Object 的比较
Object
和 Map
类似的是,它们都是一种用来存储键值对的数据类型,因此我们过去一直把对象当成 Map
使用。但在有些场景情况下 Map
会是更好的选择:
Map
默认不包含任何键,只能通过显式插入,而Object
有一个原型,原型链上的键名有可能和你自己设置的键名产生冲突;Map
的键可以是任意值(包括对象、函数和基本类型),而Object
的键只能是String
或者Symbol
;Map
的键是有序的,而Object
的键是无序的;Map
是 iterable 的,所以可以直接被迭代;- …
方法
clear()
移除 Map 对象的所有键/值对 。delete(key)
如果 Map 对象中存在该元素,则移除它并返回 true;否则如果该元素不存在则返回 false。随后调用 has(key) 将返回 false 。entries()
返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的 [key, value] 数组。forEach(callbackFn[, thisArg])
按插入顺序,为 Map 对象里的每一键值对调用一次 callbackFn 函数。如果为 forEach 提供了 thisArg,它将在每次回调中作为 this 值。get(key)
返回键对应的值,如果不存在,则返回 undefined。has(key)
返回一个布尔值,表示 Map 实例是否包含键对应的值。keys()
返回一个新的 Iterator 对象, 它按插入顺序包含了 Map 对象中每个元素的键 。set(key, value)
设置 Map 对象中键的值。返回该 Map 对象。values()
返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的值 。
参考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript
https://es6.ruanyifeng.com/