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"
}
});