ES6基础知识

1. ECMAScript 6.0 简介

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言

1.1 ECMAScript 和 JavaScript 的关系

ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。日常场合,这两个词是可以互换的。

2. Babel 转码器

Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行

2.1 安装babel

 npm install --save-dev @babel/core

babel转码案例

// 转码前,使用了箭头函数,Babel 将其转为普通函数
input.map(item => item + 1);

// 转码后
input.map(function (item) {
  return item + 1;
});

2.2 配置文件 .babelrc

Babel 的配置文件是.babelrc,存放在项目的根目录下

使用 Babel 的第一步,就是配置这个文件。

该文件用来设置转码规则和插件,基本格式如下。

{
  "presets": [],
  "plugins": []
}

presets:字段设定转码规则,官方提供以下的规则集,你可以根据需要安装

# 最新转码规则
npm install --save-dev @babel/preset-env

# react 转码规则
npm install --save-dev @babel/preset-react

然后配置 文件

 
{
    "presets": [
      "@babel/env",
      "@babel/preset-react"
    ],
    "plugins": []
  }

2.3 命令行转码 @babel/cli

(1) 安装:

Babel 提供命令行工具@babel/cli,用于命令行转码

 npm install --save-dev @babel/cli
(2) 基本用法:
# 转码文件example.js
$ npx babel example.js

# 转码结果写入一个文件:--out-file 或 -o 参数指定输出文件
$ npx babel example.js --out-file compiled.js
# 或者
$ npx babel example.js -o compiled.js

# 整个目录转码:--out-dir 或 -d 参数指定输出目录
$ npx babel src --out-dir lib
# 或者
$ npx babel src -d lib

# -s 参数生成source map文件
$ npx babel src -d lib -s

2.4 babel-node:es6 REPL执行环境

@babel/node模块的babel-node命令,提供一个支持 ES6 的 REPL 环境。它支持 Node 的 REPL 环境的所有功能,而且可以直接运行 ES6 代码

(1) 安装
 npm install --save-dev @babel/node
(2) 执行babel-node就进入 REPL 环境
npx babel-node
> (x => x * 2)(1)
2


# 也可以直接运行 ES6 脚本*.js 
npx babel-node es6.js

2.5 @babel/register 模块

@babel/register模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js.jsx.es.es6后缀名的文件,就会先用 Babel 进行转码

(1) 安装
 npm install --save-dev @babel/register
(2) 使用 并 执行 node index.js,就可以不用转码了
// index.js
// 使用时,必须首先加载@babel/register
require('@babel/register');
require('./1.js');  // 1.js文件里面都是es6 语法,可以直接转码
input.map(item => item + 1); // 这里的es6 不会被转码

@babel/register只会对require命令加载的文件转码,而不会对当前文件转码。另外,由于它是实时转码,所以只适合在开发环境使用

2.6 Babel浏览器环境

Babel 也可以用于浏览器环境,使用@babel/standalone模块提供的浏览器版本,将其插入网页

<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// Your ES6 code
</script>

网页实时将 ES6 代码转为 ES5,对性能会有影响。生产环境需要加载已经转码完成的脚本

3. let和const

3.1 let命令

ES6 新增了let命令,用来声明变量。

它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效

{
  let a = 10;  // 只在当前代码块内有效
  var b = 1;
}

//   let声明的变量只在它所在的代码块有效。
a // ReferenceError: a is not defined.
b // 1


// for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域
for (let m = 0; m < 5; m++) {
	console.log(m)
}
console.log(m) // ReferenceError: m is not defined

for (var m = 0; m < 5; m++) {
	console.log(m)
}
console.log(m) // m==5

3.2 不存在变量提升

使用let声明的变量,没有预解析,不存在变量提升:

变量提升是指:变量可以在声明之前可以使用,只不过值是 undefined;如果是使用let声明的变量,就不存在变量提升,即一定要先声明再使用,否则就会报错

// var 定义的变量
console.log(foo); // 输出 undefined,不会报错
var foo = 2;

// let 定义的变量
console.log(bar); // 报错 ReferenceError: bar is not defined
let bar = 2;

3.3 暂时性死区:在代码块内,使用let命令声明变量之前,该变量都是不可用的

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响

/*
下面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错
*/
var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错

总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)

// 在let命令声明变量tmp之前,都属于变量tmp的“死区”
if (true) {
  // TDZ开始
  tmp = 'abc'; // ReferenceError
  console.log(tmp); // ReferenceError

  let tmp; // TDZ结束
  console.log(tmp); // undefined

  tmp = 123;
  console.log(tmp); // 123
}

3.4 不允许重复声明

let不允许在相同作用域内,重复声明同一个变量

// 报错
function func() {
  let a = 10;
  var a = 1;  // SyntaxError: Identifier 'a' has already been declared
}

// 报错
function func() {
  let a = 10;
  let a = 1;   // SyntaxError: Identifier 'a' has already been declared
}

3.5 块级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域

ES6 引入了块级作用域,明确允许在块级作用域之中声明函数

3.6 const命令

const声明一个只读的常量。一旦声明,常量的值就不能改变

注意:对数组元素的修改和对对象内部的修改是可以的(数组和对象存的是引用地址)

// 如果是 数组或对象的元素成员的话,是可以修改数据的,不算作是对常量的修改,不会报错
const arr = ['a', 'b'];
arr.push('c')
console.log(arr); // 返回:['a','b','c']

// 但是不能引用赋值 就会报错
arr = [3, 4, 6]
console.log(arr) // 返回: TypeError: Assignment to constant variable.

// 如果我们不想要const常量被修改,可以使用 Object.freeze 来冻结对象
const arr1 = Object.freeze(['a', 'b']);
arr1.push('c')
console.log(arr1); // 返回:Cannot add property 2, object is not extensible

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值

const foo; // 一旦声明变量,必须立即初始化 ,否则就会报错

const foo='hello' // 不会报错了

const的作用域与let命令相同:只在声明所在的块级作用域内有效

const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用

const声明的常量,也与let一样不可重复声明

3.7 let const var

1、 使用 var 声明的变量,其作用域为该语句所在的函数内,且存在变量提升现象

2、 使用 let 声明的变量,其作用域为该语句所在的代码块内,不存在变量提升

3、 使用 const 声明的是常量,在后面出现的代码中不能再修改该常量的值

4. 顶层对象

1、 顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象

2、 ES5 之中,顶层对象的属性与全局变量是等价的

window.a = 1;
a // 1

a = 2;
window.a // 2

3、 ES6 环境中,var命令和function命令声明的全局变量,依旧是顶层对象的属性;

let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩

var a = 1; // a是顶层对象的属性
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

4、 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window

5、 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self

6、 Node 里面,顶层对象是global,但其他环境都不支持

5. 变量的解构赋值

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

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象

由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError

左右两边,结构的格式要保持一致

5.1 数组的解构赋值

  • 从数组中提取值,按照对应位置,对变量赋值
  • 如果解构不成功,变量的值就等于undefined
// 从数组中提取值,按照对应位置,对变量赋值
// 本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值
let [a, b, c] = [2, 3, 4]
console.log(a) // 2
console.log(b) // 3
console.log(c) // 4


let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

let [x, , y] = [1, 2, 3];
x // 1
y // 3

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
  • 不完全解构:即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功
let [x, y] = [1, 2, 3];
x // 1
y // 2

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

  • 如果等号的右边不是数组(或者严格地说,不是可遍历的结构),那么将会报错

    // 报错
    let [foo] = 1;
    let [foo] = false;
    let [foo] = NaN;
    let [foo] = undefined;
    let [foo] = null;
    let [foo] = {};
    
    

5.1.1 数组的默认值:解构赋值允许指定默认值

let [foo = true] = [];
foo // true

let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

  • 注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效

    // 如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined
    let [x = 1] = [undefined];
    x // 1
    
    let [x = 1] = [null];
    x // null
    
    
  • 如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值

    function f() {
      console.log('aaa');
    }
    
    let [x = f()] = [1];
    
    

5.2 对象的解构赋值

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
foo // "aaa"
bar // "bbb"

// 变量必须与属性同名,才能取到正确的值
let { baz } = { foo: 'aaa', bar: 'bbb' };
baz // undefined

// 变量必须与属性同名,才能取到正确的值
let {foo} = {bar: 'baz'};
foo // undefined

如果变量名与属性名不一致,必须写成下面这样:相当于 重命名

// 相当于 重命名
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"

let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'

5.2.1 对象的默认值

对象的解构也可以指定默认值

var {x = 3} = {};
x // 3

var {x, y = 5} = {x: 1};
x // 1
y // 5

var {x: y = 3} = {};
y // 3

var {x: y = 3} = {x: 5};
y // 5

var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

默认值生效的条件是,对象的属性值严格等于undefined

var {x = 3} = {x: undefined};
x // 3

var {x = 3} = {x: null};
x // null

5.3 字符串的解构赋值

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

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值

let {length : len} = 'hello';
len // 5

5.4 数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象

// 数值和布尔值的包装对象都有toString属性,因此变量s都能取到值

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

5.5 函数参数的解构赋值

函数的参数也可以使用解构赋值

// 函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y
// 对于函数内部的代码来说,它们能感受到的参数就是x和y
function add([x, y]){
  return x + y;
}

add([1, 2]); // 3

函数参数的解构也可以使用默认值

// 函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值
function move({x = 0, y = 0} = {}) {
  return [x, y];
}

move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]

5.6 解构赋值的用途

5.6.1 交换变量的值

let x = 1;
let y = 2;

[x, y] = [y, x];

5.6.2 从函数返回多个值

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便

// 返回一个数组
function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象
function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();

5.6.3 函数参数的定义

解构赋值可以方便地将一组参数与变量名对应起来

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
                       

5.6.4 提取 JSON 数据

解构赋值对提取 JSON 对象中的数据,尤其有用

let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;

console.log(id, status, number);
// 42, "OK", [867, 5309]

5.6.5 函数参数的默认值

// 指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';这样的语句
jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
} = {}) {
  // ... do stuff
};

5.6.6 遍历 Map 结构的数据

任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便

const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world

如果只想获取键名,或者只想获取键值,可以写成下面这样

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

5.6.7 输入模块的指定方法

加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰

const { SourceMapConsumer, SourceNode } = require("source-map");

6. 字符串的扩展

6.1 字符的 Unicode 表示法

ES6 加强了对 Unicode 的支持,允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点,这种表示法只限于码点在\u0000~\uFFFF之间的字符

"\u0061"
// "a"

超出这个范围的字符,可以将码点放入大括号,就能正确解读该字符

"\u{20BB7}"
// "𠮷"

"\u{41}\u{42}\u{43}"
// "ABC"

let hello = 123;
hell\u{6F} // 123

'\u{1F680}' === '\uD83D\uDE80'
// true

6.2 字符串的遍历器接口 for...of

ES6 为字符串添加了遍历器接口,使得字符串可以被for...of循环遍历

for (let codePoint of 'foo') {
  console.log(codePoint)
}
// "f"
// "o"
// "o"

6.3 模板字符串

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

// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`

// 如果在模板字符串中需要使用反引号,则前面要用反斜杠转义
let greeting = `\`Yo\` World!`;

如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`);

如果你不想要这个换行,可以使用trim方法消除它

$('#list').html(`
<ul>
  <li>first</li>
  <li>second</li>
</ul>
`.trim());

大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性

let x = 1;
let y = 2;

`${x} + ${y} = ${x + y}`
// "1 + 2 = 3"

`${x} + ${y * 2} = ${x + y * 2}`
// "1 + 4 = 5"

let obj = {x: 1, y: 2};
`${obj.x + obj.y}`
// "3"

function fn() {
  return "Hello World";
}

// 模板字符串之中还能调用函数
`foo ${fn()} bar`
// foo Hello World bar

6.4 includes(), startsWith(), endsWith()

  • includes():返回布尔值,表示是否找到了参数字符串。
  • startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部
  • 以上三个方法都支持第二个参数:表示开始搜索的位置
let s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true


// 设置第二个参数

let s = 'Hello world!';

s.startsWith('world', 6) // true  从第n个位置直到字符串结束
s.endsWith('Hello', 5) // true   前n个字符
s.includes('Hello', 6) // false   从第n个位置直到字符串结束


6.5 repeat()

repeat方法返回一个新字符串,表示将原字符串重复n

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

 // 参数如果是小数,会被取整
'na'.repeat(2.9) // "nana" 

// 如果repeat的参数是负数或者Infinity,会报错
'na'.repeat(Infinity)  // RangeError
'na'.repeat(-1)  // RangeError

// 如果参数是 0 到-1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到-1 之间的小数,取整以后等于-0,repeat视同为 0
'na'.repeat(-0.9) // ""

// 参数NaN等同于 0
'na'.repeat(NaN) // ""

// 如果repeat的参数是字符串,则会先转换成数字
'na'.repeat('na') // ""
'na'.repeat('3') // "nanana"

6.6 padStart(),padEnd()

如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全

// 第一个参数是字符串补全生效的最大长度
// 第二个参数是用来补全的字符串。

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'

'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'


// 1.  如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'

// 2.  如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串
'abc'.padStart(10, '0123456789')
// '0123456abc'

// 3.  如果省略第二个参数,默认使用空格补全长度
'x'.padStart(4) // '   x'
'x'.padEnd(4) // 'x   '


// 4.   padStart()的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串
'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"


// 5.  提示字符串的格式
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"


6.7 trimStart(),trimEnd()

trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格

它们返回的都是新字符串,不会修改原始字符串

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

const s = '  abc  ';

s.trim() // "abc"  // 消除字符串头部和尾部的空格
s.trimStart() // "abc  "  // 消除字符串头部的空格
s.trimEnd() // "  abc"   // 消除尾部的空格

6.8 replaceAll()

只能替换第一个匹配:replace() 返回一个新字符串,不会改变原字符串

替换所有的字符:replaceAll() 返回一个新字符串,不会改变原字符串

// 只可以替换第一个匹配的字符 === replace
'aabbcc'.replace('b', '_')
// 'aa_bcc'   

// 匹配所有的字符 === 正则表达式
'aabbcc'.replace(/b/g, '_')
// 'aa__cc'

// 匹配所有的字符 === replaceAll 
'aabbcc'.replaceAll('b', '_')
// 'aa__cc'

语法:

String.prototype.replaceAll(searchValue, replacement)

1. searchValue是搜索模式,可以是一个字符串,也可以是一个全局的正则表达式(但是必须带有g修饰符)

// 不报错 replace可以不加 g
'aabbcc'.replace(/b/, '_')

// 报错:replaceAll如果是正则的话,必须是带有 g 的修饰符
'aabbcc'.replaceAll(/b/, '_')

2. 第二个参数replacement是一个字符串,表示替换的文本,其中可以使用一些特殊字符串。

`$&`:匹配的子字符串
`$` `:匹配结果前面的文本
`$'`:匹配结果后面的文本
`$n`:匹配成功的第`n`组内容,`n`是从1开始的自然数。这个参数生效的前提是,第一个参数必须是正则表达式
`$$`:指代美元符号`$`

// $& 表示匹配的字符串,即`b`本身
// 所以返回结果与原字符串一致
'abbc'.replaceAll('b', '$&')
// 'abbc'

// $` 表示匹配结果之前的字符串
// 对于第一个`b`,$` 指代`a`
// 对于第二个`b`,$` 指代`ab`
'abbc'.replaceAll('b', '$`')
// 'aaabc'

// $' 表示匹配结果之后的字符串
// 对于第一个`b`,$' 指代`bc`
// 对于第二个`b`,$' 指代`c`
'abbc'.replaceAll('b', `$'`)
// 'abccc'

// $1 表示正则表达式的第一个组匹配,指代`ab`
// $2 表示正则表达式的第二个组匹配,指代`bc`
'abbc'.replaceAll(/(ab)(bc)/g, '$2$1')
// 'bcab'

// $$ 指代 $
'abc'.replaceAll('b', '$$')
// 'a$c'

7. 函数的扩展

7.1 函数参数默认值

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

// 设置函数的默认值
function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello


function Point(x = 0, y = 0) {
  this.x = x;
  this.y = y;
}
const p = new Point();
p // { x: 0, y: 0 }

参数变量是默认声明的,所以不能用letconst再次声明

// 参数变量x是默认声明的,在函数体中,不能用let或const再次声明,否则会报错
function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

7.2 解构赋值和默认值的结合使用

// 对象的解构赋值 默认值
function foo({x, y = 5}) {
  console.log(x, y);
}

foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2

只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值生成;如果函数foo调用时没提供参数,变量x和y就不会生成,从而报错

foo() // TypeError: Cannot read property 'x' of undefined

如果是通过提供函数参数的默认值,就可以避免这种情况。

// 通过提供函数参数的默认值
function foo({x, y = 5} = {}) {
  console.log(x, y);
}
foo() // undefined 5

如果函数fetch的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数

function fetch(url, { body = '', method = 'GET', headers = {} }) {
  console.log(method);
}

// 第二个参数 不可以省略
fetch('http://example.com', {})
// "GET"

fetch('http://example.com')
// 报错


// 如果是设置了 就出现了双重默认值,就可以省略第二个参数了
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
  console.log(method);
}

fetch('http://example.com')
// "GET"

7.3 参数默认值的位置

通常情况下,定义了默认值的参数,应该是函数的尾参数;

如果非尾部的参数设置默认值,实际上这个参数是没法省略的;

// 例一
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错 因为不是在函数的尾部设置的默认值,所以第一个参数是不可以省略的
f(undefined, 1) // [1, 1]

// 例二
function f(x, y = 5, z) {
  return [x, y, z];
}

f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2] // 如果是显式输入undefined,也是可以的

如果传入undefined,将触发该参数等于默认值,null则没有这个效果

function foo(x = 5, y = 6) {
  console.log(x, y);
}

foo(undefined, null)
// 5 null

7.4 函数的 length 属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数:即函数的参数个数减去指定了默认值的参数个数

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

// 数的length属性,不包括 rest 参数
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1

7.5 reset参数(...变量名):用于获取函数的多余参数

ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了

rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中

rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错

function add(...values) {
  // values=[2,5,3]
  let sum = 0;

  for (var val of values) {
    sum += val;
  }

  return sum;
}

add(2, 5, 3) // 10

reset方法设置sort排序, rest 参数代替arguments变量的例子

// arguments变量的写法
// arguments对象不是数组,而是一个类似数组的对象
// 所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();

7.6 严格模式

从 ES5 开始,函数内部可以设定为严格模式

function doSomething(a, b) {
  'use strict';
  // code
}

ES6规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错

// 报错
function doSomething(a, b = a) {
  'use strict';
  // code
}

// 报错
const doSomething = function ({a, b}) {
  'use strict';
  // code
};

// 报错
const doSomething = (...a) => {
  'use strict';
  // code
};

const obj = {
  // 报错
  doSomething({a, b}) {
    'use strict';
    // code
  }
};

7.7 name 属性

函数的name属性,返回该函数的函数名

function foo() {}
foo.name // "foo"

ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"

Function 构造函数返回的函数实例,name属性的值为anonymous

(new Function).name // "anonymous"

bind返回的函数,name属性值会加上bound前缀

function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

7.8 箭头函数 =>

定义

()=>{}

1、 函数体中只有一句代码,且代码的执行结果就是返回值,可以省略大括号

// 普通函数写法
var f = function (v) {
  return v;
};
// 箭头函数写法 如果形参只有一个,可以省略小括号
var f = v => v;


// 普通函数写法
[1,2,3].map(function (x) {
  return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);

// 普通函数写法
var result = values.sort(function (a, b) {
  return a - b;
});
// 箭头函数写法
var result = values.sort((a, b) => a - b);

2、 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

3、 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回

var sum = (num1, num2) => { return num1 + num2; }

4、由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错

// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

5、 箭头函数可以与变量解构结合使用

const full = ({ first, last }) => first + ' ' + last;

// 等同于
function full(person) {
  return person.first + ' ' + person.last;
}

6、 rest 参数与箭头函数结合的例子

const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]


const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]

7.8.1 箭头函数,使用注意点:

  • 箭头函数没有自己的this对象,就是定义时所在的对象,而不是使用时所在的对象
  • 不可以当作构造函数,也就是说,不可以对箭头函数使用new命令,否则会抛出一个错误
  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替
  • 不可以使用yield命令,因此箭头函数不能用作 Generator 函数

7.8.2 this指向

普通函数:内部的this指向函数运行时所在的对象;普通函数的this指向是可变的

箭头函数:它没有自己的this对象,内部的this就是定义时上层作用域中的this

箭头函数内部的this指向是固定的,它始终指向函数声明时所在作用域下的 this

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  // this绑定定义时所在的作用域(即Timer函数)    
  setInterval(() => this.s1++, 1000);
    
  // 普通函数
  // this指向运行时所在的作用域(即全局对象window)  
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// 3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。
// s1: 3
// s2: 0


let obj = {
    name: '小明',
    say: function () {
        // 此处的this 指的是 obj对象
        console.log(this.name)
    }
}
obj.say(); // 返回:小明


let obj1 = {
    name: '小明',
    say: () => {
        // 此处的this 指的是 window 对象
        console.log(this.name)
    }
}
obj1.say();

箭头函数实际上可以让this指向固定化,绑定this使得它不再可变,这种特性很有利于封装回调函数

/*

   代码的init()方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象
 
   如果回调函数是普通函数,那么运行this.doSomething()这一行会报错,因为此时this指向document对象

*/
var handler = {
  id: '123456',

  init: function() {
    document.addEventListener('click',
      event => this.doSomething(event.type), false);
  },

  doSomething: function(type) {
    console.log('Handling ' + type  + ' for ' + this.id);
  }
};

8. 数组的扩展

8.1 扩展运算符(...): 将一个数组转为用逗号分隔的参数序列

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

扩展运算符的主要功能就是:将一个数组,变为参数序列 [1,2,3]=>1,2,3

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

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

// 函数调用
function add(x, y) {
  return x + y;
}
const numbers = [1, 2];
add(...numbers) // 3

注意:只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错

(...[1, 2])
// Uncaught SyntaxError: Unexpected number

console.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected number

console.log(...[1, 2])
// 1 2

8.2 替代函数的applay方法

由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了

// ES5 的写法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);



// ES6的写法
function f(x, y, z) {
  // ...
}
let args = [0, 1, 2];
f(...args);

8.3 应用

8.3.1 复制数组

ES5复制数组

const a1 = [1, 2, 3]
const a2=a1.concat()

console.log(a1)   // [1,2,3]
console.log(a2)   // [1,2,3]

ES6复制数组

const a1 = [1, 2, 3]
const a2=[...a1]

console.log(a1)   // [1,2,3]
console.log(a2)   // [1,2,3]

8.3.2 合并数组

扩展运算符提供了数组合并的新写法,这两种方法都是浅拷贝,使用的时候需要注意

const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

// ES5 的合并数组
arr1.concat(arr2, arr3);  // [ 'a', 'b', 'c', 'd', 'e' ]


// ES6 的合并数组
[...arr1, ...arr2, ...arr3]   // [ 'a', 'b', 'c', 'd', 'e' ]

8.3.3 与解构赋值结合

扩展运算符可以与解构赋值结合起来,用于生成数组

// ES5
a = list[0], rest = list.slice(1)

// ES6
[a, ...rest] = list

const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest  // [2, 3, 4, 5]

const [first, ...rest] = [];
first // undefined
rest  // []

const [first, ...rest] = ["foo"];
first  // "foo"
rest   // []

如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错

const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错

const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错

8.3.4 字符串

扩展运算符还可以将字符串转为真正的数组

[...'hello']
// [ "h", "e", "l", "l", "o" ]

8.3.5 实现了 Iterator 接口的对象

任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组

// querySelectorAll方法返回的是一个NodeList对象。它不是数组,而是一个类似数组的对象
let nodeList = document.querySelectorAll('div');

// 扩展运算符可以将其转为真正的数组,原因就在于NodeList对象实现了 Iterator 
let array = [...nodeList];

8.3.6 Map 和 Set 结构,Generator 函数

扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符

Map 结构

let map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

let arr = [...map.keys()]; // [1, 2, 3]

Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符

const go = function*(){
  yield 1;
  yield 2;
  yield 3;
};

[...go()] // [1, 2, 3]

8.4 Array.from()

Array.from方法用于将两类对象转为真正的数组

​ 1. 类似数组的对象(array-like object)

​ 2. 可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)

let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike);    // ['a', 'b', 'c']

// ES6的写法
let arr2 = Array.from(arrayLike);    // ['a', 'b', 'c']


// 如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组
Array.from([1, 2, 3])  // [1, 2, 3]

常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组

// NodeList对象
// querySelectorAll方法返回的是一个类似数组的对象
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
  return p.textContent.length > 100;
});

// arguments对象
function foo() {
  var args = Array.from(arguments);
  // ...
}

只要是部署了 Iterator 接口的数据结构,Array.from都能将其转为数组

// 符串和 Set 结构都具有 Iterator 接口,因此可以被Array.from转为真正的数组

Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']

let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

8.5 Array.of() : 将一组值,转换为数组

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

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]

Array.of()基本上可以用来替代Array()new Array(),并且不存在由于参数不同而导致的重载

// 会存在由于参数不同而导致的重载
Array() // []
// 参数只有一个正整数时,实际上是指定数组的长度
Array(3) // [, , ,]
// 只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组
Array(3, 11, 8) // [3, 11, 8]

// Array.of() 不存在由于参数不同而导致的重载
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]

8.6 copyWithin()

数组实例的copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。

使用这个方法,会修改当前数组。

Array.prototype.copyWithin(target, start = 0, end = this.length)

它接受三个参数。

  • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
[1, 2, 3, 4, 5].copyWithin(0, 3)  // [4, 5, 3, 4, 5]

// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)  // [4, 2, 3, 4, 5]


// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)  // [4, 2, 3, 4, 5]


// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)  // {0: 1, 3: 1, length: 5}


// 将2号位到数组结束,复制到0号位
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]

// 对于没有部署 TypedArray 的 copyWithin 方法的平台
// 需要采用下面的写法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]

8.7 find() 和 findIndex()

8.7.1 find() :用于找出第一个符合条件的数组成员

  • 数组实例的find方法,用于找出第一个符合条件的数组成员
  • 它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined
  • 参数:当前的值、当前的位置、原数组
[1, 4, -5, 10].find((n) => n < 0)
// -5

[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10

8.7.2 findIndex() :返回第一个符合条件的数组成员的位置

数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

8.7.3 第二个参数:用来绑定回调函数的this对象

find()方法和findIndex()方法,都可以接受第二个参数,用来绑定回调函数的this对象

function f(v){
  return v > this.age;
}

let person = {name: 'John', age: 20};
// find函数接收了第二个参数person对象,回调函数中的this对象指向person对象
[10, 12, 26, 15].find(f, person);    // 26

这两个方法都可以发现NaN,弥补了数组的indexOf方法的不足

[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0

8.8 fill() :使用给定值,填充一个数组

// 可以用于空数组的初始化
new Array(3).fill(7)
// [7, 7, 7]


['a', 'b', 'c'].fill(7)
// [7, 7, 7]

// fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象

let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr  // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]


let arr = new Array(3).fill([]);
arr[0].push(5);
arr  // [[5], [5], [5]]


8.9 entries(),keys() 和 values() : 遍历数组

ES6 提供三个新的方法——entries()keys()values()——用于遍历数组

它们都返回一个遍历器对象,可以用for...of循环进行遍历

keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历


for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1


for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"

8.10 includes() : 查询某个数组是否包含给定的值

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

第二个参数:表示搜索的起始位置,默认为0

如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始

[1, 2, 3].includes(3, 3);  // false
[1, 2, 3].includes(3, -1); // true

8.11 flat(),flatMap()

8.11.1 flat()方法

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

[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

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

// 默认是拉平 1 层
[1, 2, [3, [4, 5]]].flat()  // [1, 2, 3, [4, 5]]


// 指定拉平 2层
[1, 2, [3, [4, 5]]].flat(2)  // [1, 2, 3, 4, 5]


// 如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数
[1, [2, [3]]].flat(Infinity)  // [1, 2, 3]

// 如果原数组有空位,flat()方法会跳过空位
[1, 2, , 4, 5].flat()  // [1, 2, 4, 5]


8.11.2 flatMap()方法

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

flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组;flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this

flatMap()只能展开一层数组

// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

8.12 数组的空位

数组的空位是指数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位

// 返回一个具有 3 个空位的数组
Array(3) // [, , ,]


// 数组的 0 号位置是有值的
0 in [undefined, undefined, undefined] // true

// 数组的 0 号位置没有值
0 in [, , ,] // false

空位不是undefined,一个位置的值等于undefined,依然是有值的

空位是没有任何值

ES6 则是明确将空位转为undefined

// Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位
Array.from(['a',,'b'])  // [ "a", undefined, "b" ]

// 扩展运算符(...)也会将空位转为undefined
[...['a',,'b']]   // [ "a", undefined, "b" ]

// copyWithin()会连空位一起拷贝
[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]

// fill()会将空位视为正常的数组位置
new Array(3).fill('a') // ["a","a","a"]

// for...of循环也会遍历空位
let arr = [, ,];
for (let i of arr) {
  console.log(1);
}
// 1
// 1




// entries()、keys()、values()、find()和findIndex()会将空位处理成undefined
// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]

// keys()
[...[,'a'].keys()] // [0,1]

// values()
[...[,'a'].values()] // [undefined,"a"]

// find()
[,'a'].find(x => true) // undefined

// findIndex()
[,'a'].findIndex(x => true) // 0

9. 对象的扩展

9.1 属性的简洁表示法

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

const foo = 'bar';
const baz = {foo};
// 简写
const baz = {foo: foo};

// 简写
function f(x, y) {
  return {x, y};
}
// 等同于
function f(x, y) {
  return {x: x, y: y};
}

// 方法的简写
const o = {
  method() {
    return "Hello!";
  }
};
// 等同于
const o = {
  method: function() {
    return "Hello!";
  }
};

CommonJS 模块输出一组变量,就非常合适使用简洁写法

let ms = {};

function getItem (key) {
  return key in ms ? ms[key] : null;
}

function setItem (key, value) {
  ms[key] = value;
}

function clear () {
  ms = {};
}

module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
};

9.2 属性名表达式

JavaScript 定义对象的属性,有两种方法

// 方法一:直接用标识符作为属性名
obj.foo = true;

// 方法二:用表达式作为属性名,但是表达式必须放在方括号之内
obj['a' + 'bc'] = 123;

// 但是,如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性
var obj = {
  foo: true,
  abc: 123
};

ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内

let propKey = 'foo';
let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};


let lastWord = 'last word';
const a = {
  'first word': 'hello',
  [lastWord]: 'world'
};
a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"


// 表达式还可以用于定义方法名
let obj = {
  ['h' + 'ello']() {
    return 'hi';
  }
};
obj.hello() // hi

9.3 方法的 name 属性

函数的name属性,返回函数名

const person = {
  sayName() {
    console.log('hello!');
  },
};

person.sayName.name   // "sayName"

如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的getset属性上面,返回值是方法名前加上getset

const obj = {
  get foo() {},
  set foo(x) {}
};

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

9.4 属性的可枚举性和遍历

9.4.1 可枚举性

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
 
//    enumerable: true, 可枚举性,如果该属性为false,就表示某些操作会忽略当前属性

//    configurable: true
//  }

目前,有四个操作会忽略enumerablefalse的属性

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。

9.4.2 属性的遍历

ES6 一共有 5 种方法可以遍历对象的属性

以下的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列
(1)for...in

for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

(2)Object.keys(obj)

Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

(3)Object.getOwnPropertyNames(obj)

Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

(4)Object.getOwnPropertySymbols(obj)

Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。

(5)Reflect.ownKeys(obj)

Reflect.ownKeys返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

9.5 对象的解构赋值

对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }


// 由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined或null,就会报错,因为它们无法转为对象
let { ...z } = null; // 运行时错误
let { ...z } = undefined; // 运行时错误


// 构赋值必须是最后一个参数,否则会报错
let { ...x, y, z } = someObject; // 句法错误
let { x, ...y, ...z } = someObject; // 句法错误

// 扩展运算符的解构赋值,不能复制继承自原型对象的属性
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined
// 对象o3复制了o2,但是只复制了o2自身的属性,没有复制它的原型对象o1的属性

注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本

9.6 对象的扩展运算符 ...

对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中

let z = { a: 3, b: 4 };
let n = { ...z };    // { a: 3, b: 4 }

由于数组是特殊的对象,所以对象的扩展运算符也可以用于数组。

9.7 Super关键字

this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super指向当前对象的原型对象

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错

10 Symbol

Symbol是ES6 引入的一种新的原始数据类型,表示独一无二的值,是一种类似于字符串的数据类型。它属于 JavaScript 语言的数据类型之一,其他数据类型是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、大整数(BigInt)、对象(Object)

Symbol 值通过Symbol()函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突

const sm1 = Symbol();
console.log(sm1); // 输出:Symbol()

Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分

const sm1 = Symbol('foo');
console.log(sm1); // Symbol('foo')

const sm2 = Symbol('bar');
console.log(sm2); // Symbol('bar')

Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的

// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();

s1 === s2 // false


// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 === s2 // false

10.1 作为属性名使用

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性

let mySymbol = Symbol();

// 第一种写法
let a = {}
a[mySymbol] = 'hello'

// 第二种写法
let a = {
    [mySymbol]: 'hello'
}

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'hello' })

console.log(a) // 返回:{ Symbol(30): "hello" }  

// 注意:获取属性值的时候,只能采用 []的 方式,不可以使用 obj.属性名的方式
console.log(a[mySymbol]) // 返回:  hello

10.2 作为常量使用

Symbol 类型可以用于定义一组常量,可以保证这组常量的值都是不相等的

常量使用 Symbol 值最大的好处,就是其他任何值都不可能有相同的值了

const log = {
    INFO: Symbol('info'),
    ERROR: Symbol('error'),
    WARNING: Symbol('warning'),
}

// 返回:Symbol(error) Symbol(error) Symbol(warning)
console.log(log.ERROR, log.ERROR, log.WARNING);

10.3 属性名的遍历

Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回

const log = {
    [Symbol('name')]: 'Tom',
    [Symbol('age')]: 18,
    sex: '男'
}
for (let key in log) {
    // 只会输出:sex 男,不会输出 name和age
    console.log(key, log[key])
}

如果想要获取 指定对象的所有 Symbol 属性名,可以使用 Object.getOwnPropertySymbols(对象),返回数组,里面是当前对象的所有用作属性名的 Symbol 值

const log = {
    [Symbol('name')]: 'Tom',
    [Symbol('age')]: 18,
    sex: '男'
}

// 返回一个数组:[Symbol(name), Symbol(age)] 
console.log(Object.getOwnPropertySymbols(log));

10.4 Reflect.ownKeys():返回所有类型的键名

一个新的 API,Reflect.ownKeys(对象名)方法可以返回所有类型的键名,包括常规键名和 Symbol 键名

const obj = {
    [Symbol('name')]: 'Tom',
    [Symbol('age')]: 18,
    sex: '男'
}

// 返回一个数组:['sex', Symbol(name), Symbol(age)]
console.log(Reflect.ownKeys(obj));

10.5 Symbol.for(),Symbol.keyFor()

Symbol.for() 方法,可以使用同一个 Symbol 值。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true

Symbol.for()为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行

10.5.1 Symbol.for()Symbol的区别:

1、 Symbol.for()Symbol()这两种写法,都会生成新的 Symbol

2、 Symbol.for() 会被登记在全局环境中供搜索,Symbol()不会

3、 Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值

Symbol.for("bar") === Symbol.for("bar")
// true

Symbol("bar") === Symbol("bar")
// false

10.5.2 Symbol.keyFor()

Symbol.keyFor() 方法返回一个已登记的 Symbol 类型值的key

// 创建一个 已经登记的 Symbol 
const obj = Symbol.for('foo');
// 返回key值 : foo 
console.log(Symbol.keyFor(obj));

11 SetMap数据结构

11.1 Set

11.1.1 概述

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值

由于集合实现了 iterator 接口,所以可以使用『扩展运算符...』和『for…of…』进行遍历

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

Set 结构没有键名,只有键值,或者说键名和键值是同一个值

11.1.2 创建

const s = new Set();

// 通过add()方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。
[1, 10, 8, 9, 10, 8].forEach(v => s.add(v))

console.log(s); // 返回:{1,10,8,9 } 没有重复的值

Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化

// 例一
const s = new Set([1, 10, 8, 9, 10, 8]);
console.log([...s]); // 返回:[1,10,8,9 ]  并且没有重复的值

// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5

// 例三
const set = new Set(document.querySelectorAll('div'));
set.size // 56

// 例四:还可以 字符串去除 重复的字符 
console.log([...new Set('ababbc')]) // 返回:['a','b''c']

向 Set 加入值的时候,不会发生类型转换,所以5"5"是两个不同的值

11.1.3 Set实例的属性和方法

属性

1、 Set.prototype.constructor:构造函数,默认就是Set函数

2、 Set.prototype.size:返回Set实例的成员总数(去重后)

方法

以下是4个常用的操作方法

1、 Set.prototype.add(value):添加某个值,返回 Set 结构本身

2、 Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。

3、 Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。

4、 Set.prototype.clear():清除所有成员,没有返回值

let set = new Set();
set.add(1).add(2).add(2)

// 返回 [1,2]
console.log([...set])

以下是4个常用的遍历方法

1、 Set.prototype.keys():返回键名的遍历器

2、 Set.prototype.values():返回键值的遍历器

3、 Set.prototype.entries():返回键值对的遍历器

4、 Set.prototype.forEach():使用回调函数遍历每个成员

Set的遍历顺序就是插入顺序

keys方法、values方法、entries方法返回的都是遍历器对象。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致

let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
    console.log(item);
}
// red
// green
// blue

for (let item of set.values()) {
    console.log(item);
}
// red
// green
// blue


// entries方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等
for (let item of set.entries()) {
    console.log(item);
}
// ['red', 'red']
// ['green', 'green']
// ['blue', 'blue']

注意:我们是可以直接使用 for...of直接遍历 Set 的,省略了 set.values()

let set = new Set(['red', 'green', 'blue']);
for (let x of set) {
    console.log(x);
}
// red
// green
// blue

Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值

let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9

forEach方法的参数就是一个处理函数。该函数的参数与数组的forEach一致,依次为键值、键名、集合本身

forEach方法还可以有第二个参数,表示绑定处理函数内部的this对象

Set其他应用

// 1.   转化为数组
let set = new Set(['red', 'green', 'blue']);
let arr = [...set]; // 或者 使用    let arr = Array.from(set);
// ['red', 'green', 'blue']

// 2.  数组去重
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]

// 3.  数组的map和filter方法可以间接的用于set
let set = new Set([1, 2, 3]);
set = new Set([...set].map(x => x * 2));
// 返回Set结构:{2, 4, 6}

let set = new Set([1, 2, 3, 4, 5]);
set = new Set([...set].filter(x => (x % 2) == 0));
// 返回Set结构:{2, 4}


// 4.  实现 并集、交集、差集
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}

// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}

11.2 Map

11.2.1 概述

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合。但是“键”的范围不限于字符串,各种类 型的值(包括对象)都可以当作键。Map 也实现了iterator 接口,所以可以使用『扩展运算符...』和 『for…of…』进行遍历

11.2.2 创建

const m = new Map();
// 1.  添加数据
m.set('name', '小明');
m.set('age', 18);
console.log(m); // 返回:Map 对象 === {'name' => '小明', 'age' => 18}
// 2. 获取数据
console.log(m.get('name')); // 返回:小明

作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组

const map = new Map([
    ['name', '张三'],
    ['title', 'Author']
]);

console.log(map) {'name' => '张三', 'title' => 'Author'}
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"

11.2.3 Map实例的属性和方法

属性

1、 size 属性:返回 Map 结构的成员总数

const map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2

操作方法

1、 Map.prototype.set(key, value)

set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键

const m = new Map();

m.set('edition', 6)        // 键是字符串
m.set(262, 'standard')     // 键是数值
m.set(undefined, 'nah')    // 键是 undefined

// set方法返回的是当前的Map对象,因此可以采用链式写法
let map = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');

2、 Map.prototype.get(key)

get 方法读取 key 对应的键值,如果找不到 key ,返回 undefined

const m = new Map();

const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 设置值,键是函数

m.get(hello)  // 获取值,Hello ES6!

3、 Map.prototype.has(key)

has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中

const m = new Map();

m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');

m.has('edition')     // true
m.has('years')       // false
m.has(262)           // true
m.has(undefined)     // true

4、 Map.prototype.delete(key)

delete方法删除某个键,返回true。如果删除失败,返回false

const m = new Map();
m.set(undefined, 'nah');
m.has(undefined)     // true

m.delete(undefined)
m.has(undefined)       // false

5、 Map.prototype.clear()

clear方法清除所有成员,没有返回值

let map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
map.clear()
map.size // 0

遍历方法

Map 结构原生提供三个遍历器生成函数和一个遍历方法

Map.prototype.keys():返回键名的遍历器。

Map.prototype.values():返回键值的遍历器。

Map.prototype.entries():返回所有成员的遍历器。

Map.prototype.forEach():遍历 Map 的所有成员

const map = new Map([
    ['name', '小米'],
    ['age', 13],
]);

for (let key of map.keys()) {
    console.log(key);
}
// "name"
// "age"

for (let value of map.values()) {
    console.log(value);
}
// "小米"
// 18

for (let item of map.entries()) {
    console.log(item[0], item[1]);
}
// name 小米
// age 13

// 或者
for (let [key, value] of map.entries()) {
    console.log(key, value);
}
// name 小米
// age 13

// 等同于使用map.entries() -- 推荐
for (let [key, value] of map) {
    console.log(key, value);
}
// name 小米
// age 13

11.2.4 与其他数据结构的互相转换

Map 转为数组

const myMap = new Map()
  .set(true, 7)
  .set({foo: 3}, ['abc']);
// 使用 扩展运算符
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]

数组 转为 Map

将数组传入 Map 构造函数,就可以转为 Map

new Map([
  [true, 7],
  [{foo: 3}, ['abc']]
])

Map 转为对象

如果所有 Map 的键都是字符串,它可以无损地转为对象;如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名

function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }

对象转为 Map

对象转为 Map 可以通过Object.entries()

let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));

Map 转为 JSON

Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON

function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'

另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON

function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}

let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'

JSON 转为 Map

SON 转为 Map,正常情况下,所有键名都是字符串

function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}

12 Class 类

12.1 概述

ES6 引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已

12.2 创建类

ES5创建类

JavaScript 语言中,生成实例对象的传统方法是通过构造函数:

    // 1.  创建构造函数
    function Person(name, age) {
      // 设置属性
      this.name = name;
      this.age = age;
    }
    // 设置方法
    Person.prototype.sing = function () {
      console.log('我会唱歌');
    }

    // 2.  创建类的实例
    var p = new Person('小米', 18);
    console.log(p.name, p.age); // 返回:小米 18
    p.sing();// 返回:我会唱歌

ES6创建类

ES6 通过类 创建对象 如下:

    // 创建 类
    class Person {
      // 设置属性
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
      // 设置方法
      sing() {
        // this关键字 : 代表实例对象  
        console.log(`我叫${this.name},我今年${this.age}岁了,我会唱歌`);
      }
    }
    // 2.  创建类的实例
    var p = new Person('小米', 18);
    console.log(p.name, p.age); // 返回:小米 18
    p.sing();// 返回:我叫小米,我今年18岁了,我会唱歌

1、 构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面

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

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于
Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

因此,在类的实例上面调用方法,其实就是调用原型上的方法

class B {}
const b = new B();

b.constructor === B.prototype.constructor // true

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

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

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

12.3 constructor 方法

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

constructor()方法默认返回实例对象(即this

类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行

class Point {
}

// 等同于
class Point {
  constructor() {}
}

12.4 类的实例

生成类的实例的写法,与 ES5 完全一样,是使用new命令,注意:Class类的实例必须使用 new关键字创建

class Point {
  // ...
}

// 报错
var point = Point(2, 3);

// 正确
var point = new Point(2, 3);

12.5 注意点

12.5.1 严格模式

类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式

12.5.2 不存在提升

类不存在变量提升,这一点与 ES5 完全不同

new Foo(); // ReferenceError
class Foo {}
// Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部

12.5.3 name 属性

由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性

class Point {}
Point.name // "Point"
// name属性总是返回紧跟在class关键字后面的类名

12.5.4 Generator 方法

如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数

class Foo {
  constructor(...args) {
    this.args = args;
  }
  * [Symbol.iterator]() {
    for (let arg of this.args) {
      yield arg;
    }
  }
}

for (let x of new Foo('hello', 'world')) {
  console.log(x);
}
// hello
// world

12.5.5 this 的指向

类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错

12.6 静态方法

12.6.1 概述和定义

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”

class Foo {
    static classMethod() {
        return 'hello';
    }
}

// 直接通过类来调用
Foo.classMethod() // 'hello'

var foo = new Foo();
// 会报错:foo.classMethod is not a function
foo.classMethod()

注意,如果静态方法包含this关键字,这个this指的是类,而不是实例

12.6.2 父类的静态方法,可以被子类继承

class Foo {
  static classMethod() {
    return 'hello';
  }
}

class Bar extends Foo {
}

Bar.classMethod() // 'hello'

12.6.3 静态方法也可以从super对象上调用

    class Foo {
      // 静态方法:classMethod
      static classMethod() {
        return 'hello';
      }
    }

    class Bar extends Foo {
      static classMethod() {
        return super.classMethod() + ', too';
      }
    }

    Bar.classMethod() // "hello, too"

12.7 类的继承 extends

12.7.1 简介

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法

除了私有属性,父类的所有属性和方法,都会被子类继承,其中包括静态方法

    // 父类
    class Point {
      constructor(x, y) {
        this.x = x;
        this.y = y;
      }
      toString() {

      }
    }

    // 子类
    class ColorPoint extends Point {
      constructor(x, y, color) {
        // 调用父类属性
        super(x, y); // 调用父类的constructor(x, y)
        this.color = color;
      }

      toString() {
        // 调用父类的方法
        return this.color + ' ' + super.toString(); // 调用父类的toString()
      }
    }

ES6 规定,子类必须在constructor()方法中调用super(),否则就会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象

如果子类没有定义constructor()方法,这个方法会默认添加,并且里面会调用super()。也就是说,不管有没有显式定义,任何一个子类都有constructor()方法

class ColorPoint extends Point {
}

// 等同于
class ColorPoint extends Point {
  constructor(...args) {
    super(...args);
  }
}

ES5实现类的继承

    // 父级:手机
    function Phone(brand, price) {
      this.brand = brand;
      this.price = price;
    }
    Phone.prototype.call = function () {
      console.log("我可以打电话!");
    }

    // 子级:智能手机
    function SmartPhone(brand, price, color, size) {
      // 继承父级的属性
      Phone.call(this, brand, price);
      // 自定义自己的属性
      this.color = color;
      this.size = size;
    }
    // 设置子级构造函数的原型(继承父级的方法----重点)
    SmartPhone.prototype = new Phone;
    SmartPhone.prototype.constructor = SmartPhone;

    // 声明子类自己的方法
    SmartPhone.prototype.photo = function () {
      console.log("我可以拍照!");
    }
    SmartPhone.prototype.game = function () {
      console.log("我可以玩游戏!");
    }

    const chuizi = new SmartPhone("锤子", 2499, "黑色", "5.5inch");
    console.log(chuizi);
    chuizi.call(); // 我可以打电话!
    chuizi.photo(); // 我可以拍照!
    chuizi.game(); // 我可以玩游戏

ES6实现类的继承

   class Phone {
      constructor(brand, price) {
        this.brand = brand;
        this.price = price;
      }
      call() {
        console.log("我可以打电话!");
      }
    }
    class SmartPhone extends Phone {
      // 构造函数
      constructor(brand, price, color, size) {
        super(brand, price); // 调用父类构造函数
        this.color = color;
        this.size = size;
      }
      photo() {
        console.log("我可以拍照!");
      }
      game() {
        console.log("我可以玩游戏!");
      }
    }
    const chuizi = new SmartPhone("小米", 1999, "黑色", "5.15inch");
    console.log(chuizi);
    chuizi.call();
    chuizi.photo();
    chuizi.game();

12.7.2 Object.getPrototypeOf()

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

class Point { /*...*/ }

class ColorPoint extends Point { /*...*/ }

Object.getPrototypeOf(ColorPoint) === Point
// true

12.7.3 super 关键字

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

1、 super作为函数调用时

super作为函数调用时,代表父类的构造函数

ES6 要求,子类的构造函数必须执行一次super函数

class A {}

class B extends A {
  constructor() {
    // 此处的super(),代表调用的是父类的构造函数  
    super();
  }
}

注意:

1、 super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)

2、 作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错

2 、 super作为对象时

super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类

class A {
  p() {
    return 2;
  }
}

class B extends A {
  constructor() {
    super();
    // 调用父类的普通方法 p(),指向的是 A.prototype(原型对象),所以super.p()就相当于A.prototype.p()。
    console.log(super.p()); // 2
  }
}

let b = new B();
注意:

1、 由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的

class A {
  constructor() {
    this.p = 2;
  }
}

class B extends A {
  get m() {
    // undefined  
    return super.p;
  }
}

let b = new B();
b.m // undefined

如果属性定义在父类的原型对象上,super就可以取到

class A {}
A.prototype.x = 2;

class B extends A {
  constructor() {
    super();
    console.log(super.x) // 2
  }
}

let b = new B();

2、 ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例

class A {
  constructor() {
    this.x = 1;
  }
  print() {
    console.log(this.x);
  }
}

class B extends A {
  constructor() {
    super();
    // 此处的this指向的当前子类实例B  
    this.x = 2;
  }
  m() {
    super.print();
  }
}

let b = new B();
b.m() // 2

// super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)

3、 如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象

class Parent {
  static myMethod(msg) {
    console.log('static', msg);
  }

  myMethod(msg) {
    console.log('instance', msg);
  }
}

class Child extends Parent {
  static myMethod(msg) {
    // `super` 将指向父类  
    super.myMethod(msg);
  }

  myMethod(msg) {
    super.myMethod(msg);
  }
}

Child.myMethod(1); // static 1

var child = new Child();
child.myMethod(2); // instance 2

在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例

12.7.4 类的 prototype 属性和__proto__属性

大多数浏览器的 ES5 实现之中,每一个对象都有__proto__(对象原型)属性,指向对应的构造函数的prototype属性

Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链

(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

13 async函数

13.1 简介

ES2017 标准引入了 async 函数,使得异步操作变得更加方便

async 和 await 两种语法结合可以让异步代码看起来像同步代码一样;

13.2 基本用法

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句

async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name);
  const stockPrice = await getStockPrice(symbol);
  return stockPrice;
}

getStockPriceByName('goog').then(function (result) {
  console.log(result);
});

async 函数有多种使用形式

// 1.  函数声明
async function foo() {}

// 2.  函数表达式
const foo = async function () {};

// 3.  对象的方法
let obj = { async foo() {} };
obj.foo().then(...)

// 4.  Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars');
  }

  async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
  }
}

const storage = new Storage();
storage.getAvatar('jake').then(…);

// 5.  箭头函数
const foo = async () => {};

13.3 语法

13.3.1 返回 Promise 对象

async函数返回一个 Promise 对象。

async函数内部return语句返回的值,会成为then方法回调函数的参数

async function f() {
  return 'hello world';
}

f().then(v => console.log(v))
// "hello world"

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到

async function f() {
  throw new Error('出错了');
}

f().then(
  v => console.log('resolve', v),
  e => console.log('reject', e)
)
//reject Error: 出错了

13.3.2 Promise 对象的状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数

13.3.3 await 命令

await 必须写在 async 函数中;

await 右侧的表达式一般为 promise 对象

await 返回的是 promise 成功的值

await 的 promise 失败了, 就会抛出异常, 需要通过 try...catch 捕获处理;

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。

1、 如果await命令后面不是返回的 Promise 对象,就直接返回对应的值

async function f() {
  // 等同于
  // return 123;
  return await 123;
}

f().then(v => console.log(v))
// 123

2、 如果await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象

class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  then(resolve, reject) {
    const startTime = Date.now();
    setTimeout(
      () => resolve(Date.now() - startTime),
      this.timeout
    );
  }
}

(async () => {
  const sleepTime = await new Sleep(1000);
  console.log(sleepTime);
})();
// 1000

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行

async function f() {
  await Promise.reject('出错了');
  // 下面的await语句是不会执行的,因为第一个await语句状态变成了reject  
  await Promise.resolve('hello world'); 
}

13.3.4 错误处理

如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('出错了');
  });
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))  // 可以捕获异常
// Error:出错了

为了防止出错的方法,可以将其放在try...catch代码块之中

async function main() {
  try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log('Final: ', val3);
  }
  catch (err) {
    console.error(err);
  }
}

14 迭代器

14.1 概述

遍历器(Iterator)就是一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数 据结构只要部署 Iterator 接口,就可以完成遍历操作

14.2 特性

ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费; 原生具备 iterator 接口的数据(可用 for of 遍历)

Array;

Arguments;

Set;

Map;

String;

TypedArray;

NodeList;

14.3 工作原理

1、 创建一个指针对象,指向当前数据结构的起始位置

2、 第一次调用对象的 next 方法,指针自动指向数据结构的第一个成员

3、 接下来不断调用 next 方法,指针一直往后移动,直到指向最后一个成员

4、 每调用 next 方法返回一个包含 value 和 done 属性的对象;

需要自定义遍历数据的时候,要想到迭代器;

    const names = ['小明', '小红', '小米', '晓东'];
    let iterator = names[Symbol.iterator]();

    console.log(iterator.next()); // {value: '小明', done: false}
    console.log(iterator.next()); // {value: '小红', done: false}
    console.log(iterator.next()); // {value: '小米', done: false}
    console.log(iterator.next()); // {value: '晓东', done: false}
    console.log(iterator.next()); // {value: undefined, done: true}

15 可选链操作符 ?.

如果存在则往下走,省略对对象是否传入的层层判断

  function main(config) {
      // 传统写法:判断config里面是否存在相关对象属性
      // const dbHost = config && config.db && config.db.host;

      // 可选链操作符
      const dbHost = config?.db?.host;
      console.log(dbHost);
    }
    main({
      db: {
        host: "192.168.1.100",
        username: "root"
      },
      cache: {
        host: "192.168.1.200",
        username: "admin"
      }
    });
posted @ 2019-01-28 17:00  songxia777  阅读(93)  评论(0编辑  收藏  举报