ES6从入门到放弃
作用域
作用域主要分为以下几种类型:
- 全局作用域(
global / window
); - 函数作用域(
function
); - 块级作用域(
{}
); - 动态作用域(
this
)。
var
在讲解 let
、const
之前,先来了解下 var
。
var 声明一个变量,并可选地将其初始化为一个值。
var
用以声明变量;var
声明的变量,不存在块级作用域,在全局范围内都有效;var
存在变量提升,因此var
定义的变量可以先使用,后声明;
example one:
1 | function fn() { |
example two:
1 | function fn() { |
上面这段代码打印出 undefined
,是因为 var
变量提升,等价于:
1 | function fn() { |
example three:
现在有如下代码,如何只暴露 fn
一个全局变量呢?
1 | var a = 1 |
因为 var a = 1
会产生一个全局变量 a
。
假如我们把代码包裹在一个函数里,代码如下:
1 | function fn2() { |
如果像上面这样,a
虽然是局部变量了,但是呢这个函数有名字 fn2
,也是一个全局变量。
所以需要使用立即执行函数:
1 | (function() { |
但是这样代码太繁琐了,在 ES6
中,可以使用 let
就可以很方便解决此问题!
let
let 语句声明一个块级作用域的本地变量,并且可选的将其初始化为一个值。
let
关键字用来声明变量;let
关键字声明的变量不能重复声明;- 存在块级作用域,只在其声明的块或子块中可用;
- 不存在变量提升,只可以先声明,后使用;
let
声明的变量存在暂时性死区,只要块级作用域中存在let
,那么它所声明的变量就绑定了这个区域,不再受外部的影响;
example one:
1 | { |
example two:
1 | { |
example three:
1 | { |
const
const 关键字用来声明常量,常量是块级作用域,很像使用 let 语句定义的变量。常量的值不能通过重新赋值来改变,并且不能重新声明。
const
在声明时必须赋予初始值,一旦声明,其声明的值就不允许改变,更不允许重复声明;const
用于声明只读的常量;- 存在块级作用域,只在其声明的块或子块中可用;
- 标识符一般为大写;
- 不存在变量提升,只可以先声明,后使用;
const
声明的变量存在暂时性死区,只要块级作用域中存在const
,那么它所声明的变量就绑定了这个区域,不再受外部的影响;
example one:
1 | { |
example two:
1 | const arr = [1, 2, 3] |
注意:对于数组和对象的元素修改,不算做对常量的修改,因此不会报错。
相关题目
Topic One:
下面代码将打印什么?
1 | for (var i = 0; i < 5; i++) {} |
Topic Two:
下面代码将打印什么?
1 | for (var i = 0; i < 5; i++) { |
Topic Three:
点击第三个 li
将打印什么,如何解决此问题?
1 | var liTags = document.querySelectorAll('li') // 假设只有6个li |
参考资料
变量的解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构赋值。
数组的解构赋值
1 | let name = ['zww', 'lq', 'lqzww'] |
对象的解构赋值
1 | let info = { |
字符串的解构赋值
字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。
1 | let str = 'hello' |
字符串的扩展
模板字符串
模板字符串是字符串的增强版写法,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
- 模板字符串中可以出现换行符;
- 可以使用
${xxx}
的形式嵌入变量; - 模板字符串中还能调用函数;
1 | let str = `我也是字符串` |
trimStart()、trimEnd()
它们的行为与 trim()
一致,trimStart()
消除字符串头部的空格,trimEnd()
消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。
除了空格键,这两个方法对字符串头部(或尾部)的 tab
键、换行符等不可见的空白符号也有效。
浏览器还部署了额外的两个方法,trimLeft()
是 trimStart()
的别名,trimRight()
是 trimEnd()
的别名。
1 | let str = ' hello world ' |
数组的扩展
扩展运算符
扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列,对数组进行解包。
1 | let name = ['zww', 'lq', 'lqzww'] |
1 | // 扩展运算符可用于数组的合并 |
1 | // 扩展运算符可用于数组的克隆 - 浅拷贝 |
1 | // 扩展运算符可将伪数组转为真正的数组 |
Array.from()
Array.from()
方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
1 | let arr = { |
1 | let arr = { |
Array.of()
Array.of()
方法用于将一组值,转换为数组。
Array.of()
总是返回参数值组成的数组。如果没有参数,就返回一个空数组。
1 | console.log(Array.of(1, 2, 3)) // (3) [1, 2, 3] |
includes()
该方法表示某个数组是否包含给定的值,返回一个布尔值。
该方法的第二个参数表示搜索的起始位置,默认为 0
。如果第二个参数为负数,则表示倒数的位置。
1 | let arr = ['天龙八部', '英雄联盟', '王者荣耀', '部落冲突'] |
flat()、flatMap()
flat()
flat()
用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。
flat()
默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将 flat()
方法的参数写成一个整数,表示想要拉平的层数,默认为 1
。如果参数为 Infinity
,表示可展开任意深度的嵌套数组。
1 | let arr = [1, 2, [3, 4, [5, 6, [7]]]] |
flatMap()
flatMap()
方法对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map()
),然后对返回值组成的数组执行 flat()
方法。该方法返回一个新数组,不改变原数组。
flatMap()
方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。
1 | let arr = [1, 2, 3] |
函数的扩展
函数参数默认值
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
1 | function add(a, b) { |
1 | function add(a, b = 10) { |
rest参数
ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,用来代替 arguments,rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
1 | function fn(...args) { |
1 | function fn(a, b, ...args) { |
注意:rest 参数必须要放到参数最后。
箭头函数
ES6 允许使用“箭头”(=>)定义函数。
1 | let fn1 = function() {} |
1 | // 简写形式 |
1 | // 箭头函数 this 指向声明时所在作用域下 this 的值 |
1 | // 箭头函数不能作为构造函数实例化 |
1 | // 箭头函数不能使用 arguments |
总结:
- 如果形参只有一个,那么小括号可以省略;
- 函数体如果只有一条语句,则花括号可以省略,函数的返回值为该条语句的执行结果;
- 箭头函数
this
指向声明时所在作用域下this
的值; - 箭头函数不能作为构造函数实例化,也就是说,不可以使用
new
命令,否则会抛出一个错误。 - 不能使用
arguments
,该对象在函数体内不存在。如果要用,可以用rest
参数代替。
数值的扩展
二进制和八进制表示法
ES6
提供了二进制和八进制数值的新的写法,分别用前缀 0b(或0B)和 0o(或0O)表示。
如果要将 0b
和 0o
前缀的字符串数值转为十进制,要使用 Number
方法。
1 | let b = 0b111; |
Number.EPSILON
ES6
在 Number
对象上面,新增一个极小的常量 Number.EPSILON
。它表示 1
与大于 1
的最小浮点数之间的差。
Number.EPSILON
实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
Number.EPSILON
的实质是一个可以接受的最小误差范围。
1 | function equal(a, b) { |
Number.isFinite() 与 Number.isNaN()
Number.isFinite()
用来检查一个数值是否为有限的(finite),即不是 Infinity
。如果参数类型不是数值,Number.isFinite
一律返回 false
。
Number.isNaN()
用来检查一个值是否为 NaN
。如果参数类型不是 NaN
,Number.isNaN
一律返回 false
。
1 | console.log(Number.isFinite(1)) // true |
Number.parseInt() 与 Number.parseFloat()
ES6 将全局方法 parseInt()
和 parseFloat()
,移植到 Number
对象上面,行为完全保持不变。
1 | console.log(Number.parseInt('1.34abc')) // 1 |
Number.isInteger()
Number.isInteger()
用来判断一个数值是否为整数。
1 | console.log(Number.isInteger(123)) // true |
Math.trunc()
Math.trunc
方法用于去除一个数的小数部分,返回整数部分。
1 | console.log(Math.trunc(123)) // 123 |
Math.sign()
Math.sign
方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。
它有如下五种返回值:
- 参数为正数,返回
+1
; - 参数为负数,返回
-1
; - 参数为
0
,返回0
; - 参数为
-0
,返回-0
; - 其他值,返回
NaN
。
1 | console.log(Math.sign(0)) // 0 |
指数运算符
指数运算符(**)用来实现幂运算,功能与 Math.pow
结果相同。
1 | console.log(Math.pow(2, 10)) // 1024 |
注意:指数运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。
对象的扩展
简化对象的写法
ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。
1 | let name = 'zww' |
Object.is()
Object.is()
它是用来比较两个值是否严格相等,返回 true / false
,与严格比较运算符(===)的行为基本一致。
Object.is()
方法如果满足以下条件则两个值相等:
- 都是
undefined
; - 都是
null
; - 都是
true
或false
; - 都是相同长度的字符串且相同字符按相同顺序排列;
- 都是相同对象(意味着每个对象有同一个引用);
- 都是数字且都是
+0
;都是-0
;都是NaN
;或都是非零而且非NaN
且为同一个值;
1 | Object.is('hello','hello') // true |
Object.assign()
Object.assign()
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
Object.assign()
方法的第一个参数是目标对象,后面的参数都是源对象。
1 | let obj1 = { |
注意:
- 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性;
- 如果只有一个参数,
Object.assign()
会直接返回该参数; - 如果该参数不是对象,则会先转成对象,然后返回;
- 由于
undefined
和null
无法转成对象,所以如果它们作为参数,就会报错; Object.assign()
方法实行的是浅拷贝,而不是深拷贝,也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用;
该方法有如下常见用途:
- 为对象添加属性;
- 为对象添加方法;
- 克隆对象;
- 合并多个对象;
- 为属性指定默认值;
Object.setPrototypeOf()、Object.getPrototypeOf()
Object.setPrototypeOf()
:用来设置一个对象的原型对象(prototype),返回参数对象本身。
Object.getPrototypeOf()
:用于读取一个对象的原型对象。
1 | let a = { |
Object.keys()、Object.values()、Object.entries()
Object.keys()
ES5 引入了 Object.keys
方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。
ES2017 引入了跟 Object.keys
配套的 Object.values
和 Object.entries
,作为遍历一个对象的补充手段,供 for...of
循环使用。
1 | var obj = { |
Object.values()
Object.values
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值,它只返回对象自身的可遍历属性。
1 | var obj = { |
Object.entries()
Object.entries()
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。
1 | var obj = { |
Object.entries
方法的另一个用处是,将对象转为真正的 Map
结构。
Object.getOwnPropertyDescriptors()
Object.getOwnPropertyDescriptors()
方法,返回指定对象所有自身属性(非继承属性)的描述对象。
1 | var obj = { |
Object.fromEntries()
Object.fromEntries()
方法是 Object.entries()
的逆操作,用于将一个键值对数组转为对象。
该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map
结构转为对象。
1 | let arr = Object.fromEntries([ |
Symbol
ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
1 | // 通过 Symbol() 创建 |
1 | // 通过 Symbol.for() 创建 |
1 | // 对象添加 Symbol 类型的两种方式 |
注意:
Symbol
的值是唯一的,用来解决命名冲突的问题Symbol
值不能与其他数据进行运算Symbol
定义的对象属性不能使用for…in
循环遍历,但是可以使用Reflect.ownKeys
来获取对象的所有键名
迭代器
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator
接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator
的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令 for...of
循环,Iterator
接口主要供 for...of
消费。
原生具备 Iterator
接口的数据(可用 for of 遍历)的有:Array
、Arguments
、Set
、Map
、String
、TypedArray
、NodeList
。
1 | let arr = [1, 2, 3] |
Iterator
的工作原理如下:
- 创建一个指针对象,指向当前数据结构的起始位置;
- 第一次调用对象的
next
方法,指针自动指向数据结构的第一个成员; - 接下来不断的调用
next
方法,指针一直往后移动,直到指向最后一个成员; - 每调用
next
方法返回一个包含value
和done
属性的对象;
1 | let arr = [1, 2, 3] |
使用迭代器可以自定义遍历数据,例如:
1 | // 遍历 info 里的 like 数组 |
生成器
Generator
函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
1 | function* gen() { |
生成器函数可以传入参数:
1 | function* gen(arg) { |
使用生成器函数可以避免回调地狱:
1 | function one() { |
注意:
*
的位置没有限制,但必须在function
与函数名之间;- 生成器函数返回的结果是迭代器对象,调用迭代器对象的
next
方法可以得到yield
语句后的值; yield
相当于函数的暂停标记,也可认为是函数的分隔符,每调用一次next
方法,执行一段代码;next
方法可以传递实参,作为yield
语句的返回值;
Promise
async、await
Set
ES6 提供了新的数据结构 Set(集合)。它类似于数组,但是成员的值都是唯一的,没有重复的值(自带去重)。它实现了 iterator
接口,所以可以使用扩展运算符、for...of
。
Set
本身是一个构造函数,用来生成 Set
数据结构。
1 | let s = new Set() |
Set
结构的实例具有以下几个属性:
Set.prototype.constructor
:构造函数,默认就是Set
函数;Set.prototype.size
:返回Set
实例的成员总数;
Set
实例的方法分为两大类,分别为操作方法和遍历方法。
操作方法如下:
.add(value)
:用于添加某个值,返回Set
结构本身;.delete(value)
:用于删除某个值,返回的是布尔值,表示是否删除成功;.has(value)
:用于检测该值是否为Set
成员,返回一个布尔值;.clear()
:用于清除Set
所有成员,没有返回值;
1 | let s = new Set([1, 2, 3, 2, 1]) |
遍历方法如下:
.keys()
:返回键名的遍历器;.values()
:返回键值的遍历器;.entries()
:返回键值对的遍历器;.forEach()
:使用回调函数遍历每个成员;
注意:Set 的遍历顺序就是插入顺序。
Map
ES6 提供了 Map
数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object
结构提供了“字符串—值”的对应,Map
结构提供了“值—值”的对应,是一种更完善的 Hash
结构实现。它也实现了 iterator
接口,所以可以使用扩展运算符、for...of
。
1 | let m = new Map() |
Map
结构的实例具有以下属性和操作方法:
.size
:返回Map
结构的成员总数;.set(key, value)
:设置键名key
对应的键值为value
,返回整个Map
结构。如果key
已经有值,则键值会被更新,否则就新生成该键。set 方法返回的是当前的Map
对象,因此可以采用链式写法;.get(key)
:读取key
对应的键值,如果找不到key
,返回undefined
;.has(key)
:表示某个键是否在当前Map
对象之中,返回一个布尔值;.delete(key)
:删除某个键,返回true
。如果删除失败,返回false
;.clear()
:清除所有成员,没有返回值;
1 | let m = new Map() |
class
基本使用
在没有 ES6 class 之前的常规写法:
1 | function Person(name, age) { |
在 ES6 中新增加了类的概念,可以使用 class
关键字声明一个类,之后以这个类来实例化对象。
类抽象了对象的公共部分,它泛指某一大类。
对象特指某一个,通过类实例化一个具体的对象。
1 | class Star { |
注意:
- 类必须使用
new
实例化对象; - 通过
class
关键字创建类,类名首字母一般大写; - 类里面有个
constructor
函数,可以接收传递过来的参数,同时返回实例对象; - 类里面所有函数都不需要写
function
; - 多个函数方法之间不需要用逗号隔开;
set、get
我们先来看看 ES5 中 set
、get
的使用:
1 | var info = { |
下面我们再来看看在 ES6 中 class
里使用 set
、get
:
1 | class Info { |
静态方法
如果在 class
中定义了一个方法,该方法是可以被实例所调用的,那么如果我们不希望实例继承这个方法,只想这个方法被类本身被调用的时候,就需要将方法标记为静态方法,使用 static
关键字来标识一个静态方法,下面是基本的使用方法:
1 | class Point { |
new.target
new
是从构造函数生成实例对象的命令。ES6 为 new
命令引入了一个 new.target
属性,该属性一般用在构造函数之中,返回 new
命令作用于的那个构造函数。如果构造函数不是通过 new
命令或 Reflect.construct()
调用的,new.target
会返回 undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
在普通函数中使用:
1 | function Point() { |
在 class 中使用:
1 | class Point { |
在 class 的继承中使用:
1 | class Parent { |
类的继承
JavaScript 中的类可以继承某个类,其中被继承的类称为父类,而继承父类的被称为子类。
子类可以有自己的函数和构造器,当子类中存在父类相同的方法时,则该方法不会从父类继承,而使用子类的方法。
我们首先来看看在 ES5 中如何实现继承:
1 | function Father(name, age) { |
接下来我们再来看看 ES6 中是如何实现继承的:
1 | class Father { |
Object.getPrototypeOf()
Object.getPrototypeOf()
方法可以用来从子类上获取父类。因此,可以使用这个方法判断,一个类是否继承了另一个类。
1 | class Father {} |
## super关键字
super
关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数。
super
这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
首先我们来看看 super 作为函数使用:
1 | class Father { |
接下来看看 super
作为对象使用:
- 在普通方法中,它指向的是父类的原型对象;
- 在静态方法中,它指向的是父类;
1 | class Father { |
1 | class Father { |
注意:
- 在 ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象;
- 类里面的共有的属性和方法一定要加
this
; this
的指向问题;constructor
里面的this
指向的是创建的实例对象;方法里面的this
指向这个方法的调用者;- 子类的
__proto__
指向父类本身; - 子类的
prototype
属性的__proto__
指向父类的prototype
属性; - 实例的
__proto__
属性的__proto__
指向父类实例的__proto__
;
原生构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。原生构造函数大致如下:
Boolean()
;Number()
;String()
;Array()
;Date()
;Function()
;RegExp()
;Error()
;Object()
;
1 | class MyArray extends Array { |
模块化
模块功能主要由两个命令构成:export
和 import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
export
暴露模块的三种方法:
- 分别暴露;
- 统一暴露;
- 默认暴露;
1 | // 1. 分别暴露 |
import
引入模块的三种方式:
- 通用方式;
- 解构赋值形式;
- 简便形式,只适用于默认暴露;
1 | // 1. 通用方式 |
注意:在引入模块时,要在 script
标签写上 type="module"
。