作用域

作用域主要分为以下几种类型:

  1. 全局作用域(global / window);
  2. 函数作用域(function);
  3. 块级作用域({});
  4. 动态作用域(this)。

var

在讲解 letconst 之前,先来了解下 var

var 声明一个变量,并可选地将其初始化为一个值。

  1. var 用以声明变量;
  2. var 声明的变量,不存在块级作用域,在全局范围内都有效;
  3. var 存在变量提升,因此 var 定义的变量可以先使用,后声明;

example one:

1
2
3
4
5
function fn() {
var a = 1
console.log(a) // 1
}
fn()

example two:

1
2
3
4
5
6
7
8
function fn() {
if (true) {
console.log(a) // undefined
} else {
var a = 1
}
}
fn()

上面这段代码打印出 undefined,是因为 var 变量提升,等价于:

1
2
3
4
5
6
7
8
9
function fn() {
var a
if (true) {
console.log(a)
} else {
a = 1
}
}
fn()

example three:

现在有如下代码,如何只暴露 fn 一个全局变量呢?

1
2
3
4
var a = 1
window.fn1 = function() {
console.log(a)
}

因为 var a = 1 会产生一个全局变量 a

假如我们把代码包裹在一个函数里,代码如下:

1
2
3
4
5
6
function fn2() {
var a = 1
window.fn1 = function() {
console.log(a)
}
}

如果像上面这样,a 虽然是局部变量了,但是呢这个函数有名字 fn2,也是一个全局变量。

所以需要使用立即执行函数:

1
2
3
4
5
6
(function() {
var a = 1
window.fn1 = function() {
console.log(a)
}
}())

但是这样代码太繁琐了,在 ES6 中,可以使用 let 就可以很方便解决此问题!


let

let 语句声明一个块级作用域的本地变量,并且可选的将其初始化为一个值。

  1. let 关键字用来声明变量;
  2. let 关键字声明的变量不能重复声明;
  3. 存在块级作用域,只在其声明的块或子块中可用;
  4. 不存在变量提升,只可以先声明,后使用;
  5. let 声明的变量存在暂时性死区,只要块级作用域中存在 let,那么它所声明的变量就绑定了这个区域,不再受外部的影响;

example one:

1
2
3
4
5
6
7
8
{
let a = 1
console.log(a) // 1
{
let a = 2
console.log(a) // 2
}
}

example two:

1
2
3
4
5
6
7
8
{
let a = 1
console.log(a) // 1
{
console.log(a) // 报错
let a = 2
}
}

example three:

1
2
3
4
5
{
let a = 1
console.log(a) // 1
let a = 2 // 报错,a 已经声明
}

const

const 关键字用来声明常量,常量是块级作用域,很像使用 let 语句定义的变量。常量的值不能通过重新赋值来改变,并且不能重新声明。

  1. const 在声明时必须赋予初始值,一旦声明,其声明的值就不允许改变,更不允许重复声明;
  2. const 用于声明只读的常量;
  3. 存在块级作用域,只在其声明的块或子块中可用;
  4. 标识符一般为大写;
  5. 不存在变量提升,只可以先声明,后使用;
  6. const 声明的变量存在暂时性死区,只要块级作用域中存在 const,那么它所声明的变量就绑定了这个区域,不再受外部的影响;

example one:

1
2
3
4
5
{
const a = 1
console.log(a) // 1
a = 2 // 报错
}

example two:

1
2
3
4
5
6
7
8
9
10
const arr = [1, 2, 3]
arr[0] = 2
console.log(arr) // [2, 2, 3]

const obj = {
name: 'zww',
age: 18
}
obj.name = 'lq'
console.log(obj) // {name: "lq", age: 18}

注意:对于数组和对象的元素修改,不算做对常量的修改,因此不会报错。


相关题目

Topic One:

下面代码将打印什么?

1
2
for (var i = 0; i < 5; i++) {}
console.log(i)
查看答案
1
2
3
4
// 等价于
var i
for (i = 0; i < 5; i++) {}
console.log(i) // 5

Topic Two:

下面代码将打印什么?

1
2
3
4
5
6
7
for (var i = 0; i < 5; i++) {
function fn() {
console.log(i)
}
button.onclick = fn
}
console.log(i)
查看答案

都将打印 5


Topic Three:

点击第三个 li 将打印什么,如何解决此问题?

1
2
3
4
5
6
var liTags = document.querySelectorAll('li')  // 假设只有6个li
for (var i = 0; i < liTags.length; i++) {
liTags[i].onclick = function() {
console.log(i)
}
}
查看答案

不管点击哪个 li 都将打印出 6

  1. 方法一:

    1
    2
    3
    4
    5
    6
    7
    var liTags = document.querySelectorAll('li')
    for (var i = 0; i < liTags.length; i++) {
    let j = i
    liTags[j].onclick = function() {
    console.log(j)
    }
    }
  2. 方法二:

    1
    2
    3
    4
    5
    6
    7
    8
    var liTags = document.querySelectorAll('li')
    for (var i = 0; i < liTags.length; i++) {
    (function(j) {
    liTags[j].onclick = function() {
    console.log(j)
    }
    })(i)
    }
  3. 方法三:

    1
    2
    3
    4
    5
    6
    var liTags = document.querySelectorAll('li')
    for (let i = 0; i < liTags.length; i++) {
    liTags[i].onclick = function() {
    console.log(i)
    }
    }

参考资料

阮一峰 - let和const命令

MDN - var

MDN - let

MDN - const

知乎 - 我用了两个月的时间才理解let


变量的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构赋值。

数组的解构赋值

1
2
3
let name = ['zww', 'lq', 'lqzww']
let [a, b, c, d] = name
console.log(a, b, c, d); // zww lq lqzww undefined

对象的解构赋值

1
2
3
4
5
6
7
8
9
10
let info = {
name: 'zww',
age: 18,
like: function() {
console.log("game");
}
}
let { name, age, like } = info
console.log(name, age, like); // zww 18 ƒ ()
like() // game

字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

1
2
3
4
5
6
let str = 'hello'
let [a, b, c, d, e, f] = str
console.log(a, b, c, d, e, f) // h e l l o undefined

let { length: len } = str
console.log(len) // 5

字符串的扩展

模板字符串

模板字符串是字符串的增强版写法,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

  1. 模板字符串中可以出现换行符;
  2. 可以使用 ${xxx} 的形式嵌入变量;
  3. 模板字符串中还能调用函数;
1
2
3
4
5
6
7
8
9
10
11
12
13
let str = `我也是字符串`
console.log(str, typeof str); // 我也是字符串 string

let name = `<ul><li>a</li><li>b</li></ul>`

let like = 'game'
let out = `${like}喜欢`
console.log(out); // game喜欢

function fn() {
console.log("fn")
}
`${fn()}` // fn

trimStart()、trimEnd()

它们的行为与 trim() 一致,trimStart() 消除字符串头部的空格trimEnd() 消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。

浏览器还部署了额外的两个方法,trimLeft()trimStart() 的别名,trimRight()trimEnd() 的别名。

1
2
3
4
5
let str = '  hello world  '

console.log(str) // hello world
console.log(str.trimStart()) // hello world
console.log(str.trimEnd()) // hello world

数组的扩展

扩展运算符

扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列,对数组进行解包。

1
2
3
4
5
6
7
let name = ['zww', 'lq', 'lqzww']

function fn() {
console.log(arguments) // Arguments [Array(3), callee: ƒ, Symbol(Symbol.iterator): ƒ]
console.log(...arguments) // (3) ["zww", "lq", "lqzww"]
}
fn(name)
1
2
3
4
5
// 扩展运算符可用于数组的合并
let name = ['zww', 'lq', 'lqzww']
let age = [18, 20]
var info = [...name, ...age]
console.log(info) // (5) ["zww", "lq", "lqzww", 18, 20]
1
2
3
4
// 扩展运算符可用于数组的克隆 - 浅拷贝
let name = ['zww', 'lq', 'lqzww']
let copyName = [...name]
console.log(copyName) // (3) ["zww", "lq", "lqzww"]
1
2
3
4
5
6
7
8
// 扩展运算符可将伪数组转为真正的数组
<div></div>
<div></div>

var div = document.querySelectorAll("div")
var arrDiv = [...div]
console.log(div) // NodeList(2) [div, div]
console.log(arrDiv) // (2) [div, div]

Array.from()

Array.from() 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

1
2
3
4
5
6
7
let arr = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
console.log(Array.from(arr)) // (3) ["a", "b", "c"]
1
2
3
4
5
6
7
8
9
10
11
12
let arr = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
let array = Array.from(arr, function(a, b) {
console.log(a, b)
// a 0
// b 1
// c 2
})

Array.of()

Array.of() 方法用于将一组值,转换为数组。

Array.of() 总是返回参数值组成的数组。如果没有参数,就返回一个空数组。

1
2
console.log(Array.of(1, 2, 3))    // (3) [1, 2, 3]
console.log(Array.of()) // []

includes()

该方法表示某个数组是否包含给定的值,返回一个布尔值。

该方法的第二个参数表示搜索的起始位置,默认为 0。如果第二个参数为负数,则表示倒数的位置。

1
2
3
4
5
6
let arr = ['天龙八部', '英雄联盟', '王者荣耀', '部落冲突']

console.log(arr.includes('天龙八部')) // true
console.log(arr.includes('皇室战争')) // false

console.log(arr.includes('天龙八部', 1)) // false

flat()、flatMap()

flat()

flat() 用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

flat() 默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将 flat() 方法的参数写成一个整数,表示想要拉平的层数,默认为 1。如果参数为 Infinity,表示可展开任意深度的嵌套数组。

1
2
3
4
5
let arr = [1, 2, [3, 4, [5, 6, [7]]]]

console.log(arr.flat()) // (5) [1, 2, 3, 4, Array(3)]
console.log(arr.flat(2)) // (7) [1, 2, 3, 4, 5, 6, Array(1)]
console.log(arr.flat(Infinity)) // (7) [1, 2, 3, 4, 5, 6, 7]

flatMap()

flatMap() 方法对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map()),然后对返回值组成的数组执行 flat() 方法。该方法返回一个新数组,不改变原数组。

flatMap() 方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。

1
2
3
4
5
6
let arr = [1, 2, 3]
let res1 = arr.map(item => [item * 2])
console.log(res1) // (3) [Array(1), Array(1), Array(1)]

let res2 = arr.flatMap(item => [item * 2])
console.log(res2) // (3) [2, 4, 6]

函数的扩展

函数参数默认值

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

1
2
3
4
5
function add(a, b) {
return a + b;
}
console.log(add(1, 2)); // 3
console.log(add(1)); // NaN
1
2
3
4
5
function add(a, b = 10) {
return a + b;
}
console.log(add(1, 2)); // 3
console.log(add(1)); // 11

rest参数

ES6 引入 rest 参数(形式为…变量名),用于获取函数的多余参数,用来代替 arguments,rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

1
2
3
4
function fn(...args) {
console.log(args)
}
fn(1, 2) // (2) [1, 2]
1
2
3
4
function fn(a, b, ...args) {
console.log(args)
}
fn(1, 2, 3, 4, 5) // (3) [3, 4, 5]

注意:rest 参数必须要放到参数最后。


箭头函数

ES6 允许使用“箭头”(=>)定义函数。

1
2
3
let fn1 = function() {}
// 等价于
let fn2 = () => {}
1
2
3
4
5
6
// 简写形式
var f = x => n * n
var f = (x, y) => x + y
var f = (x, y) => {
return x + y
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 箭头函数 this 指向声明时所在作用域下 this 的值
let fn1 = function() {
console.log(this.name);
}
let fn2 = () => {
console.log(this.name);
}

var name = 'zww';
const home = {
name: "home"
}

fn1() // zww
fn2() // zww

fn1.call(home) // home
fn2.call(home) // zww
1
2
3
4
5
6
// 箭头函数不能作为构造函数实例化
let Person = name => {
this.name = name;
}
let person = new Person('zww');
console.log(person); // 报错:Person is not a constructor
1
2
3
4
5
// 箭头函数不能使用 arguments
let fn = () => {
console.log(arguments);
}
fn(1, 2); // 报错:arguments is not defined

总结:

  1. 如果形参只有一个,那么小括号可以省略;
  2. 函数体如果只有一条语句,则花括号可以省略,函数的返回值为该条语句的执行结果;
  3. 箭头函数 this 指向声明时所在作用域下 this 的值;
  4. 箭头函数不能作为构造函数实例化,也就是说,不可以使用 new 命令,否则会抛出一个错误。
  5. 不能使用 arguments,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

数值的扩展

二进制和八进制表示法

ES6 提供了二进制和八进制数值的新的写法,分别用前缀 0b(或0B)和 0o(或0O)表示。

如果要将 0b0o 前缀的字符串数值转为十进制,要使用 Number 方法。

1
2
3
4
5
6
7
8
9
10
11
let b = 0b111;
console.log(b) // 7

let o = 0o234;
console.log(o) // 156

let x = 0xabc;
console.log(x) // 2748

console.log(Number('0b111')) // 7
console.log(Number('0o10')) // 8

Number.EPSILON

ES6Number 对象上面,新增一个极小的常量 Number.EPSILON。它表示 1 与大于 1 的最小浮点数之间的差。

Number.EPSILON 实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

Number.EPSILON 的实质是一个可以接受的最小误差范围。

1
2
3
4
5
6
7
8
9
function equal(a, b) {
if (Math.abs(a - b) < Number.EPSILON) {
return true;
} else {
return false;
}
}
console.log(0.1 + 0.2 === 0.3) // false
console.log(equal(0.1 + 0.2, 0.3)) // true

Number.isFinite() 与 Number.isNaN()

Number.isFinite() 用来检查一个数值是否为有限的(finite),即不是 Infinity。如果参数类型不是数值,Number.isFinite 一律返回 false

Number.isNaN() 用来检查一个值是否为 NaN。如果参数类型不是 NaNNumber.isNaN 一律返回 false

1
2
3
4
5
6
7
8
9
10
console.log(Number.isFinite(1))  // true
console.log(Number.isFinite(1 / 0)) // false
console.log(Number.isFinite(Infinity)) // false
console.log(Number.isFinite(-Infinity)) // false

console.log(Number.isNaN(1)) // false
console.log(Number.isNaN(NaN)) // true
console.log(Number.isNaN(NaN / 0)) // true
console.log(Number.isNaN('true' / 0)) // true
console.log(Number.isNaN('true' / 'true')) // true

Number.parseInt() 与 Number.parseFloat()

ES6 将全局方法 parseInt()parseFloat(),移植到 Number 对象上面,行为完全保持不变。

1
2
console.log(Number.parseInt('1.34abc'))  // 1
console.log(Number.parseFloat('1.34abc')) // 1.34

Number.isInteger()

Number.isInteger() 用来判断一个数值是否为整数。

1
2
console.log(Number.isInteger(123))  // true
console.log(Number.isInteger(1.34)) // false

Math.trunc()

Math.trunc 方法用于去除一个数的小数部分,返回整数部分。

1
2
console.log(Math.trunc(123))  // 123
console.log(Math.trunc(1.34)) // 1

Math.sign()

Math.sign 方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。

它有如下五种返回值:

  1. 参数为正数,返回 +1
  2. 参数为负数,返回 -1
  3. 参数为 0,返回 0
  4. 参数为 -0,返回 -0;
  5. 其他值,返回 NaN
1
2
3
4
5
console.log(Math.sign(0))  // 0
console.log(Math.sign(-0)) // -0
console.log(Math.sign(1)) // 1
console.log(Math.sign(-1)) // -1
console.log(Math.sign(NaN)) // NaN

指数运算符

指数运算符(**)用来实现幂运算,功能与 Math.pow 结果相同。

1
2
3
4
5
console.log(Math.pow(2, 10))  // 1024

console.log(2 ** 10) // 1024

console.log(2 ** 3 ** 2) // 512

注意:指数运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。


对象的扩展

简化对象的写法

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

1
2
3
4
5
6
7
8
9
10
11
12
let name = 'zww'
let change = function() {
console.log('change');
}
let info = {
name,
change,
like() {
console.log('game');
}
}
console.log(info); // {name: "zww", change: ƒ, like: ƒ}

Object.is()

Object.is() 它是用来比较两个值是否严格相等,返回 true / false,与严格比较运算符(===)的行为基本一致。

Object.is() 方法如果满足以下条件则两个值相等:

  1. 都是 undefined
  2. 都是 null
  3. 都是 truefalse
  4. 都是相同长度的字符串且相同字符按相同顺序排列;
  5. 都是相同对象(意味着每个对象有同一个引用);
  6. 都是数字且都是 +0;都是 -0;都是 NaN;或都是非零而且非 NaN 且为同一个值;
1
2
3
4
5
6
7
8
9
10
11
12
Object.is('hello','hello')  // true
Object.is('hello','hi') // false

Object.is([],[]) // false

Object.is(null,null) // true
Object.is(null,undefined) // false

Object.is(0,+0) // true
Object.is(0,-0) // false
Object.is(-0,+0) // false
Object.is(NaN,0/0) // true

Object.assign()

Object.assign() 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

Object.assign() 方法的第一个参数是目标对象,后面的参数都是源对象。

1
2
3
4
5
6
7
8
9
10
11
let obj1 = {
name: 'zww',
age: 18,
like: 'game'
}
let obj2 = {
name: 'lq',
age: 111,
size: 22
}
console.log(Object.assign(obj1, obj2)) // {name: "lq", age: 111, like: "game", size: 22}

注意:

  1. 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性;
  2. 如果只有一个参数,Object.assign() 会直接返回该参数;
  3. 如果该参数不是对象,则会先转成对象,然后返回;
  4. 由于 undefinednull 无法转成对象,所以如果它们作为参数,就会报错;
  5. Object.assign() 方法实行的是浅拷贝,而不是深拷贝,也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用;

该方法有如下常见用途:

  1. 为对象添加属性;
  2. 为对象添加方法;
  3. 克隆对象;
  4. 合并多个对象;
  5. 为属性指定默认值;

Object.setPrototypeOf()、Object.getPrototypeOf()

Object.setPrototypeOf():用来设置一个对象的原型对象(prototype),返回参数对象本身。

Object.getPrototypeOf():用于读取一个对象的原型对象。

1
2
3
4
5
6
7
8
let a = {
name: 'zww'
}
let b = {
age: 18
}
console.log(Object.setPrototypeOf(a, b)) // {name: "zww"}
console.log(Object.getPrototypeOf(a)) // {age: 18}

Object.keys()、Object.values()、Object.entries()

Object.keys()

ES5 引入了 Object.keys 方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

ES2017 引入了跟 Object.keys 配套的 Object.valuesObject.entries,作为遍历一个对象的补充手段,供 for...of 循环使用。

1
2
3
4
5
var obj = {
name: 'zww',
age: 22
};
console.log(Object.keys(obj)) // (2) ["name", "age"]

Object.values()

Object.values 方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值,它只返回对象自身的可遍历属性。

1
2
3
4
5
var obj = {
name: 'zww',
age: 22
};
console.log(Object.values(obj)) // (2) ["zww", 22]

Object.entries()

Object.entries() 方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。

1
2
3
4
5
var obj = {
name: 'zww',
age: 22
};
console.log(Object.entries(obj)) // (2) [Array(2), Array(2)]

Object.entries 方法的另一个用处是,将对象转为真正的 Map 结构。


Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptors() 方法,返回指定对象所有自身属性(非继承属性)的描述对象。

1
2
3
4
5
6
var obj = {
name: 'zww',
age: 22
}

console.log(Object.getOwnPropertyDescriptors(obj)) // {name: {…}, age: {…}}

Object.fromEntries()

Object.fromEntries() 方法是 Object.entries() 的逆操作,用于将一个键值对数组转为对象。

该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。

1
2
3
4
5
6
7
8
9
10
let arr = Object.fromEntries([
['name', 'zww'],
['age', 18]
])
console.log(arr) // {name: "zww", age: 18}

let m = new Map()
m.set('name', 'lq')
let res = Object.fromEntries(m)
console.log(res) // {name: "lq"}

Symbol

ES6 引入了一种新的原始数据类型 Symbol,表示独一无二的值。它是JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

1
2
3
4
5
6
7
8
// 通过 Symbol() 创建
let s = Symbol()
console.log(s, typeof s) // Symbol() "symbol"

let s1 = Symbol('zww')
let s2 = Symbol('zww')
console.log(s1 == s2) // false
console.log(s1 === s2) // false
1
2
3
4
5
6
7
8
// 通过 Symbol.for() 创建
let s = Symbol.for('1')
console.log(s, typeof s) // Symbol(1) "symbol"

let s1 = Symbol.for('zww')
let s2 = Symbol.for('zww')
console.log(s1 == s2) // true
console.log(s1 === s2) // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 对象添加 Symbol 类型的两种方式
// 第一种
let info = {
name: 'zww',
like: function() {}
}

let myInfo = {
like: Symbol()
}

info[myInfo.like] = function() {
console.log('hh')
}
console.log(info) // {name: "zww", like: ƒ, Symbol(): ƒ}

// 第二种
let game = {
name: 'tlbb',
[Symbol('ts')]: function() {},
[Symbol('em')]: function() {}
}
console.log(game) // {name: "tlbb", Symbol(ts): ƒ, Symbol(em): ƒ}

注意:

  1. Symbol 的值是唯一的,用来解决命名冲突的问题
  2. Symbol 值不能与其他数据进行运算
  3. Symbol 定义的对象属性不能使用 for…in 循环遍历,但是可以使用 Reflect.ownKeys 来获取对象的所有键名

阮一峰 - Symbol


迭代器

遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。

原生具备 Iterator 接口的数据(可用 for of 遍历)的有:ArrayArgumentsSetMapStringTypedArrayNodeList

1
2
3
4
5
let arr = [1, 2, 3]

for (let i of arr) {
console.log(i) // 1 2 3
}

Iterator 的工作原理如下:

  1. 创建一个指针对象,指向当前数据结构的起始位置;
  2. 第一次调用对象的 next 方法,指针自动指向数据结构的第一个成员;
  3. 接下来不断的调用 next 方法,指针一直往后移动,直到指向最后一个成员;
  4. 每调用 next 方法返回一个包含 valuedone 属性的对象;
1
2
3
4
5
6
7
8
let arr = [1, 2, 3]

let iterator = arr[Symbol.iterator]();
console.log(iterator) // Array Iterator {}
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}

使用迭代器可以自定义遍历数据,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 遍历 info 里的 like 数组
const info = {
name: "zww",
like: ['tlbb', 'xylm', 'wzry'],
[Symbol.iterator]() {
let index = 0;
let that = this;
return {
next: function() {
if (index < that.like.length) {
const result = {
value: that.like[index],
done: false
};
index++;
return result;
} else {
return {
value: undefined,
done: true
}
}
}
}
}
}

for (let i of info) {
console.log(i); // tlbb xylm wzry
}

阮一峰 - Iterator 和 for…of 循环


生成器

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

1
2
3
4
5
6
7
8
9
function* gen() {
yield 'hi'
yield 'generator'
}
let iterator = gen()
console.log(iterator) // gen {<suspended>}
console.log(iterator.next()) // {value: "hi", done: false}
console.log(iterator.next()) // {value: "generator", done: false}
console.log(iterator.next()) // {value: undefined, done: true}

生成器函数可以传入参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* gen(arg) {
console.log(arg)
let one =
yield 'one'
console.log(one)
let two =
yield 'two'
console.log(two)
}

let iterator = gen('A')
console.log(iterator.next())
console.log(iterator.next('B'))
console.log(iterator.next('CCC'))

使用生成器函数可以避免回调地狱:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function one() {
setTimeout(() => {
let data = 'tlbb'
iterator.next(data);
}, 1000)
}

function two() {
setTimeout(() => {
let data = 'yxlm'
iterator.next(data);
}, 2000)
}

function* gen() {
let t = yield one();
console.log(t)
let y = yield two();
console.log(y)
}

let iterator = gen();
iterator.next();

注意:

  1. * 的位置没有限制,但必须在 function 与函数名之间;
  2. 生成器函数返回的结果是迭代器对象,调用迭代器对象的 next 方法可以得到 yield 语句后的值;
  3. yield 相当于函数的暂停标记,也可认为是函数的分隔符,每调用一次 next 方法,执行一段代码;
  4. next 方法可以传递实参,作为 yield 语句的返回值;

阮一峰 - Generator 函数的语法


Promise

Promise从入门到放弃


async、await

Promise从入门到放弃


Set

ES6 提供了新的数据结构 Set(集合)。它类似于数组,但是成员的值都是唯一的,没有重复的值(自带去重)。它实现了 iterator 接口,所以可以使用扩展运算符、for...of

Set 本身是一个构造函数,用来生成 Set 数据结构。

1
2
let s = new Set()
console.log(s, typeof s) // Set(0) {} "object"

Set 结构的实例具有以下几个属性:

  1. Set.prototype.constructor:构造函数,默认就是 Set 函数;
  2. Set.prototype.size:返回 Set 实例的成员总数;

Set 实例的方法分为两大类,分别为操作方法遍历方法

操作方法如下:

  1. .add(value):用于添加某个值,返回 Set 结构本身;
  2. .delete(value):用于删除某个值,返回的是布尔值,表示是否删除成功;
  3. .has(value):用于检测该值是否为 Set 成员,返回一个布尔值;
  4. .clear():用于清除 Set 所有成员,没有返回值;
1
2
3
4
5
6
7
8
9
10
11
12
13
let s = new Set([1, 2, 3, 2, 1])
console.log(s) // Set(3) {1, 2, 3}
console.log(s.size) // 3

console.log(s.add(4)) // Set(4) {1, 2, 3, 4}

console.log(s.delete(1)) // true
console.log(s) // Set(3) {2, 3, 4}

console.log(s.has(2)) // true

s.clear()
console.log(s) // Set(0) {}

遍历方法如下:

  1. .keys():返回键名的遍历器;
  2. .values():返回键值的遍历器;
  3. .entries():返回键值对的遍历器;
  4. .forEach():使用回调函数遍历每个成员;

注意:Set 的遍历顺序就是插入顺序。

阮一峰 - ECMAScript6入门 - Set


Map

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。它也实现了 iterator 接口,所以可以使用扩展运算符、for...of

1
2
let m = new Map()
console.log(m, typeof m) // Map(0) {} "object"

Map 结构的实例具有以下属性和操作方法:

  1. .size:返回 Map 结构的成员总数;
  2. .set(key, value):设置键名 key 对应的键值为 value,返回整个 Map 结构。如果 key 已经有值,则键值会被更新,否则就新生成该键。set 方法返回的是当前的 Map 对象,因此可以采用链式写法;
  3. .get(key):读取 key 对应的键值,如果找不到 key,返回 undefined
  4. .has(key):表示某个键是否在当前 Map 对象之中,返回一个布尔值;
  5. .delete(key):删除某个键,返回 true。如果删除失败,返回 false
  6. .clear():清除所有成员,没有返回值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let m = new Map()

m.set("name", "zww")
m.set("like", function() {
console.log("game")
})
console.log(m) // Map(2) {"name" => "zww", "like" => ƒ}

console.log(m.size) // 2

console.log(m.get("name")) // zww
console.log(m.get("age")) // undefined

console.log(m.has("name")) // true

console.log(m.delete("like")) // true
console.log(m) // Map(1) {"name" => "zww"}

m.clear()
console.log(m) // Map(0) {}

阮一峰 - ECMAScript6入门 - Map


class

基本使用

在没有 ES6 class 之前的常规写法:

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.sayHi = function(sing) {
console.log(this.name + "唱了" + sing)
}

var zname = new Person('zww', 22)
console.log(zname) // Person {name: "zww", age: 22}
zname.sayHi('啊哈哈') // zww唱了啊哈哈

在 ES6 中新增加了类的概念,可以使用 class 关键字声明一个类,之后以这个类来实例化对象。

类抽象了对象的公共部分,它泛指某一大类。

对象特指某一个,通过类实例化一个具体的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Star {
constructor(name, age) {
this.name = name
this.age = age
}
sayHi(sing) {
console.log(this.name + "唱了" + sing);
}
}
let zname = new Star("zww", 11)
console.log(zname) // Star {name: "zww", age: 11}
zname.sayHi("我爱你") // zww唱了我爱你

console.log(zname.hasOwnProperty('name')) // true
console.log(zname.hasOwnProperty('sayHi')) // false
console.log(zname.__proto__.hasOwnProperty('sayHi')) // true

注意:

  1. 类必须使用 new 实例化对象;
  2. 通过 class 关键字创建类,类名首字母一般大写;
  3. 类里面有个 constructor 函数,可以接收传递过来的参数,同时返回实例对象;
  4. 类里面所有函数都不需要写 function
  5. 多个函数方法之间不需要用逗号隔开;

set、get

我们先来看看 ES5 中 setget 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var info = {
_age: 18,
set age(newValue) {
if (newValue > 18) {
console.log("大于18")
} else {
console.log("小于18")
}
},
get age() {
console.log("get age")
return this._age
}
}
info.age = 10 // 小于18
info.age = 100 // 大于18
info.age // get age

下面我们再来看看在 ES6 中 class 里使用 setget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Info {
constructor(age) {
this._age = age
}
set age(newValue) {
if (newValue > 18) {
console.log("大于18")
} else {
console.log("小于18")
}
}
get age() {
console.log("get age")
return this._age
}
}
let info = new Info(18)
info.age = 10 // 小于18
info.age = 100 // 大于18
info.age // get age

静态方法

如果在 class 中定义了一个方法,该方法是可以被实例所调用的,那么如果我们不希望实例继承这个方法,只想这个方法被类本身被调用的时候,就需要将方法标记为静态方法,使用 static 关键字来标识一个静态方法,下面是基本的使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
getPosition() {
return `(${this.x},${this.y})`
}
static getClassName() {
return Point.name
}
}
let p = new Point(1, 2)
console.log(p.getPosition()) // (1,2)
// console.log(p.getClassName()) // 报错,p.getClassName is not a function
console.log(Point.getClassName()) // Point

new.target

new 是从构造函数生成实例对象的命令。ES6 为 new 命令引入了一个 new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令或 Reflect.construct() 调用的,new.target 会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的。

在普通函数中使用:

1
2
3
4
5
function Point() {
console.log(new.target)
}
let p1 = new Point() // ƒ Point() { window.runnerWindow.proxyConsole.log(new.target) }
let p2 = Point() // undefined

在 class 中使用:

1
2
3
4
5
6
class Point {
constructor() {
console.log(new.target)
}
}
let p = new Point() // class Point { constructor() { window.runnerWindow.proxyConsole.log(new.target) } }

在 class 的继承中使用:

1
2
3
4
5
6
7
8
9
10
11
class Parent {
constructor() {
console.log(new.target)
}
}
class Child extends Parent {
constructor() {
super()
}
}
let c = new Child() // class Child extends Parent { constructor() { super() } }

类的继承

JavaScript 中的类可以继承某个类,其中被继承的类称为父类,而继承父类的被称为子类。

子类可以有自己的函数和构造器,当子类中存在父类相同的方法时,则该方法不会从父类继承,而使用子类的方法。

我们首先来看看在 ES5 中如何实现继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Father(name, age) {
this.name = name
this.age = age
}
Father.prototype.sayHi = function(sing) {
console.log(this.name + '的年龄是' + this.age + ',并且唱了' + sing)
}

function Son() {

}
Son.prototype = new Father('zww', 18)
let son = new Son()
son.sayHi('哈哈') // zww的年龄是18,并且唱了哈哈

接下来我们再来看看 ES6 中是如何实现继承的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Father {
constructor(name, age) {
this.name = name
this.age = age
}
sayHi(sing) {
console.log(this.name + '的年龄是' + this.age + ',并且唱了' + sing)
}
}
class Son extends Father { }
var father = new Father('zww', 18)
console.log(father) // Father {name: "zww", age: 18}

var son = new Son('lq', 22)
console.log(son) // Son {name: "lq", age: 22}
son.sayHi('呵呵') // lq的年龄是22,并且唱了呵呵

console.log(son instanceof Son) // true
console.log(son instanceof Father) // true

Object.getPrototypeOf()

Object.getPrototypeOf() 方法可以用来从子类上获取父类。因此,可以使用这个方法判断,一个类是否继承了另一个类。

1
2
3
4
class Father {}
class Son extends Father {}

console.log(Object.getPrototypeOf(Son) === Father) // true

## super关键字

super 关键字用于访问和调用对象父类上的函数。可以调用父类的构造函数,也可以调用父类的普通函数。

super 这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

首先我们来看看 super 作为函数使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Father {
constructor(name, age) {
this.name = name
this.age = age
}
sayHi() {
console.log('父类函数')
}
}
class Son extends Father {
constructor(name, age, sex) {
super(name, age)
this.sex = sex
}
sonfn() {
super.sayHi()
console.log('子类函数')
}
}
var son = new Son('lq', 22, '女')
console.log(son) // Son {name: "lq", age: 22, sex: "女"}
son.sonfn('丫丫') // 父类函数 子类函数

接下来看看 super 作为对象使用:

  1. 在普通方法中,它指向的是父类的原型对象;
  2. 在静态方法中,它指向的是父类;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Father {
constructor() {
this.type = 'father'
}
getName() {
return this.type
}
}
Father.getType = () => {
return 'Father'
}
class Son extends Father {
constructor() {
super()
console.log('constructor:' + super.getName())
}
getFatherName() {
console.log('getFatherName:' + super.getName())
}
}
var son = new Son() // constructor:father
son.getFatherName() // getFatherName:father
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Father {
constructor() {
this.type = 'father'
}
getName() {
return this.type
}
}
Father.getType = () => {
return 'is Father'
}
class Son extends Father {
constructor() {
super()
console.log('constructor:' + super.getName())
}
getFatherName() {
console.log('getFatherName:' + super.getName())
}
static getFatherType() {
console.log('getFatherType:' + super.getType())
}
}
var son = new Son() // constructor:father
son.getFatherName() // getFatherName:father
Son.getFatherType() // getFatherType:is Father

注意:

  • 在 ES6 中类没有变量提升,所以必须先定义类,才能通过类实例化对象;
  • 类里面的共有的属性和方法一定要加 this
  • this 的指向问题;constructor 里面的 this 指向的是创建的实例对象;方法里面的 this 指向这个方法的调用者;
  • 子类的 __proto__ 指向父类本身;
  • 子类的 prototype 属性的 __proto__ 指向父类的 prototype 属性;
  • 实例的 __proto__ 属性的 __proto__ 指向父类实例的 __proto__

原生构造函数的继承

原生构造函数是指语言内置的构造函数,通常用来生成数据结构。原生构造函数大致如下:

  1. Boolean()
  2. Number()
  3. String()
  4. Array()
  5. Date()
  6. Function()
  7. RegExp()
  8. Error()
  9. Object()
1
2
3
4
5
6
7
8
class MyArray extends Array {
constructor(...args) {
super(...args)
}
}
let arr = new MyArray(4, 5, 6)
console.log(arr)
console.log(arr.join('--'))

模块化

模块功能主要由两个命令构成:exportimportexport 命令用于规定模块的对外接口,import 命令用于输入其他模块提供的功能。

export 暴露模块的三种方法:

  1. 分别暴露;
  2. 统一暴露;
  3. 默认暴露;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 分别暴露
export var name = 'zww';
export function like() {
console.log("like");
}

// 2. 统一暴露
var name = 'zww';

function like() {
console.log("like");
}
export { name, like }

// 3. 默认暴露
export default {
name: 'zww',
like: function() {
console.log("like");
}
}

import 引入模块的三种方式:

  1. 通用方式;
  2. 解构赋值形式;
  3. 简便形式,只适用于默认暴露;
1
2
3
4
5
6
7
8
9
10
// 1. 通用方式
import * as m from './module.js'

// 2. 解构赋值形式
import { name, like } from './module.js'
import { name as myname, like } from './module.js' // 如果重名,可以使用 as 来别名
import { default as m } from './module.js'

// 3. 简便形式
import m from './module.js'

注意:在引入模块时,要在 script 标签写上 type="module"


参考链接

阮一峰 - ECMAScript6入门

尚硅谷 - ES6教程