1、let 和 const 命令
1、let 命令
基本用法
let 用于变量声明,类似于var,但其声明的变量只在let命令所在的代码块内有效
{
var a = 10;
let b = 5;
}
console.log(a); // 10
console.log(b); // b is not defined;
for循环的计数器
for (let i = 0; i < 10; i ++) {
// todo
}
console.log(i); // i is not defined;
由此可见,i只在for循环体内有效,循环外引用报错
如下代码,如果使用var ,输出的会是10;
var a = [];
for(var i = 0; i < 10; i ++) {
a[i] = function() {
console.log(i);
}
}
a[6](); // 10; var定义的变量为全局变量,console.log(i)即为全局的i,最后一次循环完后i的值为10;
但如果使用let定义,输出的会是6;
var a = [];
for(let i = 0; i < 10; i ++) {
a[i] = function() {
console.log(i);
}
}
a[6](); // 6 let定义的i,每次循环都只在本轮循环中生效
另外,for循环有个特别之处,设置循环变量的部分是一个父级作用域,而循环体内的部分是一个单独的子作用域
for (let i = 0; i < 3; i ++) {
let i = 'abc';
console.log(i);
}
// 输出3遍 abc, 说明循环变量i和循环体内i在不同的作用域,有各自单独的作用域
不存在变量提升
var 定义变量会存在变量提升
var i = 10;
function fn() {
console.log(i);
var i = 5;
};
fn(); // undefined; 存在变量提升 => var i; console.log(i); i = 5;
let 命令改变了这种语法,let声明的变量一定要在声明后使用,否则报错
let i = 10;
function fn() {
console.log(i);
let i = 5;
};
fn(); // 直接报错
暂时性死区
只要块级作用域存在let命令,它所声明的变量就绑定这个区域,不再受外部影响;
var a = 123;
function fn() {
a = 456;
let a;
}
fn(); // 直接报错,块级作用域中let声明变量之前进行赋值导致报错
总之,在let声明变量之前,该变量都是不可用的,这在语法上,称为“暂时性死区(TDZ)”;
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
不允许重复声明
let不允许在同一作用域内,重复声明变量
function fn() {
let a = 10;
var a = 5; // 报错
}
function fn() {
let a = 10;
let a = 5; // 报错
}
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错,声明变量不在同一作用域内
2、块级作用域
为什么需要块级作用域?
ES5只有全局作用域和函数作用域,没有块级作用域,带来许多不合理的场景
第一种场景,内层变量可能会覆盖外层变量
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined 原因在于变量提升,内层的变量覆盖了外层的变量
第二种场景,用来计数的循环变量泄漏为全局变量
var s = 'hello';
for(var i = 0; i < s.length; i ++) {
console.log(s[i]);
}
console.log(i); // 5
ES6的块级作用域
let实际上为Javascript提供了块级作用域
function fn () {
let a = 10;
if (true) {
let a = 5;
}
console.log(i);// 5 外层代码块不受内层代码块影响,如果用var 声明,则会输出10;
}
内层作用域可以定义外层作用域的同名变量。
{{{{
let insane = 'Hello World';
{let insane = 'Hello World'}
}}}};
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
3、const 命令
基本用法
const 声明一个只读的常量,一旦声明,不能改变
const PI = 3.1415;
console.log(PI); // 3.1415
PI = 4; // 报错
const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。
const foo;
// SyntaxError: Missing initializer in const declaration
const 的作用域与let一样,只在定义变量所在的作用域内有效
if (true) {
const a = 7;
}
console.log(a); // 报错
const 声明的变量同样不存在变量提升,存在暂时性死区,只能声明后使用,不可重复声明;
本质
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {};
foo.prop = 'abc'; // 为其添加属性,成功
foo = {}; // 将foo指向另外一个对象,报错
另一个例子
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
如果真的想把对象冻结,可以使用Object.freeze()方法
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
4、顶层对象的属性
顶层对象,在浏览器环境指的是window
对象,在 Node 指的是global
对象。ES5 之中,顶层对象的属性与全局变量是等价的。顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。
window.a = 1;
a // 1
a = 2;
window.a // 2
ES6 为了改变这一点,一方面规定,为了保持兼容性,var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
2、变量的解构赋值
1、数组的解构赋值
基本用法
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
以前,为变量赋值,只能直接指定值
let a = 1;
let b = 2;
let c = 3;
ES6允许写成下边这样
let [a, b, c] = [1, 2, 3]
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,等式左边就会被赋予相应的值
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 如果解构不成功,值就为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 = true] = [];
foo; // true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
注意⚠️:ES6内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined时,默认值才会生效
let [x = 1] = [undefined] // x = 1
let [x = 1] = [null] // null, 因为null 不严格等于 undefined
如果默认值是一个表达式,那么这个表达式是惰性求值的,只有在用到的时候,才会求值
function fn() {
console.log('aaa');
}
let [a = fn()] = [1] // a = 1,因为 a 能取到值,所以fn()不会求值
默认值可以引用解构赋值的其他变量,但该变量必须已经声明
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined 当时y还没有声明
2、对象的解构赋值
基本用法
对象的解构与数组有个很重要的不同。数组的元素是按照次序排列的,元素的取值是由他的位置决定;而对象的属性没有次序,变量与属性必须同名,才能取到正确的值
let {foo, bar} = {bar: 'bbb', foo: 'aaa'};
foo; // aaa
bar; // bbb
let {baz} = {bar: 'bbb', foo: 'aaa'};
baz; // undefined
对象的解构赋值,可以很方便的把现有的方法,赋值到某个变量
let {sin, cos, log} = Math;
const {log} = console;
log('hello'); // hello
如果变量名与属性名不一致
let {foo: baz} = {foo: 'aaa', bar: 'bbb'}
baz; // aaa
这实际说明,对象的解构赋值是下面形式的简写
let {foo: foo, bar: bar} = {foo: 'aaa', bar: 'bbb'};
// 也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
下面是赋值嵌套的例子
let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
obj // {prop:123}
arr // [true]
注意,对象的解构赋值可以取到继承的属性
const obj1 = {};
const obj2 = {foo: 'aaa'};
Object.setPrototypeOf(obj1, obj2);
const {foo} = obj1;
foo; // aaa
默认值
默认值生效的条件是,对象的属性值严格等于undefined
。
var {x: y = 3} = {};
y // 3
var {x: y = 3} = {x: 5};
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
注意点
1、如果要将一个已经声明的变量用于解构赋值,要非常小心
// 错误的写法
let a;
{a} = {a: 'hhh'}; // Javascript会将{a}认为是一个代码块,只有不将大括号写在行首,才会避免错误
// 正确的写法
let a;
({a} = {a: 'hhh'})
2、解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
({} = [true, false]);
({} = 'abc');
({} = []);
3、由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3
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
4、数值和布尔值的解构赋值
解构赋值时,如果等号右边是数值或布尔值,都会先转化为对象
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
//数值和布尔值的包装对象都有toString属性,因此变量s都能取到值
解构赋值的规则是,只要等号右边的值不为对象或数组,都先转换为对象。由于undefined和null不能转换为对象,所以无法解析赋值
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
5、函数参数的解构赋值
函数的参数也可以进行解构赋值
function fn([a, b]) {
return a + b;
}
fn([1,2]); // 3
函数参数的解构也可设置默认值
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]
注意,下面的写法会得到不一样的结果。
// 此代码为函数move的参数指定默认值,而不是为变量x和y指定默认值,所以会得到与前一种写法不同的结果。
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
undefined
就会触发函数参数的默认值。
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
6、圆括号问题
不能使用圆括号的情况
1、变量声明语句
// 全部报错
let [(a)] = [1];
let {x: (c)} = {};
let ({x: c}) = {};
let {(x: c)} = {};
let {(x): c} = {};
let { o: ({ p: p }) } = { o: { p: 2 } };
2、函数参数
// 报错
function f([(z)]) { return z; }
// 报错
function f([z,(x)]) { return x; }
3、赋值语句的格式
// 全部报错
({ p: a }) = { p: 42 };
([a]) = [5];
可以使用圆括号的情况
只有一种,赋值语句的非模式部分,可以使用圆括号
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
7、用途 ⭐️
1、交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
2、从函数返回多个值
// 返回一个数组
function fn () {
return [1, 2, 3]
}
let [x, y, z] = fn()
// 返回一个对象
function fn () {
return {
foo: 'abc',
bar: 'efd'
}
}
let {foo, bar} = fn();
3、函数参数的定义
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
4、提取json数据
let jsonData = {
id: 888,
str: 'hhh',
data: [123,456]
}
let {id, str:name, data:number} = jsonData
console.log(id, name, number) // 888, hhh, [123,456]
5、函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
6、遍历map结构
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) {
console.log(key)
}
// 只获取值
for (let [,value] of map) {
console.log(value)
}
7、输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");
3、字符串模版及新增方法
1、字符串模版
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量
// 普通字符串
let a = `I am a China`;
// 多行字符串
let b = `In JavaScript this is
not legal.`
// 字符串嵌入变量
let name = 'Jason';
let age = 18;
let str = `他的名字叫${name}, 年龄是${age}岁`;
console.log(str); // 他的名字叫Jason, 年龄是18岁
2、字符串新增方法
字符串查找
ES5 只有indexOf()方法,用来判断一个字符串是否包含在另一个字符串中
ES6 又提供了3种方法:
includes(): 返回布尔值,表示是否找到了参数字符串;
startWith(): 返回布尔值,表示参数字符串是否在原字符串的开头;
endsWith(): 返回布尔值,表示参数字符串是否在原字符串的结尾;
let str = 'apple banana pear';
// 传统方法 indexOf 返回索引 没找到返回-1
console.log(str.indexOf('banana')) // 6
console.log(str.indexOf('orange')) // -1
// includes 返回true/false
console.log(str.includes('apple')) // true
console.log(str.includes('orange')) // false
// startWith
let str = 'https://study.163.com/course/courseLearn.htm?courseId=1005211046';
let str1 = 'http://www.baidu.com/';
console.log(str.startsWith('https')) // true
console.log(str1.startsWith('https')) // false
// endWith
let imageUrl = 'hhh.png';
let imageUrl1 = 'hhh.jpg';
console.log(imageUrl.endsWith('png')) // true
console.log(imageUrl1.endsWith('png')) // false
这三个方法都支持第二个参数,表示开始搜索的位置。
let s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 6) // false
使用第二个参数n时,endsWith
与其他两个不同,它针对前n个字符,而其他两个针对从第n个到字符串结束的字符。
重复字符串repeat
该方法返回一个新的字符串,表示将原字符串重复n次
'x'.repeat(3) // "xxx"
参数如果是小数,会被取整
'na'.repeat(2.9) // "nana"
参数是负数或者Infinity
,会报错
'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError
参数NaN等同于0
'na'.repeat(NaN) // ""
参数是字符串,会先转换成数字
'na'.repeat('3') //nanana
填充字符串 padStart、padEnd
字符串自动补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。
'x'.padStart(5, 'ab'); //ababx
'x'.padEnd(3, 'ab'); // xab
如果原字符串大于或等于最大长度,则字符串补全不生效,返回原字符串
`xxx`.padStart(2, 'ab'); // xxx
如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位置的补全字符串
'abc'.padStart(10, '0123456789')
// '0123456abc'
如果省略第二个参数,默认用空格补全长度
'abc'.padStart(6); // 'abc '
padStart
常见用途
// 为数值补全指定位数
'1'.padStart(10, '0') // "0000000001"
// 提示字符串格式
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
如果不确定填充字符串的长度
let a = 'apple';
let str = 'xxx';
console.log(a.padStart(a.length+str.length, str))
console.log(a.padEnd(a.length+str.length, str))
去除空格方法 trimStart、trimEnd
trimStart
和trimEnd
行为与trim()一致,去除字符串空格。trimStart
消除字符串头部的空格,trimEnd
消除字符串尾部的空格。他们返回的都是新字符串
const s = ' abc ';
s.trim() // "abc"
s.trimStart() // "abc "
s.trimEnd() // " abc"
除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。
4、函数的扩展
1、函数参数的默认值
基本用法
ES6之前,不能为函数的参数直接定义默认值,只能采用变通的方法
function log(x,y) {
y = y || 'world'
console.log(x, y)
}
log('hello') // hello
log('hello', 'china') // hello china
log('hello', '') // hello world
ES6允许为函数的参数定义默认值
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
函数的参数默认声明,不能再使用let或const再次声明
function fn (a = 18) {
// let a = 29; // 报错 Identifier 'a' has already been declared,因为函数的参数默认已经定义了。在函数内部不能再使用let或const声明
console.log(a)
}
fn()
使用参数默认值时,函数不能有同名参数
function fn(x,x,y=1) {
console.log(x,y); // 报错 SyntaxError: Duplicate parameter name not allowed in this context
}
另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
与解构赋值默认值结合使用
参数默认值可以与解构赋值默认值结合使用
function fn({x = 0, y = 0}) {
console.log(x, y)
}
fn({x: 1, y: 2}); // 1, 2
fn({x: 1}); // 1 0
fn({}); // 0 0
fn(); // TypeError: Cannot read property 'x' of undefined
// 如果没有提供参数,函数foo的参数默认为一个空对象
function fn({x = 0, y = 0} = {}) {
console.log(x, y)
}
fn(); // 0 0
参数默认值的位置
通常情况下,定义参数默认值的位置,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]
函数的length属性
指定了默认值之后,函数的length属性,将返回没有指定参数默认值的参数个数,即指定默认值后,length属性将失真
(function(a) {}).length // 1
(function(a = 1) {}).length // 0
因为length
属性的含义是,该函数预期传入的参数个数。rest参数也不会记入length
属性
(function(...args) {}).length // 0
如果设置了参数默认值的不是尾参数,那么也不会记入length
属性
(function(a = 1, b, c) {}).length // 0
作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2 形成一个单独的作用域,x在函数内已经定义,所以不会再指向外部全局的x
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1 x本身没有定义,所以指向外部全局的x,不会指向函数内部的局部变量x,如果此时全局没有定义x,则会报错
应用
利用参数默认值,可以指定某个参数不得省略,如果省略就抛出一个错误
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
另外,可以将参数设置为undefined,说明参数是可以省略的
function fn(optional = undefined){
...
}
2、rest参数
ES6 引入了rest参数(…变量名),用于获取函数的多余参数,这样就不需要arguments对象了。rest参数搭配的变量是一个数组,该变量多余的参数放入数组中
function add(...values) {
let sum = 0;
for(var val of values) {
sum += val
}
return sum;
}
add(2,3,5); // 10
rest参数既可以展开数组,也可以重置数组
function fn (...x) {
console.log(x)
}
fn(1,8,0); // [1, 8, 0]
function fn1 (a, b, c) {
console.log(a, b, c)
}
fn1(...[2,3,5]); // 2,3,5
注意,rest参数之后不能有其他参数,即只能是最后一个参数,否则会报错
function fn2 (a, b, ...c) {
console.log(a, b); // 1 2
console.log(c); // [3, 4, 5]
}
fn2(1,2,3,4,5)
3、严格模式
ES2016规定,只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显示设置为严格模式,否则会报错
// 报错
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
}
};
4、name属性
函数的name属性,返回该函数的函数名。如果将一个匿名函数赋给一个变量,ES5的name
会返回一个空字符串,而ES6会返回实际的函数名
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"
5、箭头函数
基本用法
ES6允许使用箭头(=>)定义函数
let x = (a, b) => (a+b)
console.log(x(5,9)) // 14
箭头函数的一个用处是简化回调函数
// 正常函数写法
[1,2,3].map(function(x) {
return x*x;
})
// 箭头函数写法
[1,2,3].map(x => x*x)
另一个用处是
// 正常函数写法
var result = values.sort(function(x,y) {
return x - y
})
// 箭头函数写法
var result = values.sort((x, y) => (x - y))
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]]
使用注意点
1、函数体内的this
对象,就是定义时所在的对象,而不是调用时所在的对象;
2、不可以当构造函数,也就是说,不可以使用new
命令,否则会抛出一个错误;
3、不可以使用arguments
对象,该对象在函数体内不存在,如果要用,可以使用rest
参数代替;
4、不可以使用yield
命令,因此箭头函数不能用作 Generator 函数。
上面四点中,第一点尤其注意⚠️,this
对象的指向是可变的,但是在箭头函数中,它是固定的
var id = 123;
function fn() {
setTimeout(() => {
console.log(this.id)
}, 1000)
}
fn.call({id: 10}); // 10 this指向函数定义时所在的对象 即{id: 10}
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3 绑定定义时所在的作用域,即Timer函数,所以会执行3次
// s2: 0 指向全局对象
this
指向的固定化,并不是因为箭头函数内部有绑定this
的机制,实际原因是箭头函数根本没有自己的this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。
除了this
,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments
、super
、new.target
。
function foo() {
setTimeout(() => {
console.log('args:', arguments); // 此arguments实际上是箭头函数foo的arguments变量
}, 100);
}
foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
另外,由于箭头函数没有自己的this
,所以当然也就不能用call()
、apply()
、bind()
这些方法去改变this
的指向。
(function() {
return [
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });
// ['outer']
不适用场合
第一个场合是定义对象的方法,且该方法中包含this
let cat = {
lives: 9,
jumps: ()=> {
this.lives --;
}
}
// NAN 因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域
第二个场合是需要动态this
的时候,也不应使用箭头函数
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
// button的监听函数是一个箭头函数,导致里面的this就是全局对象
嵌套的箭头函数
箭头函数内部,还可以再使用箭头函数
6、尾调用
什么是尾调用
尾调用就是指一个函数的最后一步是调用另一个函数
function f(x) {
return g(x);
}
以下3种情况中,都不属于尾调用
// 情况1
function fn(x) {
let a = g(x);
return a; // 调用函数之后,还有赋值操作,不属于尾调用
}
// 情况2
function fn(x) {
return g(x) + 1; // 调用后还有操作,不属于尾调用
}
// 情况3
function fn(x) {
g(x); // 等同于 g(x); return undefined;
}
尾调用不一定在函数尾部,只要在最后一步调用即可
function fn(x) {
if (x > 0) {
return a(x)
}
return b(x)
}
// 函数 a 和 b都属于尾调用,都是函数fn 的最后一步操作
尾调用优化
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
上面代码中,如果函数g
不是尾调用,函数f
就需要保存内部变量m
和n
的值、g
的调用位置等信息。但由于调用g
之后,函数f
就结束了,所以执行到最后一步,完全可以删除f(x)
的调用帧,只保留g(3)
的调用帧。
这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
上面代码是一个阶乘函数,计算n
的阶乘,最多需要保存n
个调用记录,复杂度 O(n)
如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
严格模式
ES6的尾调用优化只在严格模式下开启,正常模式是无效的
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
func.arguments
:返回调用时函数的参数。func.caller
:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
function restricted() {
'use strict';
restricted.caller; // 报错
restricted.arguments; // 报错
}
restricted();
7、函数参数的尾逗号
ES2017允许函数的最后一个参数有尾逗号
function clownsEverywhere(
param1,
param2,
) { /* ... */ }
clownsEverywhere(
'foo',
'bar',
);
8、Function.prototype.toString()
toString()
方法返回函数代码本身,以前会省略注释和空格
function /* foo comment */ foo () {}
foo.toString()
// function foo() {}
修改后的toString()
方法,明确要求返回一模一样的原始代码。
function /* foo comment */ foo () {}
foo.toString()
// "function /* foo comment */ foo () {}"
9、catch命令的参数省略
JavaScript 语言的try...catch
结构,以前明确要求catch
命令后面必须跟参数,接受try
代码块抛出的错误对象。
try {
// ...
} catch (err) {
// 处理错误
}
ES2019 做出了改变,允许catch
语句省略参数。
try {
// ...
} catch {
// ...
}
5、数组的循环
1、forEach()
代替普通for循环,接收两个参数 (循环回调函数,this的指向)
arr.forEach(function(val, index, arr) {
console.log(val, index, arr);
//apple 0 (3) ["apple", "orange", "banana"]
//forEach.html:13 orange 1 (3) ["apple", "orange", "banana"]
//forEach.html:13 banana 2 (3) ["apple", "orange", "banana"]
console.log(this) // window 此时的this指向window
})
arr.forEach(function(val, index, arr) {
console.log(val, index, arr);
//apple 0 (3) ["apple", "orange", "banana"]
//forEach.html:13 orange 1 (3) ["apple", "orange", "banana"]
//forEach.html:13 banana 2 (3) ["apple", "orange", "banana"]
console.log(this) // Number {123} 此时的this指向传入的 123
}, 123)
// 箭头函数 forEach
arr.forEach((val, index, arr) => {
console.log(this, val, index, arr)
// this 指window,加参数不变,因为箭头函数this指向函数定义所在的对象 为windows
}, 123)
2、arr.map()
正常情况下,需要配合return
使用,返回一个新数组,如果不加return
,相当于forEach
主要用于整理后台返回数据
let arr = [
{ title: 'aaa', read: 10, hot: true },
{ title: 'bbb', read: 11, hot: true },
{ title: 'ccc', read: 12, hot: false },
{ title: 'ddd', read: 13, hot: true }
]
let newArr = arr.map((val, index, arr) => {
let json = {}
json.t = `^_^${val.title}`
json.read = val.read + 100
json.hot = val.hot == true ? '真棒' : '笨蛋'
return json;
})
console.log(newArr) // ["aaa", "bbb", "ccc", "ddd"]
3、arr.filter()
用于过滤某些不符合条件的元素
let arr = [
{ title: 'aaa', read: 10, hot: true },
{ title: 'bbb', read: 11, hot: true },
{ title: 'ccc', read: 12, hot: false },
{ title: 'ddd', read: 13, hot: true }
]
let newArr = arr.filter((val, index, arr) => {
return val.hot;
})
console.log(newArr) // 返回 arr 中为true的对象
4、arr.some()
类似查找,数组中只要某一个元素符合条件,返回true
let arr = ['apple', 'orange', 'banana']
let b = arr.some((val, index, arr) => {
return val == 'banana'
})
console.log(b) // true
5、arr.every()
数组里必须每个元素都符合条件才会返回true
let arr = [1, 3, 5, 7, 9]
let a = arr.every((val, index, arr) => {
return val % 2 == 1;
})
console.log(a) // true
6、arr.reduce() 和 arr.reduceRight()
注意:接收的参数与其他不同
arr.reduce()
// 求数组的和
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
// prev 代表前一个值,cur代表当前值
let sum = arr.reduce((prev, cur, index, arr) => {
return prev + cur
})
console.log(sum) // 55
let arr1 = [2, 2, 4]
let num = arr1.reduce((prev, cur, index, arr) => {
// return Math.pow(prev, cur) Math.pow(2, 3) 表示2的3次方
return prev**cur // ES2017新增 幂 运算符 2**3
})
console.log(num) // 256
console.log(Math.pow(2,3)) // 8
console.log(2**3) // 8
arr.reduceRight()
从右向左依次执行
let arr = [2, 2, 3] // 3的2次方的2次方
let num = arr.reduceRight((prev, cur, index, arr) => {
// return Math.pow(prev, cur)
return prev**cur
})
console.log(num) // 81
7、for…of()
let arr = ['apple', 'orange', 'banana']
for (let val of arr) {
console.log(val) // apple orange banana
}
// arr.keys() 数组下标
for (let key of arr.keys()) {
console.log(key) // 0 1 2
}
// arr.entries() 数组某一项
for (let item of arr.entries()) {
console.log(item) // [0, "apple"] [1, "orange"] [2, "banana"]
}
for (let [value, key] of arr.entries()) {
console.log(value, key) // 0 "apple" 1 "orange" 2 "banana"
}
6、数组的扩展
1、扩展运算符
含义
扩展运算符是三个点(...
),将一个数组转换为用逗号分隔的参数序列
console.log(1,...[2,3,4],5); // 1,2,3,4,5
该运算符主要用于函数调用
function add(x, y) {
return x + y
}
const numbers = [4,6]
add(...numbers); // 10
替代函数的apply方法
由于扩展运算符可以展开数组,所以不需要apply
方法,将数组转为函数的参数了
// ES5
function fn(x,y,z) {
...
}
var args = [1,2,3]
fn.apply(null, args)
// ES6
function fn(x,y,z) {
...
}
var args = [1,2,3]
fn(...args)
另一个例子,通过push函数,将一个数组添加到另一个数组的尾部
// ES5
let arr1 = [0,1,2];
let arr2 = [3,4,5];
Array.prototype.push.apply(arr1, arr2);
//ES6
let arr1 = [0,1,2];
let arr2 = [3,4,5];
arr1.push(...arr2);
扩展运算符的应用
(1)、复制数组
const a1 = [1,2,3];
// 第一种写法
const a2 = [...a1];
// 第二种写法
const [...a2] = a1;
(2)、合并数组
扩展运算符提供了数组合并的新写法
const a1 = [1,2];
const a2 = [3];
const a3 = [4,5];
// ES5
a1.concat(a2, a3); // [1,2,3,4,5]
// ES6
[...a1,...a2,...a3]; // [1,2,3,4,5]
(3)、与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组;如果将扩展运算符用于数组赋值,只能放在参数的最后一位
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; // []
(4)、字符串
扩展运算符还可以将字符串转换为真正的数组
[...'hello'];
// ['h','e','l','l','o']
(5)、实现了 Iterator 接口的对象
任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。
let nodeList = document.querySelectorAll('div'); // nodeList不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList对象实现了 Iterator 。
let array = [...nodeList];
(6)、Map 和 Set 结构,Generator 函数
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]
2、Array.from()
Array.from()
方法用于将两类对象转换为真正的数组:类似数组的对象和可遍历的对象(包括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']
实际应用中,最常见的类似数组的对象是DOM操作返回的NodeList集合,以及函数内部的arguments对象,Array.from
都可将他们转换为数组
// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
return p.textContent.length > 100;
});
function show() {
let args = Array.from(arguments)
console.log(args) // [1, 2, 3, 4, 5]
}
show(1,2,3,4,5)
字符串转数组
let str = 'jason'
// ES5
// let strArr = str.split('');
// ES6
let strArr = Array.from(str)
console.log(strArr) // ["j", "a", "s", "o", "n"]
如果参数是一个真正的数组,Array.from()
会返回一个一模一样的数组
let arr = [1,2,3];
let arr1 = [...arr];
let arr2 = Array.from(arr)
console.log(arr2) // [1,2,3]
Array.from
方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length
属性。因此,任何有length
属性的对象,都可以通过Array.from
方法转为数组,而此时扩展运算符就无法转换。
let json = {
0: 'apple',
1: 'banana',
2: 'orange'
}
let jsonArr = Array.from(json)
console.log(jsonArr) // []
let json1 = {
0: 'apple',
1: 'banana',
2: 'orange',
length: 3
}
let jsonArr1 = Array.from(json1)
console.log(jsonArr1) // ["apple", "banana", "orange"]
下面的例子将数组中布尔值为false
的成员转为0
。
Array.from([1, , 2, , 3], (n) => n || 0)
// [1, 0, 2, 0, 3]
Array.from({ length: 2 }, () => 'jack')
// ['jack', 'jack']
3、Array.of()
Array.of()
方法用于将一组值转换为数组
Array.of(1,2,3); // [1,2,3]
Array.of(); // [] 如果没有参数,就返回一个空数组
Array.of(undefined); // [undefined]
4、数组实例的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号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。
5、数组实例的find()和findIndex()
数组实例的find
方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有成员依次执行该回调函数,直到找出第一个返回true的成员,然后返回该成员;如果没有符合条件的成员,返回undefined
let arr = [10,30,78,69,90]
let res = arr.find((val, index, arr) => {
return val > 70;
})
console.log(res) // 78
findIndex()
方法与find()
方法类似,返回第一个符合条件的数组成员的位置,如果没有,返回-1
let arr = [10,30,78,69,90]
let idnex = arr.findIndex((val, index, arr) => {
return val > 70;
})
console.log(idnex) // 2
6、数组实例的fill()
fill
方法使用给定值,填充一个数组
['a','b','c'].fill(7);
// [7,7,7]
new Array(3).fill(7);
// [7,7,7]
fill
还可以接收第二个和第三个参数,用于指定填充的起始位置和结束位置
let arr = new Array(10);
arr.fill('默认值', 1, 3)
console.log(arr) // [empty, "默认值", "默认值", empty × 7]
7、数组实例的 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、数组实例的includes()
Array.prototype.includes()
方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes
相似
[1,2,3].includes(2); // true
[1,2,3].includes(0); // false
该方法的第二个参数表示搜索的起始位置,默认为0
。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4
,但数组长度为3
),则会重置为从0
开始。
[1, 2, 3].includes(3, -1); // true
没有该方法之前,我们通常使用数组的indexOf
方法,检查是否包含某个值。
indexOf
方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1
,表达起来不够直观。二是,它内部使用严格相等运算符(===
)进行判断,这会导致对NaN
的误判。
// ES5
[NaN].indexOf(NaN); // -1
// ES6
[NaN].includes(NaN); // true
另外,Map 和 Set 数据结构有一个has
方法,需要注意与includes
区分。
- Map 结构的
has
方法,是用来查找键名的,比如Map.prototype.has(key)
、WeakMap.prototype.has(key)
、Reflect.has(target, propertyKey)
。 - Set 结构的
has
方法,是用来查找值的,比如Set.prototype.has(value)
、WeakSet.prototype.has(value)
。
9、数组实例的flat(),flatMap()
Array.prototype.flat
用于将嵌套的数组‘拉平’,变成一维的数组。该方法返回一个新数组,对原数据没有影响
[1,2,[3,4]];
// [1,2,3,4]
flat()
默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()
方法的参数写成一个整数,表示想要拉平的层数,默认为1。
[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]
flatMap()
方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()
),然后对返回值组成的数组执行flat()
方法。该方法返回一个新数组,不改变原数组
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
// 只能展开一层数组
flatMap()
方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。
arr.flatMap(function callback(currentValue[, index[, array]]) {
// ...
}[, thisArg])
10、数组的空位
数组的空位指,数组的某一个位置没有任何值
Array(3); // [,,,]
注意,空位不是undefined
,一个位置的值等于undefined
,依然是有值的。空位是没有任何值,in
运算符可以说明这一点。
0 in [undefined,undefined,undefined]; // true
0 in [,,,]; // false
ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。
forEach()
,filter()
,reduce()
,every()
和some()
都会跳过空位。map()
会跳过空位,但会保留这个值join()
和toString()
会将空位视为undefined
,而undefined
和null
会被处理成空字符串。
// forEach方法
[,'a'].forEach((x,i) => console.log(i)); // 1
// filter方法
['a',,'b'].filter(x => true) // ['a','b']
// every方法
[,'a'].every(x => x==='a') // true
// reduce方法
[1,,2].reduce((x,y) => x+y) // 3
// some方法
[,'a'].some(x => x !== 'a') // false
// map方法
[,'a'].map(x => 1) // [,1]
// join方法
[,'a',undefined,null].join('#') // "#a##"
// toString方法
[,'a',undefined,null].toString() // ",a,,"
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
11、Array.prototype.sort()的排序稳定性
排序稳定性(stable sorting)是排序算法的重要属性,指的是排序关键字相同的项目,排序前后的顺序不变。
const arr = [
'peach',
'straw',
'apple',
'spork'
];
const stableSorting = (s1, s2) => {
if (s1[0] < s2[0]) return -1;
return 1;
};
arr.sort(stableSorting)
// ["apple", "peach", "straw", "spork"]
7、对象的扩展
1、对象的简洁语法
ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法
let name = 'Jason';
let age = 30;
let json = {
name, // name: name,
age, // age: age
// showA: function() {
// return this.name;
// }
showA() { // 一定注意,不要使用箭头函数
return this.name
}
}
console.log(json.showA())
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};
注意,简写的对象方法不能用做构造函数,会报错
const obj = {
f() {
this.foo = 'bar';
}
};
new obj.f() // 报错
2、属性名表达式
ES6 允许字面量定义对象时,把表达式放在方括号内。
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
注意,属性名表达式与简洁表示法,不能同时使用,会报错。
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
3、方法的name属性
函数的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"
4、属性的可枚举性和遍历
可枚举性
对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
描述对象的enumerable
属性,称为“可枚举性”,如果该属性为false
,就表示某些操作会忽略当前属性。
目前,有四个操作会忽略enumerable
为false
的属性。
for...in
循环:只遍历对象自身的和继承的可枚举的属性。Object.keys()
:返回对象自身的所有可枚举的属性的键名。JSON.stringify()
:只串行化对象自身的可枚举的属性。Object.assign()
: 忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。
属性的遍历
ES6 一共有 5 种方法可以遍历对象的属性。
(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 或字符串,也不管是否可枚举
以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。
- 首先遍历所有数值键,按照数值升序排列。
- 其次遍历所有字符串键,按照加入时间升序排列。
- 最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()] 首先是数值属性2和10,其次是字符串属性b和a,最后是 Symbol 属性。
5、super关键字
ES6新增了一个关键字super
,指向当前对象的原型对象
const proto = {
foo: 'hello'
};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto); // setPrototypeOf() 设置一个指定对象的原型
obj.find() // "hello"
注意,super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
const proto = {
x: 'hello',
foo() {
console.log(this.x);
},
};
const obj = {
x: 'world',
foo() {
super.foo();
}
}
Object.setPrototypeOf(obj, proto);
obj.foo() // "world"
// super.foo指向原型对象proto的foo方法,但是绑定的this却还是当前对象obj,因此输出的就是world。
6、对象的扩展运算符
解构赋值
对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。
let {x, y, ...z} = {x: 1, y: 2, c: 3, d: 4}
console.log(x, y, z) // 1 2 {c: 3, d: 4} 解构赋值必须为最后一个参数,否则会报错
由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefined
或null
,就会报错,因为它们无法转为对象。
let { ...z } = null; // 运行时错误
let { ...z } = undefined; // 运行时错误
注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
扩展运算符
对象的扩展运算符(...
)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。
let json = {a: 1, b: 2}
let json2 = {...json}
delete(json2.a)
console.log(json2) // {b: 2}
console.log(json) // {a: 1, b: 2}
由于数组是特殊的对象,所以对象的扩展运算符可以用于数组
let foo = { ...['a', 'b', 'c'] };
foo
// {0: "a", 1: "b", 2: "c"}
对象的扩展运算符等同于使用Object.assign()
方法。
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
7、链判断运算符
编程实务中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。比如,要读取message.body.user.firstName
,安全的写法是写成下面这样。
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
或者使用三元运算符?:
,判断一个对象是否存在。
const fooInput = myForm.querySelector('input[name=foo]')
const fooValue = fooInput ? fooInput.value : undefined
ES2020 引入了“链判断运算符”(optional chaining operator)?.
,简化上面的写法。
const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value
链判断运算符有三种用法。
obj?.prop
// 对象属性obj?.[expr]
// 同上func?.(...args)
// 函数或对象方法的调用
a?.b
// 等同于
a == null ? undefined : a.b
a?.[x]
// 等同于
a == null ? undefined : a[x]
a?.b()
// 等同于
a == null ? undefined : a.b()
a?.()
// 等同于
a == null ? undefined : a()
8、Null 判断运算符
读取对象属性的时候,如果某个属性的值是null
或undefined
,有时候需要为它们指定默认值。常见做法是通过||
运算符指定默认值。
const headerText = response.settings.headerText || 'Hello, world!';
const animationDuration = response.settings.animationDuration || 300;
const showSplashScreen = response.settings.showSplashScreen || true;
// 但是存在一个问题,开发者的意愿是当等号左边的值为undefined或null时,才使用默认值,但是现在这种写法,等号左边的值为0或者空字符串时,也会使用默认值
为了避免这种情况,ES2020引入了新的运算符??
,它的行为类似||
,但是只有等号左边的值为undefined
或null
时,才会生效
与链判断运算符?.
一起使用
const animationDuration = response.settings?.animationDuration ?? 300
8、对象新增方法
1、Object.is()
用来比较两个值是否严格相等
console.log(Object.is('foo', 'foo')); // true
console.log(NaN == NaN); // false
console.log(Number.isNaN(NaN)); // true
console.log(Object.is(NaN, NaN)) // true
console.log(+0 == -0); // true
console.log(Object.is(+0, -0)); // false
2、Object.assign()
基本用法
Object.assign
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)
let json1 = {a: 1};
let json2 = {b: 2};
let json3 = {c: 3}
// let 新的对象 = Object.assign(target, source1, source1)
let obj = Object.assign({}, json1, json2, json3)
// {a: 1, b: 2, c: 3}
注意,如果目标对象与源对象有同名属性,或者多个源有同名属性,则后面的属性会覆盖前面的属性
let json1 = {a: 1};
let json2 = {b: 2, a: 2};
let json3 = {c: 3}
let obj = Object.assign({}, json1, json2, json3)
console.log(obj) // {a: 2, b: 2, c: 3}
由于undefined
和null
无法转换成对象,所以如果他们作为参数,就会报错
Object.assign(undefined); // 报错
Object.assign(null); // 报错
如果非对象参数出现在源对象的位置,这些参数都会先转换为对象,如果无法转换成对象,则跳过,这意味着,如果undefined
和null
不在首参数,就不会报错
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
其他类型的值(即数组、字符串和布尔值)不在首参数,也不会报错,但是,除了字符串会以数组形式,拷贝到目标对象,其他值不会产生效果
const v1 = 'abc'
const v2 = true
const v3 = 10
const obj = Object.assign({},v1,v2,v3)
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
注意点
(1)浅拷贝
Object.assign
方法实行的是浅拷贝,而不是深拷贝,也就是说,如果源对象的某个属性值是对象,那么目标对象拷贝得到的是这个对象的引用
const obj1 = {a:{b: 1}};
const obj2 = Object.assign({},obj1);
obj1.a.b = 2;
console.log(obj2.a.b); // 2
(2)同名属性的替换
遇到同名属性,Object.assign
的方法是替换,而不是添加
const target = { a: { b: 'c', d: 'e' } }
const source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }
(3)数组的处理
Object.assign
可以用来处理数组,但是会把数组转换为对象
Object.assign([1,2,3], [4,5]);
// 4,5,3 Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1
(4)取值函数的处理
Object.assign
只能进行值的复制,如果要复制的是一个取值函数,那么将求值后再复制
const source = {
get foo() { return 1 }
};
const target = {};
Object.assign(target, source)
// { foo: 1 }
常见用途
(1)为对象添加属性
class Point {
constructor(x, y) {
Object.assign(this, {x, y}); // 将x属性和y属性添加到Point类的对象实例
}
}
(2)为对象添加方法
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}
});
// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
(3)克隆对象
function clone(origin) {
return Object.assign({}, origin);
}
(4)合并多个对象
将多个对象合并到某个对象
const merge =
(target, ...sources) => Object.assign(target, ...sources);
(5)为属性指定默认值
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'
};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}
3、Object.keys()、Object.values()、Object.entries()
let {keys, values, entries} = Object
let json = {
a: 1,
b: 2,
c: 3
}
for (let key of keys(json)) { // 相当于(let key of Object.keys(json))
console.log(key) // a b c
}
for (let value of values(json)) {
console.log(value) // 1 2 3
}
for (let item of entries(json)) {
console.log(item) // ["a", 1] ["b", 2] ["c", 3]
}
for (let [key, val] of entries(json)) {
console.log(key, val) // a 1 b 2 c 3
}
4、Object.fromEntries()
Object.fromEntries
方法是Object.entries
的逆操作,用于将一个键值对数组转化为对象
Object.fromEntries([
['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
9、Promise 对象
1、Promise的含义
Promise是异步编程的一种解决方案,比传统的解决方案(回调函数和事件)更合理和强大
从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理
2、基本用法
ES6规定,Promise
对象是一个构造函数,用来生成Promise
实例
const promise = new Promise(function(resolve, reject) {
if (/*异步操作成功*/) {
resolve(value)
} else {
reject(error)
}
})
Promise
构造函数接收一个函数作为参数,该函数接收两个参数分别是resolve
和reject
Promise
实例生成后,可以用then
方法指定resolved
和rejected
状态的回调函数
promise.then(function(res) {
// success
}, function(error) {
// fail
})
// 等同于
promise.then((res) => {
// success
},(error) => {
// fail
})
下面是异步加载图片的例子
function loadImageAsync(url) {
return new Promise(function(resolve, reject) {
const image = new Image();
image.onload = function() {
resolve(image)
}
image.onerror = function() {
reject(new Error('Could not load image at ' + url))
}
image.src = url;
})
}
下面是用Promise对象实现Ajax的例子
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
注意,调用resolve
或reject
并不会终结Promise函数的执行
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1
一般来说,调用resolve
或reject
以后,Promise 的使命就完成了,后继操作应该放到then
方法里面,而不应该直接写在resolve
或reject
的后面。所以,最好在它们前面加上return
语句,这样就不会有意外
new Promise((resolve, reject) => {
return resolve(1);
// 后面的语句不会执行
console.log(2);
})
3、Promise.prototype.then()
Promise实例具有then
方法,then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数
promise.then(res=> {
console.log(res)
}, err => {
console.log(err)
})
then
方法返回的是一个新的Promise
实例,因此可以采用链式写法,即then
方法后面再调用一个then
方法
getJSON("/posts.json").then(function(json) {
return json.post;
}).then(function(post) {
// ...
});
4、Promise.prototype.catch()
Promise.prototype.catch
用于指定发生错误时的回调函数
getJson('/posts.json').then(function(res) {
//
}).catch(function(error) {
console.log('发生错误' + error)
})
reject
方法的作用,等同于抛出错误
如果Promise状态已经变成resolved
,再抛出错误是无效的
const promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test');
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
Promise对象的错误具有‘冒泡’属性,会一直向后传递,直到被捕获为止,也就是说,错误总是被下一个catch
捕获
getJson('/posts.json').then(function(post) {
return getJson(post.commentUrl)
}).then(function(comments) {
// ...
}).catch(function(error) {
// 处理前面3个Promise的错误 两个then和getJson
})
一般来说,不要在then
方法里面定义 Reject 状态的回调函数(即then
的第二个参数),总是使用catch
方法
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
跟传统的try/catch
语法不同,如果没有使用catch
方法指定错误处理的函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing().then(function() {
console.log('everything is great');
});
setTimeout(() => { console.log(123) }, 2000);
// Uncaught (in promise) ReferenceError: x is not defined
// 123
一般总是建议,Promise 对象后面要跟catch
方法,这样可以处理 Promise 内部发生的错误。catch
方法返回的还是一个 Promise 对象,因此后面还可以接着调用then
方法。
const someAsyncThing = function() {
return new Promise(function(resolve, reject) {
// 下面一行会报错,因为x没有声明
resolve(x + 2);
});
};
someAsyncThing()
.catch(function(error) {
console.log('oh no', error);
})
.then(function() {
console.log('carry on');
});
// oh no [ReferenceError: x is not defined]
// carry on
// 如果没有报错,则会跳过catch
5、Promise.prototype.finally()
finnly
方法用于指定不管Promise对象最后状态如何,都会执行的操作
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
下面是一个例子,服务器使用Promise处理请求,然后使用finally
关掉服务器
sever.listen(port).then(function() {
// ...
}).finally(sever.stop)
finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果
// finally 本质上是then方法的特例
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
finally
总是会返回原来的值
// resolve 的值是 undefined
Promise.resolve(2).then(() => {}, () => {})
// resolve 的值是 2
Promise.resolve(2).finally(() => {})
// reject 的值是 undefined
Promise.reject(3).then(() => {}, () => {})
// reject 的值是 3
Promise.reject(3).finally(() => {})
6、Promise.all()
Promise.all
用于将多个Promise实例,包装成一个新的Promise实例
const p = Promise.all([p1,p2,p3])
p
的状态由p1
、p2
、p3
决定,分成两种情况。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
let pro1 = Promise.resolve('aaa')
let pro2 = Promise.resolve('bbb')
let pro3 = Promise.resolve('ccc')
Promise.all([pro1, pro2, pro3]).then(res => {
console.log(res) // ["aaa", "bbb", "ccc"]
let [res1, res2, res3] = res
console.log(res1, res2, res3) // aaa bbb ccc
})
注意,如果作为参数的Promise实例,自己定义了catch
方法,那么它一旦被rejected
,并不会出发Promise.all
的catch
方法
const p1 = new Promise((resolve, reject) => {
resolve('hello');
})
.then(result => result)
.catch(e => e);
const p2 = new Promise((resolve, reject) => {
throw new Error('报错了');
})
.then(result => result)
.catch(e => e);
Promise.all([p1, p2])
.then(result => console.log(result))
.catch(e => console.log(e));
// ["hello", Error: 报错了]
// 如果p2没有自己的catch方法,就会调用Promise.all的catch方法
7、Promise.race()
Promise.race
同样是将多个Promise实例,包装成一个新的Promise实例
const p = Promise.race([p1,p2,p3])
上面代码中,只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject
,否则变为resolve
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p
.then(console.log)
.catch(console.error);
8、Promise.allSettled()
Promise.allSettled
方法接受一组Promise实例作为参数,包装成一个新的Promise实例。只有等到这些实例都返回结果,不管是fullfilled
还是rejected
,包装实例才会结束
const promises = [
fetch('/api-1'),
fetch('/api-2'),
fetch('/api-3'),
];
await Promise.allSettled(promises);
removeLoadingIndicator(); // 对服务器发出三个请求,等到三个请求都结束,不管请求成功还是失败,加载的滚动图标就会消失。
该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled
,不会变成rejected
。状态变成fulfilled
后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()
的 Promise 实例。
const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
// fulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值
下面是返回值用法的例子
const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ];
const results = await Promise.allSettled(promises);
// 过滤出成功的请求
const successfulPromises = results.filter(p => p.status === 'fulfilled');
// 过滤出失败的请求,并输出原因
const errors = results
.filter(p => p.status === 'rejected')
.map(p => p.reason);
9、Promise.any()
Promise.any()
方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态
Promise.any()
跟Promise.race()
方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected
状态而结束
const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
try {
const first = await Promise.any(promises);
console.log(first);
} catch (error) {
console.log(error);
}
// 只要有一个变成fulfilled,Promise.any()返回的 Promise 对象就变成fulfilled。如果所有三个操作都变成rejected,那么就会await命令就会抛出错误
10、Promise.resolve()
Promise.resolve
方法用于将现有对象转换为Promise对象
const p = Promise.resolve($.ajax('/posts.json'))
Promise.resolve()
等价于下面的写法
Promise.resolve('foo');
// 等价于
new Promise(resolve => resolve('foo'));
Promise.resolve()
方法的参数分为四种情况
(1)参数是一个Promise实例
如果参数是Promise实例,那么Promise.resolve
将不做任何修改,原封不动返回这个实例
(2)参数是一个thenable
对象
thenable
对象指的是具有then
方法的对象,Promise.resolve
方法会将这个对象转为 Promise 对象,然后就立即执行thenable
对象的then
方法。
let thenable = {
then: function(resolve, reject) {
resolve(20)
}
}
let p1 = Promise.resolve(thenable)
p1.then((value) => {
console.log(value); // 20
})
(3)参数不是具有then
方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有then
方法的对象,则Promise.resolve
返回一个新的Promise对象,状态为resolved
const p = Promise.resolve('hello');
p.then(function(res) {
console.log(res); // hello
})
(4)不带有任何参数
Promise.resolve
方法允许调用时不带参数,直接返回一个resolved
状态的Promise对象
const p = Promise.resolve();
p.then((res) => {
// ...
})
需要注意的是,立即resolve()
的 Promise 对象,是在本轮“事件循环”(event loop)的结束时执行,而不是在下一轮“事件循环”的开始时。
setTimeout(function() {
console.log('three')
},0)
Promise.resolve().then(function() {
console.log('two')
})
console.log('one')
// one
// two
// three
11、Promise.reject()
Promise.reject(reason)
方法也会返回一个新的Promise实例,该实例的状态为rejected
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function(err) {
console.log(err) // 出错了
})
注意,Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
12、应用
加载图片
const loadImage = function (path) {
return new Promise(function(resolve, reject) {
const image = new Image();
image.onload = resolve();
image.onerror = reject();
image.src = path;
})
}
❓Generator 函数与 Promise 的结合
function getFoo () {
return new Promise(function (resolve, reject){
resolve('foo');
});
}
const g = function* () {
try {
const foo = yield getFoo();
console.log(foo);
} catch (e) {
console.log(e);
}
};
function run (generator) {
const it = generator();
function go(result) {
if (result.done) return result.value;
return result.value.then(function (value) {
return go(it.next(value));
}, function (error) {
return go(it.throw(error));
});
}
go(it.next());
}
run(g);
13、Promise.try()
有两种写法,让同步函数同步执行,异步函数异步执行,并且具有统一的API
第一种写法是async
函数
const f = ()=> console.log('now');
(asyns() => f())(); // f是同步的会得到同步的结果
(async() => f())() // f是异步的就可以使用`then`方法指定下一步
.then(...)
console.log('next');
// now
// next
第二种写法是使用new Promise
const f = () => console.log('now');
(
() => new Promise(
resolve => resolve(f())
)
)();
console.log('next');
// now
// next
鉴于这是一个很常见的需求,所以提供Promise.try()
方法替代上边的写法
const f = () => console.log('now')
Promise.try(f);
console.log('next')
10、Module的语法
1、概述
JavaScript 一直没有模块体系,在ES6之前,社区制定了一套模块规范,主要的是CommonJS和AMD两种。前者主要用于服务端,后者用于浏览器。
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
// 实质是整体加载fs模块,生成一个对象,然后再读取这三个方法,这种加载称为‘运行时加载’,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
// ES6模块
import { stat, exists, readFile } from 'fs';
// 实质是从fs模块加载这3个方法,其他方法不加载,这种加载称为’编译时加载‘或静态加载
除了静态加载带来的各种好处,ES6 模块还有以下好处。
- 不再需要
UMD
模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。 - 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者
navigator
对象的属性。 - 不再需要对象作为命名空间(比如
Math
对象),未来这些功能可以通过模块提供。
2、严格模式
ES6的模块自动采用严格模式
严格模式主要有以下限制
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with
语句 - 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
eval
不会在它的外层作用域引入变量eval
和arguments
不能被重新赋值arguments
不会自动反映函数参数的变化- 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象 - 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
3、export 命令
模块功能主要有两个命令组成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部都无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export
命令输出该变量
// profile.js
export let firstName = 'Jason';
export let lastName = 'Jie';
// 还有一种写法(优先考虑这种写法)
let firstName = 'Jason';
let lastName = 'Jie';
export {
firstName,
lastName
}
export
命令除了输出变量,还可以输出函数或类
export function sum (x, y) {
return x + y;
}
通常情况下,export
输出的变量就是本来的名字,但是可以使用as
关键字重命名
function v1() {
// ...
}
let str = 'hhh'
export {
v1 as f1,
str as s
}
需要特别注意的是,export
命令规定的是对外的接口,必须与模块内部的变量建立一对一的对应关系
// 报错
export 1;
// 报错
var m = 1;
export m;
// 报错
function f() {}
export f;
// 正确写法
// 变量
export let m = 1;
//
let m = 1;
export {m};
//
let m = 1;
export {m as n};
// 方法
export function f () {}
//
function f() {}
export {f}
另外,export
语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以获取到模块内部实时的值
export let foo = 'hhh';
setTimeout(() => {
foo = 'xxx'; // 1s后变为xxx
}, 1000)
4、import 命令
使用export
命令定义了模块的对外接口后,其他JS文件就可以通过import
命令加载这个模块了
// 大括号里的变量名,必须与被导入模块对外接口的名称相同
import {firstName, lastName} from './profile.js'
// 使用
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
如果想为输入的变量重新起名字,import
命令要使用as
关键字,将输入的变量重名
import {lastName as surName} from './profile.js'
import
输入的变量都是只读的,也就是说,不允许在加载模块的脚本里,改写接口,但是改写对象的属性是允许的
// 变量
import {a} from './xxx.js'
a = {}; // Syntax Error : 'a' is read-only;
// 对象
import {a} from './xxx.js'
a.foo = 'hello'; // 合法操作,由于此写法很难查错,建议凡是输入的变量,都当作完全可读,不要轻易改变它的属性
import
后边的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
文件名可省略
注意,import
命令具有提升效果,会提升到整个模块的顶部,首先执行
foo();
import { foo } from './profile';
由于import
是静态执行,所以不能使用变量和表达式
// 报错
import { 'f' + 'oo' } from 'my_module';
// 报错
let module = 'my_module';
import { foo } from module;
// 报错
if (x === 1) {
import { foo } from 'module1';
} else {
import { foo } from 'module2';
}
如果多次重复执行同一条import
语句,那么只会执行一次,不会执行多次
// 只执行一次
import 'lodash';
import 'lodash';
5、模块的整体加载
除了指定加载某个输出值,还可以使用整体加载,即用星号*
指定一个对象,所有输出值都加载在这个对象上
// circle.js
export function area(radius) {
return Math.PI * radius * radius;
}
export function circumference(radius) {
return 2 * Math.PI * radius;
}
// 加载模块
// main.js
import * as circle from './circle.js';
console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14))
6、export default 命令
从上面可以看出,使用import
命令的时候,用户必须要知道所要加载的变量名或者函数名,对于用户来说不方便,为了使他们不阅读文档就能加载模块,就要用到export default
命令,为模块指定默认输出。
// export-default.js
export default function() {
console.log('foo');
}
// 其他模块加载该模块时,import 命令可以为该模块指定任意名字
import customName from './export-default.js'; // 需要注意的是,这时import 后边,不需要大括号
customName(); // foo
export default
用在非匿名函数前,也是可以的
export default function foo() {
// ...
}
// 或者
function foo() {
// ...
}
export default foo;
下面比较一下默认输出和正常输出
// 默认输出
export default function foo() {
// ...
}
import foo from 'foo';
// 正常输出
export function foo() {
// ...
}
import {foo} from 'foo';
export default
命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default
命令只能使用一次。所以,import
后边才不用加大括号,因为只可能唯一对应export default
命令。
本质上,export default
就是输出一个叫default
的变量或方法,然后系统允许你给他起任意名字。
// modules.js
function add(x, y) {
return x * y;
}
export {add as default};
// 等同于
// export default add;
// app.js
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
正是因为export default
命令其实是输出一个叫default
的变量,所以他后面不能跟变量声明语句。
// 正确
export var a = 1;
// 正确
var b = 10;
export default b;
// 错误
export default var c = 8;
// 正确
export default 7;
// 报错
export 7
如果想在一条import
语句中,同时输入默认方法和其他接口,可以写成下面这样
import _, {foo, bar} from 'xxx';
// 对应如下
export default function() {
// ...
}
export function foo() {}
export function bar() {}
export default
也可以用来输出类
// MyClass.js
export default class { ... }
// main.js
import MyClass from 'MyClass';
let o = new MyClass();
7、export 和 import 的复合写法
如果在一个模块中,先输入后输出同一个模块,import
语句可以与export
语句写在一起
export {foo, bar} from './profile.js';
// 可以简单理解为
import {foo, bar} from './profile.js';
export {foo, bar};
// 需要注意的是,写成一行后,foo 和 bar实际上并没有被导入到当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo 和 bar
// 接口改名
export { foo as myFoo } from 'my_module';
// 整体输出
export * from 'my_module';
// 默认接口
export {default} from 'my_module';
// 具名接口改为默认接口的写法
export { es6 as default } from './someModule';
// 等同于
import { es6 } from './someModule';
export default es6;
// 默认接口也可以改为具名接口
export { default as es6 } from './someModule';
8、模块的继承
模块之间也可以继承
假设有一个circleplus
模块,继承了circle
模块。
// circleplus.js
export * from 'circle'; // export * 命令会忽略circle里的默认方法
export var e = 2.71828182846;
export default function(x) {
return Math.exp(x);
}
这时,也可以将circle
的属性或方法,改名后再输出
// circleplus.js
export { area as circleArea } from 'circle';
加载上面模块的写法如下
// main.js
import * as math from 'circleplus';
import exp from 'circleplus';
console.log(exp(math.e));
9、跨模块常量
const
声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法
// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
如果要使用的常量非常多,可以建一个专门的constants
目录,将各种常量写在不同的文件里,保存在该目录下
// constants/db.js
export const db = {
url: 'http://my.couchdbserver.local:5984',
admin_username: 'admin',
admin_password: 'admin password'
};
// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
然后,将这些文件输出的常量,合并在index.js
里面。
// constants/index.js
export {db} from './db';
export {users} from './users';
使用的时候,直接加载index.js
就可以了。
// script.js
import {db, users} from './constants/index';
10、import()
简介
前面介绍过,import
命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行(import
命令叫做“连接” binding 其实更合适)。所以,下面的代码会报错。
// 报错
if (x === 2) {
import MyModual from './myModual';
}
import()
函数,支持动态加载模块。
import('./modules/1.js').then((res) => {
console.log(res)
})
import()
返回一个Promise对象
const main = document.querySelector('main');
import(`./section-modules/${someVariable}.js`)
.then(module => {
module.loadPageInto(main);
})
.catch(err => {
main.textContent = err.message;
});
适用场合
(1)按需加载
import()
可以在需要的时候,再加载某个模块
button.addEventListener('click', event => {
import('./dialogBox.js')
.then(dialogBox => {
dialogBox.open();
})
.catch(error => {
/* Error handling */
})
});
(2)条件加载
import()
可以放在if
代码块,根据不同的情况,加载不同的模块
if (condition) {
import('moduleA').then(...);
} else {
import('moduleB').then(...);
}
(3)动态的模块路径
import()
允许模块路径动态生成
import(f())
.then(...);
注意点
import()
加载模块成功以后,这个模块会作为一个对象,当作then
方法的参数。因此,可以使用对象解构赋真的方法,获取输出接口。
import('xxx.js').then(({export1, export2}) => {
console.log(export1, export2)
})
如果模块有default
输出接口,可以用参数直接获得
import('xxx.js').then((res) => {
console.log(res.default)
})
也可以使用具名输入的形式
import('xxx.js').then(({default: theDefault}) => {
console.log(theDefault)
})
如果想同时加载多个模块,可以采用下面的方法
Promise.all([
import('./modules/1.js'),
import('./modules/2.js')
]).then(([mod1, mod2]) => {
console.log(mod1, mod2)
})
import()
也可用在async
函数之中
async function main() {
const modOne = await import('./modules/1.js');
const modTwo = await import('./modules/2.js');
console.log(modOne, modTwo);
// 等同于
const [m1, m2] = await Promise.all([
import('./modules/1.js'),
import('./modules/2.js')
])
console.log(m1, m2)
}
main()
11、Module的加载实现
1、浏览器加载
传统方法
HTML网页中,浏览器通过<script>
标签加载Javascript脚本
<!-- 页面内嵌的脚本 -->
<script type="application/javascript">
// module code
</script>
<!-- 外部脚本 -->
<script type="application/javascript" src="path/to/myModule.js">
</script>
// 默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间
如果脚本体积很大,下载和执行时间就会很长,就会造成浏览器阻塞,所以浏览器允许脚本异步加载
<script src="path/to/myModule.js" defer></script> // defer 是'渲染完执行'
<script src="path/to/myModule.js" async></script> // async 是'下载完执行'
加载规则
浏览器加载ES6模块,也使用script
标签,但是要加入type = 'module'
属性
<script type='module' src='./foo.js'></script>
浏览器对于带有type="module"
的script
,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了标签的defer
属性
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
// 如果网页有多个 `type='module'`,会按照页面出现的顺序依次执行
<script>
标签的async
属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再继续渲染。
<script type="module" src="./foo.js" async></script>
// 一旦使用了async属性,<script type="module">就不会按照页面出现的顺序执行,而是只要该模块加载完成,就执行该模块
ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。
<script type="module">
import utils from "./utils.js";
// other code
</script>
对于外部的模块脚本,有几点需要注意。
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明
use strict
。 - 模块之中,可以使用
import
命令加载其他模块(.js
后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export
命令输出对外接口。 - 模块之中,顶层的
this
关键字返回undefined
,而不是指向window
。也就是说,在模块顶层使用this
关键字,是无意义的。 - 同一个模块如果加载多次,将只执行一次
import utils from 'https://example.com/js/utils.js';
const x = 1;
console.log(x === window.x); //false
console.log(this === undefined); // true
2、ES6模块与CommonJS模块的差异
它们有两个重大差异
- CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用
- CommonJS模块是运行时加载,而ES6模块是编译时输出接口
第二个差异是因为CommonJS加载的是一个对象(即 module.export
属性),该对象只有在脚本运行完才会生成。
而ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
下面重点解释第一个差异
CommonJS模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
// lib.js
var counter = 3;
function incCounter() {
counter ++;
}
module.export = {
counter: counter,
incCounter: incCounter
}
// main.js 加载这个模块
var mod = require('./lib.js');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
// 上面代码说明,模块加载以后,它的内部变化影响不到输出的mod.counter,因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变化的值
// lib.js
var counter = 3;
function incCounter() {
counter ++;
}
module.export = {
get counter() {
return counter;
},
incCounter: incCounter
}
// main.js
var mod = require('./lib.js');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 4
而ES6模块是动态引用,并不会缓存值,模块里面的变量绑定所在的模块
// lib.js
export let counter = 3;
export function incCounter() {
counter ++;
}
// main.js
import {counter, incCounter} from './lib.js'
console.log(counter); // 3
incCounter();
console.log(counter); // 4
ES6输入的模块变量是只读的,所以对他重新赋值,会报错
// lib.js
export let obj = {};
// main.js
import { obj } from './lib';
obj.prop = 123; // OK 可以添加属性
obj = {}; // TypeError 但是重新赋值会报错
export
通过接口,输出的都是同一个值。不同的脚本加载这个模块,得到的都是同一个实例
// mod.js
function C() {
this.sum = 0;
this.add = function () {
this.sum += 1;
};
this.show = function () {
console.log(this.sum);
};
}
export let c = new C();
// x.js
import {c} from './mod';
c.add();
// y.js
import {c} from './mod';
c.show();
// main.js
import './x';
import './y';
// 执行main.js,输出结果是1
12、Class的基本语法
1、简介
类的由来
JavaScript中,生成实例对象的方法是通过构造函数
function Person(name, age) {
this.name = name
this.age = age
}
// Person.prototype.showName = function() {
// return `名字为 ${this.name}`
// }
// Person.prototype.showAge = function() {
// return `年龄为 ${this.age}`
// }
// 等同于下面这种写法
Object.assign(Person.prototype, {
showName() {
return `名字为 ${this.name}`
},
showAge() {
return `年龄为 ${this.age}`
}
})
let p1 = new Person('jason', 10)
console.log(p1.showName(), p1.showAge()) // 名字为 jason 年龄为 10
ES6提供了Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类
let aaa = 'fn'
let bbb = 'method'
class Person{ // 也可以用 const Person = class {}
constructor(name, age) { // 构造函数(方法),只要执行new,就会自动调用
// console.log(`构造函数执行了,${name}, ${age}`) // 构造函数执行了,Jason, 10
// 属性
this.name = name;
this.age = age;
}
// 方法直接写在class里,不需要加逗号
showName() {
return `名字为:${this.name}`
}
showAge() {
return `年龄为:${this.age}`
}
// 方法名可以用表达式定义
[aaa + bbb]() {
return '方法名自定义'
}
}
let p1 = new Person('Jason', 10)
console.log(p1.showName(), p1.showAge()) // 名字为:Jason 年龄为:10
console.log(p1[aaa + bbb]()) // 方法名自定义
console.log(p1.fnmethod()) // 方法名自定义
// 类的数据类型就是函数,类本身就指向构造函数
使用的时候,也是直接对类使用new
命令
class Bar{
doStuff() {
console.log('stuff')
}
}
let a = new Bar();
console.log(a.doStuff()); // stuff
类的所有方法都定义在类的prototype
属性上面
class Point{
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor(){},
toString(){},
toValue(){}
}
Object.assign()
可以方便地一次向类添加多个方法
class Point{
constructor() {
// ...
}
}
Object.assign(Point.prototype, {
toString() {},
toValue() {}
})
另外,类的内部定义的所有方法,都是不可枚举的
class Point {
constructor(x, y) {
// ...
}
toString() {
// ...
}
}
Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]
constructor 方法
constructor
方法是类的默认方法,通过new
命令生成实例对象时,自动调用该方法。一个类必须有constructor
方法,如果没有显示定义,一个空的constructor
方法会被默认添加。
class Point{
}
// 等同于
class Point{
constructor() {}
}
constructor
方法默认返回实例对象(即this
),完全可以指定返回另外一个对象
class Foo{
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
类的实例
生成类的实例的写法,与ES5一样,使用new
命令生成。
class Point {
// ...
}
let p1 = Point(2,4); // 报错
let p2 = new Point(2,4); // 正确
与ES5一样,实例的属性除非显示定义在其本身(即this
对象上),否则都是定义在原型上(即class
上)
class Point{
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x}, ${this.y})`
}
}
let point = new Point(2,3);
point.toString(); // (2,3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false 是原型对象的属性
point.__proto__.hasOwnProperty('toString') // true
与ES5一样,类的所有实例对象共享一个原型对象
var p1 = new Point(1, 1);
var p2 = new Point(2, 3);
p1.__proto__ === p2.__proto__; // true
取值函数(getter) 和 存值函数(setter)
在类的内部可以使用get
和set
关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为
class Person{
constructor() {
}
// 获取值
get aaa() {
return `获取aaa的值`
}
// 设置值
set aaa(val) {
console.log(`设置aaa的值,为${val}`) // 设置aaa的值,为hhh
}
}
let p1 = new Person();
p1.aaa = 'hhh'
console.log(p1.aaa) // 获取aaa的值
属性表达式
类的属性名,可以采用表达式
let methodName = 'getArea';
class Square{
constructor() {
// ...
}
[methodName]() {
// ...
}
}
Class表达式
与函数一样,类也可以使用表达式的方式定义
const MyClass = class Me{
getClassName() {
return Me.name
}
}
let inst = new MyClass();
inst.getClassName(); // Me
Me.getClassName(); // ReferenceError: Me is not defined Me只在Class内部有定义
// 注意,这个类的名字是 Me, 但是Me只在Class的内部可用,指代当前类。在这个类外部,只能用MyClass引用
如果类的内部没有用到的话,可以省略Me,也就是可以写成下面的形式
const MyClass = class {
// ...
}
采用Class表达式。可以写出立即执行的Class
let person = class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name)
}
}('Jason')
person.sayName(); // Jason
注意点
(1)严格模式
类和模块的内部,默认就是严格模式
(2)不存在提升
类不存在变量提升
new Point(); // ReferenceError
class Point{}
(3)name属性
存在name
属性
class Point{}
Point.name; // Point
(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
(5)this 的指向
类的方法内部如果含有this
,它默认指向类的实例
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`);
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
// 这时,this指向方法运行时所在的环境(由于class内部是严格模式,所以this指向undefined)
一个比较简单的方法是,在构造方法中绑定this
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
}
另一种方法是使用箭头函数
class Obj {
constructor() {
this.getThis = () => this;
}
}
const myObj = new Obj();
myObj.getThis() === myObj // true
//箭头函数内部的this总是指向定义时所在的对象。上面代码中,箭头函数位于构造函数内部,它的定义生效的时候,是在构造函数执行的时候。这时,箭头函数所在的运行环境,肯定是实例对象,所以this会总是指向实例对象。
❓还有一种解决方法是使用Proxy
,获取方法的时候,自动绑定this
。
function selfish (target) {
const cache = new WeakMap();
const handler = {
get (target, key) {
const value = Reflect.get(target, key);
if (typeof value !== 'function') {
return value;
}
if (!cache.has(value)) {
cache.set(value, value.bind(target));
}
return cache.get(value);
}
};
const proxy = new Proxy(target, handler);
return proxy;
}
const logger = selfish(new Logger());
2、静态方法
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”
class Person{
constructor() {
// ...
}
showName() {
return '这是showName方法'
}
static aaa() {
return '这是静态方法'
}
}
let p1 = new Person();
console.log(p1.showName()) // 这是showName方法
console.log(Person.aaa()) // 这是静态方法
console.log(p1.aaa()) // TypeError: p1.aaa is not a function
注意,如果静态方法包含this
关键字,这个this
指的是类,而不是实例
class Foo{
static bar () {
this.baz(); // this 值的是 Foo类
}
static baz() {
console.log('hello')
}
baz() { // 还可以看出静态方法可以与非静态方法重名
console.log('world')
}
}
Foo.bar(); // hello
父类的静态方法,可以被子类继承
class Foo{
static sayName() {
console.log('hello')
}
}
class Bar extends Foo{
}
Bar.sayName(); // hello
静态方法也是可以从super
对象上调用的
class Foo {
static classMethod() {
return 'hello';
}
}
class Bar extends Foo {
static classMethod() {
return super.classMethod() + ', too';
}
}
Bar.classMethod() // "hello, too"
3、实例属性的新写法
实例属性除了定义在constructor()
方法里面的this
上面,也可以定义在类的最顶层
class IncreasingCounter {
constructor() {
this._count = 0;
}
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
// 也可以写成下面这种方式
class IncreasingCounter {
_count = 0;
get value() {
console.log('Getting the current value!');
return this._count;
}
increment() {
this._count++;
}
}
4、静态属性
静态属性指的是 Class 本身的属性,即Class.propName
,而不是定义在实例对象this
上的属性
写法是在实例属性的前面,加上static
关键字
// 老写法
class Foo{
// ...
}
Foo.prop = 1;
// 新写法
class Foo{
static prop = 1;
}
❓5、私有方法和私有属性
现有的解决方案
私有方法和私有属性,是只能在类的内部访问的方法和属性,外部无法访问。这是常见需求,有利于代码的封装,但是ES6不提供,只能通过变通方法模拟实现。
1、在命名上加以区别 (不保险,在类的外部还是可以调用到)
class Foo{
bar() {
console.log('bar')
}
_baz() {
console.log('baz')
}
}
2、将私有方法移出模块(因为模块内部的所有方法都是对外可见的)
class Widget {
foo (baz) {
bar.call(this, baz);
}
// ...
}
function bar(baz) {
return this.snaf = baz;
}
3、利用Symbol
值的唯一性,将私有方法的名字命名为一个Symbol
值
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
私有属性的提案
目前,有一个提案,为class
添加私有属性。方法是在属性名之前,使用#
表示
class IncreasingCounter {
#count = 0;
get value() {
console.log('Getting the current value!');
return this.#count;
}
increment() {
this.#count++;
}
}
// 私有属性,只能在类的内部使用,如果在类的外部调用,会报错
const counter = new IncreasingCounter();
counter.#count // 报错
counter.#count = 42 // 报错
这种写法还可以用来写私有方法。
class Foo {
#a;
#b;
constructor(a, b) {
this.#a = a;
this.#b = b;
}
#sum() {
return #a + #b;
}
printSum() {
console.log(this.#sum());
}
}
另外,私有属性也可以设置getter
和setter
方法
class Counter {
#xValue = 0;
constructor() {
super();
// ...
}
get #x() { return #xValue; }
set #x(value) {
this.#xValue = value;
}
}
私有属性和私有方法前面,也可以加static
关键字,表示这是一个静态的私有方法或私有属性
class FakeMath {
static PI = 22 / 7;
static #totallyRandomNumber = 4;
static #computeRandomNumber() {
return FakeMath.#totallyRandomNumber;
}
static random() {
console.log('I heard you like random numbers…')
return FakeMath.#computeRandomNumber();
}
}
FakeMath.PI // 3.142857142857143
FakeMath.random()
// I heard you like random numbers…
// 4
FakeMath.#totallyRandomNumber // 报错
FakeMath.#computeRandomNumber() // 报错
6、new.target 属性
ES6 为new
命令引入了一个new.target
属性,该属性一般用在构造函数之中,返回new
命令作用于的那个构造函数。如果构造函数不是通过new
命令或Reflect.construct()
调用的,new.target
会返回undefined
,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
Class 内部调用new.target
,返回当前Class
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
this.length = length;
this.width = width;
}
}
var obj = new Rectangle(3, 4); // 输出 true
需要注意的是,子类继承父类时,new target
会返回子类
class Rectangle {
constructor(length, width) {
console.log(new.target === Rectangle);
// ...
}
}
class Square extends Rectangle {
constructor(length, width) {
super(length, width);
}
}
var obj = new Square(3); // 输出 false
利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
class Shape { // Shape类不能被实例化,只能用于继承
constructor() {
if (new.target === Shape) {
throw new Error('本类不能实例化');
}
}
}
class Rectangle extends Shape {
constructor(length, width) {
super();
// ...
}
}
var x = new Shape(); // 报错
var y = new Rectangle(3, 4); // 正确
13、Class 的继承
1、简介
Class 可以通过extends
关键字实现继承
// 父类
class Person {
constructor(name) {
this.name = name;
}
showName() {
console.log('父类的showName方法')
}
}
// 子类
class Student extends Person {
constructor(name, skill) {
super(name); // 使用super关键字继承父类的属性
this.skill = skill;
}
showName() {
super.showName(); // 调用父类的方法
// TODO
console.log('子类的showName方法')
}
}
子类必须在 constructor
方法中调用 super
方法,否则新建实例会报错
class Person{
// ...
}
class Student extends Person{
constructor() {
}
}
let stu = new Student(); // 报错
如果子类没有定义constructor
方法,会被默认添加
class Student extends Person {
// ...
}
// 等同于
class Student extends Person{
constructor(...args) {
super(args)
}
}
另外,在子类的构造函数中,只有调用super
方法之后,才能使用this
,否则会报错
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
class Student extends Person {
constructor(name, age, skill) {
this.skill = skill; // 报错
super(name, age);
this.skill = skill; // 正确
}
}
父类的静态方法也会被子类继承
class Person {
static sayHello() {
console.log('hello')
}
}
class Student extends Person{
}
Student.sayHello(); // hello
2、Object.getPrototypeOf()
Object.getPrototypeOf()
方法可以用来在子类上获取父类
Object.getPrototypeOf(Student) === Person
// true
// 因此,可以使用这个方法判断,一个类是否继承了另一个类
3、super关键字
super
关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同
第一种情况,super
作为函数调用时,代表父类的构造函数。ES6要求,子类的构造函数必须执行一次super
函数
class A {
}
class B extends A {
constructor() {
super();
}
}
super()
内部的this
指向子类的实例,因此super()
在这里相当于A.prototype.constructor.call(this)
。
class A {
console.log(new.target.name); // new.target指向当前正在执行的函数
}
class B extends A {
constructor() {
super();
}
}
new A(); // A
new B(); // B
作为函数时,super
只能用在子类的构造函数中,用在其他地方会报错
class A {
}
class B extends A {
m() {
super(); // 报错
}
}
第二种情况,super
作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类
class A{
p() {
return 2;
}
}
class B extends A{
constructor() {
super();
console.log(super.p());
}
}
let b = new B(); // 2
这里需要注意,由于super
指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super
调用的
class A {
constructor() {
this.a = 1;
}
}
class B extends A{
get m() {
return super.a
}
}
let b = new B();
b.m; // undefined
如果属性定义在父类的原型对象上,super
就可以取到
class A{}
A.prototype.a = 1;
class B extends A {
constructor() {
super();
console.log(super.a);
}
}
let b = new B();
ES6规定,在子类普通方法中通过super
调用父类的方法时,方法内部的this
指向当前的子类实例
class A{
constructor() {
this.x = 1
}
print() {
console.log(this.x)
}
}
class B extends A{
constructor() {
super();
this.x = 2;
}
bPrint() {
super.print();
}
}
let b = new B();
b.bPrint(); // 2
由于this
指向子类实例,所以如果通过super
对某个属性赋值,这时super
就是this
,赋值的属性会变成子类实例的属性
class A{
constructor() {
this.x = 1;
}
}
class B extends A {
constructor() {
super();
this.x = 2;
super.x = 3;
console.log(super.x); // undefined
console.log(this.x); // 3
}
}
let b = new B();
如果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.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); // static 1
let child = new Child();
child.myMethod(2); // instance 2
另外,在子类的静态方法中通过super
调用父类的方法时,方法内部的this
指向当前的子类,而不是子类的实例
class A {
constructor() {
this.x = 1;
}
static print() {
console.log(this.x);
}
}
class B extends A{
constructor() {
super();
this.x = 2;
}
static m() {
super.print();
}
}
B.x = 3;
B.m(); //
注意,使用super
的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错
class A{}
class B extends A{
constructor() {
super();
console.log(super); // 报错
}
}
由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super
关键字
let obj = {
toString() {
return `myObject: ${super.toString()}`
}
}
console.log(obj.toString()) // MyObject: [object Object]
4、类的prototype属性和__ proto__属性
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
这样的结果是因为,类的继承是按照下面这种模式实现的
class A{}
class B{}
// B的实例继承A的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// 等同于
B.prototype.__proto__ = A.prototype
// B继承A的静态属性
Object.setPrototypeOf(B, A);
B.__proto__ = A;
实例的__ proto__属性
子类实例的__proto__
属性的__proro__
属性,指向父类的__proto__
属性,也就是说,子类的原型的原型,是父类的原型
var p1 = new Point(1,2);
var p2 = new ColorPoint(1,2,'red');
p2.__proto__ === p1.__proto; // false
p2.__proto__.__proto__ === p1.__proto; // true
因此,可以通过子类实例的__proto__
属性的__proro__
属性,修改父类实例的行为
p2.__proto__.__proto__.printName = function() {
console.log('Ha');
}
p1.printName(); // Ha
5、原生构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
以前,这些原生构造函数是无法继承的
ES6允许继承原生构造函数定义子类
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
// extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数
6、Mixin 模式的实现
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。
const a = {
a: 'a'
}
const b = {
b: 'b'
}
const c = {...a, ...b}; // {a: 'a', b: 'b'}
下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。
function mix(...mixins) {
class Mix {
constructor() {
for (let mixin of mixins) {
copyProperties(this, new mixin()); // 拷贝实例属性
}
}
}
for (let mixin of mixins) {
copyProperties(Mix, mixin); // 拷贝静态属性
copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== 'constructor'
&& key !== 'prototype'
&& key !== 'name'
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
// 上面代码的mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}
14、Symbol
1、概述
ES6引入了一种新的原始数据类型Symbol
,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:undefined
、null
、布尔值(Boolean)、字符串(String)、对象(Object)、数值(Number)。
Symbol值通过Symbol
函数生成。这就是说,对象的属性名现在有两种形式,一种是原来就有的字符串,另一种就是新增的Symbol类型。凡是属性名是Symbol类型的,就都是独一无二的
let s = Symbol(); // 不能使用new命令
typeof s; // symbol
Symbol
函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
注意,Symbol
函数的参数只是对当前Symbol值的描述,因此相同参数的Symbol
函数的返回值是不相等的
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2; // false
let s1 = Symbol('abc');
let s2 = Symbol('abc');
s1 === s2; // false
Symbol值不能与其他类型的值进行运算,会报错
let sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
但是,Symbol可以显示转换为字符串
let sym = Symbol('My symbol');
sym.toString(); // 'Symbol(My symbol)'
String(sym); // 'Symbol(My symbol)'
另外,Symbol值也可以转为布尔值,但不能转为数值
let sym = Symbol();
Boolean(sym) // true
!sym // false
if (sym) {
// ...
}
Number(sym) // TypeError
sym + 2 // TypeError
2、Symbol.prototype.Description
创建Symbol的时候,可以添加一个描述
let s1 = Symbol('foo')
ES2019提供了一个实例属性description
,直接返回Symbol 值的描述
let s1 = Symbol('foo')
s1.description; // foo
3、作为属性名的Symbol
由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖
let mySymbol = Symbol();
// 第一种写法
let a = {}
a[mySymbol] = 'hello'
// 第二种写法
let a = {
[mySymbol]: 'hello'
}
// 第三种写法
let a = {}
Object.defineProperty(a, mySymbol, {value: 'hello'})
// 以上写法得到同样结果
a[mySymbol]; // 'hello'
注意,Symbol值作为对象属性名时,不能用点运算符
const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
同理,在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号中
let s = Symbol();
let obj = {
[s]:function(args) {
// ...
}
}
// 采用增强的对象写法,可以写的更简洁
let obj = {
[s](args){
// ...
}
}
obj[s](123)
Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
const log = {};
log.levels = {
DEBUG: Symbol('debug'),
INFO: Symbol('info'),
WARN: Symbol('warn')
};
console.log(log.levels.DEBUG, 'debug message');
console.log(log.levels.INFO, 'info message');
还有一点需要注意,Symbol值作为属性名时,该属性还是公有属性,不是私有属性。
4、属性名的遍历
Symbol作为属性名,进行遍历的时候,该属性不会出现在for...in
、for...of
循环中,也不会被Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringfy()
返回
但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()
方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
const obj = {};
let a = Symbol('a');
let b = Symbol('b');
obj[a] = 'Hello';
obj[b] = 'World';
const objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols
// [Symbol(a), Symbol(b)]
另一个新的 API,Reflect.ownKeys()
方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
let obj = {
[Symbol('my_key')]: 1,
enum: 2,
nonEnum: 3
};
Reflect.ownKeys(obj)
// ["enum", "nonEnum", Symbol(my_key)]
5、Symbol.for(), Symbol.keyFor()
有时,我们希望重新使用同一个Symbol 值,Symbol.for()
方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建一个以该参数作为名称的Symbol值,并将其注册到全局。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2; // true
Symbol.for()
与Symbol()
两种写法,都会生成新的Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。
Symbol.for()
不会每次调用就返回一个新的Symbol类型的值,而是会先检查给定的key
是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")
30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")
30 次,会返回 30 个不同的 Symbol 值。
Symbol.for('foo') === Symbol.for('foo');
// true
Symbol('foo') === Symbol('foo');
// false
Symbol.keyFor()
方法返回一个已登记的Symbol类型值的key
let s1 = Symbol.for('foo');
Symbol.keyFor(s1); // foo
let s2 = Symbol('foo');
Symbol.keyFor(s2); // undefined
注意,Symbol.for()
为Symbol值登记的名字,是全局环境的,不管有没有在全局环境运行
function foo() {
return Symbol.for('bar');
}
const x = foo();
const y = Symbol.for('bar');
console.log(x === y); // true
6、内置的Symbol值
除了定义自己使用的Symbol值以外,ES6还提供了11个内置的Symbol值,指向语言内部使用的方法。
Symbol.hasInstance
对象的Symbol.hasInstance
属性,指向一个内部方法。当其他对象使用instanceof
运算符,判读是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo
在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}
}
[1, 2, 3] instanceof new MyClass() // true
Symbol.isConcatSpreadable
对象的Symbol.isConcatSpreadable
属性等于一个布尔值,表示该对象用于Array.prototype.concat()
时,是否可以展开。
let arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e'); // ['a', 'b', 'c', 'd', 'e'] 数组的默认行为是可以展开
arr1[Symbol.isConcatSpreadable]; // undefined 默认等于undefined
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e'); // ['a', 'b', ['c', 'd'], 'e']
类似数组的对象正好相反,默认不展开。它的Symbol.isConcatSpreadable
属性设为true
时,才可以展开。
let obj = {length: 2, 0: 'c', 1: 'd'};
['a', 'b'].concat(obj, 'e') // ['a', 'b', obj, 'e']
obj[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj, 'e') // ['a', 'b', 'c', 'd', 'e']
Symbol.species
对象的Symbol.species
属性,指向一个构造函数。创建衍生对象时,会使用该属性。
class MyArray extends Array {
}
const a = new MyArray(1, 2, 3);
const b = a.map(x => x);
const c = a.filter(x => x > 1);
b instanceof MyArray // true
c instanceof MyArray // true
// a是MyArray的实例,b和c是a的衍生对象。你可能会认为,b和c都是调用数组方法生成的,所以应该是数组(Array的实例),但实际上它们也是MyArray的实例
Symbol.species
属性就是为了解决这个问题而提供的。现在,我们可以为MyArray
设置Symbol.species
属性
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
const a = new MyArray();
const b = a.map(x => x);
b instanceof MyArray // false
b instanceof Array // true
// a.map(x => x)生成的衍生对象,就不是MyArray的实例,而直接就是Array的实例。
Symbol.match
对象的Symbol.match
属性,指向一个函数。当执行str.match(myObject)
时,如果该属性存在,会调用它,返回该方法的返回值
String.prototype.match(regexp)
// 等同于
regexp[Symbol.match](this)
class MyMatcher {
[Symbol.match](string) {
return 'hello world'.indexOf(string);
}
}
'e'.match(new MyMatcher()) // 1
Symbol.replace
对象的Symbol.replace
属性,指向一个方法,当该对象被String.prototype.replace
方法调用时,会返回该方法的返回值。
String.prototype.replace(searchValue, replaceValue)
// 等同于
searchValue[Symbol.replace](this, replaceValue)
Symbol.search
对象的Symbol.search
属性,指向一个方法,当该对象被String.prototype.search
方法调用时,会返回该方法的返回值
String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)
class MySearch {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}
}
'foobar'.search(new MySearch('foo')) // 0
Symbol.split
对象的Symbol.split
属性,指向一个方法,当该对象被String.prototype.split
方法调用时,会返回该方法的返回值。
String.prototype.split(separator, limit)
// 等同于
separator[Symbol.split](this, limit)
class MySplitter {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
let index = string.indexOf(this.value);
if (index === -1) {
return string;
}
return [
string.substr(0, index),
string.substr(index + this.value.length)
];
}
}
'foobar'.split(new MySplitter('foo'))
// ['', 'bar']
'foobar'.split(new MySplitter('bar'))
// ['foo', '']
'foobar'.split(new MySplitter('baz'))
// 'foobar'
Symbol.iterator
对象的Symbol.iterator
属性,指向该对象的默认遍历器方法。
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
Symbol.toPrimitive
对象的Symbol.toPrimitive
属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。
Symbol.toPrimitive
被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。
- Number:该场合需要转成数值
- String:该场合需要转成字符串
- Default:该场合可以转成数值,也可以转成字符串
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}
};
2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
Symbol.toStringTag
对象的Symbol.toStringTag
属性,指向一个方法。在该对象上面调用Object.prototype.toString
方法时,如果这个属性存在,它的返回值会出现在toString
方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]
或[object Array]
中object
后面的那个字符串。
// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
// 例二
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
ES6 新增内置对象的Symbol.toStringTag
属性值如下。
JSON[Symbol.toStringTag]
:‘JSON’Math[Symbol.toStringTag]
:‘Math’- Module 对象
M[Symbol.toStringTag]
:‘Module’ ArrayBuffer.prototype[Symbol.toStringTag]
:‘ArrayBuffer’DataView.prototype[Symbol.toStringTag]
:‘DataView’Map.prototype[Symbol.toStringTag]
:‘Map’Promise.prototype[Symbol.toStringTag]
:‘Promise’Set.prototype[Symbol.toStringTag]
:‘Set’%TypedArray%.prototype[Symbol.toStringTag]
:'Uint8Array’等WeakMap.prototype[Symbol.toStringTag]
:‘WeakMap’WeakSet.prototype[Symbol.toStringTag]
:‘WeakSet’%MapIteratorPrototype%[Symbol.toStringTag]
:‘Map Iterator’%SetIteratorPrototype%[Symbol.toStringTag]
:‘Set Iterator’%StringIteratorPrototype%[Symbol.toStringTag]
:‘String Iterator’Symbol.prototype[Symbol.toStringTag]
:‘Symbol’Generator.prototype[Symbol.toStringTag]
:‘Generator’GeneratorFunction.prototype[Symbol.toStringTag]
:‘GeneratorFunction’
Symbol.unscopables
对象的Symbol.unscopables
属性,指向一个对象。该对象指定了使用with
关键字时,哪些属性会被with
环境排除。
Array.prototype[Symbol.unscopables]
// {
// copyWithin: true,
// entries: true,
// fill: true,
// find: true,
// findIndex: true,
// includes: true,
// keys: true
// }
Object.keys(Array.prototype[Symbol.unscopables])
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']
15、Generator 函数的语法
1、简介
基本概念
Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator函数有多种理解角度。语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。
执行Generator函数会返回一个遍历器对象。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态
形式上,Generator函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有个星号;二是,函数体内部使用yield
表达式,定义不同的内部状态
// 定义
function * gen(){
yield 'Welcome';
yield 'to';
return 'China';
}
// 调用
let gen1 = gen();
console.log(gen1) // 对象
// 使用next()
// value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束
console.log(gen1.next()); // {value: "Welcome", done: false}
console.log(gen1.next()); // {value: "to", done: false}
console.log(gen1.next()); // {value: "China", done: true}
// 如果已经为true value 就会为undefined
console.log(gen1.next()); // {value: undefined, done: true}
Generator 函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法可以恢复执行。
ES6没有规定,function
关键字与函数名之间的星号,写在哪个位置。这导致下面方法都能通过
function * foo(x, y) { ... }
function* foo(x, y) { ... } // 一般写法
function *foo(x, y) { ... }
function*foo(x, y) { ... }
yield表达式
yield
表达式就是暂停标志。
遍历器对象的next()
方法的运行逻辑如下
(1)遇到yield
表达式,就暂停执行后边的操作,并将紧跟在yield
表达式后边的值,作为返回对象的value
属性值;
(2)下一次调用next
方法时,再继续往下执行,直到遇到下一个yield
表达式;
(3)如果没有再遇到新的yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回对象的value
属性值;
(4)如果该函数没有return
语句,则返回对象的value
属性值为undefined
需要注意的是,yield
后面的表达式,只有当调用next
方法,内部指针指向该语句时才会执行
function* gen(){
yield 123 + 456;
}
// 该函数不会立即求值,而是当调用next方法时,才会求值
yield
表达式与return
语句有相似之处,也有不同。
相似之处:
- 都能返回紧跟在语句后面的表达式的值
不同之处:
- 每次遇到
yield
,函数暂停执行,下次再从该位置向后执行,而return
语句不具备位置记忆的功能 - 一个函数里,最多只能执行一次
return
,但是可以执行多次yield
表达式
Generator函数可以不用yield
表达式,这时就变成了一个单纯的暂缓执行函数
function* fn() {
console.log('执行')
}
var gen = fn();
setTimeout(() => {
gen.next();
}, 1000)
另外,yield
表达式只能用在Generator 函数中,用在其他地方会报错
(function() {
yield 1;
})()
// SyntaxError: Unexpected number
yield
表达式如果用在另一个表达式之中,必须使用圆括号
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
yield
表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
与 Iterator接口的关系
上一章说过,任意一个对象的Symbol.iterator
方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator
属性,从而使得该对象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
2、next方法的参数
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false } yield(x+1)的返回值为12 y=24
b.next(13) // { value:42, done:true } z=13
注意,由于next
方法的参数表示上一个yield
表达式的返回值,所以在第一次使用next
方法时,传递参数是无效的
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
3、for…of循环
for...of
循环可以自动遍历Generator函数运行时生成的Iterator
对象,且此时不再需要调用next
方法
function * gen(){
yield 'Welcome';
yield 'to';
return 'China';
}
let gen1 = gen();
for (let val of gen1) {
console.log(val) // Welcome to
}
// 一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象
除了for...of
以外,扩展运算符(...
)、解构赋值和Array.from
方法内部调用的,都是遍历器接口。这意味着,它们都可以将Generator函数返回的Iterator
对象作为参数
function* numbers() {
yield 1;
yield 2;
return 3;
yield 4;
}
// 扩展运算符
[...numbers()] // [1,2]
// Array.from 方法
Array.from(numbers()); // [1, 2]
// 解构赋值
let [x, y] = numbers();
x; // 1
y; // 2
// for...of 循环
for(let n of numbers()) {
console.log(n);
}
// 1
// 2
4、Generator.prototype.throw()
Generator 函数返回的遍历器对象,都有一个throw
方法,可以在函数体外抛出错误,然后在Generator 函数体内捕获。
var g = function* (){
try {
yield;
}catch(e){
console.log('内部捕获', e)
}
}
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
}catch(e) {
console.log('外部捕获', e)
}
// 内部捕获 a
// 外部捕获 b
throw
方法接收一个参数,该参数会被catch
语句接收,建议抛出Error
对象的实例
var g = function* () {
try{
yield;
}catch(e){
console.log(e)
}
}
var i = g();
i.next();
i.throw(new Error('出错了!'))
// Error: 出错了!(…)
注意,不要混淆遍历器对象的throw
方法和全局的throw
命令。
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('内部捕获', e);
}
}
};
var i = g();
i.next();
try {
throw new Error('a');
throw new Error('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 [Error: a]
// 之所以只捕获了a,是因为函数体外的catch语句块,捕获了抛出的a错误以后,就不会再继续try代码块里面剩余的语句了
如果Generator 函数内部没有部署try...catch
代码块,那么throw
方法抛出的错误,将被外部try...catch
代码块捕获。
var g = function* () {
while(true) {
yield;
console.log('内部捕获', e)
}
}
var i = g();
i.next();
try{
i.throw('a');
i.throw('b');
}catch(e) {
console.log('外部捕获', e)
}
// 外部捕获 a
如果Generator函数内部外部都没有定义try...catch
代码块,那么程序将不错,直接中断执行。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined
throw
方法抛出的错误要被内部捕获,前提是必须至少执行过一次next
方法。
var g = function* () {
try{
yield 1;
}catch(e) {
console.log('内部捕获', e)
}
}
let i = g();
i.throw(1);
// Uncaught 1
throw
方法被捕获以后,会附带执行下一条yield
表达式。也就是说,会附带执行一次next
方法。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
另外,throw
命令与g.throw
方法是无关的,两者互不影响
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
try {
throw new Error();
} catch (e) {
g.next();
}
// hello
// world
Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的catch
捕获。
function* foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err);
}
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next
方法,将返回一个value
属性等于undefined
、done
属性等于true
的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
5、Generator.prototype.return()
Generator函数返回的遍历器对象,还有一个return
方法,可以返回给定的值,并且终结遍历Generator函数
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next(); // { value: 1, done: false }
g.return('foo'); // { value: 'foo', done: true }
g.next(); // { value: undefined, done: true }
如果return
方法调用时,不提供参数,则返回值的value
属性为undefined
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next(); // { value: 1, done: false }
g.return(); // { value: undefined, done: true }
如果 Generator 函数内部有try...finally
代码块,且正在执行try
代码块,那么return
方法会导致立刻进入finally
代码块,执行完以后,整个函数才会结束。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
6、next()、throw()、return()的共同点
next()
、throw()
、return()
作用都是让Generator函数恢复执行,并且使用不同的语句替换yield
表达式
next()
是将yield
表达式替换成一个值
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;
throw()
是将yield
表达式替换成一条throw
语句
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
return()
是将yield
表达式替换成return
语句
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
7、yield* 表达式
如果在Generator函数体内部,调用另一个Generator函数。需要在前者的函数体内部,手动完成遍历。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
// 手动遍历 foo()
for (let i of foo()) {
console.log(i);
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// x
// a
// b
// y
ES6 提供了yield*
表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let i of foo()) {
yield i;
}
yield 'y';
}
for (let v of bar()) {
console.log(v);
}
从语法角度上来看,如果yield
表达式后边跟的是一个遍历器对象,需要在yield
表达式后加星号,表明它返回的是一个遍历器对象。
let delegatedIterator = (function* () {
yield 'Hello!';
yield 'Bye!';
}());
let delegatingIterator = (function* () {
yield 'Greetings!';
yield* delegatedIterator;
yield 'Ok, bye.';
}());
for(let value of delegatingIterator) {
console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
yield*
后面的Generator函数(没有return
语句时),等同于在其内部,部署一个for...of
循环
function* concat(item1, item2){
yield* item1;
yield* item2;
}
// 等同于
function* concat(item1, item2) {
for (let value of item1) {
yield value;
}
for (let value of item2) {
yield value;
}
}
任何数据结构只要有 Iterator 接口,就可以被yield*
遍历。
// 数组
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
// 字符串
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"
如果被代理的 Generator 函数有return
语句,那么就可以向代理它的 Generator 函数返回数据。
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
yield*
命令可以很方便地取出嵌套数组的所有成员
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
8、作为对象属性的Generator函数
如果一个对象的属性是Generator函数,可以写成下面这种形式
let obj = {
* gen() {
// ...
}
}
// 等同于
let obj = {
gen: function* () {
// ...
}
}
9、Generator函数的this
Generator 函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype
上的方法
function* g() {}
g.prototype.log = function() {
console.log('hello')
}
let obj = g();
obj instanceof g; // true
obj.log(); // hello
如果把g
当作普通的构造函数,并不会生效,因为g
返回的是遍历器对象,而不是this
function* g() {
this.a = 1;
}
let obj = g();
obj.next();
obj.a; // undefined
Generator函数也不能和new
一起使用,会报错
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用next
方法,又可以获得正常的this
?
下面是一个变通方法。首先,生成一个空对象,使用call
方法绑定 Generator 函数内部的this
。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
上面代码中,执行的是遍历器对象f
,但是生成的对象实例是obj
,有没有办法将这两个对象统一呢
一个办法就是将obj
换成F.prototype
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
再将F
改成构造函数,就可以对它执行new
命令了。
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
16、Generator函数的异步应用
1、传统方法
ES6诞生以前,异步编程的方法,大概有以下四种
- 回调函数
- 事件监听
- 发布/订阅
- Promise 对象
2、基本概念
异步
所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
回调函数
JavaScript语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段写在一个函数里,等到重新执行这个任务的时候,直接调用这个函数。
读取文件进行处理,是这样写的
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});
// readFile函数的第三个参数就是回调函数
Promise
Promise解决多层嵌套。它不是新的语法功能,而是新的写法,允许将回调函数的嵌套,改成链式调用。
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());
})
.catch(function (err) {
console.log(err);
});
Primise最大的问题是代码冗余
3、Generator函数
协程
协程意思是,多个线程互相协作,完成异步任务。
协程有点像函数,又有点像线程,它的运行流程大致如下
第一步,协程A开始执行;
第二步,协程A执行到一半,进入暂停,执行权转移给协程B;
第三步,(一段时间后),协程B交还执行权;
第四步,协程A恢复执行。
举例来说,读取文件的协程写法如下。
function* asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}
协程的Generator函数实现
Generator函数是协程在ES6中的实现,最大的特点就是可以交出交出函数的执行权(即暂停执行)。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield
语句注明
function* gen(x) {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
Generator函数的数据交换和错误处理
Generator函数能暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整方案:函数体外的数据交换和错误处理机制。
next
返回值的 value 属性,是Generator 函数向外输出数据;next
方法还可以接收参数,向Generator函数体内输入数据
function* gen(x) {
var y = yield x + 1;
return y;
}
let g = gen(1);
g.next(); // { value: 2, done: false }
g.next(3); // { value: 3, done: true }
Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。
function* gen(x) {
try{
var y = yield x + 1;
}catch(e) {
console.log(e)
}
}
let g = gen(1);
g.next();
g.throw('出错了')
// 出错了
异步任务的封装
使用Generator函数,执行一个真正的异步任务。
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
4、Thunk 函数
Thunk 函数是自动执行Generator函数的一种方法。
参数的求值策略
传名调用,即直接将表达式传入函数体,只在用到它的时候求值。
var x = 1;
function f(m) {
return m * 2;
}
f(x + 5)
// return (x + 5) * 2
Thunk函数的含义
编译器的“传名调用”实现,往往是将参数放到一个临时函数内,再将这个临时函数传入函数体。这个临时函数就叫做Thunk 函数。
function f(m) {
return m * 2;
}
f(x + 5)
// 等同于
var thunk = function() {
return x + 5;
}
function f(thunk) {
return thunk() * 2
}
JavaScript 语言的 Thunk 函数
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。
// ES6版本
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
function f(a, cb) {
cb(a);
}
const ft = Thunk(f);
ft(1)(console.log) // 1
Thunkify模块
生产环境的转换器,建议使用Thunkify模块
首先是安装
$ npm install thunkify
使用方式如下
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ...
});
Generator函数的流程管理
Thunk函数现在可以用户Generator函数的自动流程管理。
Generator函数可以自动执行。
function* gen() {
// ...
}
var g = gen();
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
但是,这不适合异步操作。
Thunk 函数的自动流程管理
Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
function* g() {
// ...
}
run(g);
5、co模块
基本用法
用于 Generator 函数的自动执行
下面是一个 Generator 函数,用于依次读取两个文件。
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
co 模块可以让你不用编写 Generator 函数的执行器。
var co = require('co');
co(gen);
co
函数返回一个Promise
对象,因此可以用then
方法添加回调函数。
co(gen).then(function (){
console.log('Generator 函数执行完成');
});
17、async 函数
1、含义
async函数是什么?一句话,它就是Generator函数的语法糖。
使用Generator 函数读取文件
const fs = require('fs');
// fs封装成一个promise
const readFile = function(fileName) {
return new Promise((resolve, reject) => {
fs.readFile(fileName, (err, data) => {
if(err) reject(err);
resolve(data)
})
})
}
// generator
function* gen() {
yield readFile('data/a.txt');
yield readFile('data/b.txt');
yield readFile('data/c.txt');
}
let g1 = gen();
g1.next().value.then(res => {
console.log(res.toString()) // aaaaa
return g1.next().value;
}).then(res => {
console.log(res.toString()) // bbbbb
return g1.next().value;
}).then(res => {
console.log(res.toString()) // ccccc
})
上面代码的gen
函数写成async
函数,就是下面这样
async function fn() {
let f1 = await readFile('data/a.txt') // 表示后面结果需要等待
console.log(f1.toString()) // aaaaa
let f2 = await readFile('data/b.txt')
console.log(f2.toString()) // bbbbb
let f3 = await readFile('data/c.txt')
console.log(f3.toString()) // ccccc
}
fn();
async
函数对Generator函数的改进,体现在以下四点。
(1)内置执行器
Generator 函数的执行必须依靠执行器,所以有了co
模块,而async
函数自带执行器。async
函数的执行,与普通函数一模一样,只需要一行
asyncReadFile();
(2)更好的语义
async
和await
,比起星号和yield
,语义更清楚了。async
表示函数里面有异步操作,await
表示紧跟在后面的表达式需要等待结果。
(3)更广的适应性
co
模块规定,yield
命令后面只能跟Thunk函数或Promise对象,而async
函数的await
命令后面,可以是Promise对象和原始类型的值(数值,字符串,布尔值,但这时会自动转成立即resolved的Promise对象)
(4)返回值是Promise对象
async
函数的返回值是Promise对象,可以用then
方法指定下一步操作。
2、基本用法
async
函数返回一个Promise对象,可以使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就先返回,等到异步操作完成,再继续执行函数体后面的语句。
下面是一个例子,指定多少毫秒后输出值。
function timeout(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('Hello World', 10)
// 指定10毫秒输出Hello World
由于async
返回的是Promise对象,可以作为await
命令的参数,以上代码可写成下面这种形式
async function timeout(ms) {
await new Promise((resolve, reject) => {
setTimeout(resolve, ms)
})
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('Hello World', 10)
// 指定10毫秒输出Hello World
async
函数有多种使用形式
// 函数声明
async function fn() {}
// 函数表达式
const f1 = async function() {}
// 对象的方法
let obj = { async fn() {} }
obj.fn().then()
// 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(…);
// 箭头函数
const fn = async() => {}
3、语法
返回Promise对象
async
函数返回一个Promise对象。
async
函数内部return
返回的值,会成为then
方法回调函数的参数。
async function f() {
return 'hello world';
}
f().then(res => {
console.log(res); // 'hello world'
})
async
函数内部抛出错误,会导致返回的Promise对象变为reject
状态。抛出的错误对象会被catch
方法的回调函数接收到。
async function f() {
throw new Error('出错了')
}
f().then(res => {
console.log(res);
}).catch(error => {
console.log(error);
})
// 出错了
Promise对象的状态变化
async
函数返回的Promise对象,必须等到内部所有await
命令后面的Promise对象执行完,才会发生状态改变,除非遇到return
语句或抛出错误。也就是说,只有async
函数内部的所有异步操作执行完,才会执行then
方法指定的回调函数。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
await命令
正常情况下,await
命令后面跟的是一个Promise对象,返回该对象的结果。如果不是Promise对象,返回相应的值。
async function f() {
return await 123;
// 相当于
// return 123
}
f().then(res => {
console.log(res);
})
// 123
另一种情况是,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
状态,则reject
的参数就会被catch
方法的回调函数接收到
async function fn() {
await Promise.reject('出错了')
}
fn().then(res => {
console.log(res)
}).catch(err= > {
console.log(err)
})
// 出错了
任何一个await
语句后面的Promise对象的状态变为reject
,那么整个async
函数就会中断执行。
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这是可以将第一个await
语句放在try...catch
里面,这样不管第一个异步操作是否成功,都会执行第二个await
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// hello world
另一种方法是await
后面的Promise对象再跟一个catch
方法,处理前面可能出现的错误。
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}
f()
.then(v => console.log(v))
// 出错了
// hello world
错误处理
如果await
后面的异步操作出错,就意味着async
函数返回的Promise对象被reject
async function f() {
await new Promise((resolve, reject) => {
throw new Error('出错了')
})
}
f().then(res => {
console.log(res)
}).catch(error => {
console.log(error)
})
// 出错了
防止出错的方法,可以将其放在try...catch
中
如果有多个await
语句,可以统一放在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);
}
}
使用注意点
第一点,await
命令后面的Promise对象,状态可能会为reject
,所以最好把await
命令放在try...catch
代码块中。
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
第二点,多个await
命令,如果不存在继发关系,最好让他们同时触发
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
第三点,await
命令只能用在await
函数中,用在普通函数中会报错
async function dbFuc(db) {
let docs = [{}, {}, {}];
// 报错
docs.forEach(function (doc) {
await db.post(doc);
});
}
// 正确的写法可以采用for循环
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}
// 另一种方法是使用数组的reduce方法
async function dbFuc(db) {
let docs = [{}, {}, {}];
await docs.reduce(async (_, doc) => {
await _;
await db.post(doc);
}, undefined);
}
第四点,async
函数可以保留运行堆栈
const a = async () => {
await b();
c();
};
// b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c()报错,错误堆栈将包括a()。
4、async函数的实现原理
async
函数的实现原理,就是将Generator函数和自动执行器,包装在一个函数里
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
下面给出spawn
函数的实现
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
5、与其他异步处理方法的比较
通过node
的读取文件为例,来看 async 函数与 Promise、Generator 函数的比较。
const fs = require('fs');
// fs封装成一个promise
const readFile = function(fileName) {
return new Promise((resolve, reject) => {
fs.readFile(fileName, (err, data) => {
if(err) reject(err);
resolve(data)
})
})
}
// Promise 写法
readFile('data/a.txt').then(res => {
console.log(res.toString()) // aaaaa
return readFile('data/b.txt')
}).then(res => {
console.log(res.toString()) // bbbbb
return readFile('data/c.txt')
}).then(res => {
console.log(res.toString()) // ccccc
})
// Generator函数写法
function* gen() {
yield readFile('data/a.txt');
yield readFile('data/b.txt');
yield readFile('data/c.txt');
}
let g1 = gen();
g1.next().value.then(res => {
console.log(res.toString()) // aaaaa
return g1.next().value;
}).then(res => {
console.log(res.toString()) // bbbbb
return g1.next().value;
}).then(res => {
console.log(res.toString()) // ccccc
})
// async 函数写法
async function fn() {
let [a, b, c] = await Promise.all([
readFile('data/a.txt'),
readFile('data/b.txt'),
readFile('data/c.txt')
])
console.log(a.toString())
console.log(b.toString())
console.log(c.toString())
}
6、实例:按顺序完成异步操作
async function logInOrder(urls) {
for (const url of urls) {
const response = await fetch(url);
console.log(await response.text());
}
}
但是问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求。
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
18、Set和Map数据结构
1、Set
基本用法
ES6提供了新的数据结构Set。它类似于数组,但成员的值都是唯一的,没有重复值。
Set
本身是一个构造函数,用于生成Set数据结构。
Set
函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
// Set ES6新增数据结构,类似数组
let setArr = new Set(['a', 'b', 'c']);
console.log(setArr) // {"a", "b", "c"}
// 但是set里不能有重复的值
let setArr1 = new Set(['a', 'b', 'c', 'b']);
console.log(setArr1) // {"a", "b", "c"}
// 接受类似数组的对象作为参数
const set = new Set(document.querySelectorAll('div'));
set.size // 56
可以用来去重数组
let arr = [1,2,4,5,7,4,3,6,3,2,5];
let setArr = new Set(arr)
console.log(setArr) // {1, 2, 4, 5, 7, 3, 6}
let newArr = [...setArr]
console.log(newArr) // [1, 2, 4, 5, 7, 3, 6]
Set内部判断两个值是否相等,类似于精确相等运算符(===
),主要的区别是向Set加入值时认为NaN
等于自身,而精确相等运算符认为NaN
不等于自身。
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}
// 这表明,在 Set 内部,两个NaN是相等的。
另外,两个对象是不相等的
let set = new Set();
set.add({});
set.size // 1
set.add({});
set.size // 2
Set实例的属性和方法
Set结构的实例有以下属性
Set.prototype.constructor
:构造函数,默认就是Set
函数。Set.prototype.size
:返回Set
实例的成员总数。
Set实例的方法分为两大类:操作方法和遍历方法
Set.prototype.add(value)
: 添加某个值,返回Set结构本身Set.prototype.delete(value)
: 删除某个值,返回一个布尔值,表示是否删除成功Set.prototype.has(value)
: 返回一个布尔值,表示该值是否为Set的成员Set.prototype.clear()
: 清除所有成员,没有返回值
let setArr = new Set();
setArr.add('a');
setArr.add('b');
// 也可以采用链式写法
setArr.add('c').add('d')
setArr.delete('b');
console.log(setArr.has('b')) // true
setArr.clear();
console.log(setArr); // {}
Array.from
方法可以将 Set 结构转为数组。
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
遍历操作
Set 结构的实例有四个遍历方法,可以用于遍历成员。
Set.prototype.keys()
:返回键名的遍历器Set.prototype.values()
:返回键值的遍历器Set.prototype.entries()
:返回键值对的遍历器Set.prototype.forEach()
:使用回调函数遍历每个成员
Set
的遍历顺序就是插入顺序。
(1)keys()
、values()
、entries()
let setArr = new Set(['a', 'b', 'c', 'd']);
for (let item of setArr) { // 默认是value
console.log(item) // a b c d
}
for (let key of setArr.keys()) {
console.log(key) // a b c d
}
for (let value of setArr.values()) {
console.log(value) // a b c d
}
for (let item of setArr.entries()) {
console.log(item) // ["a", "a"] ["b", "b"] ["c", "c"] ["d", "d"]
}
for (let [k,v] of setArr.entries()) {
console.log(k, v) // a a b b c c d d
}
(2)forEach()
Set 结构的实例与数组一样,也拥有forEach
方法,用于对每个成员执行某种操作,没有返回值。
let setArr = new Set(['a', 'b', 'c', 'd']);
setArr.forEach((key, value) => {
console.log(key,value) // a a b b c c d d
})
(3)遍历的应用
扩展运算符(...
)内部使用for...of
循环,所以也可用于Set结构
let set = new Set(['red', 'green', 'blue']);
let arr = [...set];
// ['red', 'green', 'blue']
扩展运算符与Set结构相结合,就可以去除数组的重复成员
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]
而且,数组的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}
因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。
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}
// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from
方法。
// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6
// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6
2、WeakSet
含义
WeakSet结构与Set类似,也是不重复值的集合。但是,它与Set有两个区别。
首先,WeakSet 的成员只能是对象,而不能是其他类型的值
const ws = new WeakSet();
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak set
语法
WeakSet 是一个构造函数,可以使用new
命令,创建WeakSet数据结构。
const ws = new WeakSet();
WeakSet 结构有以下三个方法
- WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
- WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
- WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
const ws = new WeakSet();
const obj = {};
const foo = {};
ws.add(window);
ws.add(obj);
ws.has(window); // true
ws.has(foo); // false
ws.delete(window);
ws.has(window); // false
WeakSet 没有size
属性,无法遍历它的成员。
并且,WeakSet没有clear()
方法。
3、Map
含义和基本用法
JavaScript的对象(Object),本质上是键值对的集合,但是传统上只能用字符串当作键。
为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以用做键。也就是说,Object结构提供了“字符串-值”的对应,而Map结构提供了“值-值”的对应
const ws = new WeakSet();
const obj = {};
const foo = {};
ws.add(window);
ws.add(obj);
ws.has(window); // true
ws.has(foo); // false
ws.delete(window);
ws.has(window); // false
作为构造函数,Map也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
Set
和Map
都可以用来生成新的 Map。
const set = new Set([
['foo', 1],
['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1
const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3
如果对同一个键多次赋值,后面的值将覆盖前面的值。
const map = new Map();
map
.set(1, 'aaa')
.set(1, 'bbb');
map.get(1) // "bbb"
注意,只有对同一个对象的引用,Map结构才将其视为同一个键。
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
// 实际上这是两个不同的数组实例,内存地址不一样
同理,同样的值的两个实例,在 Map 结构中被视为两个键。
const map = new Map();
const k1 = ['a'];
const k2 = ['a'];
map
.set(k1, 111)
.set(k2, 222);
map.get(k1) // 111
map.get(k2) // 222
如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0
和-0
就是一个键,布尔值true
和字符串true
则是两个不同的键。另外,undefined
和null
也是两个不同的键。虽然NaN
不严格相等于自身,但 Map 将其视为同一个键。
let map = new Map();
map.set(-0, 123);
map.get(+0) // 123
map.set(true, 1);
map.set('true', 2);
map.get(true) // 1
map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3
map.set(NaN, 123);
map.get(NaN) // 123
实例的属性和操作方法
(1)size属性
size
属性返回Map结构的成员总数
const map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size // 2
(2)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
// 同样,也可以使用链式写法
let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
(3)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!
(4)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
(5)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
(6)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([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...
)
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
结合数组的map
方法、filter
方法,可以实现 Map 的遍历和过滤(Map 本身没有map
和filter
方法)。
const map0 = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');
const map1 = new Map(
[...map0].filter(([k, v]) => k < 3)
);
// 产生 Map 结构 {1 => 'a', 2 => 'b'}
const map2 = new Map(
[...map0].map(([k, v]) => [k * 2, '_' + v])
);
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}
此外,Map 还有一个forEach
方法,与数组的forEach
方法类似,也可以实现遍历。
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});
与其他数据结构的相互转换
(1)Map转为数组
Map转数组最方便的方法,就是使用扩展运算符(...
)
const myMap = new Map()
.set(true, 10)
.set(undefined, 'hello')
[...myMap] // [[true, 10], [undefined, 'hello']]
(2)数组转Map
将数组传入 Map 构造函数,就可以转为 Map
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
(3)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 }
(4)对象转Map
对象转为 Map 可以通过Object.entries()
。
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));
(5)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"]]]'
(6)JSON转Map
JSON 转为 Map,正常情况下,所有键名都是字符串。
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
4、WeakMap
含义
WeakMap
结构与Map
结构类似,也是用于生成键值对的集合。
// WeakMap 可以使用 set 方法添加成员
const wm1 = new WeakMap();
const key = {foo: 1};
wm1.set(key, 2);
wm1.get(key) // 2
// WeakMap 也可以接受一个数组,
// 作为构造函数的参数
const k1 = [1, 2, 3];
const k2 = [4, 5, 6];
const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]);
wm2.get(k2) // "bar"
WeakMap
与Map
的区别有两点。
首先,WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。
const map = new WeakMap();
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key
WeakMap的语法
WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有keys()
、values()
和entries()
方法),也没有size
属性。
WeakMap
只有四个方法可用:get()
、set()
、has()
、delete()
。
const wm = new WeakMap();
// size、forEach、clear 方法都不存在
wm.size // undefined
wm.forEach // undefined
wm.clear // undefined
19、数值的扩展
1、二进制和八进制表示法
ES6提供二进制和八进制新的表示法,分别用前缀0b
(或oB
)和 0o
(或0O
)表示
0b111110111 === 503 // true
0o767 === 503 // true
2、Number.isFinite(), Number.isNaN()
ES6在Number
对象上,新增了Number.isFinite()
和Number.isNaN()
两个方法
Number.isFinite()
用来检查一个数值是否为有限的,即不是Infinity
Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false
注意,如果参数类型不是数值,一律返回false
Number.isNaN()
用来检查一个值是不是NaN
Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true' / 0) // true
Number.isNaN('true' / 'true') // true
注意,如果参数类型不是NaN
,一律返回false
3、Number.parseInt()、Number.parseFloat()
ES6将全局方法parseInt()
和parseFloat()
,移植到Number
上面,行为完全保持不变。这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。
// ES5的写法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45
// ES6的写法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45
4、Number.isInteger()
Number.isInteger()
用来判断一个值是否为整数。
Number.isInteger(25) // true
Number.isInteger(25.1) // false
Number.isInteger(25.0) // true
Number.isInteger('abc') // false
Number.isInteger(true) // false
Number.isInteger(null) // false
5、Number.EPSILON
ES6 在Number
对象上,新增了一个极小的常量Number.EPSILON
。根据规则,它表示1与大于1的最小浮点数之间的差。
对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的1.00..001
,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方
Number.EPSILON === Math.pow(2, -52)
// true
Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// "0.00000000000000022204"
Number.EPSILON
实际上是 JavaScript 能够表示的最小精度。
6、安全整数和Number.isSafeInteger
JavaScript 能够准确表示的整数范围在-2^53
到2^53
之间(不含两个端点),超过这个范围,无法精确表示这个值。
Math.pow(2, 53) // 9007199254740992
9007199254740992 // 9007199254740992
9007199254740993 // 9007199254740992
Math.pow(2, 53) === Math.pow(2, 53) + 1
// true
ES6 引入了Number.MAX_SAFE_INTEGER
和Number.MIN_SAFE_INTEGER
这两个常量,用来表示这个范围的上下限
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// true
Number.MAX_SAFE_INTEGER === 9007199254740991
// true
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
// true
Number.MIN_SAFE_INTEGER === -9007199254740991
// true
Number.isSafeInteger()
用来判断一个整数是否在安全范围内。
Number.isSafeInteger('a') // false
Number.isSafeInteger(null) // false
Number.isSafeInteger(NaN) // false
Number.isSafeInteger(Infinity) // false
Number.isSafeInteger(-Infinity) // false
Number.isSafeInteger(3) // true
Number.isSafeInteger(1.2) // false
Number.isSafeInteger(9007199254740990) // true
Number.isSafeInteger(9007199254740992) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false
7、Math对象的扩展
ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。
Math.trunc()
Math.trunc
方法用于去除一个数值的小数部分,返回整数部分。
Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.trunc(-4.9) // -4
Math.trunc(-0.1234) // -0
对于非数值,Math.trunc
内部使用Number
方法先将其转为数值。
Math.trunc('123.456') // 123
Math.trunc(true) //1
Math.trunc(false) // 0
Math.trunc(null) // 0
对于空值和无法截取的值,返回NaN
。
Math.trunc(NaN); // NaN
Math.trunc('foo'); // NaN
Math.trunc(); // NaN
Math.trunc(undefined) // NaN
Math.sign()
Math.sign
方法用来判断一个值是正数、负数还是0。对于非数值,会将其先转换为数值。
它会返回五种值。
- 参数为正数:返回
+1
- 参数为负数:返回
-1
- 参数为 0:返回
0
- 参数为 -0:返回
-0
- 其他值:返回
NaN
Math.sign(-5) // -1
Math.sign(5) // +1
Math.sign(0) // +0
Math.sign(-0) // -0
Math.sign(NaN) // NaN
Math.sign('') // 0
Math.sign(true) // +1
Math.sign(false) // 0
Math.sign(null) // 0
Math.sign('9') // +1
Math.sign('foo') // NaN
Math.sign() // NaN
Math.sign(undefined) // NaN
Math.cbrt()
Math.cbrt
方法用于计算一个值的立方根。对于非数值,先将其转换为数值。
Math.cbrt(-1) // -1
Math.cbrt(0) // 0
Math.cbrt(1) // 1
Math.cbrt(2) // 1.2599210498948734
Math.cbrt('8') // 2
Math.cbrt('hello') // NaN
Math.clz32()
Math.clz32()
方法将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。对于非数值,先将其转换为数值。
Math.clz32(0) // 32
Math.clz32(1) // 31
Math.clz32(1000) // 22
Math.clz32(0b01000000000000000000000000000000) // 1
Math.clz32(0b00100000000000000000000000000000) // 2
// 对于小数,只考虑整数部分
Math.clz32(3.2) // 30
Math.clz32(3.9) // 30
// 非数值,先转换
Math.clz32() // 32
Math.clz32(NaN) // 32
Math.clz32(Infinity) // 32
Math.clz32(null) // 32
Math.clz32('foo') // 32
Math.clz32([]) // 32
Math.clz32({}) // 32
Math.clz32(true) // 31
上面代码中,0 的二进制形式全为 0,所以有 32 个前导 0;1 的二进制形式是0b1
,只占 1 位,所以 32 位之中有 31 个前导 0;1000 的二进制形式是0b1111101000
,一共有 10 位,所以 32 位之中有 22 个前导 0。
Math.imul()
Math.imul
方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。
Math.imul(2, 4) // 8
Math.imul(-1, 8) // -8
Math.imul(-2, -2) // 4
// 如果只考虑最后 32 位,大多数情况下,Math.imul(a, b)与a * b的结果是相同的,即该方法等同于(a * b)|0的效果(超过 32 位的部分溢出)
等等其他方法,一般情况用不到。
8、指数运算符
ES2016新增了一个指数运算符**
2**3 // 8
这个运算符的一个特点是右运算,即多个指数运算符连用时,从右边开始计算。
// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512
指数运算符可以与等号结合,形成一个新的赋值运算符(**=
)。
let a = 1.5;
a **= 2;
// 等同于 a = a * a;
let b = 4;
b **= 3;
// 等同于 b = b * b * b;
9、BigInt 数据类型
ES2020 引入了一种新的数据类型 BigInt(大整数)。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。
为了与 Number 类型区别,BigInt 类型的数据必须添加后缀n
。
1234 // 普通整数
1234n // BigInt
// BigInt 的运算
1n + 2n // 3n
BigInt 同样可以使用各种进制表示,都要加上后缀n
。
0b1101n // 二进制
0o777n // 八进制
0xFFn // 十六进制
BigInt 与普通整数是两种值,它们之间并不相等。
42n === 42 // false
typeof
运算符对于 BigInt 类型的数据返回bigint
。
typeof 123n // 'bigint'
BigInt 可以使用负号(-
),但是不能使用正号(+
),因为会与 asm.js 冲突。
-42n // 正确
+42n // 报错
BigInt对象
JavaScript 原生提供BigInt
对象,可以用作构造函数生成 BigInt 类型的数值。转换规则基本与Number()
一致,将其他类型的值转为 BigInt。
BigInt(123) // 123n
BigInt('123') // 123n
BigInt(false) // 0n
BigInt(true) // 1n
// 必须有参数,并且参数可以正常转换为数值
new BigInt() // TypeError
BigInt(undefined) //TypeError
BigInt(null) // TypeError
BigInt('123n') // SyntaxError
BigInt('abc') // SyntaxError
// 如果参数是小数,也会报错
BigInt(1.5) // RangeError
BigInt('1.5') // SyntaxError
BigInt 对象继承了 Object 对象的两个实例方法。
BigInt.prototype.toString()
BigInt.prototype.valueOf()
它还继承了 Number 对象的一个实例方法。
BigInt.prototype.toLocaleString()
此外,还提供了三个静态方法。
BigInt.asUintN(width, BigInt)
: 给定的 BigInt 转为 0 到 2width - 1 之间对应的值。BigInt.asIntN(width, BigInt)
:给定的 BigInt 转为 -2width - 1 到 2width - 1 - 1 之间对应的值。BigInt.parseInt(string[, radix])
:近似于Number.parseInt()
,将一个字符串转换成指定进制的 BigInt。
转换规则
可以使用Boolean()
、Number()
和String()
这三个方法,将 BigInt 可以转为布尔值、数值和字符串类型。
Boolean(0n) // false
Boolean(1n) // true
Number(1n) // 1
String(1n) // "1"
另外,取反运算符(!
)也可以将 BigInt 转为布尔值。
!0n // true
!1n // false
数学运算
数学运算方面,BigInt 类型的+
、-
、*
和**
这四个二元运算符,与 Number 类型的行为一致。除法运算/
会舍去小数部分,返回一个整数。
9n / 5n
// 1n
几乎所有的数值运算符都可以用在 BigInt,但是有两个例外。
- 不带符号的右移位运算符
>>>
- 一元的求正运算符
+
BigInt 不能与普通数值进行混合运算。
1n + 1.3 // 报错
其他运算
BigInt 对应的布尔值,与 Number 类型一致,即0n
会转为false
,其他值转为true
。
if (0n) {
console.log('if');
} else {
console.log('else');
}
// else
20、ES2018新增(正则扩展及标签函数)
1、正则的扩展
s 修饰符:dotAll模式
正则表达式中,点(.
)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u
修饰符解决;另一个是行终止符(line terminator character)。
所谓行终止符,就是该字符表示一行的终结。以下四个字符属于“行终止符”。
- U+000A 换行符(
\n
) - U+000D 回车符(
\r
) - U+2028 行分隔符(line separator)
- U+2029 段分隔符(paragraph separator)
/foo.bar/.test('foo\nbar')
// false
ES2018引入s
修饰符,使得.
可以匹配任意单个字符。
/foo.bar/s.test('foo\nbar') // true
let reg = /^\w+.\w+$/;
let reg1 = /^\w+.\w+$/s;
let str = 'welcome$toChina';
let str1 = 'welcome\ntoChina';
console.log(reg.test(str)) // true
console.log(reg.test(str1)) // false
console.log(reg1.test(str1)) // true
这被称为dotAll
模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个dotAll
属性,返回一个布尔值,表示该正则表达式是否处在dotAll
模式。
const re = /foo.bar/s;
// 另一种写法
// const re = new RegExp('foo.bar', 's');
re.test('foo\nbar') // true
re.dotAll // true
re.flags // 's'
/s
修饰符和多行修饰符/m
不冲突,两者一起使用的情况下,.
匹配所有字符,而^
和$
匹配每一行的行首和行尾。
具名组匹配
简介
正则表达式使用圆括号进行组匹配。
let reg = /(\d{4})-(\d{2})-(\d{2})/;
使用exec
方法,就可以将这三组匹配结果提取出来。
let str = '2020-04-03';
let reg = /(\d{4})-(\d{2})-(\d{2})/;
console.log(reg.exec(str)) // ["2020-04-03", "2020", "04", "03", index: 0, input: "2020-04-03", groups: {…}]
let dateArr = reg.exec(str)
let year = dateArr[1]
let month = dateArr[2]
let day = dateArr[3]
组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号引用,要是组的顺序变了,引用的时候就必须修改序号。
ES2018引入了具名组匹配,允许为每一个组匹配指定一个名字,既便于阅读代码,也便于引用。
let str = '2020-04-03';
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
console.log(str.match(reg).groups) // {year: "2020", month: "04", day: "03"}
let {year, month, day} = str.match(reg).groups
console.log(year, month, day) // 2020 04 03
如果具名组没有匹配,那么对应的groups
属性会是undefined
const RE_OPT_A = /^(?<as>a+)?$/;
const matchObj = RE_OPT_A.exec('');
matchObj.groups.as // undefined
'as' in matchObj.groups // true
解构赋值和替换
有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。
let str = '2020-04-03';
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
console.log(str.match(reg).groups) // {year: "2020", month: "04", day: "03"}
let {year, month, day} = str.match(reg).groups
console.log(year, month, day) // 2020 04 03
字符串替换时,可以使用$<组名>
引用具名组。
let str = '2020-04-03';
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
str = str.replace(reg, '$<year>/$<month>/$<day>')
console.log(str) // 2020/04/03
replace
方法的第二个参数也可以是一个回调函数
let str = '2020-04-03';
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
str1 = str.replace(reg, (...args) => {
console.log(args) // ["2020-04-03", "2020", "04", "03", 0, "2020-04-03", {…}]
let {year, month, day} = args[args.length - 1];
return `${year}/${month}/${day}`
})
console.log(str1) // 2020/04/03
引用
如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>
的写法。
let reg = /^(?<str>welcome)-\k<str>$/;
let str2 = 'str-str';
let str3 = 'welcome-welcome'
console.log(reg.test(str2)) // false
console.log(reg.test(str3)) // true
数字引用(\1
)依然有效。
const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false
这两种引用语法还可以同时使用。
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;
RE_TWICE.test('abc!abc!abc') // true
RE_TWICE.test('abc!abc!ab') // false
21、Proxy
1、概述
Proxy 用于修改某些操作的默认行为,等同于在语言层面作出修改,所以属于一种“元编程”,即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
let obj = {
name: 'Jason'
}
let newObj = new Proxy(obj, {
get(target, property) {
console.log(target, property) // {name: "Jason"} "name"
return `您访问了${property}属性`
}
})
console.log(newObj.name) // 您访问了name属性
ES6原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler)
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler
参数的写法。其中,new Proxy
生成一个Proxy
实例,target
表示所要拦截的对象,handler
也是一个对象,用来定制拦截行为。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
作为构造函数,Proxy
接收两个参数,第一个参数是所要代理目标的对象,第二个参数是配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。
注意,要使得Proxy
起作用,必须对Proxy
实例进行操作,而不是针对目标对象进行操作。
如果handler
没有设置任何拦截,那就等同于直接通向原对象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
Proxy 实例也可以作为其他对象的原型对象。
var proxy = new Proxy({}, {
get: function(target, propKey) {
return 35;
}
});
let obj = Object.create(proxy);
obj.time // 35
2、Proxy实例的方法
get()
get
方法用于拦截某个属性的读取操作,可以接收三个参数,依次为目标对象,属性名和proxy实例本身,最后一个参数可选。
let obj = {
name: 'Jason'
}
let newObj = new Proxy(obj, {
get(target, property) {
if (property in target) {
return target[property]
} else {
// throw new ReferenceError(`${property}属性不在此对象内`)
console.warn(`${property}属性不在此对象内`)
return '警告'
}
}
})
console.log(newObj.name) // Jason
console.log(newObj.age) // ⚠️ age属性不在此对象内 警告
get
方法可以继承。
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET ' + propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.foo // "GET foo"
下面的例子则是利用get
拦截,实现一个生成各种 DOM 节点的通用函数dom
const DOM = new Proxy({}, {
get(target, property) {
console.log(target, property) // {} "div"
return function(attr, ...children) {
console.log(attr, children) // {id: "div1", class: "box"} (2) ["我是一个div", "这是我的内容"]
const el = document.createElement(property)
// 添加属性
for (let attrKey of Object.keys(attr)) {
el.setAttribute(attrKey, attr[attrKey])
}
// 添加内容
for (let child of children) {
if (typeof child == 'string') {
child = document.createTextNode(child)
}
el.appendChild(child)
}
return el;
}
}
})
// 可以创建各种原生标签
let oDiv = DOM.div(
{id: 'div1', class: 'box'}, '我是一个div', '这是我的内容',
DOM.a({href: 'https://www.baidu.com/'}, '我是百度链接')
)
console.log(oDiv)
window.onload = function() {
document.body.appendChild(oDiv)
}
set()
set
方法用来拦截某个属性的赋值操作,可以接收四个参数,依次为目标对象、属性名、属性值和Proxy实例本身,最后一个参数可选。
假定Person
对象有一个age
属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy
保证age
的属性值符合要求
let validator = {
set: function(obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// 对于满足条件的 age 属性以及其他属性,直接保存
obj[prop] = value;
}
};
let person = new Proxy({}, validator);
person.age = 100;
person.age // 100
person.age = 'young' // 报错
person.age = 300 // 报错
有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合get
和set
方法,就可以做到防止这些内部属性被外部读写。
const handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
},
set (target, key, value) {
invariant(key, 'set');
target[key] = value;
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property
apply()
apply
方法拦截函数的调用、call
和apply
操作。
apply
方法接收三个参数,依次为目标对象、目标对象的上下文对象(即this
)、目标对象的参数数组。
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p()
// "I am the proxy"
下面是另外一个例子。
function sum(a, b) {
return a + b;
}
let newSum = new Proxy(sum, {
apply(target, context, args) {
// console.log(target, context, args)
console.log(...arguments)
return Reflect.apply(...arguments)
}
})
console.log(newSum(2,4)) // 6
has()
has
方法用来拦截hasProperty
操作,即判断对象是否有某个属性时,这个方法会生效。典型的操作就是in
运算符。
has
方法接收两个参数,依次为目标对象、需查询的属性名。
let obj = {
a: 1,
b: 2
}
let newObj = new Proxy(obj, {
deleteProperty(target, property) {
// console.log(target, property)
console.log(`您要删除${property}属性`)
delete target[property]
},
has(target, property) {
console.log('判断是否存在,调用has方法')
return property in target
}
})
// delete newObj.a;
// console.log(newObj) // {b: 2}
console.log('a' in obj) // true
construct()
construct
方法用于拦截new
命令。
construct
方法接收三个参数,依次是目标对象、构造函数的参数对象(args)、创建实例对象时,new
命令作用的构造函数
var handler = {
construct (target, args, newTarget) {
return new target(...args);
}
};
var p = new Proxy(function () {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };
}
});
(new p(1)).value
// "called: 1"
// 10
construct
方法返回的必须是一个对象,否则报错
var p = new Proxy(function() {}, {
construct: function(target, argumentsList) {
return 1;
}
});
new p() // 报错
// Uncaught TypeError: 'construct' on proxy: trap returned non-object ('1')
deleteProperty()
deleteProperty
方法用于拦截delete
操作,如果这个方法抛出错误或返回false
,当前属性就无法被delete
命令删除。
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
delete target[key];
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property
defineProperty()
defineProperty
方法拦截了Object.defineProperty
操作。
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不会生效
上面代码中,defineProperty
方法返回false
,导致添加新属性总是无效。
注意,如果目标对象不可扩展(non-extensible),则defineProperty
不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则defineProperty
方法不得改变这两个设置。
**getOwnPropertyDescriptor() **
getOwnPropertyDescriptor
方法拦截Object.getOwnPropertyDescriptor()
,返回一个属性描述对象或者undefined
。
var handler = {
getOwnPropertyDescriptor (target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
}
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }
getPrototypeOf()
getPrototypeOf
方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。
Object.prototype.__proto__
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof
var proto = {};
var p = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
Object.getPrototypeOf(p) === proto // true
注意,getPrototypeOf
方法的返回值必须是对象或者null
,否则报错。另外,如果目标对象不可扩展(non-extensible), getPrototypeOf
方法必须返回目标对象的原型对象。
isExtensible()
isExtensible
方法拦截Object.isExtensible
操作。
var p = new Proxy({}, {
isExtensible: function(target) {
console.log("called");
return true;
}
});
Object.isExtensible(p)
// "called"
// true
注意,该方法只能返回布尔值,否则返回值会被自动转为布尔值。
这个方法有一个强限制,它的返回值必须与目标对象的isExtensible
属性保持一致,否则就会抛出错误。
Object.isExtensible(proxy) === Object.isExtensible(target)
var p = new Proxy({}, {
isExtensible: function(target) {
return false;
}
});
Object.isExtensible(p)
// Uncaught TypeError: 'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
ownKeys()
ownKeys
方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for...in
循环
let target = {
a: 1,
b: 2,
c: 3
};
let handler = {
ownKeys(target) {
return ['a'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// [ 'a' ]
注意,使用Object.keys
方法时,有三类属性会被ownKeys
方法自动过滤,不会返回。
- 目标对象上不存在的属性
- 属性名为 Symbol 值
- 不可遍历(
enumerable
)的属性
preventExtensions()
preventExtensions
方法拦截Object.preventExtensions()
。该方法必须返回一个布尔值,否则会被自动转为布尔值。
这个方法有一个限制,只有目标对象不可扩展时(即Object.isExtensible(proxy)
为false
),proxy.preventExtensions
才能返回true
,否则会报错。
var proxy = new Proxy({}, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(proxy)
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned truish but the proxy target is extensible
为了防止出现这个问题,通常要在proxy.preventExtensions
方法里面,调用一次Object.preventExtensions
。
var proxy = new Proxy({}, {
preventExtensions: function(target) {
console.log('called');
Object.preventExtensions(target);
return true;
}
});
Object.preventExtensions(proxy)
// "called"
// Proxy {}
setPrototypeOf()
setPrototypeOf
方法主要用来拦截Object.setPrototypeOf
方法。
var handler = {
setPrototypeOf (target, proto) {
throw new Error('Changing the prototype is forbidden');
}
};
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
注意,该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(non-extensible),setPrototypeOf
方法不得改变目标对象的原型。
3、Proxy.revocable()
Proxy.revocable
方法返回一个可取消的 Proxy 实例
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy.revocable
方法返回一个对象,该对象的proxy
属性是Proxy
实例,revoke
属性是一个函数,可以取消Proxy
实例。
Proxy.revocable
的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
4、this问题
虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this
关键字会指向 Proxy 代理。
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
上面代码中,一旦proxy
代理target.m
,后者内部的this
就是指向proxy
,而不是target
。
22、Reflect
1、概述
Reflect
对象与Proxy
对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect
对象的设计目的有这样几个。
(1)将Object
对象的一些明显属于语言内部的方法(比如Object.defineProperty
),放到Reflect
对象上。现阶段,某些方法同时在Object
和Reflect
对象上部署,未来的新方法将只部署在Reflect
对象上。也就是说,从Reflect
对象上可以拿到语言内部的方法。
(2)修改某些Object
方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)
在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)
则会返回false
。
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
(3) 让Object
操作都变成函数行为。某些Object
操作是命令式,比如name in obj
和delete obj[name]
,而Reflect.has(obj, name)
和Reflect.deleteProperty(obj, name)
让它们变成了函数行为
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
(4)Reflect
对象的方法与Proxy
对象的方法一一对应,只要是Proxy
对象的方法,就能在Reflect
对象上找到对应的方法。这就让Proxy
对象可以方便地调用对应的Reflect
方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy
怎么修改默认行为,你总可以在Reflect
上获取默认行为。
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target, name, value, receiver);
if (success) {
console.log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
2、静态方法
这些静态方法与Proxy
对象的方法是一一对应的。
Reflect.get(target, name, receiver)
Reflect.get
方法查找并返回target
对象的name
属性,如果没有该属性,则返回undefined
。
var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
}
Reflect.get(myObject, 'foo') // 1
Reflect.get(myObject, 'bar') // 2
Reflect.get(myObject, 'baz') // 3
如果name
属性部署了读取函数(getter),则读取函数的this
绑定receiver
。
var myObject = {
foo: 1,
bar: 2,
get baz() {
return this.foo + this.bar;
},
};
var myReceiverObject = {
foo: 4,
bar: 4,
};
Reflect.get(myObject, 'baz', myReceiverObject) // 8
如果第一个参数不是对象,Reflect.get
方法会报错。
Reflect.get(1, 'foo') // 报错
Reflect.get(false, 'foo') // 报错
Reflect.set(target, name, value, receiver)
Reflect.set
方法设置target
对象的name
属性等于value
。
var myObject = {
foo: 1,
set bar(value) {
return this.foo = value;
},
}
myObject.foo // 1
Reflect.set(myObject, 'foo', 2);
myObject.foo // 2
Reflect.set(myObject, 'bar', 3)
myObject.foo // 3
注意,如果 Proxy
对象和 Reflect
对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver
,那么Reflect.set
会触发Proxy.defineProperty
拦截。
let p = {
a: 'a'
};
let handler = {
set(target, key, value, receiver) {
console.log('set');
Reflect.set(target, key, value, receiver)
},
defineProperty(target, key, attribute) {
console.log('defineProperty');
Reflect.defineProperty(target, key, attribute);
}
};
let obj = new Proxy(p, handler);
obj.a = 'A';
// set
// defineProperty
Reflect.has(obj, name)
Reflect.has
方法对应name in obj
里面的in
运算符。
var myObject = {
foo: 1,
};
// 旧写法
'foo' in myObject // true
// 新写法
Reflect.has(myObject, 'foo') // true
Reflect.deleteProperty(obj, name)
Reflect.deleteProperty
方法等同于delete obj[name]
,用于删除对象的属性。
const myObj = { foo: 'bar' };
// 旧写法
delete myObj.foo;
// 新写法
Reflect.deleteProperty(myObj, 'foo');
该方法返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回true
;删除失败,被删除的属性依然存在,返回false
。
Reflect.construct(target, args)
Reflect.construct
方法等同于new target(...args)
,这提供了一种不使用new
,来调用构造函数的方法。
function Greeting(name) {
this.name = name;
}
// new 的写法
const instance = new Greeting('张三');
// Reflect.construct 的写法
const instance = Reflect.construct(Greeting, ['张三']);
Reflect.getPrototypeOf(obj)
Reflect.getPrototypeOf
方法用于读取对象的__proto__
属性,对应Object.getPrototypeOf(obj)
。
const myObj = new FancyThing();
// 旧写法
Object.getPrototypeOf(myObj) === FancyThing.prototype;
// 新写法
Reflect.getPrototypeOf(myObj) === FancyThing.prototype;
Reflect.getPrototypeOf
和Object.getPrototypeOf
的一个区别是,如果参数不是对象,Object.getPrototypeOf
会将这个参数转为对象,然后再运行,而Reflect.getPrototypeOf
会报错。
Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0}
Reflect.getPrototypeOf(1) // 报错
Reflect.setPrototypeOf(obj, newProto)
Reflect.setPrototypeOf
方法用于设置目标对象的原型(prototype),对应Object.setPrototypeOf(obj, newProto)
方法。它返回一个布尔值,表示是否设置成功。
const myObj = {};
// 旧写法
Object.setPrototypeOf(myObj, Array.prototype);
// 新写法
Reflect.setPrototypeOf(myObj, Array.prototype);
myObj.length // 0
Reflect.apply(func, thisArg, args)
Reflect.apply
方法等同于Function.prototype.apply.call(func, thisArg, args)
,用于绑定this
对象后执行给定函数。
const ages = [11, 33, 12, 54, 18, 96];
// 旧写法
const youngest = Math.min.apply(Math, ages);
const oldest = Math.max.apply(Math, ages);
const type = Object.prototype.toString.call(youngest);
// 新写法
const youngest = Reflect.apply(Math.min, Math, ages);
const oldest = Reflect.apply(Math.max, Math, ages);
const type = Reflect.apply(Object.prototype.toString, youngest, []);
Reflect.defineProperty(target, propertyKey, attributes)
Reflect.defineProperty
方法基本等同于Object.defineProperty
,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用Reflect.defineProperty
代替它。
function MyDate() {
/*…*/
}
// 旧写法
Object.defineProperty(MyDate, 'now', {
value: () => Date.now()
});
// 新写法
Reflect.defineProperty(MyDate, 'now', {
value: () => Date.now()
});
这个方法可以与Proxy.defineProperty
配合使用。
const p = new Proxy({}, {
defineProperty(target, prop, descriptor) {
console.log(descriptor);
return Reflect.defineProperty(target, prop, descriptor);
}
});
p.foo = 'bar';
// {value: "bar", writable: true, enumerable: true, configurable: true}
p.foo // "bar"
Reflect.getOwnPropertyDescriptor(target, propertyKey)
Reflect.getOwnPropertyDescriptor
基本等同于Object.getOwnPropertyDescriptor
,用于得到指定属性的描述对象,将来会替代掉后者。
var myObject = {};
Object.defineProperty(myObject, 'hidden', {
value: true,
enumerable: false,
});
// 旧写法
var theDescriptor = Object.getOwnPropertyDescriptor(myObject, 'hidden');
// 新写法
var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, 'hidden');
Reflect.isExtensible (target)
Reflect.isExtensible
方法对应Object.isExtensible
,返回一个布尔值,表示当前对象是否可扩展。
const myObject = {};
// 旧写法
Object.isExtensible(myObject) // true
// 新写法
Reflect.isExtensible(myObject) // true
Reflect.preventExtensions(target)
Reflect.preventExtensions
对应Object.preventExtensions
方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。
var myObject = {};
// 旧写法
Object.preventExtensions(myObject) // Object {}
// 新写法
Reflect.preventExtensions(myObject) // true
Reflect.ownKeys (target)
Reflect.ownKeys
方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames
与Object.getOwnPropertySymbols
之和。
var myObject = {
foo: 1,
bar: 2,
[Symbol.for('baz')]: 3,
[Symbol.for('bing')]: 4,
};
// 旧写法
Object.getOwnPropertyNames(myObject)
// ['foo', 'bar']
Object.getOwnPropertySymbols(myObject)
//[Symbol(baz), Symbol(bing)]
// 新写法
Reflect.ownKeys(myObject)
// ['foo', 'bar', Symbol(baz), Symbol(bing)]