ECMAScript 6.0(简称ES6),做为下一代JavaScript的语言标准正式发布于2015 年 6 月,至今已经发布4年多了,可是由于蕴含的语法之广,彻底消化须要必定的时间,这里我总结了本身在学习ES6过程当中的一些知识点,以及ES6之后新语法的知识点,使用场景,但愿对各位有所帮助.node
俗话说得好,好记性不如烂笔头,本身多敲一遍代码记得快,印象也深入程序员
严格模式是ES5引入, 严格模式主要有如下限制;es6
基本用法跟 es5 的 var 同样,可是 let 声明不存在变量提高现象算法
var 声明存在变量提高现象, let和const则不会有这种状况编程
只要块级做用域内存在let命令, 它所声明的变量就"绑定"(binding)这个区域,再也不受外部的影响json
var num = 123
if (true) {
num = 'abc' // Cannot access 'num' before initialization
let num
}
复制代码
ES6明确规定, 若是区块中存在let和const命令, 这个区块对这些命令声明的变量,从一开始就造成了封闭做用域,凡是在声明以前就使用这些变量,就会报错数组
暂时性死去的本质就是, 只要一进入当前做用域,全部使用的变量就已经存在了, 可是不可获取, 只有等声明变量的那一行代码出现,才能够获取和使用该变量promise
let 不容许在相同的做用域内, 重复声明同一个变量浏览器
// 报错
function fn() {
let a = 1
var a = 2
}
// 报错
function fn() {
let a = 1
let a = 2
}
复制代码
所以,不能在函数内部从新声明参数。bash
function fn(arg) {
let arg; // 报错
}
function fn(arg) {
{
let arg; // 不报错
}
}
复制代码
为何须要快做用域
ES5只有全局做用域和函数做用域, 没有块做用域, 这种状况带来了不少不合理的场景
第一种场景, 内层变量可能会覆盖外层变量
var num = 123
function fn() {
console.log(num)
if (false) { // 内部声明变量覆盖了全局, 因为是var声明出现变量提高上面的num值为undefined
var num = 456
}
}
fn() // undefined
复制代码
第二种场景, 用来计数的循环变量泄露成为全局变量
var s = 'hello';
for (var i = 0; i < s.length; i++) { // 这里var声明的变量自动挂载到了全局
console.log(s[i]);
}
console.log(i); // 5
复制代码
ES6 的块级做用域
function fn() {
let n = 5
if (true) {
let n = 10
}
console.log(n)
}
fn() // 5
复制代码
ES6 容许块级做用域的任意嵌套。
块级做用域的出现,实际上使得得到普遍应用的当即执行函数表达式(IIFE / 当即调用的函数表达式)再也不必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级做用域写法
{
let tmp = ...;
...
}
复制代码
块级做用域不返回值,除非t是全局变量。
const声明一个只读的常量。
const除了如下两点与let不一样外,其余特性均与let相同:
1. const一旦声明变量,就必须当即初始化,不能留到之后赋值。
2. 一旦声明,常量的值就不能改变。
复制代码
const限定的是赋值行为。
const a = 1;
a = 2; // 报错
const arr = [];
arr.push(1) // [1]
//在声明引用型数据为常量时,const保存的是变量的指针,只要保证指针不变就不会保存。下面的行为就会报错
arr = []; // 报错 由于是赋值行为。变量arr保存的指针改变了。
复制代码
顶层对象, 在浏览器环境指的是window对象, 在Node环境指的是global对象, ES5之中,顶层对象的属性与全局变量是等价的
window.a = 1
a // 1
a = 2;
window.a // 2
复制代码
顶层对象的属性与全局变量挂钩, 被认为是JavaScript语言最大的设计败笔之一
为了解决这个问题, ES6引入的let cosnt class声明的全局变量再也不属于顶层对象的属性
而同时为了向下兼容, var和function声明的变量依然属于全局对象的属性
var a = 1;
window.a // 1
let b = 1;
window.b // undefined
复制代码
ES6容许按照必定模式, 从数组和对象中提取值, 对变量进行赋值, 这被称为解构(Destructuring)
ES5一次声明多个变量
var a = 1,
b = 2,
c = 3;
复制代码
ES6一次声明多个变量
let [a, b, c] = [1, 2, 3]
// a = 1
// b = 2
// c = 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
z // []
复制代码
若是解构不成功,变量的值就等于undefined。
let [foo] = [];
let [bar, foo] = [1];
// foo 都是undefined
复制代码
另外一种状况是不彻底解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种状况下,解构依然能够成功。
let [x, y] = [1, 2, 3];
x // 1
y // 2
let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4
//上面两个例子,都属于不彻底解构,可是能够成功。
复制代码
若是等号的右边不是数组,那么将会报错。
// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
复制代码
解构赋值容许指定默认值。
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];
x // null
复制代码
若是默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。
function f() {
console.log('aaa');
}
let [x = f()] = [1]; // [1]
//等价于
let x;
if ([1][0] === undefined) {
x = f();
} else {
x = [1][0];
}
复制代码
默认值能够引用解构赋值的其余变量,但该变量必须已经声明。
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
//上面最后一个表达式之因此会报错,是由于x用到默认值y时,y尚未声明
复制代码
解构不只能够用于数组, 还能够用于对象
let { foo, bar } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
复制代码
对象的解构与数组有一个重要的不一样。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"
let { baz } = { foo: "aaa", bar: "bbb" };
baz // undefined
复制代码
若是变量名与属性名不一致,必须写成下面这样。
let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
baz // "aaa"
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj;
f // 'hello'
l // 'world'
复制代码
实际上说明,对象的解构赋值是下面形式的简写
let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
复制代码
也就是说,对象的解构赋值的内部机制,是先找到同名属性,而后再赋给对应的变量。真正被赋值的是后者,而不是前者。
let { foo: baz } = { foo: "aaa", bar: "bbb" };
baz // "aaa"
foo // error: foo is not defined
复制代码
与数组同样,解构也能够用于嵌套结构的对象。
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"
复制代码
对象的解构也能够指定默认值。
var {x = 3} = {};
x // 3
var {x, y = 5} = {x: 1};
x // 1
y // 5
var {x: y = 3} = {};
y // 3
var {x: y = 3} = {x: 5};
y // 5
var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"
复制代码
默认值生效的条件是,对象的属性值严格等于undefined。
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
复制代码
若是解构模式是嵌套的对象,并且子对象所在的父属性不存在,那么将会报错。
// 报错
let {foo: {bar}} = {baz: 'baz'};
//等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。缘由很简单,由于foo这时等于undefined,再取子属性就会报错,
复制代码
因为数组本质是特殊的对象,所以能够对数组进行对象属性的解构。
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 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
复制代码
function add([a,b]){
return a + b;
}
add([2,3])//5
复制代码
函数参数的解构也可使用默认值。
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]
复制代码
注意,下面的写法会获得不同的结果。
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]
复制代码
上面代码是为函数move的参数指定默认值,而不是为变量x和y指定默认值,因此会获得与前一种写法不一样的结果。
解构赋值时,若是等号右边是数值和布尔值,则会先转为对象。
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
s === Boolean.prototype.toString // true
复制代码
解构赋值虽然很方便,可是解析起来并不容易。对于编译器来讲,一个式子究竟是模式,仍是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。
由此带来的问题是,若是模式中出现圆括号怎么处理。ES6 的规则是,只要有可能致使解构的歧义,就不得使用圆括号。
可是,这条规则实际上不那么容易辨别,处理起来至关麻烦。所以,建议只要有可能,就不要在模式中放置圆括号。
不能使用圆括号的状况如下三种解构赋值不得使用圆括号。
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];
//上面代码将整个模式放在圆括号之中,致使报错。
// 报错
[({ p: a }), { x: c }] = [{}, {}];
复制代码
**可使用圆括号的状况 ** 可使用圆括号的状况只有一种:赋值语句的非模式部分,可使用圆括号。
[(b)] = [3]; // 正确
({ p: (d) } = {}); // 正确
[(parseInt.prop)] = [3]; // 正确
复制代码
上面三行语句均可以正确执行,由于首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。
用途
1. 除了能够一次定义多个变量
2. 还可让函数返回多个值
3. 能够方便地让函数的参数跟值对应起来
4. 提取json数据
5. 函数参数的默认值
复制代码
includes()、startsWith()、endsWith()
传统上,JavaScript 只有indexOf方法,能够用来肯定一个字符串是否包含在另外一个字符串中。ES6 又提供了三种新方法。
let s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true
复制代码
这三个方法都支持第二个参数,表示开始搜索的位置。
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"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""
复制代码
ES2017 引入了字符串补全长度的功能。若是某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
复制代码
es5的字符串模板输出一般是使用+拼接。
这样的缺点显然易见:字符串拼接内容多的时候,过于混乱,易出错。
而ES6 引入了模板字符串解决这个问题。
var name = "番茄",trait = "帅气";
//es5dDdD
var str = "他叫"+name+",人很是"+trait+",说话又好听";
//es6
var str2 = `他叫 ${name} ,人很是 ${trait} ,说话又好听`;
复制代码
模板字符串是加强版的字符串,用反引号(`)标识。它能够看成普通字符串使用,也能够用来定义多行字符串,或者在字符串中嵌入变量。
模板字符串能够紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能。
alert`123`
// 等同于
alert(123)
复制代码
标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。
若是模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。
let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);
复制代码
Number.isFinite()、Number.isNaN()
ES6 在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法。
Number.isFinite()用来检查一个数值是否为有限的(finite)。
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
复制代码
Number.isNaN()用来检查一个值是否为NaN。
和全局函数 isNaN() 相比,该方法不会强制将参数转换成数字,只有在参数是真正的数字类型,且值为 NaN 的时候才会返回 true。
Number.isNaN(NaN); // true
Number.isNaN(Number.NaN); // true
Number.isNaN(0 / 0) // true
// 下面这几个若是使用全局的 isNaN() 时,会返回 true。
Number.isNaN("NaN"); // false,字符串 "NaN" 不会被隐式转换成数字 NaN。
Number.isNaN(undefined); // false
Number.isNaN({}); // false
Number.isNaN("blabla"); // false
// 下面的都返回 false
Number.isNaN(true);
Number.isNaN(null);
Number.isNaN(37);
Number.isNaN("37");
Number.isNaN("37.37");
Number.isNaN("");
Number.isNaN(" ");
复制代码
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
复制代码
这样作的目的,是逐步减小全局性方法,使得语言逐步模块化。
Number.parseInt === parseInt // true
Number.parseFloat === parseFloat // true
复制代码
Number.isInteger()用来判断一个值是否为整数。须要注意的是,在 JavaScript 内部,整数和浮点数是一样的储存方法,因此 3 和 3.0 被视为同一个值。
Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false
复制代码
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.sign() Math.sign方法用来判断一个数究竟是正数、负数、仍是零。对于非数值,会先将其转换为数值。
它会返回五种值。
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方法内部也是先使用Number方法将其转为数值。
Math.cbrt(-1) // -1
Math.cbrt(0) // 0
Math.cbrt(1) // 1
Math.cbrt(2) // 1.2599210498948734
复制代码
Math.hypot() 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
复制代码
指数运算符 ES2016 新增了一个指数运算符(**)。
2 ** 2 // 4
2 ** 3 // 8
复制代码
指数运算符能够与等号结合,造成一个新的赋值运算符(**=)。
let a = 1.5;
a **= 2;
// 等同于 a = a * a;
let b = 4;
b **= 3;
// 等同于 b = b * b * b;
复制代码
ES6 以前,不能直接为函数的参数指定默认值,只能采用变通的方法。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
复制代码
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
复制代码
一般状况下,定义了默认值的参数,应该是函数的尾参数。由于这样比较容易看出来,到底省略了哪些参数。若是非尾部的参数设置默认值,实际上这个参数是无法省略的。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined])
f(, 1) // 报错
f(undefined, 1) // [1, 1]
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]
复制代码
指定了默认值之后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
复制代码
一旦设置了参数的默认值,函数进行声明初始化时,参数会造成一个单独的做用域。等到初始化结束,这个做用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
复制代码
上面代码中,参数y的默认值等于变量x。调用函数f时,参数造成一个单独的做用域。在这个做用域里面,默认值变量x指向第一个参数x,而不是全局变量x,因此输出是2。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
复制代码
上面代码中,函数f调用时,参数y = x造成一个单独的做用域。这个做用域里面,变量x自己没有定义,因此指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。
var x = 1;
function foo(x = x) {
// ...
}
foo() // ReferenceError: x is not defined
复制代码
上面代码中,参数x = x造成一个单独做用域。实际执行的是let x = x,因为暂时性死区的缘由,这行代码会报错”x 未定义“。
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
foo()//3
x//1
复制代码
若是将var x = 3的var去除,函数foo的内部变量x就指向第一个参数x,与匿名函数内部的x是一致的,因此最后输出的就是2,而外层的全局变量x依然不受影响。
var x = 1;
function foo(x, y = function() { x = 2; }) {
x = 3;
y();
console.log(x);
}
foo() // 2
x // 1
复制代码
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不须要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
复制代码
arguments对象不是数组,而是一个相似数组的对象。因此为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法均可以使用。下面是一个利用 rest 参数改写数组push方法的例子。
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
复制代码
注意,rest 参数以后不能再有其余参数(即只能是最后一个参数),不然会报错。
// 报错
function f(a, ...b, c) {
// ...
}
复制代码
函数的length属性,不包括 rest 参数。
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
复制代码
从 ES5 开始,函数内部能够设定为严格模式。
ES2016 作了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,不然会报错
返回函数名。
function foo() {}
foo.name // "foo"
var f = function () {}; // "f"
复制代码
ES6 容许使用“箭头”(=>)定义函数。
var f = v => v;
//上面的箭头函数等同于
var f = function(v) {
return v;
};
若是箭头函数不须要参数或须要多个参数,就使用一个圆括号表明参数部分。
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
若是箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,而且使用return语句返回。
var sum = (num1, num2) => { return num1 + num2; }
因为大括号被解释为代码块,因此若是箭头函数直接返回一个对象,必须在对象外面加上括号,不然会报错。
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
复制代码
this指向的固定化,并非由于箭头函数内部有绑定this的机制,实际缘由是箭头函数根本没有本身的this,致使内部的this就是外层代码块的this。正是由于它没有this,因此也就不能用做构造函数。
箭头函数转成 ES5 的代码以下。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
//转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有本身的this,而是引用外层的this。
复制代码
因为箭头函数没有本身的this,因此固然也就不能用call()、apply()、bind()这些方法去改变this的指向。
ES2017 容许函数的最后一个参数有尾逗号(trailing comma)。
这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。
function clownsEverywhere(
param1,
param2,
) { /* ... */ }
clownsEverywhere(
'foo',
'bar',
);
复制代码
扩展运算符(spread)是三个点(...)。它比如 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3])
// 1 2 3
console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5
[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]
复制代码
该运算符将一个数组,变为参数序列。
扩展运算符后面还能够放置表达式。
var x = 1
const arr = [...(x > 0 ? ['a'] : [], 'b')]
console.log(arr) // ['a', 'b']
复制代码
若是扩展运算符后面是一个空数组,则不产生任何效果。
var arr = [...[], 1]
console.log(arr) // [1]
复制代码
// ES5 的写法
function fn(x, y, z) {
// ...
}
var arr = [1, 2, 3]
fn.apply(null, arr)
// ES6 的写法
function fn(x, y, z) {
// ...
}
var arr = [1, 2, 3]
fn(...arr)
复制代码
es5的时候你们的利用Math.max拿数组最大值
//es5
Math.max.apply(null,[1,5,2,8]) // 8
//es6
Math.max(...[1,5,2,8]) // 8
//上面两种方法等同于
Math.max(1,5,2,8)
复制代码
// 复制数组
// ES5 的方法
var arr = [1, 2, 3]
var arr1 = arr.concat()
arr1[arr1.length] = 5
console.log(arr) // [1, 2, 3]
console.log(arr1) // [1, 2, 3, 5]
// 扩展运算符提供了复制数组的简便写法。
// 方法一
var arr = [1, 2, 3]
var arr1 = [...arr]
console.log(arr1) // [1, 2, 3]
// 方法二
var arr = [1, 2, 3]
var [...arr1] = arr
console.log(arr1) // [1, 2, 3]
复制代码
// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]
var arr1 = ['a', 'b'];
var arr2 = ['c'];
var arr3 = ['d', 'e'];
// ES5的合并数组
arr1 = arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
复制代码
扩展运算符能够与解构赋值结合起来,用于生成数组。
const [first, ...rest] = [1, 2, 3, 4, 5];
first // 1
rest // [2, 3, 4, 5]
const [first, ...rest] = [];
first // undefined
rest // []
const [first, ...rest] = ["foo"];
first // "foo"
rest // []
// 错误用法
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错
复制代码
[...'hello']
// ["h", "e", "l", "l", "o"]
复制代码
// 平时咱们获取dom节点的数组是一个类数组, 没法使用数组的方法
let nodeList = document.querySelectorAll('div');
// 经过扩展云算法转换为数组
let array = [...nodeList];
复制代码
Array.from方法用于将两类对象转为真正的数组
// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).forEach(function (p) {
console.log(p);
});
// arguments对象
function foo() {
var args = Array.from(arguments);
// ...
}
复制代码
参数:
Array.of方法用于将一组值,转换为数组。
这个方法的主要目的,是弥补数组构造函数Array()的不足。由于参数个数的不一样,会致使Array()的行为有差别。
//Array
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
复制代码
数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其余位置(会覆盖原有成员),而后返回当前数组。也就是说,使用这个方法,会修改当前数组。
它接受三个参数。
- target(必需):从该位置开始替换数据。
- start(可选):从该位置开始读取数据,默认为 0。若是为负值,表示倒数。
- end(可选):到该位置前中止读取数据,默认等于数组长度。若是为负值,表示倒数。
这三个参数都应该是数值,若是不是,会自动转为数值。
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5] // 改变第一个数字, 值从第三个值赋值
复制代码
// find()
数组实例的find方法,用于找出第一个符合条件的数组成员。
它的参数是一个回调函数,全部数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,
而后返回该成员。若是没有符合条件的成员,则返回undefined。
[1, 4, -5, 10].find((n) => n < 0)
// -5
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
//find方法的回调函数能够接受三个参数,依次为当前的值、当前的位置和原数组。
// findIndex方法的用法与find方法很是相似,返回第一个符合条件的数组成员的位置,
若是全部成员都不符合条件,则返回-1。
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
复制代码
我本身通常使用find方法比较多
这两个方法均可以接受第二个参数,用来绑定回调函数的this对象。
fill方法使用给定值,填充一个数组。
fill方法用于空数组的初始化很是方便。数组中已有的元素,会被所有抹去。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
fill方法还能够接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
var arr = [1, 2, 3]
arr.fill(6, 1, 2) // [1, 6, 3]
arr.fill(6, 1) // [1, 6, 6]
复制代码
es6引入的做为遍历全部数据结构的统一的方法。
一个数据结构只要部署了Symbol.iterator属性,就被视为具备 iterator 接口,
就能够用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方。
复制代码
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"
复制代码
方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法相似。ES2016 引入了该方法。
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true
复制代码
该方法的第二个参数表示搜索的起始位置,默认为0。若是第二个参数为负数,则表示倒数的位置,若是这时它大于数组长度(好比第二个参数为-4,但数组长度为3),则会重置为从0开始。
没有该方法以前,咱们一般使用数组的indexOf方法,检查是否包含某个值。
indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,因此要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会致使对NaN的误判。
注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符能够说明这一点。
0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false
复制代码
ES5 对空位的处理,已经很不一致了,大多数状况下会忽略空位。
ES6 则是明确将空位转为undefined。
Array.from方法会将数组的空位,转为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
复制代码
上面代码中,数组arr有两个空位,for...of并无忽略它们。若是改为map方法遍历,空位是会跳过的。
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
复制代码
因为空位的处理规则很是不统一,因此建议避免出现空位。
ES6 容许直接写入变量和函数,做为对象的属性和方法。这样的书写更加简洁。
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
复制代码
方法也能够简写。
const o = {
method() {
return "Hello!";
}
};
// 等同于
const o = {
method: function() {
return "Hello!";
}
};
复制代码
JavaScript 定义对象的属性,有两种方法。
// 方法一
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;
复制代码
可是,若是使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性。
var obj = {
foo: true,
abc: 123
};
复制代码
ES6 容许字面量定义对象时,用方法二(表达式)做为对象的属性名,即把表达式放在方括号内。
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
复制代码
表达式还能够用于定义方法名。
let obj = {
['h' + 'ello']() {
return 'hi';
}
};
obj.hello() // hi
复制代码
注意,属性名表达式与简洁表示法,不能同时使用,会报错。
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };
// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};
复制代码
ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript 缺少一种运算,在全部环境中,只要两个值是同样的,它们就应该相等。
ES6 提出同值相等算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
复制代码
不一样之处只有两个:一是+0不等于-0,二是NaN等于自身。
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
复制代码
Object.assign方法用于对象的合并,将源对象(source)的全部可枚举属性,复制到目标对象(target)。
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
复制代码
若是只有一个参数,Object.assign会直接返回该参数。
const obj = {a: 1};
Object.assign(obj) === obj // true
复制代码
因为undefined和null没法转成对象,因此若是它们做为参数,就会报错。
Object.assign(undefined) // 报错
Object.assign(null) // 报错
复制代码
注意:Object.assign能够用来处理数组,可是会把数组视为对象。
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
//把数组视为属性名为 0、一、2 的对象,所以源数组的 0 号属性4覆盖了目标数组的 0 号属性1。
复制代码
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]
复制代码
ES2017 引入了跟Object.keys配套的Object.values和Object.entries,做为遍历一个对象的补充手段,供for...of循环使用。
let {keys, values, entries} = Object;
let obj = { a: 1, b: 2, c: 3 };
for (let key of keys(obj)) {
console.log(key); // 'a', 'b', 'c'
}
for (let value of values(obj)) {
console.log(value); // 1, 2, 3
}
for (let [key, value] of entries(obj)) {
console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}
复制代码
Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)全部可遍历(enumerable)属性的键值。
const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]
复制代码
返回数组的成员顺序
const obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj)
// ["b", "c", "a"]
复制代码
上面代码中,属性名为数值的属性,是按照数值大小,从小到大遍历的,所以返回的顺序是b、c、a。
Object.entries方法返回一个数组,成员是参数对象自身的(不含继承的)全部可遍历(enumerable)属性的键值对数组。
const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]
复制代码
除了返回值不同,该方法的行为与Object.values基本一致。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
复制代码
因为解构赋值要求等号右边是一个对象,因此若是等号右边是undefined或null,就会报错,由于它们没法转为对象。
let { x, y, ...z } = null; // 运行时错误
let { x, y, ...z } = undefined; // 运行时错误
复制代码
解构赋值必须是最后一个参数,不然会报错。
let { ...x, y, z } = obj; // 句法错误
let { x, ...y, ...z } = obj; // 句法错误
复制代码
注意,解构赋值的拷贝是浅拷贝,即若是一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
let obj = { a: { b: 1 } };
let { ...x } = obj;
obj.a.b = 2;
x.a.b // 2
复制代码
扩展运算符(...)用于取出参数对象的全部可遍历属性,拷贝到当前对象之中。
let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }
复制代码
这等同于使用Object.assign方法。
let aClone = { ...a };
// 等同于
let aClone = Object.assign({}, a);
复制代码
ES5里面对象的属性名都是字符串,若是你须要使用一个别人提供的对象,你对这个对象有哪些属性也不是很清楚,但又想为这个对象新增一些属性,那么你新增的属性名就极可能和原来的属性名发送冲突,显然咱们是不但愿这种状况发生的。因此,咱们须要确保每一个属性名都是独一无二的,这样就能够防止属性名的冲突了。所以,ES6里就引入了Symbol,用它来产生一个独一无二的值。
Symbol其实是ES6引入的一种原始数据类型,除了Symbol,JavaScript还有其余5种原始数据类型,分别是Undefined、Null、Boolean、String、Number、对象,这5种数据类型都是ES5中就有的。
Symbol值是经过Symbol函数生成的,以下:
let s = Symbol();
console.log(s); // Symbol()
typeof s; // "symbol"
复制代码
Symbol函数不是一个构造函数,前面不能用new操做符。因此Symbol类型的值也不是一个对象,不能添加任何属性,它只是一个相似于字符型的数据类型。若是强行在Symbol函数前加上new操做符,会报错,以下:
let s = new Symbol();
// Uncaught TypeError: Symbol is not a constructor(…)
复制代码
用上面的方法生成的Symbol值很差进行区分,Symbol函数还能够接受一个字符串参数,来对产生的Symbol值进行描述,方便咱们区分不一样的Symbol值。
let s1 = Symbol('s1');
let s2 = Symbol('s2');
console.log(s1); // Symbol(s1)
console.log(s2); // Symbol(s2)
s1 === s2; // false
let s3 = Symbol('s2');
s2 === s3; // false
复制代码
若是Symbol函数的参数是一个对象,就会调用该对象的toString方法,将其转化为一个字符串,而后才生成一个Symbol值。因此,说到底,Symbol函数的参数只能是字符串。
既然Symbol是一种数据类型,那咱们必定想知道Symbol值是否能进行运算。告诉你,Symbol值是不能进行运算的,不只不能和Symbol值进行运算,也不能和其余类型的值进行运算,不然会报错。 Symbol值能够显式转化为字符串和布尔值,可是不能转为数值。
var mysym1 = Symbol('my symbol');
mysym1.toString() // 'Symbol('my symbol')'
String(mysym1) // 'Symbol('my symbol')'
var mysym2 = Symbol();
Boolean(mysym2); // true
Number(mysym2) // TypeError: Cannot convert a Symbol value to a number(…)
复制代码
Symbol就是为对象的属性名而生,那么Symbol值怎么做为对象的属性名呢?有下面几种写法:
let a = {};
let s4 = Symbol();
// 第一种写法
a[s4] = 'mySymbol';
// 第二种写法
a = {
[s4]: 'mySymbol'
}
// 第三种写法
Object.defineProperty(a, s4, {value: 'mySymbol'});
a.s4; // undefined
a.s4 = 'mySymbol';
a[s4] // undefined
a['s4'] // 'mySymbol'
复制代码
使用for...in和for...of都没法遍历到Symbol值的属性,Symbol值做为对象的属性名,也没法经过Object.keys()、Object.getOwnPropertyNames()来获取了。可是,不一样担忧,这种日常的需求确定是会有解决办法的。咱们可使用Object.getOwnPropertySymbols()方法获取一个对象上的Symbol属性名。也可使用Reflect.ownKeys()返回全部类型的属性名,包括常规属性名和 Symbol属性名。
let s5 = Symbol('s5');
let s6 = Symbol('s6');
let a = {
[s5]: 's5',
[s6]: 's6'
}
Object.getOwnPropertySymbols(a); // [Symbol(s5), Symbol(s6)]
a.hello = 'hello';
Reflect.ownKeys(a); // ["hello", Symbol(s5), Symbol(s6)]
复制代码
利用Symbol值做为对象属性的名称时,不会被常规方法遍历到这一特性,能够为对象定义一些非私有的可是又但愿只有内部可用的方法。
Symbol.for()函数也能够用来生成Symbol值,但该函数有一个特殊的用处,就是能够重复使用一个Symbol值。
let s1 = Symbol.for("s11");
let s2 = Symbol.for("s22");
console.log(s1===s2)//false
let s3 = Symbol("s33");
let s4 = Symbol("s33");
console.log(s3===s4)//false
console.log(Symbol.keyFor(s3))//undefined
console.log(Symbol.keyFor(s2))//"s22"
console.log(Symbol.keyFor(s1))//"s11"
复制代码
**Symbol.for()**函数要接受一个字符串做为参数,先搜索有没有以该参数做为名称的Symbol值,若是有,就直接返回这个Symbol值,不然就新建并返回一个以该字符串为名称的Symbol值。
**Symbol.keyFor()**函数是用来查找一个Symbol值的登记信息的,Symbol()写法没有登记机制,因此返回undefined;而Symbol.for()函数会将生成的Symbol值登记在全局环境中,因此Symbol.keyFor()函数能够查找到用Symbol.for()函数生成的Symbol值。
ES6提供了11个内置的Symbol值,分别是Symbol.hasInstance 、Symbol.isConcatSpreadable 、Symbol.species 、Symbol.match 、Symbol.replace 、Symbol.search 、Symbol.split 、Symbol.iterator 、Symbol.toPrimitive 、Symbol.toStringTag 、Symbol.unscopables 等。 有兴趣的能够自行了解: 地址
ES6 提供了新的数据结构 Set。它相似于数组,可是成员的值都是惟一的,没有重复的值。 Set 自己是一个构造函数,用来生成 Set 数据结构。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
//经过add方法向 Set 结构加入成员,结果代表 Set 结构不会添加剧复的值。
复制代码
Set 函数能够接受一个数组做为参数,用来初始化。
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
function divs () {
return [...document.querySelectorAll('div')];
}
const set = new Set(divs());
set.size // 56
// 相似于
divs().forEach(div => set.add(div));
set.size // 56
复制代码
// 去除数组的重复成员
[...new Set(array)]
复制代码
注意:两个对象老是不相等的。
- Set.prototype.constructor:构造函数,默认就是Set函数。
- Set.prototype.size:返回Set实例的成员总数。
Set 实例的方法分为两大类:操做方法(用于操做数据)和遍历方法(用于遍历成员)。
复制代码
- add(value):添加某个值,返回 Set 结构自己。
- delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
- has(value):返回一个布尔值,表示该值是否为Set的成员。
- clear():清除全部成员,没有返回值。
复制代码
const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);
复制代码
Set 结构的实例有四个遍历方法,能够用于遍历成员。
- keys():返回键名的遍历器
- values():返回键值的遍历器
- entries():返回键值对的遍历器
- forEach():使用回调函数遍历每一个成员
keys(),values(),entries()
keys方法、values方法、entries方法返回的都是遍历器对象。
因为 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),因此keys方法和values方法的行为彻底一致。
let set = new Set(['red', 'green', 'blue']);
for (let item of set.keys()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item);
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item);
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]
复制代码
Set 结构的实例默承认遍历,它的默认遍历器生成函数就是它的values方法。
Set 结构的实例与数组同样,也拥有forEach方法,用于对每一个成员执行某种操做,没有返回值。
let set = new Set([1, 4, 9]);
set.forEach((value, key) => console.log(key + ' : ' + value))
// 1 : 1
// 4 : 4
// 9 : 9
复制代码
forEach方法还能够有第二个参数,表示绑定处理函数内部的this对象。
WeakSet 结构与 Set 相似,也是不重复的值的集合。可是,它与 Set 有两个区别:
因为上面这个特色,WeakSet 的成员是不适合引用的,由于它会随时消失。另外,因为 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行先后极可能成员个数是不同的,而垃圾回收机制什么时候运行是不可预测的,所以 ES6 规定 WeakSet 不可遍历。
const a = [[1, 2], [3, 4]];
const ws = new WeakSet(a);
// WeakSet {[1, 2], [3, 4]}
//下面的写法不行
const b = [3, 4];
const ws = new WeakSet(b);
// Uncaught TypeError: Invalid value used in weak set(…)
复制代码
WeakSet 结构有如下三个方法。
WeakSet 没有size属性,没有办法遍历它的成员。
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),可是传统上只能用字符串看成键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6 提供了 Map 数据结构。它相似于对象,也是键值对的集合,可是“键”的范围不限于字符串,各类类型的值(包括对象)均可以看成键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。若是你须要“键值对”的数据结构,Map 比 Object 更合适。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
m.get(o) // "content"
m.has(o) // true
m.delete(o) // true
m.has(o) // 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"
复制代码
注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要很是当心。
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
复制代码
若是 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,好比0和-0就是一个键,布尔值true和字符串true则是两个不一样的键。另外,undefined和null也是两个不一样的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。
1. size属性 返回成员总数
2. set(key,value) 设置键值对,返回Map结构
3. get(key) 读取key对应的值,找不到就是undefined
4. has(key) 返回布尔值,表示key是否在Map中
5. delete(key) 删除某个键,返回true,失败返回false
6. clear() 清空全部成员,没有返回值
复制代码
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
- keys():返回键名的遍历器。
- values():返回键值的遍历器。
- entries():返回全部成员的遍历器。
- forEach():遍历 Map 的全部成员。
须要特别注意的是,Map 的遍历顺序就是插入顺序。遍历行为基本与set的一致。
复制代码
const myMap = new Map()
.set(true, 7)
.set({foo: 3}, ['abc']);
[...myMap]
复制代码
new Map([
[true, 7],
[{foo: 3}, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
复制代码
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 }
复制代码
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
复制代码
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"]]]'
复制代码
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
复制代码
可是,有一种特殊状况,整个 JSON 就是一个数组,且每一个数组成员自己,又是一个有两个成员的数组。这时,它能够一一对应地转为 Map。这每每是数组转为 JSON 的逆操做。
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
复制代码
WeakMap只有四个方法可用:get()、set()、has()、delete()。
没法被遍历,由于没有size。没法被清空,由于没有clear(),跟WeakSet类似。
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
复制代码
上面代码中,myElement是一个 DOM 节点,每当发生click事件,就更新一下状态。咱们将这个状态做为键值放在 WeakMap 里,对应的键名就是myElement。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
Proxy 用于修改某些操做的默认行为,等同于在语言层面作出修改,因此属于一种“元编程”,即对编程语言进行编程。
Proxy 能够理解成,在目标对象以前架设一层“拦截”,外界对该对象的访问,都必须先经过这层拦截,所以提供了一种机制,能够对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操做,能够译为“代理器”。 Vue3.0使用了proxy
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
// target表示要拦截的数据
// key表示要拦截的属性
// value表示要拦截的属性的值
// receiver表示Proxy{}
//上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
复制代码
上面代码说明,Proxy 实际上重载(overload)了点运算符,即用本身的定义覆盖了语言的原始定义。
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
let proxy = new Proxy(target, handler);
Proxy 对象的全部用法,都是上面这种形式,不一样的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
复制代码
Proxy 对象的全部用法,都是上面这种形式,不一样的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time = 10
proxy.time // 35 // 拦截了全部的获取属性,都会返回35
proxy.name // 35
proxy.title // 35
复制代码
若是handler没有设置任何拦截,那就等同于直接通向原对象。
var target = {};
var handler = {};
var proxy = new Proxy(target, handler);
proxy.a = 'b';
target.a // "b"
复制代码
上面代码中,handler是一个空对象,没有任何拦截效果,访问proxy就等同于访问target。
同一个拦截器函数,能够设置拦截多个操做。
对于能够设置、但没有设置拦截的操做,则直接落在目标对象上,按照原先的方式产生结果。
下面是 Proxy 支持的拦截操做一览,一共 13 种:
例如:
deleteProperty方法用于拦截delete操做,若是这个方法抛出错误或者返回false,当前属性就没法被delete命令删除。
apply方法拦截函数的调用、call和apply操做。
get方法用于拦截某个属性的读取操做。
let obj2 = new Proxy(obj,{
get(target,property,a){
//return 35;
/*console.log(target)
console.log(property)*/
let Num = ++wkMap.get(obj).getPropertyNum;
console.log(`当前访问对象属性次数为:${Num}`)
return target[property]
},
deleteProperty(target,property){
return false;
},
apply(target,ctx,args){
return Reflect.apply(...[target,[],args]);;
}
})
复制代码
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实例。上面代码中,当执行revoke函数以后,再访问Proxy实例,就会抛出一个错误。
Proxy.revocable的一个使用场景是,目标对象不容许直接访问,必须经过代理访问,一旦访问结束,就收回代理权,不容许再次访问。
虽然 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。
复制代码
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
所`Promise,简单说就是一个容器,里面保存着某个将来才会结束的事件(一般是一个异步操做)的结果。
Promise对象表明一个异步操做,有三种状态:
pending(进行中)、fulfilled(已成功)和rejected(已失败)。
只有异步操做的结果,能够决定当前是哪种状态,任何其余操做都没法改变这个状态。
写js必然不会对异步事件陌生。
settimeout(()=>{
console.log("123")
},0)
console.log("abc")
//先输出谁?
复制代码
答案我想我不用说,你们都知道
若是abc须要在123执行结束后再输出怎么办?
固然,可使用callback,可是callback使用起来是一件很让人绝望的事情。
这时:Promise这个为异步编程而生的对象站了出来....
let p = new Promise((resolve,reject)=>{
//一些异步操做
setTimeout(()=>{
console.log("123")
resolve("abc");
reject("我是错误信息")
},0)
})
.then(function(data){
//resolve状态
console.log(data)
},function(err){
//reject状态
console.log(err)
})
//'123'
//'abc'
// 我是错误信息
复制代码
这时候你应该有两个疑问:
1.包装这么一个函数有毛线用?
2.resolve('123');这是干毛的?
Promise实例生成之后,能够用then方法分别指定resolved状态和rejected状态的回调函数。
也就是说,状态由实例化时的参数(函数)执行来决定的,根据不一样的状态,看看须要走then的第一个参数仍是第二个。
resolve()和reject()的参数会传递到对应的回调函数的data或err
then返回的是一个新的Promise实例,也就是说能够继续then
因此,从表面上看,Promise只是可以简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数可以及时调用,它比传递callback函数要简单、灵活的多。因此使用Promise的正确场景是这样的:
runAsync1()
.then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return runAsync3();
})
.then(function(data){
console.log(data);
});
//异步任务1执行完成
//随便什么数据1
//异步任务2执行完成
//随便什么数据2
//异步任务3执行完成
//随便什么数据3
复制代码
runAsync一、runAsync二、runAsync3长这样↓
function runAsync1(){
var p = new Promise(function(resolve, reject){
//作一些异步操做
setTimeout(function(){
console.log('异步任务1执行完成');
resolve('随便什么数据1');
}, 1000);
});
return p;
}
function runAsync2(){
var p = new Promise(function(resolve, reject){
//作一些异步操做
setTimeout(function(){
console.log('异步任务2执行完成');
resolve('随便什么数据2');
}, 2000);
});
return p;
}
function runAsync3(){
var p = new Promise(function(resolve, reject){
//作一些异步操做
setTimeout(function(){
console.log('异步任务3执行完成');
resolve('随便什么数据3');
}, 2000);
});
return p;
}
复制代码
在then方法中,你也能够直接return数据而不是Promise对象,在后面的then中也能够接收到数据:
runAsync1()
.then(function(data){
console.log(data);
return runAsync2();
})
.then(function(data){
console.log(data);
return '直接返回数据'; //这里直接返回数据
})
.then(function(data){
console.log(data);
});
//异步任务1执行完成
//随便什么数据1
//异步任务2执行完成
//随便什么数据2
//直接返回数据
复制代码
前面的例子都是只有“执行成功”的回调,尚未“失败”的状况,reject的做用就是把Promise的状态置为rejected,这样咱们在then中就能捕捉到,而后执行“失败”状况的回调。
let num = 10;
let p1 = function() {
return new Promise((resolve,reject)=>{
if (num <= 5) {
resolve("<=5,走resolce")
console.log('resolce不能结束Promise')
}else{
reject(">5,走reject")
console.log('reject不能结束Promise')
}
})
}
p1()
.then(function(data){
console.log(data)
},function(err){
console.log(err)
})
//reject不能结束Promise
//>5,走reject
复制代码
resolve和reject永远会在当前环境的最后执行,因此后面的同步代码会先执行。
若是resolve和reject以后还有代码须要执行,最好放在then里。
而后在resolve和reject前面写上return。
Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
// 接着上面的例子
p1()
.then(function(data){
console.log(data)
})
.catch(function(err){
console.log(err)
})
//reject不能结束Promise
//>5,走reject
复制代码
Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
复制代码
p的状态由p一、p二、p3决定,分红两种状况。
promises是包含 3 个 Promise 实例的数组,只有这 3 个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。
若是做为参数的 Promise 实例,本身定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法,若是没有参数没有定义本身的catch,就会调用Promise.all()的catch方法。
Promise.race方法一样是将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
// 上面代码中,只要p一、p二、p3之中有一个实例率先改变状态,p的状态就跟着改变。
// 那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
复制代码
有时须要将现有对象转为 Promise 对象,Promise.resolve方法就起到这个做用。
const jsPromise = Promise.resolve('123');
复制代码
上面代码将123转为一个 Promise 对象。
Promise.resolve等价于下面的写法。
Promise.resolve('123')
// 等价于
new Promise(resolve => resolve('123'))
复制代码
thenable对象指的是具备then方法的对象,好比下面这个对象。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
复制代码
Promise.resolve方法会将这个对象转为 Promise 对象,而后就当即执行thenable对象的then方法。
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
复制代码
上面代码中,thenable对象的then方法执行后,对象p1的状态就变为resolved,从而当即执行最后那个then方法指定的回调函数,输出 42。
若是参数是一个原始值,或者是一个不具备then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。
const p = Promise.resolve('Hello');
p.then(function (s){
console.log(s)
});
// Hello
复制代码
上面代码生成一个新的 Promise 对象的实例p。因为字符串Hello不属于异步操做(判断方法是字符串对象不具备 then 方法),返回 Promise 实例的状态从一辈子成就是resolved,因此回调函数会当即执行。Promise.resolve方法的参数,会同时传给回调函数。
Promise.resolve方法容许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
因此,若是但愿获得一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve方法。
const p = Promise.resolve();
p.then(function () {
// ...
});
复制代码
上面代码的变量p就是一个 Promise 对象。
须要注意的是,当即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
复制代码
上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是当即执行,所以最早输出。
Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
复制代码
上面代码生成一个 Promise 对象的实例p,状态为rejected,回调函数会当即执行。
注意,Promise.reject()方法的参数,会原封不动地做为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
})
// true
复制代码
上面代码中,Promise.reject方法的参数是一个thenable对象,执行之后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。
####概念 迭代器是一种接口、是一种机制。
为各类不一样的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就能够完成遍历操做(即依次处理该数据结构的全部成员)。
Iterator 的做用有三个:
Iterator本质上,就是一个指针对象。
过程是这样的:
(1)建立一个指针对象,指向当前数据结构的起始位置。
(2)第一次调用指针对象的next方法,能够将指针指向数据结构的第一个成员。
(3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
(4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。
function myIter(obj){
let i = 0;
return {
next(){
let done = (i>=obj.length);
let value = !done ? obj[i++] : undefined;
return {
value,
done,
}
}
}
}
复制代码
原生具有 Iterator 接口的数据结构以下。
下面的例子是数组的Symbol.iterator属性。
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
复制代码
下面是另外一个相似数组的对象调用数组的Symbol.iterator方法的例子。
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // 'a', 'b', 'c'
}
复制代码
注意,普通对象部署数组的Symbol.iterator方法,并没有效果。
let iterable = {
a: 'a',
b: 'b',
c: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // undefined, undefined, undefined
}
复制代码
字符串是一个相似数组的对象,也原生具备 Iterator 接口。
var someString = "hi";
typeof someString[Symbol.iterator]
// "function"
var iterator = someString[Symbol.iterator]();
iterator.next() // { value: "h", done: false }
iterator.next() // { value: "i", done: false }
iterator.next() // { value: undefined, done: true }
复制代码
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数彻底不一样。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数仍是一个遍历器对象生成函数。返回的遍历器对象,能够依次遍历 Generator 函数内部的每个状态。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
复制代码
上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象。
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法能够恢复执行。
ES6 没有规定,function关键字与函数名之间的星号,写在哪一个位置。这致使下面的写法都能经过。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
复制代码
因为 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,因此其实提供了一种能够暂停执行的函数。yield表达式就是暂停标志。
遍历器对象的next方法的运行逻辑以下。
(1)遇到yield表达式,就暂停执行后面的操做,并将紧跟在yield后面的那个表达式的值,做为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)若是没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,做为返回的对象的value属性值。
(4)若是该函数没有return语句,则返回的对象的value属性值为undefined。
yield表达式与return语句既有类似之处
都能返回紧跟在语句后面的那个表达式的值。
不一样之处
每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具有位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,可是能够执行屡次(或者说多个)yield表达式。正常函数只能返回一个值,由于只能执行一次return;Generator 函数能够返回一系列的值,由于能够有任意多个yield。
注意:
yield表达式只能用在 Generator 函数里面,用在其余地方都会报错。
另外,yield表达式若是用在另外一个表达式之中,必须放在圆括号里面。
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield 123)); // OK
复制代码
因为 Generator 函数就是遍历器生成函数,所以能够把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具备 Iterator 接口。
Object.prototype[Symbol.iterator] = function* (){
for(let i in this){
yield this[i];
}
}
//--------------
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
复制代码
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
复制代码
这个功能有很重要的语法意义。
Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。经过next方法的参数,就有办法在 Generator 函数开始运行以后,继续向函数体内部注入值。
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 }
b.next(13) // { value:42, done:true }
复制代码
for...of循环能够自动遍历 Generator 函数时生成的Iterator对象,且此时再也不须要调用next方法。
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
复制代码
function* fibonacci() {
let [prev, curr] = [1, 1];
while(true){
[prev, curr] = [curr, prev + curr];
yield curr;
}
}
for (let n of fibonacci()) {
if (n > 10000000) break;
console.log(n);
}
复制代码
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 }
复制代码
若是在 Generator 函数内部,调用另外一个 Generator 函数,默认状况下是没有效果的。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "y"
复制代码
foo和bar都是 Generator 函数,在bar里面调用foo,是不会有效果的。
这个就须要用到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 v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
复制代码
再来看一个对比的例子。
function* inner() {
yield 'hello!';
}
function* outer1() {
yield 'open';
yield inner();
yield 'close';
}
var gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一个遍历器对象
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
var gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
复制代码
上面例子中,outer2使用了yield*,outer1没使用。结果就是,outer1返回一个遍历器对象,outer2返回该遍历器对象的内部值。
从语法角度看,若是yield表达式后面跟的是一个遍历器对象,须要在yield表达式后面加上星号,代表它返回的是一个遍历器对象。这被称为yield*表达式。
若是一个对象的属性是 Generator 函数,能够简写成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
复制代码
说实话学习了async await以后Generator 函数基本能够不用了
ES2017 标准引入了 async 函数,使得异步操做变得更加方便。
async 函数是 Generator 函数的语法糖。
什么是语法糖?
意指那些没有给计算机语言添加新功能,而只是对人类来讲更“甜蜜”的语法。语法糖每每给程序员提供了更实用的编码方式,有益于更好的编码风格,更易读。不过其并无给语言添加什么新东西。
反向还有语法盐:
主要目的是经过反人类的语法,让你更痛苦的写代码,虽然一样能达到避免代码书写错误的效果,可是编程效率很低,毕竟提升了语法学习门槛,让人齁到忧伤。。。
async函数使用时就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。
async函数对 Generator 函数的区别:
(1)内置执行器。
Generator 函数的执行必须靠执行器,而async函数自带执行器。也就是说,async函数的执行,与普通函数如出一辙,只要一行。
(2)更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操做,await表示紧跟在后面的表达式须要等待结果。
(3)正常状况下,await命令后面是一个 Promise 对象。若是不是,会被转成一个当即resolve的 Promise 对象。
(4)返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你能够用then方法指定下一步的操做。
进一步说,async函数彻底能够看做多个异步操做,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
若是await后面的异步操做出错,那么等同于async函数返回的 Promise 对象被reject。
async function f() {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
}
f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了
复制代码
上面代码中,async函数f执行后,await后面的 Promise 对象会抛出一个错误对象,致使catch方法的回调函数被调用,它的参数就是抛出的错误对象。具体的执行机制,能够参考后文的“async 函数的实现原理”。
防止出错的方法,也是将其放在try...catch代码块之中。
async function f() {
try {
await new Promise(function (resolve, reject) {
throw new Error('出错了');
});
} catch(e) {
}
return await('hello world');
}
复制代码
若是有多个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);
}
}
复制代码
应用
var fn = function (time) {
console.log("开始处理异步");
setTimeout(function () {
console.log(time);
console.log("异步处理完成");
iter.next();
}, time);
};
function* g(){
console.log("start");
yield fn(3000)
yield fn(500)
yield fn(1000)
console.log("end");
}
let iter = g();
iter.next();
复制代码
下面是async函数的写法
var fn = function (time) {
return new Promise(function (resolve, reject) {
console.log("开始处理异步");
setTimeout(function () {
resolve();
console.log(time);
console.log("异步处理完成");
}, time);
})
};
var start = async function () {
// 在这里使用起来就像同步代码那样直观
console.log('start');
await fn(3000);
await fn(500);
await fn(1000);
console.log('end');
};
start();
复制代码
class跟let、const同样:不存在变量提高、不能重复声明...
es5面向对象写法跟传统的面向对象语言(好比 C++ 和 Java)差别很大,很容易让新学习这门语言的程序员感到困惑。
ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,做为对象的模板。经过class关键字,能够定义类。
ES6 的class能够看做只是一个语法糖,它的绝大部分功能,ES5 均可以作到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
//es5
function Fn(x, y) {
this.x = x;
this.y = y;
}
Fn.prototype.add = function () {
return this.x + this.y;
};
//等价于
//es6
class Fn{
constructor(x,y){
this.x = x;
this.y = y;
}
add(){
return this.x + this.y;
}
}
var F = new Fn(1, 2);
console.log(F.add()) //3
复制代码
构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的全部方法都定义在类的prototype属性上面。
class Fn {
constructor() {
// ...
}
add() {
// ...
}
sub() {
// ...
}
}
// 等同于
Fn.prototype = {
constructor() {},
add() {},
sub() {},
};
复制代码
类的内部全部定义的方法,都是不可枚举的(non-enumerable),这与es5不一样。
//es5
var Fn = function (x, y) {
// ...
};
Point.prototype.add = function() {
// ...
};
Object.keys(Fn.prototype)
// ["toString"]
Object.getOwnPropertyNames(Fn.prototype)
// ["constructor","add"]
//es6
class Fn {
constructor(x, y) {
// ...
}
add() {
// ...
}
}
Object.keys(Fn.prototype)
// []
Object.getOwnPropertyNames(Fn.prototype)
// ["constructor","add"]
复制代码
类和模块的内部,默认就是严格模式,因此不须要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。
考虑到将来全部的代码,其实都是运行在模块之中,因此 ES6 实际上把整个语言升级到了严格模式。
onstructor方法是类的默认方法,经过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,若是没有显式定义,一个空的constructor方法会被默认添加。
class Fn {
}
// 等同于
class Fn {
constructor() {}
}
复制代码
constructor方法默认返回实例对象(即this),彻底能够指定返回另一个对象。
class Foo {
constructor() {
return Object.create(null);
}
}
new Foo() instanceof Foo
// false
//constructor函数返回一个全新的对象,结果致使实例对象不是Foo类的实例。
复制代码
类必须使用new调用,不然会报错。这是它跟普通构造函数的一个主要区别,后者不用new也能够执行。
class Foo {
constructor() {
return Object.create(null);
}
}
Foo()
// TypeError: Class constructor Foo cannot be invoked without 'new'
复制代码
与函数同样,类也可使用表达式的形式定义。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
复制代码
上面代码使用表达式定义了一个类。须要注意的是,这个类的名字是MyClass而不是Me,Me只在 Class 的内部代码可用,指代当前类。
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
复制代码
若是类的内部没用到的话,能够省略Me,也就是能够写成下面的形式。
const MyClass = class { /* ... */ };
复制代码
采用 Class 表达式,能够写出当即执行的 Class。
let person = new class {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}('张三');
person.sayName(); // "张三"
复制代码
上面代码中,person是一个当即执行的类的实例。
私有方法/私有属性是常见需求,但 ES6 不提供,只能经过变通方法模拟实现。(之后会实现)
一般是在命名上加以区别。
class Fn {
// 公有方法
foo () {
//....
}
// 伪装是私有方法(其实外部仍是能够访问)
_bar() {
//....
}
}
复制代码
class定义类时,只能在constructor里定义属性,在其余位置会报错。
若是须要在原型上定义方法可使用:
类至关于实例的原型,全部在类中定义的方法,都会被实例继承。
若是在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接经过类来调用,这就称为“静态方法”。
ES6 明确规定,Class 内部只有静态方法,没有静态属性。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
//静态属性只能手动设置
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
复制代码
class Fn{
constructor(){
this.arr = []
}
get bar(){
return this.arr;
}
set bar(value){
this.arr.push(value)
}
}
let obj = new Fn();
obj.menu = 1;
obj.menu = 2;
console.log(obj.menu)//[1,2]
console.log(obj.arr)//[1,2]
复制代码
class Fn {
}
class Fn2 extends Fn {
}
复制代码
子类必须在constructor方法中调用super方法,不然新建实例时会报错。这是由于子类没有本身的this对象,而是继承父类的this对象,而后对其进行加工。若是不调用super方法,子类就得不到this对象。
class Point { /* ... */ }
class ColorPoint extends Point {
constructor() {
super()//必须调用
}
}
let cp = new ColorPoint(); // ReferenceError
复制代码
父类的静态方法也会被继承。
Object.getPrototypeOf方法能够用来从子类上获取父类。
Object.getPrototypeOf(Fn2) === Fn
// true
复制代码
所以,可使用这个方法判断,一个类是否继承了另外一个类。
super这个关键字,既能够看成函数使用,也能够看成对象使用。在这两种状况下,它的用法彻底不一样。
第一种状况,super做为函数调用时,表明父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。
做为函数时,super()只能用在子类的构造函数之中,用在其余地方就会报错。
class A {}
class B extends A {
constructor() {
super();
}
}
复制代码
上面代码中,子类B的构造函数之中的super(),表明调用父类的构造函数。这是必须的,不然 JavaScript 引擎会报错。
注意,super虽然表明了父类A的构造函数,可是返回的是子类B的实例,即super内部的this指的是B,所以super()在这里至关于A.prototype.constructor.call(this)。
第二种状况,super做为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
复制代码
上面代码中,子类B当中的super.p(),就是将super看成一个对象使用。这时,super在普通方法之中,指向A.prototype,因此super.p()就至关于A.prototype.p()。
因为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.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,因此返回undefined。
ES6 的模块自动采用严格模式,无论你有没有在模块头部加上"use strict";。
模块功能主要由两个命令构成:export和import。
export命令用于规定模块的对外接口。
import命令用于输入其余模块提供的功能。
一个模块就是一个独立的文件。该文件内部的全部变量,外部没法获取。若是你但愿外部可以读取模块内部的某个变量,就必须使用export关键字输出该变量。
export输出变量的写法:
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
复制代码
还能够:
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
//跟上面写法等价,推荐这种写法。
复制代码
export命令除了输出变量,还能够输出函数或类(class)。
export function multiply(x, y) {
return x * y;
};
复制代码
一般状况下,export输出的变量就是原本的名字,可是可使用as关键字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
复制代码
须要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量创建一一对应关系。
// 报错
export 1;
// 报错
var m = 1;
export m;
//正确写法
// 写法一
export var m = 1;
// 写法二
var m = 1;
export {m};
// 写法三
var n = 1;
export {n as m};
复制代码
一样的,function和class的输出,也必须遵照这样的写法。
// 报错
function f() {}
export f;
// 正确
export function f() {};
// 正确
function f() {}
export {f};
复制代码
export语句输出的接口,与其对应的值是动态绑定关系,即经过该接口,能够取到模块内部实时的值。
export var foo = 'bar';
setTimeout(() => foo = 'baz', 500);
复制代码
上面代码输出变量foo,值为bar,500 毫秒以后变成baz。 export命令能够出如今模块的任何位置,只要处于模块顶层就能够。若是处于块级做用域内,就会报错,import命令也是如此。
使用export命令定义了模块的对外接口之后,其余 JS 文件就能够经过import命令加载这个模块。
// main.js
import {firstName, lastName, year} from './profile';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
复制代码
上面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其余模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。
若是想为输入的变量从新取一个名字,import命令要使用as关键字,将输入的变量重命名
import { lastName as surname } from './profile';
复制代码
import后面的from指定模块文件的位置,能够是相对路径,也能够是绝对路径,.js后缀能够省略。
注意,import命令具备提高效果,会提高到整个模块的头部,首先执行。
foo();
import { foo } from 'my_module';
//import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行以前。
复制代码
因为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 { foo } from 'my_module';
import { bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
复制代码
除了指定加载某个输出值,还可使用总体加载,即用星号(*)指定一个对象,全部输出值都加载在这个对象上面。
注意,模块总体加载所在的那个对象,不容许运行时改变。下面的写法都是不容许的。
import * as circle from './circle';
// 下面两行都是不容许的
circle.foo = 'hello';
circle.area = function () {};
复制代码
使用import命令的时候,用户须要知道所要加载的变量名或函数名,不然没法加载。
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
// export-default.js
export default function () {
console.log('foo');
}
复制代码
其余模块加载该模块时,import命令能够为该匿名函数指定任意名字。
// import-default.js
import customName from './export-default';
customName(); // 'foo'
复制代码
须要注意的是,这时import命令后面,不使用大括号。 export default命令用在非匿名函数前,也是能够的。
// export-default.js
export default function foo() {
console.log('foo');
}
// 或者写成
function foo() {
console.log('foo');
}
export default foo;
复制代码
上面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。
下面比较一下默认输出和正常输出。
// 第一组
export default function crc32() { // 输出
// ...
}
import crc32 from 'crc32'; // 输入
// 第二组
export function crc32() { // 输出
// ...
};
import {crc32} from 'crc32'; // 输入
复制代码
上面代码的两组写法,第一组是使用export default时,对应的import语句不须要使用大括号;第二组是不使用export default时,对应的import语句须要使用大括号。
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 a = 1;
export default a;
// 错误
export default var a = 1;
复制代码
上面代码中,export default a的含义是将变量a的值赋给变量default。因此,最后一种写法会报错。
一样地,由于export default命令的本质是将后面的值,赋给default变量,因此能够直接将一个值写在export default以后。
// 正确
export default 42;
// 报错
export 42;
复制代码
若是在一个模块之中,先输入后输出同一个模块,import语句能够与export语句写在一块儿。
export { foo, bar } from 'my_module';
// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };
复制代码
模块的接口更名和总体输出,也能够采用这种写法。
// 接口更名
export { foo as myFoo } from 'my_module';
// 总体输出
export * from 'my_module';
复制代码
以上就是我对ES6总结的笔记了, 断断续续差很少用了一周的时间,不少代码由于我项目中也用不到,都要本身先敲一遍看看用法在总结,在加班上班比较忙, 固然个人笔记是总结的很细的,项目中不少均可以用上面的内容进行优化代码, 但愿个人笔记对你也能有所帮助, 若是有错误的地方请你在评论区指出, 很是感谢, 感受对你有用的话请关注点赞哈!!!