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 明确规定,如果区块中存在
let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错
总之,在代码块内,使用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)
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象
由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错
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 }
参数变量是默认声明的,所以不能用let或const再次声明
// 参数变量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属性不是在该方法上面,而是该方法的属性的描述对象的get和set属性上面,返回值是方法名前加上get和set
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
// }
目前,有四个操作会忽略enumerable为false的属性
for...in循环:只遍历对象自身的和继承的可枚举的属性。Object.keys():返回对象自身的所有可枚举的属性的键名。JSON.stringify():只串行化对象自身的可枚举的属性。Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
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 语言的数据类型之一,其他数据类型是:undefined、null、布尔值(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...in、for...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 Set和Map数据结构
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"
}
});

浙公网安备 33010602011771号