1 let 和 const命令
在ES6中,我们通常实用 let 表示变量,const 表示常量,并且 let 和 const 都是块级作用域,且在当前作用域有效不能重复声明。
1.1 let 命令
let 命令的用法和 var 相似,但是 let 只在所在代码块内有效。
基础用法:
{let a = 1;let b = 2;}
并且 let 有以下特点:
- 不存在变量提升:
在ES6之前,我们var声明一个变量一个函数,都会伴随着变量提升的问题,导致实际开发过程经常出现一些逻辑上的疑惑,按照一般思维习惯,变量都是需要先声明后使用。
// varconsole.log(v1); // undefinedvar v1 = 2;// 由于变量提升 代码实际如下var v1;console.log(v1)v1 = 2;// letconsole.log(v2); // ReferenceErrorlet v2 = 2;
- 不允许重复声明:
let和const在相同作用域下,都不能重复声明同一变量,并且不能在函数内重新声明参数。
// 1. 不能重复声明同一变量// 报错function f1 (){let a = 1;var a = 2;}// 报错function f2 (){let a = 1;let a = 2;}// 2. 不能在函数内重新声明参数// 报错function f3 (a1){let a1;}// 不报错function f4 (a2){{let a2}}
1.2 const 命令
const 声明一个只读的常量。
基础用法:
const PI = 3.1415926;console.log(PI); // 3.1415926
注意点:
const声明后,无法修改值;
const PI = 3.1415926;PI = 3;// TypeError: Assignment to constant variable.
const声明时,必须赋值;
const a ;// SyntaxError: Missing initializer in const declaration.
const声明的常量,let不能重复声明;
const PI = 3.1415926;let PI = 0;// Uncaught SyntaxError: Identifier 'PI' has already been declared
2 变量的解构赋值
解构赋值概念:在ES6中,直接从数组和对象中取值,按照对应位置,赋值给变量的操作。
2.1 数组
基础用法:
// ES6 之前let a = 1;let b = 2;// ES6 之后let [a, b] = [1, 2];
本质上,只要等号两边模式一致,左边变量即可获取右边对应位置的值,更多用法:
let [a, [[b], c]] = [1, [[2], 3]];console.log(a, b, c); // 1, 2, 3let [ , , c] = [1, 2, 3];console.log(c); // 3let [a, , c] = [1, 2, 3];console.log(a,c); // 1, 3let [a, ...b] = [1, 2, 3];console.log(a,b); // 1, [2,3]let [a, b, ..c.] = [1];console.log(a, b, c); // 1, undefined, []
注意点:
- 如果解构不成功,变量的值就等于
undefined。
let [a] = []; // a => undefinedlet [a1, b1] = [1]; // a1 => 1 , b1 => undefined
- 当左边模式多于右边,也可以解构成功。
let [a, b] = [1, 2, 3];console.log(a, b); // 1, 2
- 两边模式不同,报错。
let [a] = 1;let [a] = false;let [a] = NaN;let [a] = undefined;let [a] = null;let [a] = {};
指定解构的默认值:
基础用法:
let [a = 1] = []; // a => 1let [a, b = 2] = [a]; // a => 1 , b => 2
特殊情况:
let [a = 1] = [undefined]; // a => 1let [a = 1] = [null]; // a => null
右边模式对应的值,必须严格等于undefined,默认值才能生效,而null不严格等于undefined。
2.2 对象的解构赋值
与数组解构不同的是,对象解构不需要严格按照顺序取值,而只要按照变量名去取对应属性名的值,若取不到对应属性名的值,则为undefined 。
基础用法:
let {a, b} = {a:1, b:2}; // a => 1 , b => 2let {a, b} = {a:2, b:1}; // a => 2 , b => 1let {a} = {a:3, b:2, c:1};// a => 3let {a} = {b:2, c:1}; // a => undefined
注意点:
- 若变量名和属性名不一致,则需要修改名称。
let {a:b} = {a:1, c:2};// error: a is not defined// b => 1
对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
上面代码中,a 是匹配的模式,b才是变量。真正被赋值的是变量b,而不是模式a。
- 对象解构也支持嵌套解构。
let obj = {a:[ 1, { b: 2}]};let {a, a: [c, {b}]} = obj;// a=>[1, {b: 2}], b => 2, c => 1
指定解构的默认值:
let {a=1} = {}; // a => 1let {a, b=1} = {a:2}; // a => 2, b => 1let {a:b=3} = {}; // b => 3let {a:b=3} = {a:4}; // b = >4// a是模式,b是变量 牢记let {a=1} = {a:undefined}; // a => 1let {a=1} = {a:null}; // a => null// 因为null与undefined不严格相等,所以赋值有效// 导致默认值1不会生效。
2.3 字符串的解构赋值
字符串的解构赋值中,字符串被转换成了一个类似数组的对象。
基础用法:
const [a, b, c, d, e] = 'hello';a // "h"b // "e"c // "l"d // "l"e // "o"let {length:len} = 'hello';// len => 5
2.4 数值和布尔值的解构赋值
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
// 数值和布尔值的包装对象都有toString属性let {toString: s} = 123;s === Number.prototype.toString // truelet {toString: s} = true;s === Boolean.prototype.toString // truelet { prop: x } = undefined; // TypeErrorlet { prop: y } = null; // TypeError
2.5 函数参数的解构赋值
基础用法:
function fun ([a, b]){return a + b;}fun ([1, 2]); // 3
指定默认值的解构:
function fun ({a=0, b=0} = {}){return [a, b];}fun ({a:1, b:2}); // [1, 2]fun ({a:1}); // [1, 0]fun ({}); // [0, 0]fun (); // [0, 0]function fun ({a, b} = {a:0, b:0}){return [a, b];}fun ({a:1, b:2}); // [1, 2]fun ({a:1}); // [1, undefined]fun ({}); // [undefined, undefined]fun (); // [0, 0]
2.6 应用
- 交换变量的值:
let a = 1,b = 2;[a, b] = [b, a]; // a =>2 , b => 1
- 函数返回多个值:
// 返回一个数组function f (){return [1, 2, 3];}let [a, b, c] = f(); // a=>1, b=>2, c=>3// 返回一个对象function f (){return {a:1, b:2};}let {a, b} = f(); // a=>1, b=>2
- 快速对应参数:
快速的将一组参数与变量名对应。
function f([a, b, c]) {...}f([1, 2, 3]);function f({a, b, c}) {...}f({b:2, c:3, a:1});
- 提取JSON数据:
let json = {name : 'leo',age: 18}let {name, age} = json;console.log(name,age); // leo, 18
- 遍历Map结构:
const m = new Map();m.set('a', 1);m.set('b', 2);for (let [k, v] of m){console.log(k + ' : ' + v);}// 获取键名for (let [k] of m){...}// 获取键值for (let [,k] of m){...}
- 输入模块的指定方法:
用于按需加载模块中需要用到的方法。
const {log, sin, cos} = require('math');
3 字符串的拓展
3.1 includes(),startsWith(),endsWith()
在我们判断字符串是否包含另一个字符串时,ES6之前,我们只有typeof方法,ES6之后我们又多了三种方法:
- includes():返回布尔值,表示是否找到参数字符串。
- startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
- endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let a = 'hello leo';a.startsWith('leo'); // falsea.endsWith('o'); // truea.includes('lo'); // true
并且这三个方法都支持第二个参数,表示起始搜索的位置。
let a = 'hello leo';a.startsWith('leo',1); // falsea.endsWith('o',5); // truea.includes('lo',6); // false
endsWith 是针对前 n 个字符,而其他两个是针对从第n个位置直到结束。
3.2 repeat()
repeat方法返回一个新字符串,表示将原字符串重复n次。
基础用法:
'ab'.repeat(3); // 'ababab''ab'.repeat(0); // ''
特殊用法:
- 参数为
小数,则取整
'ab'.repeat(2.3); // 'abab'
- 参数为
负数或Infinity,则报错
'ab'.repeat(-1); // RangeError'ab'.repeat(Infinity); // RangeError
- 参数为
0到-1的小数或NaN,则取0
'ab'.repeat(-0.5); // '''ab'.repeat(NaN); // ''
- 参数为
字符串,则转成数字
'ab'.repeat('ab'); // '''ab'.repeat('3'); // 'ababab'
3.3 padStart(),padEnd()
用于将字符串头部或尾部补全长度,padStart()为头部补全,padEnd()为尾部补全。
这两个方法接收2个参数,第一个指定字符串最小长度,第二个用于补全的字符串。
基础用法 :
'x'.padStart(5, 'ab'); // 'ababx''x'.padEnd(5, 'ab'); // 'xabab'
特殊用法:
- 原字符串长度,大于或等于指定最小长度,则返回原字符串。
'xyzabc'.padStart(5, 'ab'); // 'xyzabc'
- 用来补全的字符串长度和原字符串长度之和,超过指定最小长度,则截去超出部分的补全字符串。
'ab'.padStart(5,'012345'); // "012ab"
- 省略第二个参数,则用
空格补全。
'x'.padStart(4); // ' x''x'.padEnd(4); // 'x '
3.4 模版字符串
用于拼接字符串,ES6之前:
let a = 'abc' +'def' +'ghi';
ES6之后:
let a = `abcdefghi`
拼接变量:
在反引号(`)中使用${}包裹变量或方法。
// ES6之前let a = 'abc' + v1 + 'def';// ES6之后let a = `abc${v1}def`
4 正则的拓展
4.1 介绍
在ES5中有两种情况。
- 参数是字符串,则第二个参数为正则表达式的修饰符。
let a = new RegExp('abc', 'i');// 等价于let a = /abx/i;
- 参数是正则表达式,返回一个原表达式的拷贝,且不能有第二个参数,否则报错。
let a = new RegExp(/abc/i);//等价于let a = /abx/i;let a = new RegExp(/abc/, 'i');// Uncaught TypeError
ES6中使用:
第一个参数是正则对象,第二个是指定修饰符,如果第一个参数已经有修饰符,则会被第二个参数覆盖。
new RegExp(/abc/ig, 'i');
4.2 字符串的正则方法
常用的四种方法:match()、replace()、search()和split()。
4.3 u修饰符
添加u修饰符,是为了处理大于uFFFF的Unicode字符,即正确处理四个字节的UTF-16编码。
/^\uD83D/u.test('\uD83D\uDC2A'); // false/^\uD83D/.test('\uD83D\uDC2A'); // true
由于ES5之前不支持四个字节UTF-16编码,会识别为两个字符,导致第二行输出true,加入u修饰符后ES6就会识别为一个字符,所以输出false。
注意:
加上u修饰符后,会改变下面正则表达式的行为:
- (1)点字符
点字符(.)在正则中表示除了换行符以外的任意单个字符。对于码点大于0xFFFF的Unicode字符,点字符不能识别,必须加上u修饰符。
var a = "𠮷";/^.$/.test(a); // false/^.$/u.test(a); // true
- (2)Unicode字符表示法
使用ES6新增的大括号表示Unicode字符时,必须在表达式添加u修饰符,才能识别大括号。
/\u{61}/.test('a'); // false/\u{61}/u.test('a'); // true/\u{20BB7}/u.test('𠮷'); // true
- (3)量词
使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符。
/a{2}/.test('aa'); // true/a{2}/u.test('aa'); // true/𠮷{2}/.test('𠮷𠮷'); // false/𠮷{2}/u.test('𠮷𠮷'); // true
- (4)i修饰符
不加u修饰符,就无法识别非规范的K字符。
/[a-z]/i.test('\u212A') // false/[a-z]/iu.test('\u212A') // true
检查是否设置u修饰符:
使用unicode属性。
const a = /hello/;const b = /hello/u;a.unicode // falseb.unicode // true
4.4 y修饰符
y修饰符与g修饰符类似,也是全局匹配,后一次匹配都是从上一次匹配成功的下一个位置开始。区别在于,g修饰符只要剩余位置中存在匹配即可,而y修饰符是必须从剩余第一个开始。
var s = 'aaa_aa_a';var r1 = /a+/g;var r2 = /a+/y;r1.exec(s) // ["aaa"]r2.exec(s) // ["aaa"]r1.exec(s) // ["aa"] 剩余 '_aa_a'r2.exec(s) // null
lastIndex属性:
指定匹配的开始位置:
const a = /a/y;a.lastIndex = 2; // 从2号位置开始匹配a.exec('wahaha'); // nulla.lastIndex = 3; // 从3号位置开始匹配let c = a.exec('wahaha');c.index; // 3a.lastIndex; // 4
返回多个匹配:
一个y修饰符对match方法只能返回第一个匹配,与g修饰符搭配能返回所有匹配。
'a1a2a3'.match(/a\d/y); // ["a1"]'a1a2a3'.match(/a\d/gy); // ["a1", "a2", "a3"]
检查是否使用y修饰符:
使用sticky属性检查。
const a = /hello\d/y;a.sticky; // true
4.5 flags属性
flags属性返回所有正则表达式的修饰符。
/abc/ig.flags; // 'gi'
5 数值的拓展
5.1 Number.isFinite(), Number.isNaN()
Number.isFinite() 用于检查一个数值是否是有限的,即不是Infinity,若参数不是Number类型,则一律返回false 。
Number.isFinite(10); // trueNumber.isFinite(0.5); // trueNumber.isFinite(NaN); // falseNumber.isFinite(Infinity); // falseNumber.isFinite(-Infinity); // falseNumber.isFinite('leo'); // falseNumber.isFinite('15'); // falseNumber.isFinite(true); // falseNumber.isFinite(Math.random()); // true
Number.isNaN()用于检查是否是NaN,若参数不是NaN,则一律返回false。
Number.isNaN(NaN); // trueNumber.isNaN(10); // falseNumber.isNaN('10'); // falseNumber.isNaN(true); // falseNumber.isNaN(5/NaN); // trueNumber.isNaN('true' / 0); // trueNumber.isNaN('true' / 'true'); // true
区别:
与传统全局的isFinite()和isNaN()方法的区别,传统的这两个方法,是先将参数转换成数值,再判断。
而ES6新增的这两个方法则只对数值有效, Number.isFinite()对于非数值一律返回false,Number.isNaN()只有对于NaN才返回true,其他一律返回false。
isFinite(25); // trueisFinite("25"); // trueNumber.isFinite(25); // trueNumber.isFinite("25"); // falseisNaN(NaN); // trueisNaN("NaN"); // trueNumber.isNaN(NaN); // trueNumber.isNaN("NaN"); // false
5.2 Number.parseInt(), Number.parseFloat()
这两个方法与全局方法parseInt()和parseFloat()一致,目的是逐步减少全局性的方法,让语言更模块化。
parseInt('12.34'); // 12parseFloat('123.45#'); // 123.45Number.parseInt('12.34'); // 12Number.parseFloat('123.45#'); // 123.45Number.parseInt === parseInt; // trueNumber.parseFloat === parseFloat; // true
5.3 Number.isInteger()
用来判断一个数值是否是整数,若参数不是数值,则返回false。
Number.isInteger(10); // trueNumber.isInteger(10.0); // trueNumber.isInteger(10.1); // false
5.4 Math对象的拓展
ES6新增17个数学相关的静态方法,只能在Math对象上调用。
- Math.trunc:
用来去除小数的小数部分,返回整数部分。
若参数为非数值,则先转为数值。
若参数为空值或无法截取整数的值,则返回NaN。
// 正常使用Math.trunc(1.1); // 1Math.trunc(1.9); // 1Math.trunc(-1.1); // -1Math.trunc(-1.9); // -1Math.trunc(-0.1234); // -0// 参数为非数值Math.trunc('11.22'); // 11Math.trunc(true); // 1Math.trunc(false); // 0Math.trunc(null); // 0// 参数为空和无法取整Math.trunc(NaN); // NaNMath.trunc('leo'); // NaNMath.trunc(); // NaNMath.trunc(undefined); // NaN
ES5实现:
Math.trunc = Math.trunc || function(x){return x < 0 ? Math.ceil(x) : Math.floor(x);}
- Math.sign():
判断一个数是正数、负数还是零,对于非数值,会先转成数值。
返回值:- 参数为正数, 返回 +1
- 参数为负数, 返回 -1
- 参数为0, 返回 0
- 参数为-0, 返回 -0
- 参数为其他值, 返回 NaN
Math.sign(-1); // -1Math.sign(1); // +1Math.sign(0); // 0Math.sign(-0); // -0Math.sign(NaN); // NaNMath.sign(''); // 0Math.sign(true); // +1Math.sign(false);// 0Math.sign(null); // 0Math.sign('9'); // +1Math.sign('leo');// NaNMath.sign(); // NaNMath.sign(undefined); // NaN
ES5实现
Math.sign = Math.sign || function (x){x = +x;if (x === 0 || isNaN(x)){return x;}return x > 0 ? 1: -1;}
- Math.cbrt():
用来计算一个数的立方根,若参数为非数值则先转成数值。
Math.cbrt(-1); // -1Math.cbrt(0); // 0Math.cbrt(1); // 1Math.cbrt(2); // 1.2599210498Math.cbrt('1'); // 1Math.cbrt('leo'); // NaN
ES5实现
Math.cbrt = Math.cbrt || function (x){var a = Math.pow(Math.abs(x), 1/3);return x < 0 ? -y : y;}
- Math.clz32():
用于返回一个数的 32 位无符号整数形式有多少个前导 0。
Math.clz32(0) // 32Math.clz32(1) // 31Math.clz32(1000) // 22Math.clz32(0b01000000000000000000000000000000) // 1Math.clz32(0b00100000000000000000000000000000) // 2
- Math.imul():
用于返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。
Math.imul(2, 4) // 8Math.imul(-1, 8) // -8Math.imul(-2, -2) // 4
- Math.fround():
用来返回一个数的2位单精度浮点数形式。
Math.fround(0) // 0Math.fround(1) // 1Math.fround(2 ** 24 - 1) // 16777215
- Math.hypot():
用来返回所有参数的平方和的平方根。
Math.hypot(3, 4); // 5Math.hypot(3, 4, 5); // 7.0710678118654755Math.hypot(); // 0Math.hypot(NaN); // NaNMath.hypot(3, 4, 'foo'); // NaNMath.hypot(3, 4, '5'); // 7.0710678118654755Math.hypot(-3); // 3
- Math.expm1():
用来返回ex - 1,即Math.exp(x) - 1。
Math.expm1(-1) // -0.6321205588285577Math.expm1(0) // 0Math.expm1(1) // 1.718281828459045
ES5实现
Math.expm1 = Math.expm1 || function(x) {return Math.exp(x) - 1;};
- Math.log1p():
用来返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN。
Math.log1p(1) // 0.6931471805599453Math.log1p(0) // 0Math.log1p(-1) // -InfinityMath.log1p(-2) // NaN
ES5实现
Math.log1p = Math.log1p || function(x) {return Math.log(1 + x);};
- Math.log10():
用来返回以10为底的x的对数。如果x小于 0,则返回NaN。
Math.log10(2) // 0.3010299956639812Math.log10(1) // 0Math.log10(0) // -InfinityMath.log10(-2) // NaNMath.log10(100000) // 5
ES5实现
Math.log10 = Math.log10 || function(x) {return Math.log(x) / Math.LN10;};
- Math.log2():
用来返回以2为底的x的对数。如果x小于0,则返回NaN。
Math.log2(3) // 1.584962500721156Math.log2(2) // 1Math.log2(1) // 0Math.log2(0) // -InfinityMath.log2(-2) // NaNMath.log2(1024) // 10Math.log2(1 << 29) // 29
ES5实现
Math.log2 = Math.log2 || function(x) {return Math.log(x) / Math.LN2;};
- 双曲函数方法:
Math.sinh(x)返回x的双曲正弦(hyperbolic sine)Math.cosh(x)返回x的双曲余弦(hyperbolic cosine)Math.tanh(x)返回x的双曲正切(hyperbolic tangent)Math.asinh(x)返回x的反双曲正弦(inverse hyperbolic sine)Math.acosh(x)返回x的反双曲余弦(inverse hyperbolic cosine)Math.atanh(x)返回x的反双曲正切(inverse hyperbolic tangent)
5.5 指数运算符
新增的指数运算符(**):
2 ** 2; // 42 ** 3; // 82 ** 3 ** 2; // 相当于 2 ** (3 ** 2); 返回 512
指数运算符(**)与Math.pow的实现不相同,对于特别大的运算结果,两者会有细微的差异。
Math.pow(99, 99)// 3.697296376497263e+19799 ** 99// 3.697296376497268e+197
6 函数的拓展
6.1 参数默认值
// ES6 之前function f(a, b){b = b || 'leo';console.log(a, b);}// ES6 之后function f(a, b='leo'){console.log(a, b);}f('hi'); // hi leof('hi', 'jack'); // hi jackf('hi', ''); // hi leo
注意:
- 参数变量是默认声明的,不能用
let和const再次声明:
function f (a = 1){let a = 2; // error}
- 使用参数默认值时,参数名不能相同:
function f (a, a, b){ ... }; // 不报错function f (a, a, b = 1){ ... }; // 报错
与解构赋值默认值结合使用:
function f ({a, b=1}){console.log(a,b)};f({}); // undefined 1f({a:2}); // 2 1f({a:2, b:3}); // 2 3f(); // 报错function f ({a, b = 1} = {}){console.log(a, b)}f(); // undefined 1
尾参数定义默认值:
通常在尾参数定义默认值,便于观察参数,并且非尾参数无法省略。
function f (a=1,b){return [a, b];}f(); // [1, undefined]f(2); // [2, undefined]f(,2); // 报错f(undefined, 2); // [1, 2]function f (a, b=1, c){return [a, b, c];}f(); // [undefined, 1, undefined]f(1); // [1,1,undefined]f(1, ,2); // 报错f(1,undefined,2); // [1,1,2]
在给参数传递默认值时,传入undefined会触发默认值,传入null不会触发。
function f (a = 1, b = 2){console.log(a, b);}f(undefined, null); // 1 null
函数的length属性:length属性将返回,没有指定默认值的参数数量,并且rest参数不计入length属性。
function f1 (a){...};function f2 (a=1){...};function f3 (a, b=2){...};function f4 (...a){...};function f5 (a,b,...c){...};f1.length; // 1f2.length; // 0f3.length; // 1f4.length; // 0f5.length; // 2
6.2 rest 参数
rest参数形式为(...变量名),其值为一个数组,用于获取函数多余参数。
function f (a, ...b){console.log(a, b);}f(1,2,3,4); // 1 [2, 3, 4]
注意:
rest参数只能放在最后一个,否则报错:
function f(a, ...b, c){...}; // 报错
- 函数的
length属性不包含rest参数。
function f1 (a){...};function f2 (a,...b){...};f1(1); // 1f2(1,2); // 1
6.3 name 属性
用于返回该函数的函数名。
function f (){...};f.name; // fconst f = function g(){...};f.name; // g
6.4 箭头函数
使用“箭头”(=>)定义函数。
基础使用:
// 有1个参数let f = v => v;// 等同于let f = function (v){return v};// 有多个参数let f = (v, i) => {return v + i};// 等同于let f = function (v, i){return v + i};// 没参数let f = () => 1;// 等同于let f = function (){return 1};
箭头函数与变量结构结合使用:
// 正常函数写法function f (p) {return p.a + ':' + p.b;}// 箭头函数写法let f = ({a, b}) => a + ':' + b;
简化回调函数:
// 正常函数写法[1, 2, 3].map(function (x){return x * x;})// 箭头函数写法[1, 2, 3].map(x => x * x);
箭头函数与rest参数结合:
let f = (...n) => n;f(1, 2, 3); // [1, 2, 3]
注意点:
- 1.箭头函数内的
this总是指向定义时所在的对象,而不是调用时。 - 2.箭头函数不能当做构造函数,即不能用
new命令,否则报错。 - 3.箭头函数不存在
arguments对象,即不能使用,可以使用rest参数代替。 - 4.箭头函数不能使用
yield命令,即不能用作Generator函数。
不适用场景:
- 1.在定义函数方法,且该方法内部包含
this。
const obj = {a:9,b: () => {this.a --;}}
上述b如果是普通函数,函数内部的this指向obj,但是如果是箭头函数,则this会指向全局,不是预期结果。
- 2.需要动态
this时。
let b = document.getElementById('myID');b.addEventListener('click', ()=>{this.classList.toggle('on');})
上诉按钮点击会报错,因为b监听的箭头函数中,this是全局对象,若改成普通函数,this就会指向被点击的按钮对象。
6.5 双冒号运算符
双冒号暂时是一个提案,用于解决一些不适用的场合,取代call、apply、bind调用。
双冒号运算符(::)的左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边函数上。
f::b;// 等同于b.bind(f);f::b(...arguments);// 等同于b.apply(f, arguments);
若双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定到该对象上。
let f = a::a.b;// 等同于let f = ::a.b;
7 数组的拓展
7.1 拓展运算符
拓展运算符使用(...),类似rest参数的逆运算,将数组转为用(,)分隔的参数序列。
console.log(...[1, 2, 3]); // 1 2 3console.log(1, ...[2,3], 4); // 1 2 3 4
拓展运算符主要使用在函数调用。
function f (a, b){console.log(a, b);}f(...[1, 2]); // 1 2function g (a, b, c, d, e){console.log(a, b, c, d, e);}g(0, ...[1, 2], 3, ...[4]); // 0 1 2 3 4
若拓展运算符后面是个空数组,则不产生效果。
[...[], 1]; // [1]
替代apply方法
// ES6之前function f(a, b, c){...};var a = [1, 2, 3];f.apply(null, a);// ES6之后function f(a, b, c){...};let a = [1, 2, 3];f(...a);// ES6之前Math.max.apply(null, [3,2,6]);// ES6之后Math.max(...[3,2,6]);
拓展运算符的运用
- (1)复制数组:
通常我们直接复制数组时,只是浅拷贝,如果要实现深拷贝,可以使用拓展运算符。
// 通常情况 浅拷贝let a1 = [1, 2];let a2 = a1;a2[0] = 3;console.log(a1,a2); // [3,2] [3,2]// 拓展运算符 深拷贝let a1 = [1, 2];let a2 = [...a1];// let [...a2] = a1; // 作用相同a2[0] = 3;console.log(a1,a2); // [1,2] [3,2]
- (2)合并数组:
注意,这里合并数组,只是浅拷贝。
let a1 = [1,2];let a2 = [3];let a3 = [4,5];// ES5let a4 = a1.concat(a2, a3);// ES6let a5 = [...a1, ...a2, ...a3];a4[0] === a1[0]; // truea5[0] === a1[0]; // true
- (3)与解构赋值结合:
与解构赋值结合生成数组,但是使用拓展运算符需要放到参数最后一个,否则报错。
let [a, ...b] = [1, 2, 3, 4];// a => 1 b => [2,3,4]let [a, ...b] = [];// a => undefined b => []let [a, ...b] = ["abc"];// a => "abc" b => []
7.2 Array.from()
将 类数组对象 和 可遍历的对象,转换成真正的数组。
// 类数组对象let a = {'0':'a','1':'b',length:2}let arr = Array.from(a);// 可遍历的对象let a = Array.from([1,2,3]);let b = Array.from({length: 3});let c = Array.from([1,2,3]).map(x => x * x);let d = Array.from([1,2,3].map(x => x * x));
7.3 Array.of()
将一组数值,转换成数组,弥补Array方法参数不同导致的差异。
Array.of(1,2,3); // [1,2,3]Array.of(1).length; // 1Array(); // []Array(2); // [,] 1个参数时,为指定数组长度Array(1,2,3); // [1,2,3] 多于2个参数,组成新数组
7.4 find()和findIndex()
find()方法用于找出第一个符合条件的数组成员,参数为一个回调函数,所有成员依次执行该回调函数,返回第一个返回值为true的成员,如果没有一个符合则返回undefined。
[1,2,3,4,5].find( a => a < 3 ); // 1
回调函数接收三个参数,当前值、当前位置和原数组。
[1,2,3,4,5].find((value, index, arr) => {// ...});
findIndex()方法与find()类似,返回第一个符合条件的数组成员的位置,如果都不符合则返回-1。
[1,2,3,4].findIndex((v,i,a)=>{return v>2;}); // 2
7.5 fill()
用于用指定值填充一个数组,通常用来初始化空数组,并抹去数组中已有的元素。
new Array(3).fill('a'); // ['a','a','a'][1,2,3].fill('a'); // ['a','a','a']
并且fill()的第二个和第三个参数指定填充的起始位置和结束位置。
[1,2,3].fill('a',1,2);// [1, "a", 3]
7.6 entries(),keys(),values()
主要用于遍历数组,entries()对键值对遍历,keys()对键名遍历,values()对键值遍历。
for (let i of ['a', 'b'].keys()){console.log(i)}// 0// 1for (let e of ['a', 'b'].values()){console.log(e)}// 'a'// 'b'for (let e of ['a', 'b'].entries()){console.log(e)}// 0 'a'// 1 'b'
7.7 includes()
用于表示数组是否包含给定的值,与字符串的includes方法类似。
[1,2,3].includes(2); // true[1,2,3].includes(4); // false[1,2,NaN].includes(NaN); // true
第二个参数为起始位置,默认为0,如果负数,则表示倒数的位置,如果大于数组长度,则重置为0开始。
[1,2,3].includes(3,3); // false[1,2,3].includes(3,4); // false[1,2,3].includes(3,-1); // true[1,2,3].includes(3,-4); // true
7.8 flat(),flatMap()
flat()用于将数组一维化,返回一个新数组,不影响原数组。
默认一次只一维化一层数组,若需多层,则传入一个整数参数指定层数。
若要一维化所有层的数组,则传入Infinity作为参数。
[1, 2, [2,3]].flat(); // [1,2,2,3][1,2,[3,[4,[5,6]]]].flat(3); // [1,2,3,4,5,6][1,2,[3,[4,[5,6]]]].flat('Infinity'); // [1,2,3,4,5,6]
flatMap()是将原数组每个对象先执行一个函数,在对返回值组成的数组执行flat()方法,返回一个新数组,不改变原数组。flatMap()只能展开一层。
[2, 3, 4].flatMap((x) => [x, x * 2]);// [2, 4, 3, 6, 4, 8]
8 对象的拓展
8.1 属性的简洁表示
let a = 'a1';let b = { a }; // b => { a : 'a1' }// 等同于let b = { a : a };function f(a, b){return {a, b};}// 等同于function f (a, b){return {a:a ,b:b};}let a = {fun () {return 'leo';}}// 等同于let a = {fun : function(){return 'leo';}}
8.2 属性名表达式
JavaScript提供2种方法定义对象的属性。
// 方法1 标识符作为属性名a.f = true;// 方法2 字符串作为属性名a['f' + 'un'] = true;
延伸出来的还有:
let a = 'hi leo';let b = {[a]: true,['a'+'bc']: 123,['my' + 'fun'] (){return 'hi';}};// b.a => undefined ; b.abc => 123 ; b.myfun() => 'hi'// b[a] => true ; b['abc'] => 123 ; b['myfun'] => ƒ ['my' + 'fun'] (){ return 'hi'; }
注意:
属性名表达式不能与简洁表示法同时使用,否则报错。
// 报错let a1 = 'aa';let a2 = 'bb';let b1 = {[a1]};// 正确let a1 = 'aa';let b1 = { [a1] : 'bb'};
8.3 Object.is()
Object.is() 用于比较两个值是否严格相等,在ES5时候只要使用相等运算符(==)和严格相等运算符(===)就可以做比较,但是它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。
Object.is('a','a'); // trueObject.is({}, {}); // false// ES5+0 === -0 ; // trueNaN === NaN; // false// ES6Object.is(+0,-0); // falseObject.is(NaN,NaN); // true
8.4 Object.assign()
Object.assign()方法用于对象的合并,将原对象的所有可枚举属性复制到目标对象。
基础用法:
第一个参数是目标对象,后面参数都是源对象。
let a = {a:1};let b = {b:2};Object.assign(a,b); // a=> {a:1,b:2}
注意:
- 若目标对象与源对象有同名属性,则后面属性会覆盖前面属性。
let a = {a:1, b:2};let b = {b:3, c:4};Object.assign(a, b); // a => {a:1, b:3, c:4}
- 若只有一个参数,则返回该参数。
let a = {a:1};Object.assign(a) === a; // true
- 若参数不是对象,则先转成对象后返回。
typeof Object.assign(2); // 'object'
- 由于
undefined或NaN无法转成对象,所以做为参数会报错。
Object.assign(undefined) // 报错Object.assign(NaN); // 报错
Object.assign()实现的是浅拷贝。
Object.assign()拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。
let a = {a: {b:1}};let b = Object.assign({},a);a.a.b = 2;console.log(b.a.b); // 2
- 将数组当做对象处理,键名为数组下标,键值为数组下标对应的值。
Object.assign([1, 2, 3], [4, 5]); // [4, 5, 3]
9 Symbol
9.1 介绍
ES6引入Symbol作为一种新的原始数据类型,表示独一无二的值,主要是为了防止属性名冲突。
ES6之后,JavaScript一共有其中数据类型:Symbol、undefined、null、Boolean、String、Number、Object。
简单实用:
let a = Symbol();typeof a; // "symbol"
注意:
Symbol函数不能用new,会报错。由于Symbol是一个原始类型,不是对象,所以不能添加属性,它是类似于字符串的数据类型。Symbol都是不相等的,即使参数相同。
// 没有参数let a1 = Symbol();let a2 = Symbol();a1 === a2; // false// 有参数let a1 = Symbol('abc');let a2 = Symbol('abc');a1 === a2; // false
Symbol不能与其他类型的值计算,会报错。
let a = Symbol('hello');a + " world!"; // 报错`${a} world!`; // 报错
Symbol可以显式转换为字符串:
let a1 = Symbol('hello');String(a1); // "Symbol(hello)"a1.toString(); // "Symbol(hello)"
Symbol可以转换为布尔值,但不能转为数值:
let a1 = Symbol();Boolean(a1);!a1; // falseNumber(a1); // TypeErrora1 + 1 ; // TypeError
9.2 Symbol作为属性名
好处:防止同名属性,还有防止键被改写或覆盖。
let a1 = Symbol();// 写法1let b = {};b[a1] = 'hello';// 写法2let b = {[a1] : 'hello'}// 写法3let b = {};Object.defineProperty(b, a1, {value : 'hello' });// 3种写法 结果相同b[a1]; // 'hello'
需要注意: Symbol作为对象属性名时,不能用点运算符,并且必须放在方括号内。
let a = Symbol();let b = {};// 不能用点运算b.a = 'hello';b[a] ; // undefinedb['a'] ; // 'hello'// 必须放在方括号内let c = {[a] : function (text){console.log(text);}}c[a]('leo'); // 'leo'// 上面等价于 更简洁let c = {[a](text){console.log(text);}}
常常还用于创建一组常量,保证所有值不相等:
let a = {};a.a1 = {AAA: Symbol('aaa'),BBB: Symbol('bbb'),CCC: Symbol('ccc')}
9.3 应用:消除魔术字符串
魔术字符串:指代码中多次出现,强耦合的字符串或数值,应该避免,而使用含义清晰的变量代替。
function f(a){if(a == 'leo') {console.log('hello');}}f('leo'); // 'leo' 为魔术字符串
常使用变量,消除魔术字符串:
let obj = {name: 'leo'};function f (a){if(a == obj.name){console.log('hello');}}f(obj.name); // 'leo'
使用Symbol消除强耦合,使得不需关系具体的值:
let obj = {name: Symbol()};function f (a){if(a == obj.name){console.log('hello');}}f(obj.name);
9.4 属性名遍历
Symbol作为属性名遍历,不出现在for...in、for...of循环,也不被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。
let a = Symbol('aa'),b= Symbol('bb');let obj = {[a]:'11', [b]:'22'}for(let k of Object.values(obj)){console.log(k)}// 无输出let obj = {};let aa = Symbol('leo');Object.defineProperty(obj, aa, {value: 'hi'});for(let k in obj){console.log(k); // 无输出}Object.getOwnPropertyNames(obj); // []Object.getOwnPropertySymbols(obj); // [Symbol(leo)]
Object.getOwnPropertySymbols方法返回一个数组,包含当前对象所有用做属性名的Symbol值。
let a = {};let a1 = Symbol('a');let a2 = Symbol('b');a[a1] = 'hi';a[a2] = 'oi';let obj = Object.getOwnPropertySymbols(a);obj; // [Symbol(a), Symbol(b)]
另外可以使用Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
let a = {[Symbol('leo')]: 1,aa : 2,bb : 3,}Reflect.ownKeys(a); // ['aa', 'bb',Symbol('leo')]
由于Symbol值作为名称的属性不被常规方法遍历获取,因此常用于定义对象的一些非私有,且内部使用的方法。
9.5 Symbol.for()、Symbol.keyFor()
- Symbol.for()
用于重复使用一个Symbol值,接收一个字符串作为参数,若存在用此参数作为名称的Symbol值,返回这个Symbol,否则新建并返回以这个参数为名称的Symbol值。
let a = Symbol.for('aaa');let b = Symbol.for('aaa');a === b; // true
Symbol() 和 Symbol.for()区别:
Symbol.for('aa') === Symbol.for('aa'); // trueSymbol('aa') === Symbol('aa'); // false
- Symbol.keyFor()
用于返回一个已使用的Symbol类型的key:
let a = Symbol.for('aa');Symbol.keyFor(a); // 'aa'let b = Symbol('aa');Symbol.keyFor(b); // undefined
9.6 内置的Symbol值
ES6提供11个内置的Symbol值,指向语言内部使用的方法:
- 1.Symbol.hasInstance
当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)。
class P {[Symbol.hasInstance](a){return a instanceof Array;}}[1, 2, 3] instanceof new P(); // true
P是一个类,new P()会返回一个实例,该实例的Symbol.hasInstance方法,会在进行instanceof运算时自动调用,判断左侧的运算子是否为Array的实例。
- 2.Symbol.isConcatSpreadable
值为布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。
let a = ['aa','bb'];['cc','dd'].concat(a, 'ee');// ['cc', 'dd', 'aa', 'bb', 'ee']a[Symbol.isConcatSpreadable]; // undefinedlet b = ['aa','bb'];b[Symbol.isConcatSpreadable] = false;['cc','dd'].concat(b, 'ee');// ['cc', 'dd',[ 'aa', 'bb'], 'ee']
- 3.Symbol.species
指向一个构造函数,在创建衍生对象时会使用,使用时需要用get取值器。
class P extends Array {static get [Symbol.species](){return this;}}
解决下面问题:
// 问题: b应该是 Array 的实例,实际上是 P 的实例class P extends Array{}let a = new P(1,2,3);let b = a.map(x => x);b instanceof Array; // trueb instanceof P; // true// 解决: 通过使用 Symbol.speciesclass P extends Array {static get [Symbol.species]() { return Array; }}let a = new P();let b = a.map(x => x);b instanceof P; // falseb instanceof Array; // true
- 4.Symbol.match
当执行str.match(myObject),传入的属性存在时会调用,并返回该方法的返回值。
class P {[Symbol.match](string){return 'hello world'.indexOf(string);}}'h'.match(new P()); // 0
- 5.Symbol.replace
当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。
let a = {};a[Symbol.replace] = (...s) => console.log(s);'Hello'.replace(a , 'World') // ["Hello", "World"]
- 6.Symbol.hasInstance
当该对象被String.prototype.search方法调用时,会返回该方法的返回值。
class P {constructor(val) {this.val = val;}[Symbol.search](s){return s.indexOf(this.val);}}'hileo'.search(new P('leo')); // 2
- 7.Symbol.split
当该对象被String.prototype.split方法调用时,会返回该方法的返回值。
// 重新定义了字符串对象的split方法的行为class P {constructor(val) {this.val = val;}[Symbol.split](s) {let i = s.indexOf(this.val);if(i == -1) return s;return [s.substr(0, i),s.substr(i + this.val.length)]}}'helloworld'.split(new P('hello')); // ["hello", ""]'helloworld'.split(new P('world')); // ["", "world"]'helloworld'.split(new P('leo')); // "helloworld"
- 8.Symbol.iterator
对象进行for...of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器。
class P {*[Symbol.interator]() {let i = 0;while(this[i] !== undefined ) {yield this[i];++i;}}}let a = new P();a[0] = 1;a[1] = 2;for (let k of a){console.log(k);}
- 9.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 // 2463 + obj // '3default'obj == 'default' // trueString(obj) // 'str'
- 10.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]"
- 11.Symbol.unscopables
该对象指定了使用with关键字时,哪些属性会被with环境排除。
// 没有 unscopables 时class MyClass {foo() { return 1; }}var foo = function () { return 2; };with (MyClass.prototype) {foo(); // 1}// 有 unscopables 时class MyClass {foo() { return 1; }get [Symbol.unscopables]() {return { foo: true };}}var foo = function () { return 2; };with (MyClass.prototype) {foo(); // 2}
上面代码通过指定Symbol.unscopables属性,使得with语法块不会在当前作用域寻找foo属性,即foo将指向外层作用域的变量。
10 Set和Map数据结构
10.1 Set
介绍:Set数据结构类似数组,但所有成员的值唯一。Set本身为一个构造函数,用来生成Set数据结构,使用add方法来添加新成员。
let a = new Set();[1,2,2,1,3,4,5,4,5].forEach(x=>a.add(x));for(let k of a){console.log(k)};// 1 2 3 4 5
基础使用:
let a = new Set([1,2,3,3,4]);[...a]; // [1,2,3,4]a.size; // 4// 数组去重[...new Set([1,2,3,4,4,4])];// [1,2,3,4]
注意:
- 向
Set中添加值的时候,不会类型转换,即5和'5'是不同的。
[...new Set([5,'5'])]; // [5, "5"]
属性和方法:
- 属性:
Set.prototype.constructor:构造函数,默认就是Set函数。Set.prototype.size:返回Set实例的成员总数。
- 操作方法:
add(value):添加某个值,返回 Set 结构本身。delete(value):删除某个值,返回一个布尔值,表示删除是否成功。has(value):返回一个布尔值,表示该值是否为Set的成员。clear():清除所有成员,没有返回值。
let a = new Set();a.add(1).add(2); // a => Set(2) {1, 2}a.has(2); // truea.has(3); // falsea.delete(2); // true a => Set(1) {1}a.clear(); // a => Set(0) {}
数组去重:
let a = new Set([1,2,3,3,3,3]);
10.2 Set的应用
数组去重:
// 方法1[...new Set([1,2,3,4,4,4])]; // [1,2,3,4]// 方法2Array.from(new Set([1,2,3,4,4,4])); // [1,2,3,4]
遍历和过滤:
let a = new Set([1,2,3,4]);// map 遍历操作let b = new Set([...a].map(x =>x*2));// b => Set(4) {2,4,6,8}// filter 过滤操作let c = new Set([...a].filter(x =>(x%2) == 0)); // b => Set(2) {2,4}
获取并集、交集和差集:
let a = new Set([1,2,3]);let b = new Set([4,3,2]);// 并集let c1 = new Set([...a, ...b]); // Set {1,2,3,4}// 交集let c2 = new Set([...a].filter(x => b.has(x))); // set {2,3}// 差集let c3 = new Set([...a].filter(x => !b.has(x))); // set {1}
- 遍历方法:
keys():返回键名的遍历器。values():返回键值的遍历器。entries():返回键值对的遍历器。forEach():使用回调函数遍历每个成员。
Set遍历顺序是插入顺序,当保存多个回调函数,只需按照顺序调用。但由于Set结构没有键名只有键值,所以keys()和values()是返回结果相同。
let a = new Set(['a','b','c']);for(let i of a.keys()){console.log(i)}; // 'a' 'b' 'c'for(let i of a.values()){console.log(i)}; // 'a' 'b' 'c'for(let i of a.entries()){console.log(i)};// ['a','a'] ['b','b'] ['c','c']
并且 还可以使用for...of直接遍历Set。
let a = new Set(['a','b','c']);for(let k of a){console.log(k)}; // 'a' 'b' 'c'
forEach与数组相同,对每个成员执行操作,且无返回值。
let a = new Set(['a','b','c']);a.forEach((v,k) => console.log(k + ' : ' + v));
10.3 Map
由于传统的JavaScript对象只能用字符串当做键,给开发带来很大限制,ES6增加Map数据结构,使得各种类型的值(包括对象)都可以作为键。Map结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。
基础使用:
let a = new Map();let b = {name: 'leo' };a.set(b,'my name'); // 添加值a.get(b); // 获取值a.size; // 获取总数a.has(b); // 查询是否存在a.delete(b); // 删除一个值a.clear(); // 清空所有成员 无返回
注意:
- 传入数组作为参数,指定键值对的数组。
let a = new Map([['name','leo'],['age',18]])
- 如果对同一个键多次赋值,后面的值将覆盖前面的值。
let a = new Map();a.set(1,'aaa').set(1,'bbb');a.get(1); // 'bbb'
- 如果读取一个未知的键,则返回
undefined。
new Map().get('abcdef'); // undefined
- 同样的值的两个实例,在 Map 结构中被视为两个键。
let a = new Map();let a1 = ['aaa'];let a2 = ['aaa'];a.set(a1,111).set(a2,222);a.get(a1); // 111a.get(a2); // 222
遍历方法:
Map 的遍历顺序就是插入顺序。
keys():返回键名的遍历器。values():返回键值的遍历器。entries():返回所有成员的遍历器。forEach():遍历 Map 的所有成员。
let a = new Map([['name','leo'],['age',18]])for (let i of a.keys()){...};for (let i of a.values()){...};for (let i of a.entries()){...};a.forEach((v,k,m)=>{console.log(`key:${k},value:${v},map:${m}`)})
将Map结构转成数组结构:
let a = new Map([['name','leo'],['age',18]])let a1 = [...a.keys()]; // a1 => ["name", "age"]let a2 = [...a.values()]; // a2 => ["leo", 18]let a3 = [...a.entries()];// a3 => [['name','leo'], ['age',18]]
10.4 Map与其他数据结构互相转换
- Map 转 数组
let a = new Map().set(true,1).set({f:2},['abc']);[...a]; // [[true:1], [ {f:2},['abc'] ]]
- 数组 转 Map
let a = [ ['name','leo'], [1, 'hi' ]]let b = new Map(a);
- Map 转 对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
function fun(s) {let obj = Object.create(null);for (let [k,v] of s) {obj[k] = v;}return obj;}const a = new Map().set('yes', true).set('no', false);fun(a)// { yes: true, no: false }
- 对象 转 Map
function fun(obj) {let a = new Map();for (let k of Object.keys(obj)) {a.set(k, obj[k]);}return a;}fun({yes: true, no: false})// Map {"yes" => true, "no" => false}
- Map 转 JSON
(1)Map键名都是字符串,转为对象JSON:
function fun (s) {let obj = Object.create(null);for (let [k,v] of s) {obj[k] = v;}return JSON.stringify(obj)}let a = new Map().set('yes', true).set('no', false);fun(a);// '{"yes":true,"no":false}'
(2)Map键名有非字符串,转为数组JSON:
function fun (map) {return JSON.stringify([...map]);}let a = new Map().set(true, 7).set({foo: 3}, ['abc']);fun(a)// '[[true,7],[{"foo":3},["abc"]]]'
- JSON 转 Map
(1)所有键名都是字符串:
function fun (s) {let strMap = new Map();for (let k of Object.keys(s)) {strMap.set(k, s[k]);}return strMap;return JSON.parse(strMap);}fun('{"yes": true, "no": false}')// Map {'yes' => true, 'no' => false}
(2)整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组:
function fun2(s) {return new Map(JSON.parse(s));}fun2('[[true,7],[{"foo":3},["abc"]]]')// Map {true => 7, Object {foo: 3} => ['abc']}
11 Proxy
proxy 用于修改某些操作的默认行为,可以理解为一种拦截外界对目标对象访问的一种机制,从而对外界的访问进行过滤和修改,即代理某些操作,也称“代理器”。
11.1 基础使用
proxy实例化需要传入两个参数,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
let p = new Proxy(target, handler);let a = new Proxy({}, {get: function (target, handler){return 'leo';}})a.name; // leoa.age; // leoa.abcd; // leo
上述a实例中,在第二个参数中定义了get方法,来拦截外界访问,并且get方法接收两个参数,分别是目标对象和所要访问的属性,所以不管外部访问对象中任何属性都会执行get方法返回leo。
注意:
- 只能使用
Proxy实例的对象才能使用这些操作。 - 如果
handler没有设置拦截,则直接返回原对象。
let target = {};let handler = {};let p = new Proxy(target, handler);p.a = 'leo';target.a; // 'leo'
同个拦截器函数,设置多个拦截操作:
let p = new Proxy(function(a, b){return a + b;},{get:function(){return 'get方法';},apply:function(){return 'apply方法';}})
这里还有一个简单的案例:
let handler = {get : function (target, name){return name in target ? target[name] : 16;}}let p = new Proxy ({}, handler);p.a = 1;console.log(p.a , p.b);// 1 16
这里因为 p.a = 1 定义了p中的a属性,值为1,而没有定义b属性,所以p.a会得到1,而p.b会得到undefined从而使用name in target ? target[name] : 16;返回的默认值16;
Proxy支持的13种拦截操作:
13种拦截操作的详细介绍:打开阮一峰老师的链接。
get(target, propKey, receiver):
拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。set(target, propKey, value, receiver):
拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。has(target, propKey):
拦截propKey in proxy的操作,返回一个布尔值。deleteProperty(target, propKey):
拦截delete proxy[propKey]的操作,返回一个布尔值。ownKeys(target):
拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。getOwnPropertyDescriptor(target, propKey):
拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。defineProperty(target, propKey, propDesc):
拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。preventExtensions(target):
拦截Object.preventExtensions(proxy),返回一个布尔值。getPrototypeOf(target):
拦截Object.getPrototypeOf(proxy),返回一个对象。isExtensible(target):
拦截Object.isExtensible(proxy),返回一个布尔值。setPrototypeOf(target, proto):
拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。apply(target, object, args):
拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。construct(target, args):
拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。
11.2 取消Proxy实例
使用Proxy.revocale方法取消Proxy实例。
let a = {};let b = {};let {proxy, revoke} = Proxy.revocale(a, b);proxy.name = 'leo'; // 'leo'revoke();proxy.name; // TypeError: Revoked
11.3 实现 Web服务的客户端
const service = createWebService('http://le.com/data');service.employees().than(json =>{const employees = JSON.parse(json);})function createWebService(url){return new Proxy({}, {get(target, propKey, receiver{return () => httpGet(url+'/'+propKey);})})}
12 Promise对象
12.1 概念
主要用途:解决异步编程带来的回调地狱问题。
把Promise简单理解一个容器,存放着某个未来才会结束的事件(通常是一个异步操作)的结果。通过Promise对象来获取异步操作消息,处理各种异步操作。
Promise对象2特点:
- 对象的状态不受外界影响。
Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
- 一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
注意,为了行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。
Promise缺点
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
12.2 基本使用
Promise为一个构造函数,需要用new来实例化。
let p = new Promise(function (resolve, reject){if(/*异步操作成功*/){resolve(value);} else {reject(error);}})
Promise接收一个函数作为参数,该函数两个参数resolve和reject,有JS引擎提供。
resolve作用是将Promise的状态从pending变成resolved,在异步操作成功时调用,返回异步操作的结果,作为参数传递出去。reject作用是将Promise的状态从pending变成rejected,在异步操作失败时报错,作为参数传递出去。
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
p.then(function(val){// success...},function(err){// error...})
几个例子来理解 :
- 当一段时间过后,
Promise状态便成为resolved触发then方法绑定的回调函数。
function timeout (s){return new Promise((resolve, reject){setTimeout(result,ms, 'done');})}timeout(100).then(val => {console.log(val);})
Promise新建后立刻执行。
let p = new Promise(function(resolve, reject){console.log(1);resolve();})p.then(()=>{console.log(2);})console.log(3);// 1// 3// 2
异步加载图片:
function f(url){return new Promise(function(resolve, reject){const img = new Image ();img.onload = function(){resolve(img);}img.onerror = function(){reject(new Error('Could not load image at ' + url));}img.src = url;})}
resolve函数和reject函数的参数为resolve函数或reject函数:p1的状态决定了p2的状态,所以p2要等待p1的结果再执行回调函数。
const p1 = new Promise(function (resolve, reject) {setTimeout(() => reject(new Error('fail')), 3000)})const p2 = new Promise(function (resolve, reject) {setTimeout(() => resolve(p1), 1000)})p2.then(result => console.log(result)).catch(error => console.log(error))// Error: fail
调用resolve或reject不会结束Promise参数函数的执行,除了return:
new Promise((resolve, reject){resolve(1);console.log(2);}).then(r => {console.log(3);})// 2// 1new Promise((resolve, reject){return resolve(1);console.log(2);})// 1
12.3 Promise.prototype.then()
作用是为Promise添加状态改变时的回调函数,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。then方法返回一个新Promise实例,与原来Promise实例不同,因此可以使用链式写法,上一个then的结果作为下一个then的参数。
getJSON("/posts.json").then(function(json) {return json.post;}).then(function(post) {// ...});
12.4 Promise.prototype.catch()
Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
getJSON('/posts.json').then(function(posts) {// ...}).catch(function(error) {// 处理 getJSON 和 前一个回调函数运行时发生的错误console.log('发生错误!', error);});
如果 Promise 状态已经变成resolved,再抛出错误是无效的。
const p = new Promise(function(resolve, reject) {resolve('ok');throw new Error('test');});p.then(function(value) { console.log(value) }).catch(function(error) { console.log(error) });// ok
当promise抛出一个错误,就被catch方法指定的回调函数捕获,下面三种写法相同。
// 写法一const p = new Promise(function(resolve, reject) {throw new Error('test');});p.catch(function(error) {console.log(error);});// Error: test// 写法二const p = new Promise(function(resolve, reject) {try {throw new Error('test');} catch(e) {reject(e);}});p.catch(function(error) {console.log(error);});// 写法三const p = new Promise(function(resolve, reject) {reject(new Error('test'));});p.catch(function(error) {console.log(error);});
一般来说,不要在then方法里面定义Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。
// badpromise.then(function(data) {// success}, function(err) {// error});// goodpromise.then(function(data) { //cb// success}).catch(function(err) {// error});
12.5 Promise.prototype.finally()
finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise.then(result => {···}).catch(error => {···}).finally(() => {···});
finally不接收任何参数,与状态无关,本质上是then方法的特例。
promise.finally(() => {// 语句});// 等同于promise.then(result => {// 语句return result;},error => {// 语句throw error;});
上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。finally方法总是会返回原来的值。
// resolve 的值是 undefinedPromise.resolve(2).then(() => {}, () => {})// resolve 的值是 2Promise.resolve(2).finally(() => {})// reject 的值是 undefinedPromise.reject(3).then(() => {}, () => {})// reject 的值是 3Promise.reject(3).finally(() => {})
12.6 Promise.all()
用于将多个 Promise 实例,包装成一个新的 Promise 实例,参数可以不是数组,但必须是Iterator接口,且返回的每个成员都是Promise实例。
const p = Promise.all([p1, p2, p3]);
p的状态由p1、p2、p3决定,分成两种情况。
- 只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
- 只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
// 生成一个Promise对象的数组const promises = [2, 3, 5, 7, 11, 13].map(function (id) {return getJSON('/post/' + id + ".json");});Promise.all(promises).then(function (posts) {// ...}).catch(function(reason){// ...});
上面代码中,promises是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。
注意:如果Promise的参数中定义了catch方法,则rejected后不会触发Promise.all()的catch方法,因为参数中的catch方法执行完后也会变成resolved,当Promise.all()方法参数的实例都是resolved时就会调用Promise.all()的then方法。
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: 报错了]
如果参数里面都没有catch方法,就会调用Promise.all()的catch方法。
const p1 = new Promise((resolve, reject) => {resolve('hello');}).then(result => result);const p2 = new Promise((resolve, reject) => {throw new Error('报错了');}).then(result => result);Promise.all([p1, p2]).then(result => console.log(result)).catch(e => console.log(e));// Error: 报错了
12.7 Promise.race()
与Promise.all方法类似,也是将多个Promise实例包装成一个新的Promise实例。
const p = Promise.race([p1, p2, p3]);
与Promise.all方法区别在于,Promise.race方法是p1, p2, p3中只要一个参数先改变状态,就会把这个参数的返回值传给p的回调函数。
12.8 Promise.resolve()
将现有对象转换成 Promise 对象。
const p = Promise.resolve($.ajax('/whatever.json'));
12.9 Promise.reject()
返回一个rejected状态的Promise实例。
const p = Promise.reject('出错了');// 等同于const p = new Promise((resolve, reject) => reject('出错了'))p.then(null, function (s) {console.log(s)});// 出错了
注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。
const thenable = {then(resolve, reject) {reject('出错了');}};Promise.reject(thenable).catch(e => {console.log(e === thenable)})// true
13 Iterator和 for…of循环
13.1 Iterator遍历器概念
Iterator是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator三个作用:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列;
- Iterator 接口主要供ES6新增的
for...of消费;
13.2 Iterator遍历过程
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的
next方法,可以将指针指向数据结构的第一个成员。 - 第二次调用指针对象的
next方法,指针就指向数据结构的第二个成员。 - 不断调用指针对象的
next方法,直到它指向数据结构的结束位置。
每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。
value属性是当前成员的值;done属性是一个布尔值,表示遍历是否结束;
模拟next方法返回值:
let f = function (arr){var nextIndex = 0;return {next:function(){return nextIndex < arr.length ?{value: arr[nextIndex++], done: false}:{value: undefined, done: true}}}}let a = f(['a', 'b']);a.next(); // { value: "a", done: false }a.next(); // { value: "b", done: false }a.next(); // { value: undefined, done: true }
13.3 默认Iterator接口
若数据可遍历,即一种数据部署了Iterator接口。
ES6中默认的Iterator接口部署在数据结构的Symbol.iterator属性,即如果一个数据结构具有Symbol.iterator属性,就可以认为是可遍历。Symbol.iterator属性本身是函数,是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见《Symbol》一章)。
原生具有Iterator接口的数据结构有:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
13.4 Iterator使用场景
- (1)解构赋值
对数组和Set结构进行解构赋值时,会默认调用Symbol.iterator方法。
let a = new Set().add('a').add('b').add('c');let [x, y] = a; // x = 'a' y = 'b'let [a1, ...a2] = a; // a1 = 'a' a2 = ['b','c']
- (2)扩展运算符
扩展运算符(...)也会调用默认的 Iterator 接口。
let a = 'hello';[...a]; // ['h','e','l','l','o']let a = ['b', 'c'];['a', ...a, 'd']; // ['a', 'b', 'c', 'd']
- (2)yield*
yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let a = function*(){yield 1;yield* [2,3,4];yield 5;}let b = a();b.next() // { value: 1, done: false }b.next() // { value: 2, done: false }b.next() // { value: 3, done: false }b.next() // { value: 4, done: false }b.next() // { value: 5, done: false }b.next() // { value: undefined, done: true }
- (4)其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。 - for…of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如
new Map([['a',1],['b',2]])) - Promise.all()
- Promise.race()
13.5 for…of循环
只要数据结构部署了Symbol.iterator属性,即具有 iterator 接口,可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterato方法。
使用场景:for...of可以使用在数组,Set和Map结构,类数组对象,Genetator对象和字符串。
- 数组
for...of循环可以代替数组实例的forEach方法。
let a = ['a', 'b', 'c'];for (let k of a){console.log(k)}; // a b ca.forEach((ele, index)=>{console.log(ele); // a b cconsole.log(index); // 0 1 2})
与for...in对比,for...in只能获取对象键名,不能直接获取键值,而for...of允许直接获取键值。
let a = ['a', 'b', 'c'];for (let k of a){console.log(k)}; // a b cfor (let k in a){console.log(k)}; // 0 1 2
- Set和Map
可以使用数组作为变量,如for (let [k,v] of b){...}。
let a = new Set(['a', 'b', 'c']);for (let k of a){console.log(k)}; // a b clet b = new Map();b.set('name','leo');b.set('age', 18);b.set('aaa','bbb');for (let [k,v] of b){console.log(k + ":" + v)};// name:leo// age:18// aaa:bbb
- 类数组对象
// 字符串let a = 'hello';for (let k of a ){console.log(k)}; // h e l l o// DOM NodeList对象let b = document.querySelectorAll('p');for (let k of b ){k.classList.add('test');}// arguments对象function f(){for (let k of arguments){console.log(k);}}f('a','b'); // a b
- 对象
普通对象不能直接使用for...of会报错,要部署Iterator才能使用。
let a = {a:'aa',b:'bb',c:'cc'};for (let k in a){console.log(k)}; // a b cfor (let k of a){console>log(k)}; // TypeError
13.6 跳出for…of
使用break来实现。
for (let k of a){if(k>100)break;console.log(k);}
14 Generator函数和应用
14.1 基本概念
Generator函数是一种异步编程解决方案。
原理:
执行Genenrator函数会返回一个遍历器对象,依次遍历Generator函数内部的每一个状态。Generator函数是一个普通函数,有以下两个特征:
function关键字与函数名之间有个星号;- 函数体内使用
yield表达式,定义不同状态;
通过调用next方法,将指针移向下一个状态,直到遇到下一个yield表达式(或return语句)为止。简单理解,Generator函数分段执行,yield表达式是暂停执行的标记,而next恢复执行。
function * f (){yield 'hi';yield 'leo';return 'ending';}let a = f();a.next(); // {value: 'hi', done : false}a.next(); // {value: 'leo', done : false}a.next(); // {value: 'ending', done : true}a.next(); // {value: undefined, done : false}
14.2 yield表达式
yield表达式是暂停标志,遍历器对象的next方法的运行逻辑如下:
- 遇到
yield就暂停执行,将这个yield后的表达式的值,作为返回对象的value属性值。 - 下次调用
next往下执行,直到遇到下一个yield。 - 直到函数结束或者
return为止,并返回return语句后面表达式的值,作为返回对象的value属性值。 - 如果该函数没有
return语句,则返回对象的value为undefined。
注意:
yield只能用在Generator函数里使用,其他地方使用会报错。
// 错误1(function(){yiled 1; // SyntaxError: Unexpected number})()// 错误2 forEach参数是个普通函数let a = [1, [[2, 3], 4], [5, 6]];let f = function * (i){i.forEach(function(m){if(typeof m !== 'number'){yield * f (m);}else{yield m;}})}for (let k of f(a)){console.log(k)}
yield表达式如果用于另一个表达式之中,必须放在圆括号内。
function * a (){console.log('a' + yield); // SyntaxErroconsole.log('a' + yield 123); // SyntaxErroconsole.log('a' + (yield)); // okconsole.log('a' + (yield 123)); // ok}
yield表达式用做函数参数或放在表达式右边,可以不加括号。
function * a (){f(yield 'a', yield 'b'); // oklei i = yield; // ok}
14.3 next方法
yield本身没有返回值,或者是总返回undefined,next方法可带一个参数,作为上一个yield表达式的返回值。
function * f (){for (let k = 0; true; k++){let a = yield k;if(a){k = -1};}}let g =f();g.next(); // {value: 0, done: false}g.next(); // {value: 1, done: false}g.next(true); // {value: 0, done: false}
这一特点,可以让Generator函数开始执行之后,可以从外部向内部注入不同值,从而调整函数行为。
function * f(x){let y = 2 * (yield (x+1));let z = yield (y/3);return (x + y + z);}let a = f(5);a.next(); // {value : 6 ,done : false}a.next(); // {value : NaN ,done : false}a.next(); // {value : NaN ,done : true}// NaN因为yeild返回的是对象 和数字计算会NaNlet b = f(5);b.next(); // {value : 6 ,done : false}b.next(12); // {value : 8 ,done : false}b.next(13); // {value : 42 ,done : false}// x 5 y 24 z 13
14.4 for…of循环
for...of循环会自动遍历,不用调用next方法,需要注意的是,for...of遇到next返回值的done属性为true就会终止,return返回的不包括在for...of循环中。
function * f(){yield 1;yield 2;yield 3;yield 4;return 5;}for (let k of f()){console.log(k);}// 1 2 3 4 没有 5
14.5 Generator.prototype.throw()
throw方法用来向函数外抛出错误,并且在Generator函数体内捕获。
let f = function * (){try { yield }catch (e) { console.log('内部捕获', e) }}let a = f();a.next();try{a.throw('a');a.throw('b');}catch(e){console.log('外部捕获',e);}// 内部捕获 a// 外部捕获 b
14.6 Generator.prototype.return()
return方法用来返回给定的值,并结束遍历Generator函数,如果return方法没有参数,则返回值的value属性为undefined。
function * f(){yield 1;yield 2;yield 3;}let g = f();g.next(); // {value : 1, done : false}g.return('leo'); // {value : 'leo', done " true}g.next(); // {value : undefined, done : true}
14.7 next()/throw()/return()共同点
相同点就是都是用来恢复Generator函数的执行,并且使用不同语句替换yield表达式。
next()将yield表达式替换成一个值。
let f = function * (x,y){let r = yield x + y;return r;}let g = f(1, 2);g.next(); // {value : 3, done : false}g.next(1); // {value : 1, done : true}// 相当于把 let r = yield x + y;// 替换成 let r = 1;
throw()将yield表达式替换成一个throw语句。
g.throw(new Error('报错')); // Uncaught Error:报错// 相当于将 let r = yield x + y// 替换成 let r = throw(new Error('报错'));
next()将yield表达式替换成一个return语句。
g.return(2); // {value: 2, done: true}// 相当于将 let r = yield x + y// 替换成 let r = return 2;
14.8 yield* 表达式
用于在一个Generator中执行另一个Generator函数,如果没有使用yield*会没有效果。
function * a(){yield 1;yield 2;}function * b(){yield 3;yield * a();yield 4;}// 等同于function * b(){yield 3;yield 1;yield 2;yield 4;}for(let k of b()){console.log(k)}// 3// 1// 2// 4
14.9 应用场景
- 控制流管理
解决回调地狱:
// 使用前f1(function(v1){f2(function(v2){f3(function(v3){// ... more and more})})})// 使用PromisePromise.resolve(f1).then(f2).then(f3).then(function(v4){// ...},function (err){// ...}).done();// 使用Generatorfunction * f (v1){try{let v2 = yield f1(v1);let v3 = yield f1(v2);let v4 = yield f1(v3);// ...}catch(err){// console.log(err)}}function g (task){let obj = task.next(task.value);// 如果Generator函数未结束,就继续调用if(!obj.done){task.value = obj.value;g(task);}}g( f(initValue) );
- 异步编程的使用
在真实的异步任务封装的情况:
let fetch = require('node-fetch');function * f(){let url = 'http://www.baidu.com';let res = yield fetch(url);console.log(res.bio);}// 执行该函数let g = f();let result = g.next();// 由于fetch返回的是Promise对象,所以用thenresult.value.then(function(data){return data.json();}).then(function(data){g.next(data);})
15 Class语法和继承
15.1 介绍
ES6中的class可以看作只是一个语法糖,绝大部分功能都可以用ES5实现,并且,类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。
// ES5function P (x,y){this.x = x;this.y = y;}P.prototype.toString = function () {return '(' + this.x + ', ' + this.y + ')';};var a = new P(1, 2);// ES6class P {constructor(x, y){this.x = x;this.y = y;}toString(){return '(' + this.x + ', ' + this.y + ')';}}let a = new P(1, 2);
值得注意:
ES6的类的所有方法都是定义在prototype属性上,调用类的实例的方法,其实就是调用原型上的方法。
class P {constructor(){ ... }toString(){ ... }toNumber(){ ... }}// 等同于P.prototyoe = {constructor(){ ... },toString(){ ... },toNumber(){ ... }}let a = new P();a.constructor === P.prototype.constructor; // true
类的属性名可以使用表达式:
let name = 'leo';class P {constructor (){ ... }[name](){ ... }}
Class不存在变量提升:
ES6中的类不存在变量提升,与ES5完全不同:
new P (); // ReferenceErrorclass P{...};
Class的name属性:name属性总是返回紧跟在class后的类名。
class P {}P.name; // 'P'
15.2 constructor()方法
constructor()是类的默认方法,通过new实例化时自动调用执行,一个类必须有constructor()方法,否则一个空的constructor()会默认添加。constructor()方法默认返回实例对象(即this)。
class P { ... }// 等同于class P {constructor(){ ... }}
15.3 类的实例对象
与ES5一样,ES6的类必须使用new命令实例化,否则报错。
class P { ... }let a = P (1,2); // 报错let b = new P(1, 2); // 正确
与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。
class P {constructor(x, y){this.x = x;this.y = y;}toString(){return '(' + this.x + ', ' + this.y + ')';}}var point = new Point(2, 3);point.toString() // (2, 3)point.hasOwnProperty('x') // truepoint.hasOwnProperty('y') // truepoint.hasOwnProperty('toString') // falsepoint.__proto__.hasOwnProperty('toString') // true// toString是原型对象的属性(因为定义在Point类上)
15.4 Class表达式
与函数一样,类也可以使用表达式来定义,使用表达式来作为类的名字,而class后跟的名字,用来指代当前类,只能再Class内部使用。
let a = class P{get(){return P.name;}}let b = new a();b.get(); // PP.name; // ReferenceError: P is not defined
如果类的内部没用到的话,可以省略P,也就是可以写成下面的形式。
let a = class { ... }
15.5 私有方法和私有属性
由于ES6不提供,只能变通来实现:
- 1.使用命名加以区别,如变量名前添加
_,但是不保险,外面也可以调用到。
class P {// 公有方法f1 (x) {this._x(x);}// 私有方法_x (x){return this.y = x;}}
- 2.将私有方法移除模块,再在类内部调用
call方法。
class P {f1 (x){f2.call(this, x);}}function f2 (x){return this.y = x;}
- 3.使用
Symbol为私有方法命名。
const a1 = Symbol('a1');const a2 = Symbol('a2');export default class P{// 公有方法f1 (x){this[a1](x);}// 私有方法[a1](x){return this[a2] = x;}}
15.6 this指向问题
类内部方法的this默认指向类的实例,但单独使用该方法可能报错,因为this指向的问题。
class P{leoDo(thing = 'any'){this.print(`Leo do ${thing}`)}print(text){console.log(text);}}let a = new P();let { leoDo } = a;leoDo(); // TypeError: Cannot read property 'print' of undefined// 问题出在 单独使用leoDo时,this指向调用的环境,// 但是leoDo中的this是指向P类的实例,所以报错
解决方法:
- 1.在类里面绑定
this
class P {constructor(){this.name = this.name.bind(this);}}
- 2.使用箭头函数
class P{constructor(){this.name = (name = 'leo' )=>{this.print(`my name is ${name}`)}}}
15.7 Class的getter和setter
使用get和set关键词对属性设置取值函数和存值函数,拦截属性的存取行为。
class P {constructor (){ ... }get f (){return 'getter';}set f (val) {console.log('setter: ' + val);}}let a = new P();a.f = 100; // setter : 100a.f; // getter
15.8 Class的generator方法
只要在方法之前加个(*)即可。
class P {constructor (...args){this.args = args;}*[Symbol.iterator](){for (let arg of this.args){yield arg;}}}for (let k of new P('aa', 'bb')){console.log(k);}// 'aa'// 'bb'
15.9 Class的静态方法
由于类相当于实例的原型,所有类中定义的方法都会被实例继承,若不想被继承,只要加上static关键字,只能通过类来调用,即“静态方法”。
class P (){static f1 (){ return 'aaa' };}P.f1(); // 'aa'let a = new P();a.f1(); // TypeError: a.f1 is not a function
如果静态方法包含this关键字,则this指向类,而不是实例。
class P {static f1 (){this.f2();}static f2 (){console.log('aaa');}f2(){console.log('bbb');}}P.f2(); // 'aaa'
并且静态方法可以被子类继承,或者super对象中调用。
class P{static f1(){ return 'leo' };}class Q extends P { ... };Q.f1(); // 'leo'class R extends P {static f2(){return super.f1() + ',too';}}R.f2(); // 'leo , too'
15.10 Class的静态属性和实例属性
ES6中明确规定,Class内部只有静态方法没有静态属性,所以只能通过下面实现。
// 正确写法class P {}P.a1 = 1;P.a1; // 1// 无效写法class P {a1: 2, // 无效static a1 : 2, // 无效}P.a1; // undefined
新提案来规定实例属性和静态属性的新写法
- 1.类的实例属性
类的实例属性可以用等式,写入类的定义中。
class P {prop = 100; // prop为P的实例属性 可直接读取constructor(){console.log(this.prop); // 100}}
有了新写法后,就可以不再contructor方法里定义。
为了可读性的目的,对于那些在constructor里面已经定义的实例属性,新写法允许直接列出。
// 之前写法:class RouctCounter extends React.Component {constructor(prop){super(prop);this.state = {count : 0}}}// 新写法class RouctCounter extends React.Component {state;constructor(prop){super(prop);this.state = {count : 0}}}
- 2.类的静态属性
只要在实例属性前面加上static关键字就可以。
class P {static prop = 100;constructor(){console.log(this.prop)}; // 100}
新写法方便静态属性的表达。
// oldclass P { .... }P.a = 1;// newclass P {static a = 1;}
15.11 Class的继承
主要通过extends关键字实现,继承父类的所有属性和方法,通过super关键字来新建父类构造函数的this对象。
class P { ... }class Q extends P { ... }class P {constructor(x, y){// ...}f1 (){ ... }}class Q extends P {constructor(a, b, c){super(x, y); // 调用父类 constructor(x, y)this.color = color ;}f2 (){return this.color + ' ' + super.f1();// 调用父类的f1()方法}}
子类必须在constructor()调用super()否则报错,并且只有super方法才能调用父类实例,还有就是,父类的静态方法,子类也可以继承到。
class P {constructor(x, y){this.x = x;this.y = y;}static fun(){console.log('hello leo')}}// 关键点1 调用superclass Q extends P {constructor(){ ... }}let a = new Q(); // ReferenceError 因为Q没有调用super// 关键点2 调用superclass R extends P {constructor (x, y. z){this.z = z; // ReferenceError 没调用super不能使用super(x, y);this.z = z; // 正确}}// 关键点3 子类继承父类静态方法R.hello(); // 'hello leo'
super关键字:
既可以当函数使用,还可以当对象使用。
- 1.当函数调用,代表父类的构造函数,但必须执行一次。
class P {... };class R extends P {constructor(){super();}}
- 2.当对象调用,指向原型对象,在静态方法中指向父类。
class P {f (){ return 2 };}class R extends P {constructor (){super();console.log(super.f()); // 2}}let a = new R()
注意:super指向父类原型对象,所以定义在父类实例的方法和属性,是无法通过super调用的,但是通过调用super方法可以把内部this指向当前实例,就可以访问到。
class P {constructor(){this.a = 1;}print(){console.log(this.a);}}class R extends P {get f (){return super.a;}}let b = new R();b.a; // undefined 因为a是父类P实例的属性// 先调用super就可以访问class Q extends P {constructor(){super(); // 将内部this指向当前实例return super.a;}}let c = new Q();c.a; // 1// 情况3class J extends P {constructor(){super();this.a = 3;}g(){super.print();}}let c = new J();c.g(); // 3 由于执行了super()后 this指向当前实例
16 Module语法和加载实现
16.1 介绍
ES6之前用于JavaScript的模块加载方案,是一些社区提供的,主要有CommonJS和AMD两种,前者用于服务器,后者用于浏览器。
ES6提供了模块的实现,使用export命令对外暴露接口,使用import命令输入其他模块暴露的接口。
// CommonJS模块let { stat, exists, readFire } = require('fs');// ES6模块import { stat, exists, readFire } = from 'fs';
16.2 严格模式
ES6模块自动采用严格模式,无论模块头部是否有"use strict"。
严格模式有以下限制:
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with语句 - 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop,会报错,只能删除属性delete * global[prop] eval不会在它的外层作用域引入变量eval和arguments不能被重新赋值arguments不会自动反映函数参数的变化- 不能使用
arguments.callee - 不能使用
arguments.caller - 禁止
this指向全局对象 - 不能使用
fn.caller和fn.arguments获取函数调用的堆栈 - 增加了保留字(比如
protected、static和interface)
特别是,ES6中顶层this指向undefined,即不应该在顶层代码使用this。
16.3 export命令
使用export向模块外暴露接口,可以是方法,也可以是变量。
// 1. 变量export let a = 'leo';export let b = 100;// 还可以let a = 'leo';let b = 100;export {a, b};// 2. 方法export function f(a,b){return a*b;}// 还可以function f1 (){ ... }function f2 (){ ... }export {a1 as f1,a2 as f2}
可以使用as重命名函数的对外接口。
特别注意:export暴露的必须是接口,不能是值。
// 错误export 1; // 报错let a = 1;export a; // 报错// 正确export let a = 1; // 正确let a = 1;export {a}; // 正确let a = 1;export { a as b}; // 正确
暴露方法也是一样:
// 错误function f(){...};export f;// 正确export function f () {...};function f(){...};export {f};
16.4 import命令
加载export暴露的接口,输出为变量。
import { a, b } from '/a.js';function f(){return a + b;}
import后大括号指定变量名,需要与export的模块暴露的名称一致。
也可以使用as为输入的变量重命名。
import { a as leo } from './a.js';
import不能直接修改输入变量的值,因为输入变量只读只是个接口,但是如果是个对象,可以修改它的属性。
// 错误import {a} from './f.js';a = {}; // 报错// 正确a.foo = 'leo'; // 不报错
import命令具有提升效果,会提升到整个模块头部最先执行,且多次执行相同import只会执行一次。
16.5 模块的整体加载
当一个模块暴露多个方法和变量,引用时可以用*整体加载。
// a.jsexport function f(){...}export function g(){...}// b.jsimport * as obj from '/a.js';console.log(obj.f());console.log(obj.g());
但是,不允许运行时改变:
import * as obj from '/a.js';// 不允许obj.a = 'leo';obj.b = function(){...};
16.6 export default 命令
使用export default命令,为模块指定默认输出,引用的时候直接指定任意名称即可。
// a.jsexport default function(){console.log('leo')};// b.jsimport leo from './a.js';leo(); // 'leo'
export default暴露有函数名的函数时,在调用时相当于匿名函数。
// a.jsexport default function f(){console.log('leo')};// 或者function f(){console.log('leo')};export default f;// b.jsimport leo from './a.js';
export default其实是输出一个名字叫default的变量,所以后面不能跟变量赋值语句。
// 正确export let a= 1;let a = 1;export default a;// 错误export default let a = 1;
export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。
// 正确export detault 1;// 错误export 1;
16.7 export 和 import 复合写法
常常在先输入后输出同一个模块使用,即转发接口,将两者写在一起。
export {a, b} from './leo.js';// 理解为import {a, b} from './leo.js';export {a, b}
常见的写法还有:
// 接口改名export { a as b} from './leo.js';// 整体输出export * from './leo.js';// 默认接口改名export { default as a } from './leo.js';
常常用在模块继承。
16.8 浏览器中的加载规则
ES6中,可以在浏览器使用<script>标签,需要加入type="module"属性,并且这些都是异步加载,避免浏览器阻塞,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。
<script type="module" src="./a.js"></script>
另外,ES6模块也可以内嵌到网页,语法与外部加载脚本一致:
<script type="module">
import a from './a.js';
</script>
注意点:
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明
use strict。 - 模块之中,可以使用
import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。 - 模块之中,顶层的
this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。 - 同一个模块如果加载多次,将只执行一次。
