一、介绍
现在的网络上已经有各样关于 ECMAScript 规范介绍和分析的文章,而我自己重新学习一遍这些规范,整理出这么一份笔记,比较精简,主要内容涵盖ES6、ES7、ES8、ES9,后续会增加面试题,框架入门等笔记,欢迎吐槽交流。
这份资料的ES6部分将会参考阮一峰老师的 ECMAScript6入门 ,精简和整理出快速实用的内容。
另外ES7/ES8/ES9则会从网络综合参考和整理。
ES全称ECMAScript:
目前JavaScript使用的ECMAScript版本为ECMAScript-262。
ECMAScript版本 | 发布时间 | 新增特性 |
ECMAScript 2009(ES5) | 2009年11月 | 扩展了Object、Array、Function的功能等 |
ECMAScript 2015(ES6) | 2015年6月 | 类,模块化,箭头函数,函数参数默认值等 |
ECMAScript 2016(ES7) | 2016年3月 | includes,指数操作符 |
ECMAScript 2017(ES8) | 2017年6月 | async/await,Object.values(),Object.entries(),St123 ; b['myfun'] => 'hi'ring padding等 |
本文博客 CuteECMAScript
本文开源地址 CuteECMAScript
个人博客 ping'anの博客
二、正文
1. ES6
1.1 let 和 const命令
在ES6中,我们通常实用 let
表示变量,const
表示常量,并且 let
和 const
都是块级作用域,且在当前作用域有效不能重复声明。
1.1.1 let 命令
let
命令的用法和 var
相似,但是 let
只在所在代码块内有效。
基础用法:
{ let a = 1; let b = 2; }
并且 let
有以下特点:
- 不存在变量提升:
在ES6之前,我们var
声明一个变量一个函数,都会伴随着变量提升的问题,导致实际开发过程经常出现一些逻辑上的疑惑,按照一般思维习惯,变量都是需要先声明后使用。
// var console.log(v1); // undefined var v1 = 2; // 由于变量提升 代码实际如下 var v1; console.log(v1) v1 = 2; // let console.log(v2); // ReferenceError let 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.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
1.2 变量的解构赋值
解构赋值概念:在ES6中,直接从数组和对象中取值,按照对应位置,赋值给变量的操作。
1.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, 3 let [ , , c] = [1, 2, 3]; console.log(c); // 3 let [a, , c] = [1, 2, 3]; console.log(a,c); // 1, 3 let [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 => undefined let [a, b] = [1]; // a => 1 , b => 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 => 1 let [a, b = 2] = [a]; // a => 1 , b => 2
特殊情况:
let [a = 1] = [undefined]; // a => 1 let [a = 1] = [null]; // a => null
右边模式对应的值,必须严格等于undefined
,默认值才能生效,而null
不严格等于undefined
。
1.2.2 对象的解构赋值
与数组解构不同的是,对象解构不需要严格按照顺序取值,而只要按照变量名去取对应属性名的值,若取不到对应属性名的值,则为undefined
。
基础用法:
let {a, b} = {a:1, b:2}; // a => 1 , b => 2 let {a, b} = {a:2, b:1}; // a => 2 , b => 1 let {a} = {a:3, b:2, c:1};// a => 3 let {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 => 1 let {a, b=1} = {a:2}; // a => 2, b => 1 let {a:b=3} = {}; // b => 3 let {a:b=3} = {a:4}; // b = >4 // a是模式,b是变量 牢记 let {a=1} = {a:undefined}; // a => 1 let {a=1} = {a:null}; // a => null // 因为null与undefined不严格相等,所以赋值有效 // 导致默认值1不会生效。
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
1.2.4 数值和布尔值的解构赋值
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined
和null
无法转为对象,所以对它们进行解构赋值,都会报错。
// 数值和布尔值的包装对象都有toString属性 let {toString: s} = 123; s === Number.prototype.toString // true let {toString: s} = true; s === Boolean.prototype.toString // true let { prop: x } = undefined; // TypeError let { prop: y } = null; // TypeError
1.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]
1.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');
1.3 字符串的拓展
1.3.1 includes(),startsWith(),endsWith()
在我们判断字符串是否包含另一个字符串时,ES6之前,我们只有typeof
方法,ES6之后我们又多了三种方法:
- includes():返回布尔值,表示是否找到参数字符串。
- startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
- endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let a = 'hello leo'; a.startsWith('leo'); // false a.endsWith('o'); // true a.includes('lo'); // true
并且这三个方法都支持第二个参数,表示起始搜索的位置。
let a = 'hello leo'; a.startsWith('leo',1); // false a.endsWith('o',5); // true a.includes('lo',6); // false
endsWith
是针对前 n
个字符,而其他两个是针对从第n
个位置直到结束。
1.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'
1.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 '
1.3.4 模版字符串
用于拼接字符串,ES6之前:
let a = 'abc' + 'def' + 'ghi';
ES6之后:
let a = ` abc def ghi `
拼接变量: 在**反引号(`)**中使用${}
包裹变量或方法。
// ES6之前 let a = 'abc' + v1 + 'def'; // ES6之后 let a = `abc${v1}def`
1.4 正则的拓展
1.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');
1.4.2 字符串的正则方法
常用的四种方法:match()
、replace()
、search()
和split()
。
1.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 // false b.unicode // true
1.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'); // null a.lastIndex = 3; // 从3号位置开始匹配 let c = a.exec('wahaha'); c.index; // 3 a.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
1.4.5 flags属性
flags
属性返回所有正则表达式的修饰符。
/abc/ig.flags; // 'gi'
1.5 数值的拓展
1.5.1 Number.isFinite(), Number.isNaN()
Number.isFinite()
用于检查一个数值是否是有限的,即不是Infinity
,若参数不是Number
类型,则一律返回false
。
Number.isFinite(10); // true Number.isFinite(0.5); // true Number.isFinite(NaN); // false Number.isFinite(Infinity); // false Number.isFinite(-Infinity); // false Number.isFinite('leo'); // false Number.isFinite('15'); // false Number.isFinite(true); // false Number.isFinite(Math.random()); // true
Number.isNaN()
用于检查是否是NaN
,若参数不是NaN
,则一律返回false
。
Number.isNaN(NaN); // true Number.isNaN(10); // false Number.isNaN('10'); // false Number.isNaN(true); // false Number.isNaN(5/NaN); // true Number.isNaN('true' / 0); // true Number.isNaN('true' / 'true'); // true
区别:
与传统全局的isFinite()
和isNaN()
方法的区别,传统的这两个方法,是先将参数转换成数值,再判断。
而ES6新增的这两个方法则只对数值有效, Number.isFinite()
对于非数值一律返回false
,Number.isNaN()
只有对于NaN
才返回true
,其他一律返回false
。
isFinite(25); // true isFinite("25"); // true Number.isFinite(25); // true Number.isFinite("25"); // false isNaN(NaN); // true isNaN("NaN"); // true Number.isNaN(NaN); // true Number.isNaN("NaN"); // false
1.5.2 Number.parseInt(), Number.parseFloat()
这两个方法与全局方法parseInt()
和parseFloat()
一致,目的是逐步减少全局性的方法,让语言更模块化。
parseInt('12.34'); // 12 parseFloat('123.45#'); // 123.45 Number.parseInt('12.34'); // 12 Number.parseFloat('123.45#'); // 123.45 Number.parseInt === parseInt; // true Number.parseFloat === parseFloat; // true
1.5.3 Number.isInteger()
用来判断一个数值是否是整数,若参数不是数值,则返回false
。
Number.isInteger(10); // true Number.isInteger(10.0); // true Number.isInteger(10.1); // false
1.5.4 Math对象的拓展
ES6新增17个数学相关的静态方法,只能在Math对象上调用。
- Math.trunc:
用来去除小数的小数部分,返回整数部分。
若参数为非数值,则先转为数值。
若参数为空值或无法截取整数的值,则返回NaN。
// 正常使用 Math.trunc(1.1); // 1 Math.trunc(1.9); // 1 Math.trunc(-1.1); // -1 Math.trunc(-1.9); // -1 Math.trunc(-0.1234); // -0 // 参数为非数值 Math.trunc('11.22'); // 11 Math.trunc(true); // 1 Math.trunc(false); // 0 Math.trunc(null); // 0 // 参数为空和无法取整 Math.trunc(NaN); // NaN Math.trunc('leo'); // NaN Math.trunc(); // NaN Math.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); // -1 Math.sign(1); // +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('leo');// NaN Math.sign(); // NaN Math.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); // -1 Math.cbrt(0); // 0 Math.cbrt(1); // 1 Math.cbrt(2); // 1.2599210498 Math.cbrt('1'); // 1 Math.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) // 32 Math.clz32(1) // 31 Math.clz32(1000) // 22 Math.clz32(0b01000000000000000000000000000000) // 1 Math.clz32(0b00100000000000000000000000000000) // 2
- Math.imul():
用于返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。
Math.imul(2, 4) // 8 Math.imul(-1, 8) // -8 Math.imul(-2, -2) // 4
- Math.fround():
用来返回一个数的2位单精度浮点数形式。
Math.fround(0) // 0 Math.fround(1) // 1 Math.fround(2 ** 24 - 1) // 16777215
- Math.hypot():
用来返回所有参数的平方和的平方根。
Math.hypot(3, 4); // 5 Math.hypot(3, 4, 5); // 7.0710678118654755 Math.hypot(); // 0 Math.hypot(NaN); // NaN Math.hypot(3, 4, 'foo'); // NaN Math.hypot(3, 4, '5'); // 7.0710678118654755 Math.hypot(-3); // 3
- Math.expm1():
用来返回ex - 1
,即Math.exp(x) - 1
。
Math.expm1(-1) // -0.6321205588285577 Math.expm1(0) // 0 Math.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.6931471805599453 Math.log1p(0) // 0 Math.log1p(-1) // -Infinity Math.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.3010299956639812 Math.log10(1) // 0 Math.log10(0) // -Infinity Math.log10(-2) // NaN Math.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.584962500721156 Math.log2(2) // 1 Math.log2(1) // 0 Math.log2(0) // -Infinity Math.log2(-2) // NaN Math.log2(1024) // 10 Math.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)
1.5.5 指数运算符
新增的指数运算符(**
):
2 ** 2; // 4 2 ** 3; // 8 2 ** 3 ** 2; // 相当于 2 ** (3 ** 2); 返回 512
指数运算符(**
)与Math.pow
的实现不相同,对于特别大的运算结果,两者会有细微的差异。
Math.pow(99, 99) // 3.697296376497263e+197 99 ** 99 // 3.697296376497268e+197
1.6 函数的拓展
1.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 leo f('hi', 'jack'); // hi jack f('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 1 f({a:2}); // 2 1 f({a:2, b:3}); // 2 3 f(); // 报错 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; // 1 f2.length; // 0 f3.length; // 1 f4.length; // 0 f5.length; // 2
1.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); // 1 f2(1,2); // 1
1.6.3 name 属性
用于返回该函数的函数名。
function f (){...}; f.name; // f const f = function g(){...}; f.name; // g
1.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
就会指向被点击的按钮对象。
1.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;
1.7 数组的拓展
1.7.1 拓展运算符
拓展运算符使用(...
),类似rest
参数的逆运算,将数组转为用(,
)分隔的参数序列。
console.log(...[1, 2, 3]); // 1 2 3 console.log(1, ...[2,3], 4); // 1 2 3 4
拓展运算符主要使用在函数调用。
function f (a, b){ console.log(a, b); } f(...[1, 2]); // 1 2 function 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]; // ES5 let a4 = a1.concat(a2, a3); // ES6 let a5 = [...a1, ...a2, ...a3]; a4[0] === a1[0]; // true a5[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 => []
1.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));
1.7.3 Array.of()
将一组数值,转换成数组,弥补Array
方法参数不同导致的差异。
Array.of(1,2,3); // [1,2,3] Array.of(1).length; // 1 Array(); // [] Array(2); // [,] 1个参数时,为指定数组长度 Array(1,2,3); // [1,2,3] 多于2个参数,组成新数组
1.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
1.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]
1.7.6 entries(),keys(),values()
主要用于遍历数组,entries()
对键值对遍历,keys()
对键名遍历,values()
对键值遍历。
for (let i of ['a', 'b'].keys()){ console.log(i) } // 0 // 1 for (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"]
1.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
1.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]
1.8 对象的拓展
1.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'; } }
1.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'};
1.8.3 Object.is()
Object.is()
用于比较两个值是否严格相等,在ES5时候只要使用相等运算符(==
)和严格相等运算符(===
)就可以做比较,但是它们都有缺点,前者会自动转换数据类型,后者的NaN
不等于自身,以及+0
等于-0
。
Object.is('a','a'); // true Object.is({}, {}); // false // ES5 +0 === -0 ; // true NaN === NaN; // false // ES6 Object.is(+0,-0); // false Object.is(NaN,NaN); // true
1.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]
1.9 Symbol
1.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; // false Number(a1); // TypeError a1 + 1 ; // TypeError
1.9.2 Symbol作为属性名
好处:防止同名属性,还有防止键被改写或覆盖。
let a1 = Symbol(); // 写法1 let b = {}; b[a1] = 'hello'; // 写法2 let b = { [a1] : 'hello' } // 写法3 let b = {}; Object.defineProperty(b, a1, {value : 'hello' }); // 3种写法 结果相同 b[a1]; // 'hello'
需要注意: Symbol作为对象属性名时,不能用点运算符,并且必须放在方括号内。
let a = Symbol(); let b = {}; // 不能用点运算 b.a = 'hello'; b[a] ; // undefined b['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') }
1.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);
1.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值作为名称的属性不被常规方法遍历获取,因此常用于定义对象的一些非私有,且内部使用的方法。
1.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'); // true Symbol('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
1.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]; // undefined let 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; // true b instanceof P; // true // 解决: 通过使用 Symbol.species class P extends Array { static get [Symbol.species]() { return Array; } } let a = new P(); let b = a.map(x => x); b instanceof P; // false b 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 // 246 3 + obj // '3default' obj == 'default' // true String(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
将指向外层作用域的变量。