ES6 —— 从一脸懵逼到灵活运用
本文为笔者学习 ES6 时自用的知识梳理笔记,如有错误,欢迎指出~

var let const

var let const 的比较

声明与赋值

  • var声明的变量是可以重新赋值的,也可以重复声明

  • letconst声明的变量都是不可以重复声明的

    ​ 在不同作用域内可以出现同名变量,但并不相同,只能在各自的作用域中使用

  • 不同的是, let声明的变量是可以重新赋值的,但 const不行

    ​ 注意:用const声明的变量并不是完全不可以改变的

    ​ 如果用const来声明一个对象,虽然无法给这个对象重新赋值,但是我们可以改变对象的属性值(对象是引 用类型变量,只改变对象的属性并不会影响指针指向)

    ​ 如果你也不希望改变属性值的话可以使用Object.freeze()方法

变量作用域

  • varfunction scope 即函数作用域

    在函数中声明的变量只能在函数中使用

    如果在在iffor等语句里定义的变量你只希望在内部使用,var就无法满足,因为它没有在函数里声明,所以会变成一个全局变量,污染全局作用域

  • letconstblock scope 即块级作用域

    一对大括号 { } 所包裹的内容即为一个块级作用域,声明的变量只能在块内使用,在块级作用域外调用则会报错

let 和 const 的使用场景

  1. letconst代替 IIFE

    IIFE 即 立即执行函数 ,应用之一就用来生成一个私有变量

    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
    }
  2. for 循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    for (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
2
3
4
5
6
7
8
9
10
console.log(color);		// 不会报错 而是输出 undefined
var color = 'yellow';

// 因为有了变量提升实际上这段代码是这样的

var color;
console.log(color); // 所以这里会输出 undefined
color = 'yellow';

// 如果将 var 换成 let 则会报 ReferenceError

在 ES6 中, let 也会将变量提升到块级作用域顶部,但你想在块级作用域中变量的声明之前引用的话就会报 ReferenceError, 因为它是在临时性死区中的, const 亦是如此
需要注意的是,因为 const 定义的是一个常量,所以声明的同时必须赋初始值,否则会报错

使用建议

  • 默认使用 const
  • 当变量需要重新赋值时使用 let
  • 尽量不使用 var

Arrow Function 箭头函数

优点:

  1. 简明的语法
  2. 可以隐式返回
  3. 不绑定this

简明的语法

例如我们要用 map 遍历一个数组使其中的数乘以二返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const nums = [1, 3, 0, 5];

const double = nums.map(function (num) {
return num * 2;
});

console.log(double); // [2, 6, 0, 10]

// 改用箭头函数
const double2 = nums.map((num) => {
return num * 2;
});

console.log(double2); // [2, 6, 0, 10]

箭头函数写法:去掉function关键字,加上 =>

如果箭头函数只有一个参数的话,()可以省略,没有参数或者有多个参数则必须使用括号并且参数之间用,隔开

隐式返回

显式返回即return关键字加上返回的内容。

箭头函数中的隐式返回:

​ 去掉return关键字 , 去掉 {} , 将表达式写到一行中

用于我们只想简单返回一些内容时使用,使代码更加简洁

1
2
// like this
const double3 = nums.map((num) => num * 2);

注: 因为箭头函数都是匿名函数,匿名函数在递归或者作为回调函数等场景时非常好用,但如果你只想作为一个简单函数的话我们一般把它赋值给一个变量来使用

this 问题

在使用箭头函数以前我们经常遇到这样的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const Tom = {
name: 'Tom',
hobbies: ['Coding', 'Sleeping', 'Reading'],
showHobbies: function () {
this.hobbies.forEach(function (hobby) {
console.log(this.name + ' likes ' + hobby);
})
}
}

Tom.showHobbies();
/* 输出:
likes Coding
likes Coding
likes Reading
咦,你会发现 Tom 不见了,看来是 this.name 的 this 指向出了问题
*/

这里的showHobbies()是由 对象Tom调用的,所以this指向的是对象Tom,因此 this.hobbies 可以正常取到;

forEach()方法里的回调函数他不是作为对象的方法调用,也没有使用applycall等方法来改变this指向,所以这里的this指向的是 Window 或者说全局(严格模式下为 undefined)

以前我们通常的做法是在这之前var self = this;,然后用self代替this来使用

在 ES6 中我们可以借助箭头函数来代替这种 hack 写法,因为箭头函数没有自己的this,它的this值是继承它的父级作用域的(词法作用域,由上下文确定)

箭头函数不适用的场景

  1. 作为构造函数,向原型对象中添加方法
  2. 当你真的需要this的时候,例如事件绑定
  3. 需要使用arguments对象时

参数默认值

1
2
3
4
5
6
7
8
9
10
11
// 直接定义在函数的形参后面
function multiply (a, b = 1) {
return a * b;
}

multiply(); // NaN
multiply(3); // 3
multiply(3, 5); // 15
// 传入 null 并不会使用默认值
multiply(5, null); // 0

模板字符串

在过去我们要组合 变量 和 字符串 的时候需要不停地用+进行连接,这样既繁琐又容易出错而且不易检查。

有了 ES6 的模板字符串就变得容易多了

模板字符串 允许我们用一对反引号 ` 来定义字符串

${} 里面可以是任意的 JS 表达式,包括对象的属性,甚至是一个函数

1
2
3
4
5
6
const name = 'Tom';
const age = 5;
const text = `${name} is ${age * 5} years old.`;

console.log(text); // "Tom is 25 years old."

New String Methods

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
const id = 'adcd123456x';
const fan = 'I love ES6.'

// .startsWith()
// 检查字符串是否以xx开头,返回布尔值,第二个参数传入开始位置索引,不传默认为0,大小写敏感

id.startsWith('abc'); // true
id.startsWith('123', 5);// true
fan.startsWith('I'); // true
fan.startsWith('i'); // false

// .endsWith()
// 检查字符串是否以xx结尾,返回布尔值,第二个参数传入结尾位置索引,不传默认为最后,大小写敏感

id.endsWith('x'); // true
id.endsWith('X'); // false
fan.endsWith('love', 6); // true

// .includes()
// 检查子字符串是否包含于字符串中,返回布尔值,可以传入第二个参数,指定匹配开始位置索引,大小写敏感

fan.indexOf('ES6') !== -1; // true
fan.includes('ES6'); // true


// .repeat()
// 将字符串重复n次,参数为重复次数( >=0 )

解构

对象解构

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
31
32
33
34
35
36
37
const Tom = {
name: 'Tom Jones',
age: 25,
family: {
mother: 'Norah Jones',
father: 'Richard Jones',
brother: 'Howard Jones'
}
}

// 1. without destructuring

// const name = Tom.name;
// const age = Tom.age;
// ...

// 2. with destructuring

// 这行语句的意思:先声明变量 name 和 age ,然后在 Tom 对象中寻找同名属性,找到把属性值后赋值给变量
const { name, age } = Tom;
console.log(name); // 'Tom Jones'
console.log(age); // 25

// 如果你想要赋值给已经声明的变量 将语句用()包裹起来
// 因为如果不加 () 的话,解析器会把 { } 内的内容解析成一个代码块,而不是解构语法
// let name = '';
// ({ name, age } = Tom);

// 对象解构中还允许对变量进行重命名
// 比如 father 变量名已经被提前占用,下面语法中把 father 重新命名成了 dad
// 如果我们去获取一个对象没有的属性时,则会返回 undefined
// 当然也允许我们传入一个默认值,但只有在当对象该属性的值为 undefined 时才会使用默认值,null false 等不行
const father = "Tom's Dad";
const { father: dad, son, sister = 'no sister' } = Tom.family;
console.log(dad); // 'Richard Jones'
console.log(son); // undefined
console.log(sister); // 'no sister'

数组解构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const numbers = [1, 2, 3, 4, 5]

// 获取相对应位置的值
const [one, two] = numbers
console.log(one, two) // 1 2

// 像这样用逗号隔开可以跳过某个索引位置
const [yi, , san] = numbers
console.log(yi, san) // 1 3

// 如果变量数量多余数组的值的话 多余的会被赋值 undefined ,
// 同样,我们也可以提供默认值, 当值为绝对的(===) undefined 时会使用默认值

// 还有一种写法 ...others 会将剩余的内容保存到一个数组里
const [first, ...others] = numbers
console.log(first, others) // 1 [2, 3, 4, 5]

// ... + 变量名 是 rest参数 , 后面我们再介绍
// 注意它只能出现在数组的最后面,否则会报错

以前我们要交换两个变量的值的时候,经常会定义一个中间变量,有了解构赋值我们就可以方便的进行交换了

1
2
3
4
5
6
7
8
9
10
11
12
13
let a = 10
let b = 20

// 引入中间变量
let temp
temp = a
a = b
b = temp
console.log(a, b) // 20 10

// 解构赋值
[a, b] = [b, a]
console.log(a, b) // 10 20

for of 循环

for of 是 ES6 新增的一种循环方式

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
31
32
33
34
35
36
const fruits = ['apple', 'banana', 'orange', 'mango']

// 我们在遍历一个数组的时候, 常常有这几种方法:
// 1. for 循环 - 这种方式既繁琐可读性又差
for (let i = 0; i < fruits.length; i++) {
console.log(fruits[i])
}

// 2. 数组的 forEach() 方法 - 简化了for循环,而且可以有新的变量名 fruit,但此方法无法中止或跳出循环
fruits.forEach(fruit => {
console.log(fruit)
})

// 3. for in 循环 -
for (let fruit in fruits) {
console.log(fruit) // 0 1 2 3 : for in 循环的值是属性名而不是属性值,所以在遍历数组是返回的是索引值
}

for (let index in fruits) {
console.log(fruits[index]) // apple banana orange mango
}

// 但是还有一个问题,for in 循环遍历的是对象上的所以可枚举属性, 即使你是加在原型对象上
// 比如我们给 fruits 加一个属性,这时候再去遍历输出, 你会发现输出多了个 My favorit fruits
// for in 更适合用来遍历对象,不适合用来遍历数组
fruits.description = 'My favorit fruits'
for (let index in fruits) {
console.log(fruits[index]) // apple banana orange mango My favorit fruits
}

// 为了解决以上几种方法的缺陷 ES6 引入了新的循环方法 for of
// 它解决了 for in 遍历内容为属性名 和 会遍历数组的非数字属性 的问题
// 而且相对 forEach() 方法而言,它还支持循环中止和跳过
for (let fruit of fruits) {
console.log(fruit) // 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) // 18
  • findIndex()方法与find()方法类似,唯一不同就是它返回的是满足条件的元素的索引值

  • some()every()用法类似,只不过返回的是一个布尔值

    ​ 前者表示至少有一个满足,即找到一个满足条件的元素时返回true

    ​ 后者表示每一个都满足,即当所有元素都满足条件时才会返回true,当找到一个不满足条件的元素时就会立即返回false

剩余参数

剩余参数语法允许我们将一个不定数量的参数表示为一个数组

当我们定义一个函数的时候,如果在最后一个参数前面加...前缀,就会将剩余参数存到一个数组中

语法:

1
2
3
function(a, b, ...theArgs) {
// ...
}
1
2
3
4
5
6
7
8
9
10
// 比如我们定义一个函数来计算商品的折后价
// 第一个参数传入的是折扣 后面的参数为商品原价
// 这里我们就可以用到剩余参数啦,将价格存到一个数组中去
function discount(rate, ...prcies) {
// 因为prices是一个数组,所以我们可以直接调用 map 方法
return prices.map((price) => price * rate)
}

const discountPrices = discount(0.8, 100, 150, 1000)
console.log(discountPrices) // [80, 120, 800]

前面我们提到过,剩余参数还以用于变量的解构

1
2
3
4
// 我们定义一个变量来记录玩家的 name id 和 scores
// 利用剩余参数我们就可以方便的将分数都保存到一个数组里面
const player = ['Tom', 123456, 5.6, 7.3, 3,4, 8.9]
const [name, id, ...scores] = player

扩展运算符(…)

扩展运算符用法与剩余参数相反,它用于把可遍历对象的元素扩展为一个新的参数序列

1
2
3
4
// 把一个字符串中的每一个字符存到一个数组中
const name = 'Iverson'
const arr = [...name]
console.log(arr) // ["I", "v", "e", "r", "s", "o", "n"]
1
2
3
4
5
6
7
// 把两个数组进行拼接可以用 concat 方法,但如果我们还要在两个数组的元素中间加入一个元素,就不太方便了
// 这时候扩展运算符就出来拯救我们了
const youngers = ['George', 'John', 'Thomas']
const olders = ['James', 'Adrew', 'Martin']

const members = [...youngers, 'Mary', ...olders]
console.log(members) // ["George", "John", "Thomas", "Mary", "James", "Adrew", "Martin"]

对象字面量的扩展

当我们在声明一个对象的时候,如果属性名和属性值所指向的变量名一致,可以简化书写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const name = 'Tom'
const age = 20
const gender = '男'

const Tom = {
name, // => name: name,
age, // => age: age,
gender // => gender: gender
// 对象的方法也同样可以简写
/* getAge: function () {
alert(`I'm ${this.name}`)
} */
getAge () {
alert(`I'm ${this.name}`)
}
}

ES6 还提供了计算属性,你可以在对象的属性名和属性值处写入 js 语句

1
2
3
4
5
6
7
8
9
10
const keys = ['name', 'age', 'gender']
const values = ['Tom', 20, '男']

const Tom = {
[keys.shift()]: values.shift(),
[keys.shift()]: values.shift(),
[keys.shift()]: values.shift()
}

console.log(Tom) // {name: "Tom", age: 20, gender: "男"}

Promise

以下内容来自于 MDN Web 文档, 查看更多

因为大部分情况下我们只是使用已创建的 Promise 实例对象,所以此处只简单介绍用法,要想了解 Promise 构造函数,点击此处

Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。

本质上,Promise 是一个被某些函数传出的对象,我们附加回调函数(callback)使用它,而不是将回调函数传入那些函数内部。

约定

不同于“老式”的传入回调,在使用 Promise 时,会有以下约定:

  • 本轮 Javascript event loop(事件循环)运行完成 之前,回调函数是不会被调用的。

  • 通过 then() 添加的回调函数总会被调用,即便它是在异步操作完成之后才被添加的函数。

  • 通过多次调用 then(),可以添加多个回调函数,它们会按照插入顺序一个接一个独立执行。

因此,Promise 最直接的好处就是链式调用chaining)。

链式调用

在过去,要想做多重的异步操作,会导致经典的回调地狱:

1
2
3
4
5
6
7
doSomething(function(result) {
doSomethingElse(result, function(newResult) {
doThirdThing(newResult, function(finalResult) {
console.log('Got the final result: ' + finalResult);
}, failureCallback);
}, failureCallback);
}, failureCallback);

通过新的功能方法,我们把回调绑定到被返回的 Promise 上代替以往的做法,形成一个 Promise 链:

1
2
3
4
5
6
7
8
9
10
doSomething().then(function(result) {
return doSomethingElse(result);
})
.then(function(newResult) {
return doThirdThing(newResult);
})
.then(function(finalResult) {
console.log('Got the final result: ' + finalResult);
})
.catch(failureCallback);

then 里的参数是可选的,catch(failureCallback)then(null, failureCallback) 的缩略形式。如下所示,我们也可以用箭头函数来表示:

1
2
3
4
5
6
7
doSomething()
.then(result => doSomethingElse(result))
.then(newResult => doThirdThing(newResult))
.then(finalResult => {
console.log(`Got the final result: ${finalResult}`);
})
.catch(failureCallback);

Symbol

symbol 是 ES6 新加的一种基本数据类型。

Symbol()函数会返回symbol类型的值,但它并不是构造函数,因为它不支持语法:new Symbol()

每个从Symbol()返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符,这是该数据类型仅有的目的。

语法: Symbol([description])

description 为可选参数,字符串类型,是对 symbol 的描述

1
2
3
const sym1 = Symbol()
const sym2 = Symbol('foo')
const sym3 = Symbol('foo')

上面的代码创建了三个新的 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
    5
    export const name = 'square'

    export function funcName() {
    // ...
    }

    你能够导出函数,varletconst, 和类。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 {}

    注意,不能使用 varletconst 用于导出默认值 export default

  • 关于两种导出方式

    你能够在每一个模块中定义多个命名导出,但是只允许有一个默认导出。
    在导出多个值时,命名导出非常有用。在导入期间,必须使用相应对象的相同名称。但是,你可以使用任何名称导入默认导出的模块。

导入模块 import

语法

1
2
3
4
5
import defaultExport from "module-name"
import * as name from "module-name"
import { export } from "module-name"
import { export as alias } from "module-name"
import { export1 , export2 } from "module-name"

defaultExport 导入默认导出时的的引用名

module-name 要导入的模块。通常是包含目标模块的.JavaScript文件的相对或绝对路径名,可以不包括.JavaScript扩展名。

name 导入模块对象整体的别名

export, export1, export2 被导入模块的导出接口的名称 ( 命名导出,导入时需使用相同的名称 )

alias 用于引用指定导入的名称 ( 同导出时,导入时也可以使用 as 进行重命名 )

Class

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

基本语法

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
// 传统方法 通过构造函数定义并生成一个新的对象
function Person (name, age) {
this.name = name
this.age = age
}
// 向原型对象中添加方法
Person.prototype.sayName = function () {
console.log(`My name is ${this.name}`)
}
// 创造实例
const p1 = new Person('Tom', 21)

// ===========================================

// 定义类
class Person {
constructor(x, y) {
this.x = x
this.y = y
}

sayName() {
console.log(`My name is ${this.name}`)
}
}

上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Person,对应 ES6 的Person类的构造方法。

Person类除了构造方法,还定义了一个sayName方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

ES6 的类,完全可以看作构造函数的另一种写法。

1
2
3
4
5
6
class Person {
// ...
}

typeof Person // "function"
Person === Person.prototype.constructor // true

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。在类的实例上面调用方法,其实就是调用原型上的方法。

由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。

1
2
3
4
5
6
7
8
9
10
class Person {
constructor(){
// ...
}
}

Object.assign(Person.prototype, {
sayName(){},
sayAge(){}
})

另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。

支持计算属性:类的属性名,可以采用表达式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let methodName = 'greet'

class MyClass {
constructor(){
// ...
}

[methodName] () {
console.log('Hello')
}
}

const class1 = new MyClass()
class1.greet() // "Hello"

constructor 方法

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

1
2
3
4
5
6
7
8
class Foo {
constructor() {
return Object.create(null);
}
}

new Foo() instanceof Foo
// false

类的实例对象

生成类的实例对象的写法,与 ES5 完全一样,也是使用new命令。如果忘记加上new,像函数那样调用Class,将会报错。

与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//定义类
class Person {

constructor(name, age) {
this.name = name
this.age = age
}

sayName() {
console.log(`My name is ${this.name}`)
}

}

var person = new Person('Tom', 21);

person.sayName() // My name is Tom

person.hasOwnProperty('name') // true
person.hasOwnProperty('age') // true
person.hasOwnProperty('sayName') // false
person.__proto__.hasOwnProperty('sayName') // true

类的所有实例共享一个原型对象。

1
2
3
4
5
var p1 = new Person('Tom',21);
var p2 = new Person('Jerry',20);

p1.__proto__ === p2.__proto__
//true

上面代码中,p1p2都是 Point 的实例,它们的原型都是 Point.prototype,所以__proto__属性是相等的。

这也意味着,可以通过实例的__proto__属性为 Class 添加方法。

不存在变量提升

Class 不存在变量提升(hoist),这一点与 ES5 完全不同。

Class 表达式

与函数一样,类也可以使用表达式的形式定义。

1
2
3
4
5
const MyClass = class Me {
getClassName() {
return Me.name
}
}

上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是MeMe只在 Class 的内部代码可用,指代当前类。

1
2
3
let inst = new MyClass()
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined

上面代码表示,Me只在 Class 内部有定义。

如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。

1
const MyClass = class { /* ... */ }

采用 Class 表达式,可以写出立即执行的 Class。

1
2
3
4
5
6
7
8
9
10
11
let person = new class {
constructor(name) {
this.name = name
}

sayName() {
console.log(this.name)
}
}('张三')

person.sayName() // "张三"

上面代码中,person是一个立即执行的类的实例。

静态方法

静态方法就是直接定义在构造函数上的方法,例如 Array.from()Array.of(),只能通过构造函数 Array 来调用,而不能通过实例进行调用。

在 class 类中,我们通过static关键字来定义一个静态方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyClass() {
constructor() {
// ...
}

static func(){
console.log('I am a static method')
}
}

const class1 = new MyClass()

MyClass.func() // "I am a static method"
class1.func() // error

getter 和 setter

在 Class 内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass {
constructor() {
// ...
}
get prop() {
return 'getter'
}
set prop(value) {
console.log('setter: '+value)
}
}

const inst = new MyClass()

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

class 的继承

Class 之间可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

1
class Cat extends Pet {}

上面代码定义了一个Cat类,该类通过extends关键字,继承了Pet类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Pet类。下面,我们在Cat内部加上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Pet {
constructor(name, age) {
this.name = name
this.age = age
}

greet() {
console.log('Hello')
}
}

class Cat extends Pet {
constructor(name, age, food) {
super(name, age);
this.food = 'fish'
}

greet() {
console.log('Meow~')
}
}

上面代码中,constructor方法中出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

子类Catgreet方法覆盖了父类中的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”,它类似于精确相等运算符(===)。NaNundefined都可以被存储在 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
2
3
let arr = [3, 5, 2, 2, 5, 5]
let unique = [...new Set(arr)]
// [3, 5, 2]

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 的比较

ObjectMap 类似的是,它们都是一种用来存储键值对的数据类型,因此我们过去一直把对象当成 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/