@yacent
2018-03-03T18:41:40.000000Z
字数 48938
阅读 990
前端学习笔记
Tutorial _via_ruanyf_es2015
1996年 Javascript发布第一个版本
1997年 EcmaScript 1.0 发布
1998年 EcmaScript 2.0 发布
1999年 EcmaScript 3.0 发布
2008年 EcmaScript 3.1 发布,即 ES5
2015.6 EcmaScript2015 发布,即ES6
目前的浏览器,并不是所有都支持ES6的,还是有一些特性是不支持的,所以需要使用一个转码器,即 JS Compiler,即通常所使用的 Babel去实现,将ES6转成ES5
具体Babel的使用方法,可以看官网如何进行使用,需要配置 babelrc
当中的 presets
来进行配置babel的转码规则,根据需要进行安装即可。
Babel-cli
Babel-node
Babel-register
为 require添加一个钩子,当使用.js .jsx .es .es6后缀名文件,就先用 babel进行转码
Babel-polyfill
Babel默认不转码很多新的API,所以需要增加一个垫片,来进行使用
目前使用 Babel的话,在babel 6.0之后,是不能直接在浏览器当中使用了,如果想要使用可以通过5版本当中 browser.min.js
载入到文件当中,然后代码写在 script
标签当中,就可以了,当然,也可以是cdn
有兴趣可以了解一下,google公司的。
下面就要进行正式的学习了,请端正态度,认真学习。
let 类似于 var
,以下主要对比以下let和var之间的区别。
let其所声明的变量只有在 块级作用域内 才有效
# 基本语法
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
let所定义的变量不存在变量提升,而使用var,会产生变量提升。
# var
console.log(a); // undefined.
var a = 10;
# 实际上,使用var,上面代码实际形式如下
var a;
console.log(a);
a = 10;
# 但如果使用let,不存在提升,必须 **先声明,后使用**
console.log(b); // 报错ReferenceError
let b = 10;
由于不存在变量提升,只要块级作用域内存在let
命令,它声明的变量就会绑定在这个区域内,不会受外部的影响。
var tmp = 123;
if (1) {
tmp = 'abc'; // ReferenceError
let tmp;
}
在使用let之后,在块级作用域内,必须先声明变量,再使用变量。同名的变量不受全局变量的影响
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
// 报错
function() {
let a = 10;
var a = 1;
}
// 报错
function() {
let a = 10;
let a = 1;
}
内层变量会覆盖外层变量,因为会变量声明提升
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
因为变量提升,导致内层的变量覆盖了外层的变量
用来计数的循环变量泄露为 全局变量
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
上面的i在循环结束之后没有消失,而泄露成了全局变量,这是因为es5当中,最小的作用域是函数,所以i实际上是属于全局作用域,所以可以访问到。
块级作用域当中,内部可以访问外部的作用域,但是外部不能访问内部的作用域,这也是遵循了es5当中的作用域链的要求。但是其内外部之分从原来的函数,变为了块级作用域
有了块级作用域,实际上可以替代经常使用 IIFE 立即执行匿名函数
,使用立即执行匿名函数的目的,就是为了创造局部作用域而不会影响其他。但是现在用let就OK了
const
声明一个只读的常量。一旦声明,常量的值就不能改变。并且必须 立即初始化
const PI = 3.1415
PI // 3.1415
PI = 3; // TypeError: Assignment to constant variable
因为不能改变const的变量值,所以在声明之后必须马上定义,即初始化。但是不要担心,只声明不初始化会报错。
const foo;
// SyntaxError: Missing initializer in const declaration
const 的作用域 和 let 的相同,只在块级作用域内有效。也不提升,所以也存在暂时性死区。特性见上面的let
由于const声明的变量只能有一次初始化,这意味着后面的初始化都是错误的,倘若如果声明的变量类型是对象(object 或者 array之类的),那么const的意思就是 一开始就绑定了变量所指向的地址,但是其内容可以改变
什么意思?
简单来说,就是只能new一次,然后可以随意采取对象所有的方法,但是不可以再次初始化这个对象。
const a = [];
a.push('h');
a.length = 0;
a = []; // 报错
上面报错的原因是 因为 a = []相当于又 new Array()
一次,但是 const已经将 a指向的地址指向了第一个 [],所以不可以再次改变其地址。
除了es5当中的 var 和 function 命令还会使全局变量直属于顶层对象,es6当中的 let
const
class
import
命令声明的变量都会与顶层对象脱钩
var a = 1;
window.a; // 1
let b = 1;
window.b // undefined
ES6允许按照一定模式,即模式匹配来从数组当中提取值,进行赋值
var [a, b, c] = [1, 2, 3];
a // 1
b // 2
c // 3
采取 模式匹配 的方式,只要等号两边的模式相同,左边的变量就会被赋予相应的值,当然如果两边模式不匹配,只有匹配的那些能够成功匹配到,未匹配到,则为 undefined
,即解构不成功
# case 1
let [bar, foo] = [1];
foo; // undefined
# case 2
let [bar] = [1, 2];
bar; // 1
# case 3
let [foo] = 1; // error,右边必须是可以遍历的解构
解构赋值不仅适用于 var,也适用于 let 和 const命令
let [foo = true] = [];
foo; // true
默认值的工作原理是,判断左边被赋值 是否 ===
undefined,如果是 严格等于 undefined
的话,默认值有效,否则无效。看下面的代码展示
var [x = 1] = [undefined]; // x = 1
var [x = 1] = []; // x = 1
var [x = 1] = [null]; // x = null. 因为 x 被赋予了 null
上面最后一种方式,因为x被赋予了 null
值,即 x === undefined
为 false,则默认值不起作用。
默认值还可以使用解构赋值的其他变量
let [x = 1, y = x] = [2]; // x = 2, y = 2
但是,其他解构变量必须已经声明了,才不会出错,因为 let不可以变量提升。
对象的解构赋值和数组的解构赋值相似,但是和数组解构赋值最不同的一点在于,其顺序是可以打乱的,因为其赋值匹配模式是由其属性名所决定的。如下
var {foo, bar} = {bar: 'world', foo: 'Hello'};
foo // 'Hello'
bar // 'world'
但是,实际上,赋值的过程,是,然后再赋给对应的变量。真正的变量是后者。见代码
var {foo: baz} = {foo: 'aaa', bar: 'bbb'};
baz; // 'aaa'
foo; // error: foo is not defined
foo
是匹配的模式, baz
才是真正的变量,被赋值的是变量 baz
,而不是匹配模式 foo
。当然,如果不写成上面的形式,而是第一段代码中的省略变量,则匹配模式名 和 变量名为同一个。
要区分好 匹配的模式 和真正的变量 是什么!
由于 let 和 const 不可以重复命名变量,所以如果是使用 let 或者 const 来声明了变量再使用对象的解构赋值的话,赋值语句需要加上 圆括号 (),否则。 {}
会被当成代码块来执行, 如下
let foo;
({foo} = {foo: 1});
# error
let foo;
{foo} = {foo: 1};
默认值的规则和数组解构赋值的一样,都是变量 === undefined
,默认值才会起作用
var {x = 3} = {}; // x = 3;
var {x:y = 3} = {x : 1}; // y = 1
字符串的解构类似于数组的解构,字符串会被转换成一个类似数组的对象
const [a, b, c, d, e] = 'Hello';
a // 'h'
b // 'e'
...
解构赋值时,如果等号右边是数值和布尔值,则会先将他们转为对象
let {toString: s} = 123;
s === Number.prototype.toString // true
let {toString: s} = true;
解构的规则是,只要等号右边不是对象,就先将其转为对象,由于 undefined
和 null
无法转为对象,所以会报错
#错误
let {prop: x} = null;
let {prop: y} = undefined;
函数的参数也可以使用解构赋值,当然,函数的参数解构就能使用数组、对象、字符串、数值和布尔值来进行了。
#example 1
function add([x, y]) {
return x + y;
}
add([2, 3]); // 5
#example 2
[[1, 2], [3, 4]].map(([a, b]) => a + b); // [3, 7]
但是,在使用默认值的时候,要注意分清楚 是 函数的默认值 还是 函数参数解构变量的默认值,具体看下面的例子。
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(); // [0, 0]
move({}); // [undefined, undefined]
move({x: 3}) // [3, undefined]
实际上,能触发函数默认值的条件是函数参数为 undefined
,如果传入 undefined
或者不传入参数,就会使用函数参数默认值,然后如果解构变量有默认值,则使用解构变量默认值。
不要在模式当中放置圆括号
以下几种情况不能使用圆括号
变量声明语句中,不能带有圆括号
#全部报错
var [(a)] = [1];
var {x: (c)} = {};
var ({x: c}) = {};
var {(x: c)} = {};
var {(x): c} = {};
var {o: ({p: p})} = {o: {p: 2}};
上面的声明当中使用圆括号,都是错误的。在声明当中,模式不能使用圆括号
函数参数中,模式不能带圆括号
# 报错
function f([(z)]) {return z;}
赋值语句中,不能将整个模式,或嵌套模式中的一层,放在圆括号中。
#报错
({p:a}) = {p: 42}
([a]) = [5]
可以使用圆括号的情况
可以使用圆括号的情况,只有一种,记得可以使用的情况就好。
赋值语句的非模式部分
可以使用,注意不是声明
#正确
[(b)] = [3];
({p: (d)} = {});
[(parseInt.prop)] = [3];
提醒:没什么必要,还是不要使用圆括号了。不要用!!!
1) 交换变量的值
[x, y] = [y, x]
2) 从函数返回多个值
// 返回一个数组
function example() {
return [1, 2, 3];
}
var [a, b, c] = example();
// 返回一个对象
function example() {
return {
foo: 1,
bar: 2
};
}
var { foo, bar } = example();
3) 函数参数的定义
能更好地将参数与变量名对应起来
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
4) 提取JSON数据
很有用,能够很方便地提取出JSON的数据
var jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
5) 遍历Map解构
var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
// first is hello
// second is world
#获取键名
for (let [key] of map) {
// ...
}
#获取键值
for (let [,value] of map) {
// ...
}
'\z' === 'z' // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true
如果直接用 \u007A
的方式来表示字符,那么其表示范围是 \u0000 ~ \uFFFF
之间的字符,如果是超过这个范围,即为中文,那么需要用两个字符来表示。
通常,更多时候,也可以用最后一种方式,es6使花括号包含起来,其范围不限制。
受限于ES5当中的,对于字符串的操作,读取字符串,其不能读取两个字符的情况,charAt()
和 charCodeAt()
都不可以,都只能读取一个字符的类型,如果是一个字符占据4个字节的话,那么,无法读取,es6新增加 codePointAt()
,可以正确处理 四个字节
存储的字符,返回一个字符的码点。
比较好的应用,是用来检测一个字符是由两个字节还是四个字节组成
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
is32Bit('𠮷'); // true
es5当中,提供了 String.fromCharCode()
的方法来从码点返回对应字符,但其不能识别32位的UTF-16的字符,即一个字符占4个字节。
String.fromCodePoint(0x20BB7)
// "ஷ"
字符串可被 for ... of
循环遍历,此遍历器的最大优点是可以识别大于 0xFFFF
的码点
es6对es5的补充实现,能识别utf-16的完整编码,但是需要通过垫片库来实现,即 es-shims
es5: indexOf(××, pos)
es6: includes
startsWith
endsWith
返回一个新的字符串
'x'.repeat(3); // 'xxx'
若为小数,则会被 向下取整
参数为 负数 或 Infinity,报错!
参数为 0 ~ -1之间,等同于 0,即返回 ''
参数为 NaN,等同于 0
参数为字符串,则先转为number,再根据上面情况进行选择
如果没有第二个参数,默认为 空格
补全功能,es7的。主要用途有以下两个
为数值补全指定位数
'1'.padStart(10, '0'); // '0000000001'
提示字符串格式
'12'.padStart(12, 'YYYY-MM-DD'); // 'YYYY-MM-12'
在原本的 js语言当中,如要要插入html模板代码,通常是如下的形式
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
);
有了es6之后,一切都变成了下面这个样子
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
以 ``
开始结束,还可以直接js当中使用变量当做模板
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
模板字符串中嵌入变量,需要将变量名写在${}之中
用于正确处理匹配四个字节的UTF-16编码
/\uD83D/u.test('\uD83D\uDC2A'); // false
以前有 gim三种模式,现在多一种 u 模式
var r = /xyz/u
y修饰符和 g修饰符的作用类似,也是全局匹配,但是与g不同的是,g修饰符只要剩余位置存在匹配就可以了,但是y修饰符确保匹配必须从剩余字符串的第一个位置开始,就有匹配,否则,返回null
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"]
r2.exec(s) // null,接着开始第一个是 _
从效果上来看,y
修饰符号隐含了头部匹配的标志 ^
,用y 修饰符的目的就是为了让 ^
也能全局匹配,而不会只匹配一次。
如果是将 y修饰符 应用于 match 方法的话,则是使用 gy
修饰符,才能达到全局效果
引入s修饰符,使得 .
能够匹配任何单个字符,包括换行 \r
\n
之类的。原本的 .
只能匹配除了 换行意外的所有单个字符
/foo.bar/.test('foo\nbar')
// false
/foo.bar/s.test('foo\nbar')
// true
字符转义的实现
es6当中,二进制和八进制必须添加前缀 0b
(或 oB
),八进制则是 0o
(或 0O
)。十六进制当然还是 0x
如果要将 0b
和 0o
前缀的字符串数值转为十进制,要使用 Number
方法
Number('0b111'); // 7
Number('0o10'); // 8
如果是在 es5当中要进行进制的转换的话,使用 toString()
方法即可
var a = 01101;
a.toString(16); // 209
a.toString(10); // ...
a.toString(2); //
与es5当中,全局环境下的 isFinite 和 isNaN 不同的是,绑定在Number上的,如果被检测的变量不是数值,一律返回 false
。
但是es5当中的 isFinite()
和 isNaN()
,则会先调用 Number方法,将其转为数值,再进行判断。
行为与 es5当中的完全一样,移植到 Number
对象上的原因是,逐步减少全局性方法,使得语言逐步模块化,方法只绑定在特定的对象之上
判断是否为整数,注意,在js当中,整数和浮点数是同样的储存方法,所以 3
和 3.0
视为同一个值
在js当中,浮点数的运算时有误差的,有精度误差,所以通常只要误差<Number.EPSILON
的话,即认为为正确结果。实质上,其为一个可以接受的误差范围
为Math对象增添一些静态方法
Math.trunc() 去除整数的小数部分
Math.cbrt() 一个数的立方根
...
es7当中支持 指数运算符 (**
)
2 ** 2 // 4
2 ** 3 // 8
如果浏览器不支持,就用es5的
Math.pow(2, 2);
Array.from(list, func, context)
能将 伪数组 或者任何可遍历的对象(字符串、set、map)之类的,都可以将其转为数组.
在ES5当中,是只能将伪数组(eg: nodeList、arguments) 转为数组。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
以上所谓类似数组的对象,其本质特征有一点,就是必须要有 length
属性,所以,任何有length属性的对象,都可以通过 Array.from 方法转为数组。
Array.from
还可以接受第二个参数,作用类似于数组的 map
方法
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
用于将一组值,转为数组,用此特性主要是因为弥补 Array 构造函数的不足
new Array(3); // [undefined * 3]
Array.of(3); // [3]
Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组。
Array.prototype.copyWithin(target, start = 0, end = this.length)
不常用,反正,Array.prototype.splice 还是无敌
作用和 Array.indexOf相似,find() 返回 符合条件的第一个数组项,findIndex 则返回符合条件的第一个数组项的位置
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
不过,find和indexOf还是有以下一些不同的地方
1. find的参数是一个回调函数,在函数当中对值进行处理,而indexOf
是直接待查询的值
2. find 可以查询到数组当中为 NaN 的值,但是,indexOf只会返回-1
var a = [1, 2, NaN];
a.indexOf(NaN); // -1
a.find(function(value) { return isNaN(value);})
用于填充一个数组,常常用来初始化一个数组的所有项都为同一个值
[].fill(value, startIndex, endIndex)
后两个参数不填则默认为整个数组
[2, 3, 4, 5, 6, 7].fill(0, 2, 4); // [2, 3, 0, 0, 6, 7]
# 以前不支持,初始化数组,使用 for 循环
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()
差不多,查找值是否存在,并且也可以查找 NaN 值 是否存在
为什么要使用函数参数默认值,在es5当中,当然也可以实现为函数参数赋予默认值,但是没有那么简介
#es5
function log(x, y) {
y = y || 'World';
}
console.log('hello', ''); // Hello world
虽然es5也可以实现参数设置默认值,但是这种方式会有一个缺点,就是当函数参数的值所对应的布尔值为 false
的话,那么,将会使用默认值,但是这是不正确的,应该要当没有输入,或者变量值为 undefined
的时候,才使用默认值。
function log(x, y = 'world') {
console.log(x, y);
}
log('Hello', ''); // Hello
function fetch(url, { method = 'GET' } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
上面代码中,函数 fetch
没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值GET。
指定了默认值之后,函数的 length
属性,将返回没有指定默认值的参数个数,同时,如果参数当中也有 rest参数,那么也不算入 length 当中
(function (a) {}).length; // 1
(function (a, b, c = 1) {}).length; // 2
函数的length属性的含义是说,预期将会传入的参数的个数,但是,如果设置默认值,说明该参数是可以省略的,就不一定需要这个参数。
如果设置了默认值的参数不是 尾参数,那么 length
属性也不再计入后面的参数
(function (a, b = 1, c) {}).length; // 1
一个需要注意的地方是,如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
上面代码中,参数 y
的默认值等于 x
。调用时,由于函数作用域内部的变量 x
已经生成,所以 y
等于参数 x
,而不是全局变量 x
。
如果调用时,函数作用域内部的变量 x
没有生成,结果就会不一样。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
如果在赋值的时候,在函数参数部分没有x这个变量,那么就会去访问 全局作用域当中的 x,如果全局作用域下没有 x
,那么将会出错
function f(y = x) {
let x = 2;
console.log(y);
}
f() // ReferenceError: x is not defined
如果函数参数默认值是一个函数的话,那么,函数所在的作用域,就是其声明时所在的作用域。
下面看一个综合一点的复杂的例子。
var x = 1;
function foo(x, y = function() { console.log(x); }) {
var x = 3;
y();
console.log(x); // 这里的 x 是函数局部的x
}
foo();
// undefined
// 3
#如果函数参数没有x
var x = 1;
function foo(y = function() { console.log(x); }) {
var x = 3;
y();
console.log(x); // 这里的 x 是函数局部的x
}
foo();
// 1,y当中访问的是 全局变量 x
// 3
#如果函数内部的x也设置为全局变量,那么,此时,y当中的x和函数内部的x其实是一样的。
var x = 1;
function foo(y = function() { console.log(x); }) {
x = 3; // 这里改变了x的值
y();
console.log(x); // 这里的 x 是函数局部的x
}
foo();
// 3
// 3
rest 参数(形式为 "...变量名"),用于获取函数的多余参数,注意:rest参数之后不能再有其他参数了,rest参数只能是最后一个参数
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) {}
#rest参数只能放最后一个
扩展运算符(spread)是三个点(...
),其就像rest参数的逆运算,rest参数是将输入的参数转为一个数组,而spread则是将数组转为用逗号分隔的参数序列
console.log(...[1, 2, 3]);
// 1 2 3,实际上 ...[1, 2, 3] 转为了 1, 2, 3,那么console.log(1, 2, 3) 就是 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.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
与解构赋值结合
如果将 扩展运算符用于数组赋值或者函数参数,那么只能放在参数的最后一位,否则会报错,实际上就是 rest参数。
# 报错
const [...butLast, last] = [1, 2, 3, 4, 5]
函数的返回值
JavaScript的函数只能返回一个值,如果需要返回多个值,只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。
var dateFields = readDateFields(database);
var d = new Date(...dateFields);
字符串
扩展运算符还可以将字符串转为真正的数组
[...'hello']
// [ "h", "e", "l", "l", "o" ]
实际上,任何有遍历器接口的都可以用 spread 运算符转为数组
实现Iterator接口的对象
其可以将任何 Iterator接口的对象,转为真正的数组,但是,如果对象本身没有Iterator接口,而只是类似数组的对象,扩展运算符就无法将其转为真正的数组,这是其与 Array.from
的最大区别,Array.from可以将伪数组和任何有Iterator接口的对象转为数组
# 不能转换类数组的对象,但是NodeList、arguments这些本来就是伪数组的就可以, 因为其都有 iterator 接口
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// TypeError: Cannot spread non-iterable object.
let arr = [...arrayLike];
# nodelist
var nodeList = document.querySelectorAll('div');
var array = [...nodeList];
在es5当中,可以在函数的内部设置严格模式,但是在es6当中,明确表示了,如果在函数当中使用了 参数默认值
解构赋值
或者 扩展运算符
,那么函数内部就不能显示设定为严格模式,否则会报错。
那么,如果规避这种限制,有以下两种方法
#全局严格模式
'use strict'
function doSomething(a, b = a) {}
#将函数包在一个无参数的立即执行函数里面
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
}
})()
对于将匿名函数赋值给变量的,es5和es6的显示不一样
var foo = function() {}
#es5
foo.name; // ''
#es6
foo.name; // foo
这个很重要,必须掌握
基本用法
函数体代码块部分默认为返回值
#es6
var f = v => v
#等同于es5
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;}
因为加上大括号,其会被js解释为代码块,所以如果使用箭头函数要返回一个对象的话,需要在对象外部加上 圆括号
var getTempItem = id => ({ id: id, name: "Temp" });
使用箭头函数注意几个事项,如下
1. 函数体内的 this
对象,就是定义时所在的对象,而不是使用时所在的对象。并且,使用箭头函数的话,this绑定之后便不可更改。
2. 不可以当作构造函数,即不可以使用 new
命令,否则会抛出一个错误
3. 不可以使用arguments
对象,该对象在函数体内不存在,若要用,可以使用 rest
参数替代。
4. 不可以使用 yield
命令,因此箭头函数不能作 Generator函数
实际上,箭头函数都没有自己的this值,其并不创建自身的上下文,其上下文this,取决于其在定义时的外部函数。即在调用的时候那个对象是谁就是谁,不可以再改变。
再去重点学习一些如何优化递归,比如可以用循环解决之类的。
es6允许直接写入变量和函数,如下
var foo = 'baz';
var a = {foo}; // 等同于 {foo: 'baz'}
es6会将变量处理为其属性名,变量的值为其属性值
function f(x, y) {
return {x, y};
}
#等同于
function f(x, y) {
return {x: x, y: y};
}
方法也是可以简写的,如下
var o = {
method() {
return 'Hello!'
}
}
#等同于
var o = {
method: function() {
return 'Hello!';
}
}
属性的赋值器(setter) 和 取值器(getter)
#ES5
Object.defineProperty(person, year, {
get: function() {
return this._year;
},
set: function(value) {
//...
}
})
#ES6
var person = {
_year: 4,
get year() {
return this._year;
},
set year(value) {
//...
}
}
在用字面量定义对象的时候,允许使用属性名表达式进行定义属性
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
#等同于
let obj = {
'abc': 123
}
但是属性名表达式不能和属性简洁表示方法共用,会报错。
用于比较两个值是否严格相当,其中可以实现 NaN
相等,这是 ==
或者 ===
做不到的
Object.assign()
方法用于对象的合并,将源对象的所有可枚举属性,复制到目标对象上
var target = {a: 1}
var source1 = {a: 2, b: 3};
var source2 = {b: 4, c: 5};
Object.assign(target, source1, source2);
target; // {a: 2, b: 4, c: 5}
后面的同名属性,会覆盖前面的,即会将原来的同名属性更换掉。
如果对于源对象不是对象的话,那么 undefined
和 null
会报错,因为其不能转为对象,而其他,除了字符串和数组,都会略过
字符串的话,其会将其拆成对象,譬如 abc
拆成 {0: 'a', 1: 'b', 2: 'c'}
var v1 = 'abc';
var v2 = true;
var v3 = 10;
var obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
注意点
1. Object.assign 是浅拷贝,即如果源对象的某个属性的属性值是对象,那么,将会拷贝到该引用,而不是独立的
2. 源对象的可枚举属性才能拷贝。
3. 拷贝只会拷贝源对象本身的属性值,不会拷贝其继承的值
应用
1. 为对象添加属性
2. 为对象添加方法
3. 克隆对象
4. ......
ES6当中总共有5中方法进行遍历对象的属性,在es5的基础之上,增加了两种方式
以上五种遍历方式的遍历顺序都按照如下的顺序
具体例子看一下
Object.keys({[Symbol()]: 0, b: 1, 10: 2, 3: 3, a: 1})
// ["3", "10", "b", "a"]
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
Object.setPrototypeOf()
// 格式
Object.setPrototypeOf(object, prototype)
// 用法
var o = Object.setPrototypeOf({}, null);
#例子
let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);
proto.y = 20;
proto.z = 40;
obj.x // 10
obj.y // 20
obj.z // 40
就可以通过 Object.setPrototypeOf()
来该表其指向的原型对象,在es5当中,是可以通过隐形指针 __proto__
来进行改变
Object.getPrototypeOf()
获取对象的原型对象
Object.getPrototypeOf(obj);
使用 Object.entries()
和 Object.values()
的时候,其返回顺序与上面说的遍历的顺序是一致的,由遍历决定值,既先 数字,然后字符串,最后是symbol
Object.values()
返回一个包含对象值的数组
Object.entries()
其表现形式和 Object.values一样,只不过是值不一样
使用Symbol的原因,是比如es5的对象属性名当中,都是字符串,这很容易造成属性名的冲突,如果能够有一种机制,保证每个属性的名字都是独一无二的。这就是ES6引入 Symbol
的原因,确保属性名是独一无二的。
ES6引进的Symbole类型,是JS当中的第七种数据类型,除了原本的ES5的五种基本数据类型和 Object之外,引入的第七种数据类型。
Symbol值通过 Symbol
函数生成,这就是说,对象的属性名可以有两种类型,一种是原来就有的字符串,另外一种则是Symbol值,如果对象的属性名为Symbol值,那么说明,其为独一无二的。如何实现独一无二,且看下面分析。
ES6中的数据类型(共7种)
五种基本数据类型:null、undefined、number、boolean、string
对象:object
Symbol类型,其为原始类型的值,不是对象
所以创建Symbol类型值得时候,不可以使用 new
命令,否则会报错。其存在,类似于一个字符串的数据类型。所以,当然也不可以为其添加属性。
let s = Symbol();
typeof s; // symbol
在初始化Symbol的时候,可以为其添加一个参数,这个参数表明Symbol实例的描述,这是为了在控制台输出的时候,或者转为字符串的时候,更容易区分,最好是在初始化的时候,加入参数,日后好识别这些symbol值
var s1 = Symbol('foo');
var s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"
如果传入的参数是 对象,那么会调用对象的 toString()
方法,之后再进行初始化赋值。
由于Symbol值是独一无二的,所以Symbol与Symbol之间是不相等的,就算是添加了相同的参数
let a = Symbol('aaa');
let b = Symbol('aaa');
a === b; // false
Symbol值可以转为字符串 和 布尔值,其他类型都不可以
let s = Symbol();
s.toString(); // 'Symbol()'
Boolean(s); // true
由于每一个Symbol值都不相等,这意味着Symbol值作为标识符,用于对象的属性名,就能保证不会出现同名的属性。Symbol值构造为对象的属性
var mySymbol = Symbol();
var a = {};
#第一种写法
a[mySymbol] = 'Hello!';
#第二种写法
a = {
[mySymbol]: 'Hello!'; 注意[]内,是没有引号的
}
#第三种写法
Object.defineProperty(a, mySymbol, {
value: 'Hello'
})
# 以上写法都得到相同的结果
a[mySymbol]; // 'Hello'
注意,Symbol值是不可以用 .
来进行定义的,对象的 .
定义的后面永远接的字符串,注意,如果使用方括号来访问属性,加上了单引号或者双引号,则代表访问的是字符串属性
var a = {};
var my = Symbol();
a.my = 'hello';
a[my] = 'world';
a['my']; // 'hello'
a[my]; // 'world'
因为Symbol值是只能唯一的,所以能够确保,如果有一个同名的symbol值,那么在设置对象的属性的时候,是不会覆盖的,是唯一的。
var a = {};
var b = Symbol();
a[b] = 'Hello';
var c = b;
var b = Symbol();
a[c]; // 'Hello'
a[b]; // undefined
a[b] = 'world';
a[b]; // world
Symbol值,只能在 Object.getOwnPropertySymbol()
和 Reflect.ownKeys()
两个方法当中获得,其余对象的迭代方法,无法获取属性值为Symbol的属性
var obj = {};
var f = Symbol('f');
obj[f] = 'Hello!';
for (var i in obj) {
console.log(i); // 无输出
}
Object.keys(obj); // 无输出
Object.getOwnPropertySymbol(obj); // [Symbol(f)]
有时候希望重新使用同一个Symbol值,Symbol.for
方法可以做到这一点。其接受一个字符串作为参数,然后搜索有没有以该参数为名称的Symbol值,有,则返回该Symbol值,没有就返回一个以该字符串为名称的Symbol值
var s1 = Symbol.for('foo');
var s2 = Symbol.for('foo');
s1 === s2; // true
Symbol()
和 Symbol.for()
都可以创建新的Symbol类型的值,但是二者还是有不同点的,首先Symbol.for 会有登记机制,每次调用都会返回同一个值
Symbol.for('foo') === Symbol.for('foo'); // true
Symbol('foo') === Symbol('foo'); // false
ES6提供了11个内置的Symbol值,指向语言内部使用的方法,多用于类的相关函数当中。
Symbol.hasInstance
Symbol.
......
Set是ES6当中是一种新的数据结构,其类似于数组,但又不是数组,其最大的特点就是数值没有重复
Set函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化
var set = new Set([1, 2, 3, 4, 4]);
[...set]; // [1, 2, 3, 4]
#example 2
var set = new Set([1, 2, 3, 4, 5, 5, 5]);
set.size; // 5
#example3
function divs () {
return [...document.querySelectorAll('div')];
}
var set = new Set(divs());
set.size // 56
// 类似于
divs().forEach(div => set.add(div));
set.size // 56
利用set,能很容易的实现数组去重,以前数组去重需要大费周章,即
function removeArr(arr) {
var docker = {},
result = [];
for (var i of arr) {
if (!docker[i]) {
docker[i] = true;
result.push(i);
}
}
return result;
}
现在要实现数组去重,只需要利用好Set 这个数据结构
[...new Set(array)]
使用Set,应注意如下的几点
1. set内部判断重复使用类似于 ===
运算符的方式
2. 为set添加元素值时,其不会发生类型转换,所以5 和 '5' 是不同的
3. 在set当中,NaN是相等的
4. 两个对象在set当中总是不等的
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set; // Set {NaN}
#对象总不相等
set.add({});
set.size; // 1
set.add({});
set.size; // 2
Set结构有以下属性
- Set.prototype.constructor
: 构造函数,默认是 Set
函数
- Set.prototype.size
:返回Set实例的成员及总数
Set结构的方法:主要分为操作方法
和遍历方法
操作方法:
特别要注意的是,因为 add
返回的是set对象本身,所以是可以链式调用的
s.add(1).add(2).add(2);
// 注意2被加入了两次
s.size // 2
s.has(1) // true
s.has(2) // true
s.has(3) // false
s.delete(2);
s.has(2) // false
遍历方法:
注意,Set的遍历顺序就是插入顺序,与对象不同,对象的遍历顺序是先数字,再字符串,再Symbol
因为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"]
forEach
方法,用于对每个成员执行某种操作,没有返回值
let set = new Set([1, 2, 3]);
set.forEach((value, key) => console.log(value * 2))
可以用与遍历的还有 ...
扩展运算符、数组的 map
、filter
#扩展运算符与数组结合,用于数组去重
let arr = [3, 5, 2, 2, 5, 5];
let unique = [...new Set(arr)];
// [3, 5, 2]
#Set使用数组的方法,实现并集、交集和差集
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);
// 并集
let union = new Set([...a, ...b]);
// Set {1, 2, 3, 4}
// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
// set {2, 3}
// 差集
let difference = new Set([...a].filter(x => !b.has(x)));
// Set {1}
目前暂时还没有直接的方法可以在遍历操作中,同步改变原来的Set结构,但是可以用变通的方法,就是用Set结构映射出一个新的结构,然后赋值给原来的Set结构,另外一个是使用 Array.from
方法
// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6
// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6
WeakSet结构与Set类似,也是不重复的值得集合,但是其与Set有两个重要的区别,如下
注意,参数,作为构造函数,WeakSet可以接受一个数组的对象作为参数。但是实例的add方法不可以直接添加。
var a = [[1,2], [3,4]];
var ws = new WeakSet(a);
ws; // WeakSet {[1, 2], [3, 4]}
#错误,数组成员不是对象
var a = [1, 2];
var ws = new WeakSet(a);
// // Uncaught TypeError: Invalid value used in weak set(…)
WeakSet的一个用处,就是储存DOM节点,而不用担心这些节点从文档移除时,会引发内存泄漏。
Map的产生,是因为原本的对象的键值只能是字符串或者Symbol,想要是其他值,并不可行
var data = {};
var element = document.getElementById('myDiv');
data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"
会发现,想要使用一个DOM来作为key值,但是发现被转换为字符串了。但是map的key值可以任何属性的值
var m = new Map();
var 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也接受一个数组作为参数,该数组的成员是一个个表示键值的数组
var map = new Map([
['name', '张三'],
['title', 'Author']
]);
map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"
#其实际过程如下
var items = [
['name', '张三'],
['title', 'Author']
];
var map = new Map();
items.forEach(([key, value]) => map.set(key, value));
如果连续对同一个键多次赋值,后面的值将会覆盖前面的值
let map = new Map();
map.set(1, 'aaa').set(1, 'bbb');
map.get(1) // "bbb"
在使用对象作为键值进行引用的时候,要非常注意使用对象的方法,因为在Map当中,只有内存位置一样的键,才将其视为是同一个键。
var map = new Map();
map.set(['a'], 555);
map.get(['a']); // undefined
#正确
var a = ['a'];
map.set(a, 555);
map.get(a); // 555
实际上,map.get
当中是新创建了 array的一个实例,表面上看,二者是相同的键,但是其内存地址不一样,所以会的到undefined的结果。
只有对象是需要考虑内存地址的,如果是简单数据类型的话,其只要严格相等,就视为是同一个键,这其中 NaN
也视为是同一个键
let map = new Map();
map.set(NaN, 123);
map.get(NaN) // 123
map.set(-0, 123);
map.get(+0) // 123
属性:
- size
:返回Map结构的成员总数
操作方法:
- set(key, value)
:返回整个Map结构,如果Key已经有值,则键值会被更新,否则则新生成改键,返回Map对象本身,所以可以链式调用
- get(key)
:返回key所对应的值,若无,返回undefined
- has(key)
:返回一个boolean
- delete(key)
:删除某个键,成功则返回true,否则,返回false
- clear()
遍历方法:
- keys()
- values()
- entries()
- forEach(function(value, key, map))
let map = new Map([
['F', 'no'],
['T', 'yes'],
]);
for (let key of map.keys()) {
console.log(key);
}
// "F"
// "T"
for (let value of map.values()) {
console.log(value);
}
// "no"
// "yes"
for (let item of map.entries()) {
console.log(item[0], item[1]); // 每一项是一个数组
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
#forEach方法
map.forEach(function(value, key, map) {
console.log("Key: %s, Value: %s", key, value);
});
Map转数组,比较快速的方法是使用扩展运算符 ...
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
[...map.keys()]
// [1, 2, 3]
[...map.values()]
// ['one', 'two', 'three']
[...map.entries()]
// [[1,'one'], [2, 'two'], [3, 'three']]
[...map]
// [[1,'one'], [2, 'two'], [3, 'three']]
与Map基本类似,但是有一些不同的地方,不同点如下
clear
操作方法,也没有 size
属性Proxy用于修改某些操作的默认行为,等同于在语言层面作出修改,是一种“元”编程。实际上相当于是一种“拦截”,即外界对该对象的访问,都必须先通过这层拦截。因此,可以对外界的访问进行过滤和改写。
下面先来看一个简单的例子
var obj = new Proxy({}, {
get(target, key, receiver) {
console.log(`gettting ${key}`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`setting ${key}`);
return Reflect.set(target, key, value, receiver);
}
});
上面的代码实际上是对一个空对象架设了一层拦截,重定义了属性的读取(get) 和 设置(set)行为看运行结果
obj.count = 1;
// setting count!
++obj.count
// getting count!
// setting count!
// 2
但是,上面的结果出现,是必须在使用 proxy实例
的前提下使用才可以,如果直接使用对象本身,即空对象本省设置属性,是不需要经过这一层拦截的,即如果不使用定义的 proxy实例来进行操作,对对象本身是不会有任何影响的,其所定义的拦截行为也不会影响对象本省的用法。并且,如果使用了proxy实例,对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。
Proxy对象的所有用法,都是以下的构造函数的方式,只不过不同的是 handler
参数的写法。target
参数表示所要拦截的目标对象,handler
参数也是一个对象,不过是用来定制拦截行为
var proxy = new Proxy(target, handler);
使用proxy,实际上可以实现很多原本对象所没有的功能,关键就在于handler的处理上,可以去通过以下handler当中所能支持的一些操作,比如get里面就可以进行操作,包括返回一个函数都是OK的
下面是一些proxy支持的一些拦截操作概览,即handler
当中的一些操作。
(1)get(target, propKey, receiver)
拦截对象属性的读取,比如 proxy.foo
和 proxy['foo']
。最后一个参数 receiver
是一个对象,可选,参见下面 Reflect.get
的部分。
(2)set(target, propKey, value, receiver)
拦截对象属性的设置,比如 proxy.foo = v
或 proxy['foo'] = v
,返回一个布尔值。
(3)has(target, propKey)
拦截 propKey in proxy
的操作,以及对象的 hasOwnProperty
方法,返回一个布尔值。
(4)deleteProperty(target, propKey)
拦截 delete proxy[propKey]
的操作,返回一个布尔值。
(5)ownKeys(target)
拦截 Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
,返回一个数组。该方法返回对象所有自身的属性,而Object.keys()仅返回对象可遍历的属性。
(6)getOwnPropertyDescriptor(target, propKey)
拦截Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。
(7)defineProperty(target, propKey, propDesc)
拦截 Object.defineProperty(proxy, propKey, propDesc)
、Object.defineProperties(proxy, propDescs)
,返回一个布尔值。
(8)preventExtensions(target)
拦截 Object.preventExtensions(proxy)
,返回一个布尔值。
(9)getPrototypeOf(target)
拦截 Object.getPrototypeOf(proxy)
,返回一个对象。
(10)isExtensible(target)
拦截 Object.isExtensible(proxy)
,返回一个布尔值。
(11)setPrototypeOf(target, proto)
拦截 Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。
如果目标对象是函数,那么还有两种额外操作可以拦截。
(12)apply(target, object, args)
拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。
(13)construct(target, args)
拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
。必须返回一个对象
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代理的话,那么目标对象target
的内部的 this
关键字就会指向Proxy代理
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
这就会导致,如果目标对象的属性是使用this来进行实现的话,那么将会导致使用了Proxy代理的情况,属性失效了。
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
jane.name // 'Jane'
const proxy = new Proxy(jane, {});
proxy.name // undefined
解决办法
this
绑定原始对象,就可以解决这个问题。
#报错
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate();
// TypeError: this is not a Date object.
// 因为getDate必须是 this值为 Date对象才可以
#修改
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target.getDate.bind(target);
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
Reflect
对象与 Proxy
对象一样,也是ES6为了操作对象而提供的新API。
为什么需要Reflect
呢
1. 将一些Object对象的一些明显属于语言内部的方法(比如 Object.defineProperty
),放到 Reflect
上
2. 修改某些Object方法的返回结果,让其更加合理,以前 Object.defineProperty(obj, prop, des)
在无法定义属性是,会跑出错误,而Reflect.de... 则会返回false
3. 让 Object
操作都变成函数行为,比如 Object
操作的 name in obj
和 delete obj.name
,在 Reflect
当中,则是 Reflect.has(name)
和 Reflect.deleteProperty(obj, name)
4. 只要 Proxy
当中有的方法,在 Reflect
当中都会有,一一对应。所以方法如上面的 Proxy
所示
for ... of
实际上是调用对象的Iterator接口,即 [Symbol.iterator]
,所以说,当一个数据结构只要具有 Symbol.iterator
属性,我们就可以认为其实"可遍历的"
var obj = {
0: 1,
1: 2,
length: 2
}
for (let v of obj) {
console.log(v); // 报错
}
上面报错的原因是因为,这种认为定义的类似数组的对象,是不具备iterator接口的,所以不可以使用 for ... of
,但是我们可以为其部署 Symbol.iterator
接口
var obj = {
0: 1,
1: 2,
length: 2,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
}
这样子去用 for...of
就没有错了。
数组、类似数组的对象(Nodelist、arguments)、Set和Map结构、字符串
Generator函数是ES6提供的一种异步编程解决方案。
从语法上来说,可以把它理解成是一个状态机,其内部封装了多个内部状态。
其从外表上看,就是一个普通的函数,但是其执行之后不会返回结果,而是返回一个遍历器对象,代表Generator函数的内部指针 ,以后每次调用遍历器对象的 next
方法,就会返回一个有 value
和 done
两个属性的对象。
其有点像上一章所说的 iterator
var obj = {
[Symbol.iterator]: function() {
return {
next: function() {
...
}
}
}
}
就相当于上面所返回对象,是一个遍历器对象,所以执行 generator函数,其返回的遍历器对象可以用来设置 iterator,当然也可以直接被 for ... of
执行
var obj = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
return 3;
}
}
# generator函数执行后,使用 for ... of进行调用
function* f() {
yield 1;
yield 2;
yield 3;
}
var o = f();
for (var i of o) {
console.log(i);
}
// 1
// 2
// 3
由于Generator函数返回的遍历器对象,只有调用 next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield语句就是暂停标志。
需要注意的是,yield
语句后面的表达式,只有当调用 next
方法、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
function* gen() {
yield 123 + 456;
}
上面的yield后面的表达式,不会在执行函数的时候就计算好了,而是在next方法将指针移动到这一句时,才会进行求值。
yield 和 return 都可以使generator对象在调用 next 方法的时候,返回紧跟其后的值作为value,但是二者的区别在于以下两点,
1. return在函数当中只能使用一次,就将函数返回,后面的不再执行,但是yield的功能是 暂停后面的执行,等待下一次next指令。
2. return 所返回的的对象中的 done
属性为 true
,而 yield
则为 false
注意:
1. 在普通函数当中,不可以使用 yield
,会报错,但是可以在 generator函数的循环体当中进行使用
2. yield
语句如果用在一个表达式当中,必须放在圆括号里面
3. yield
本身不返回任何值,或者说其返回 undefined
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield 123)); // OK
Generator函数执行后,返回一个遍历器对象,其可以将返回的对象赋值给 Symbol.iterator
属性。该对象本身也具有Symbol.iterator属性,执行后返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
设置上一次yield的返回值
yield
句本身没有返回值,或者说总是返回 undefined
。next
方法可以带一个参数,该参数就会被当做是上一个 yield
语句的返回值。
可以传参数的作用,就是可以在 Generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
可以看下面一个例子
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(); // {value: 2, done: false}
# 持续循环下去
上面会由 0 一直循环下去的原因是因为 yield
不会返回任何值,即 reset
永远都是 undefined
。但是,如果使用 next方法参数
,即设置 上一次 yield 的返回值,手动给其添加一个返回值。
g.next(true); // {value: 0, done: false}
这里使用了 next 参数的方法,实际上是设置了上一次 yield的返回值,即 reset = true
,所以 i 会被重置,重新计算。
再来看一个例子,你会更加明白,next参数
的意义
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 }
第一部分的 next 都没有设置参数,可以看到结果就是 NaN
,这是因为 yield没有返回值的原因,及第二次调用 next的时候,由于之前 y 的值为 2 * undefined = NaN,所以z = yield(y/3)
就是 NaN
,同理 z
第二部分设置了 参数,即b.next(12)
则是说明第一次yield的返回值为 12,并且赋予给y,即 y = 2 * 12 = 24
,所以第二次next的输出值是 8.
总结:调用next所输出的值是 yield
或者 return
后面所跟的值或者表达式,但是 yield
本身不返回任何值,可以使用next参数的方法来赋予下一次调用next时,赋予上一次 yield的返回值。
注意: for ... of
如果在调用 next 方法的时候,返回的对象的 done
属性为 true
时,则循环会中止。
function *foo() {
yield 1;
yield 2;
return 3;
}
for (let v of foo()) {
console.log(v);
}
// 1 2
上面是因为 return 3
返回的是 {value: 3, done: true}
,所以会中止循环
Generator函数返回的遍历器对象,有一个 throw
方法,可以抛出错误到函数体外,并且终结遍历Generator函数,但是其有一个副作用,就是会默认再执行一次 yield
了解就好!
Generator函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历Generator函数。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
如果 return 不提供参数,则 value
为 undefined
如果Generator函数内部有 try...finally
代码块,那么return方法会推迟到 finally
代码块执行完再执行。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers()
g.next() // { done: false, value: 1 }
g.next() // { done: false, value: 2 }
g.return(7) // { done: false, value: 4 }
g.next() // { done: false, value: 5 }
g.next() // { done: true, value: 7 }
上面代码中,调用return方法后,就开始执行 finally
代码块,然后等到 finally
代码块执行完,再执行 return
方法。
在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"
那么,yield*
语句,就是用来在一个 Generator函数里面执行另一个Generator函数
function* foo() {
yield 'a';
yield 'b';
}
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"
yield*
后面的 Generator
函数(没有return语句时),等同于在Generator函数内部,部署一个 for...of
循环。
但是,如果后面的 Generator
函数有 return语句时,其可以用来作为 yield* 的返回值,赋值给其他变量,众所周知,yield语句是不会返回任何东西的,或者说返回为 undefined,但是,yield* 后面的 Generator函数如果有 return 语句,则可以返回值
function *foo() {
yield 2;
return "foo";
}
function *bar() {
yield 1;
var v = yield *foo();
console.log( "v: " + v );
yield 3;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next();
// "v: foo"
// {value: 3, done: false}
it.next()
// {value: undefined, done: true}
Generator函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype对象上的方法。
但是其不能访问Generator函数体中的对象
# 访问 prototype
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
# 不能访问 g当中的对象,因为g返回的总是遍历器对象,而不是 this 对象
function* g() {
this.a = 11;
}
let obj = g();
obj.a // undefined
如果要使返回的对象能够访问g构造函数当中的属性,可以结合 prototype来进行实现
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
f能访问 F当中的 this 属性值,是因为其 prototype
上有相对应的值,所以可以访问
1) 异步操作的同步化表达
就是把异步操作写在 yield语句里面,等到调用next方法时,再往后执行,因为 yield 会暂定函数内部后面的内容的执行,实际上,这个特点就能让我们不写回调函数,实现异步操作。
2) 控制流管理
3) 部署Iterator接口
4) 作为数据结构
使其成为一个类似于数组的结构,因为可以使用 yield,并且可以直接被 for ... of
进行处理,所以类似数组
所谓 Promise
,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise提供统一的API,各种异步操作都可以用同样的方法进行处理。
传统的解决异步操作的方法是通过回调函数,但是有了 promise之后,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
Promise
对象有以下两个特点:
1. 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:Pending
(进行中)、Resolved
(已完成,又称 Fulfilled
)和 Rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
2. 一旦状态改变,就不会再变,任何时候都可以得到这个结果(有点类似于缓存)。Promise对象的状态改变,只有两种可能:从Pending
变为Resolved
和从Pending
变为Rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
promise
的优缺点
1. 可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数
2. 提供统一的接口,使得控制异步操作更加容易
缺点:
1. 无法取消 promise
,一旦新建它,就会立即执行,不能中途取消
2. 如果不设置回调函数,Promise
内部就会抛出错误,不会反应到外部
3. 当处于 Pending
状态时,无法得知目前进展到哪一个阶段(是刚开始还是即将完成)
Promise对象是一个构造函数,用来生成 Promise实例,创建实例之后,实例可以用then方法分别指定 resolved
状态和 reject
状态的回调函数,这就是 Promise的基本用法
即下面的这种格式
var promise = new Promise(function(resolve, error) {
// ... some code
if (/* 异步操作成功 */) {
resolve(value);
} else {
reject(error)
}
})
promise.then(function(value) {
// success
}), function(error) {
// failure
};
构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve
和 reject
,它们是两个函数,由javascript引擎
提供,不用自己部署
resolve
的作用,就是将 Promise对象的状态从 pending
到 resolve
,并将结果作为参数传递出去,reject
的作用,就是将 Promise对象的状态从 pengding
到 rejected
,并将报错的错误作为参数传递出去。
看一个简单的例子,并且说明 Promise新建后就会立即执行这个特点
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve('you');
});
promise.then(function(value) {
console.log('Resolved. ', value);
});
console.log('Hi!');
// Promise
// Hi!
// Resolved. you
resolve函数的参数不仅仅可以是简单的数据类型,还可以是一个 Promise对象,如果是一个 Promise对象的话,它的状态就决定了 当前 Promise对象的状态
var p1 = new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('fail')), 3000)
})
var p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 1000)
})
p2
.then(result => console.log(result))
.catch(error => console.log(error))
// Error: fail
上面代码中,p1
是一个Promise,3秒之后变为 rejected
。p2的状态在1秒之后改变,resolve
方法返回的是p1。此时,由于p2返回的是另一个Promise,所以后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected
,导致触发catch方法指定的回调函数。
注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是 Pending
,那么p2的回调函数就会等待 p1的状态改变;如果p1的状态已经是Resolved
或者Rejected
,那么p2的回调函数将会立刻执行。
它的作用是为Promise实例添加状态改变时的回调函数。then
的第一个参数是 resovled状态的回调函数,第二个参数(可选)是 rejected状态的回调函数。
then
方法返回的是一个新的Promise实例(注意:已经不是原来那个 promise实例),并且第一个回调函数的结果可以返回作为第二个回调函数的参数,因此可以采取链示写法
采用链式的then
,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。
var p1 = new Promise(function (resolve, reject) {
setTimeout(() => resolve("I am promise1"), 3000)
})
var p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve("I am promise2"), 1000)
})
p2.then(result => {console.log(result); return p1;})
.then(result => console.log(result));
// I am promise2
// I am promise1
Promise.prototype.catch
方法是.then(null, rejection)
的别名,用于指定发生错误时的回调函数。
p.then((val) => console.log("fulfilled:", val))
.catch((err) => console.log("rejected:", err));
// 等同于
p.then((val) => console.log("fulfilled:", val))
.then(null, (err) => console.log("rejected:", err));
上面代码中,如果该对象p 状态变为
Resolved
,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为Rejected
,就会调用catch
方法指定的回调函数,处理这个错误。另外,then
方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。
如果Promise状态已经变成Resolved
,再抛出错误是无效的。这就是说其状态改变后,就凝固了,不能再改变。
var promise = new Promise(function(resolve, reject) {
resolve('ok');
throw new Error('test'); // 无效
});
promise
.then(function(value) { console.log(value) })
.catch(function(error) { console.log(error) });
// ok
promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为之,并且,比较好的书写习惯是,在连续then调用后增添一个 catch来捕获错误,而不要直接在then当中写入错误处理函数(即then的第二个参数)
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) {
// success
})
.catch(function(err) {
// error
});
建议总是使用catch方法,而不使用then方法的第二个参数。
跟传统的try/catch
代码块不同的是,如果没有使用catch
方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。
Promise.all
方法用于将多个 Promise实例包装成一个新的Promise实例
var p = Promise.all([p1, p2, p3])
其接受一个数组作为参数,并且数组当中的item都是 Promise的实例,如果不是的,会先调用 Promise.resolve()
,将其转为 Promise对象后再进行处理。
p的状态则有p1、p2、p3来决定
1. 三者都为 resolved
状态时,p就为 resolved
2. 三者都为 rejected
状态时,p就为 rejected
var promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON("/post/" + id + ".json"); // 函数返回一个 promise对象
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function(reason){
// ...
});
只有等待promises 当中的所有 promise对象都返回结果后,组合成一个数组传递给回调函数,即 posts
Promise.race和Promise.all差不多,只不过race要求只要其中一个 promise状态改变了,即为组合后的状态。参数和 Promise.all()
一样,接受一个数组作为参数,如果数组中的 item 不是promise对象的话,也会先调用 Promise.resolve()
方法
var p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
])
p.then(response => console.log(response))
.catch(error => console.log(error))
上面代码中,如果5秒之内fetch
方法无法返回结果,变量p的状态就会变为rejected
,从而触发catch方法指定的回调函数。
Promise.resolve()
可以将现有对象转为 Promise对象
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'));
但是,Promise.resolve的参数存在如下四种形式
1) 参数是一个 Promise实例
如果参数是一个实例,那么 Promise.resolve
将不做任何修改、原封不动地返回这个实例。
2) 参数是一个 thenable对象
thenable
对象指的是具有 then
方法的对象,比如下面的
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
}
Promise.resolve()
会将这个对象转为 Promise对象,然后立即执行 then方法
let thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
let p1 = Promise.resolve(thenable);
p1.then(function(value) {
console.log(value); // 42
});
3) 参数不是具有then方法的对象,或者根本就不是对象
那么 Promise.resolve()
就直接将其转为Promise对象,并且将其作为参数传递到回调函数当中。
Promise.resolve('Hello')
// 等价于
new Promise(function(resolve, reject) {
resolve('Hello');
});
p.then(function(s) {
console.log(s);
})
4) 不带任何参数
不带任何参数的时候,直接返回一个 resolved
状态的Promise对象
但是,立即 resolve
的Promise对象,是在“本轮循环”(即 event loop)的结束时执行的,而不像普通的 promise对象,实在下一轮循环开始才执行的。
setTimeout(function () {
console.log('three');
}, 0);
Promise.resolve().then(function () {
console.log('two');
});
console.log('one');
// one
// two
// three
Promise.reject(reason)
方法也会返回一个新的 Promise 实例,该实例的状态为 rejected
var p = Promise.reject('出错了');
// 等同于
var p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
与 Promise.resolve()
不同的是,Promise.reject()传入的参数会原封不动的作为回调函数的参数,不论其参数是什么对象。
const thenable = {
then(resolve, reject) {
reject('出错了');
}
};
Promise.reject(thenable)
.catch(e => {
console.log(e === thenable)
});
// true
上面代码中,Promise.reject
方法的参数是一个 thenable
对象,执行以后,后面 catch
方法的参数不是reject
抛出的“出错了”这个字符串,而是thenable
对象。
Promise对象的回调链,不管以 then
方法或 catch
方法结尾,要是最后一个方法抛出错误,都可能无法捕捉到,因为Promise的错误不会冒泡到全局当中。done就是做这个的,放在回调链的最末尾
finally也是放到回调链的最末尾,但是和done不同的是,done一般是只捕捉最后发生错误,但是finally放在最末尾是无论如何都会执行的操作。其可以接受一个普通的回调函数作为参数。
Generator用来控制管理流程,在遇到异步操作的时候,通常返回一个 Promise
对象
ES6诞生以前,异步编程的方法,大概有下面四种
异步的意思就是一个任务分成两段。
callback hell
的问题,是因为回调函数的嵌套导致的,这样导致了函数不是纵向发展的,而是横向发展的,如下
fs.readFile(fileA, function (err, data) {
fs.readFile(fileB, function (err, data) {
// ...
});
});
如果采取 Promise的写法,就可以以同步的书写方式来实现异步操作
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function(data){
console.log(data.toString());
})
.then(function(){
return readFile(fileB);
})
.then(function(data){
console.log(data.toString());
})
.catch(function(err) {
console.log(err);
});
但是 Promise的不好之处也是代码冗余,一堆 then
与 Promise进行配合,实现异步任务
var fetch = require('node-fetch');
function* gen() {
var url = 'http://www.....';
var result = yield fetech(url);
console.log(result);
}
var g = gen();
var result = g.next();
g.value.then(data => data.json())
.then(data => g.next(data));
在讲Thunk函数之前,要先了解一下 传值调用 和 传名调用 的区别,下面一个函数就能很清晰的知道二者的区别。
var x = 1;
function f(m){
return m * 2;
}
# 传值调用
f(x + 5);
// 传值调用时,等同于
f(6);
# 传名调用
f(x + 5);
// 传名调用时,等同于
(x + 5) * 2
人们在做编译器的,更倾向于是用传名调用,因为这样的好处是避免了在传入参数不用的时候而做了额外的计算工作。那么,trunk函数实际上就相当于这个 名,将传入参数的部分作为一个函数,凡是用到的地方,都调用这个函数。
function f(m){
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function() {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}
实现Generator的自动运行,不需要认为的进行操作,并且可以实现异步任务的进行,等待异步数据到来后自动执行,但是实现Generator的自动执行不仅仅可以使用 Thunkify模块,有很多其他的方法也可以实现,譬如 Promise、回调函数、co模块
使用Thunk实现Generator函数的自动执行
var fs = require('fs');
var thunkify = require(thunkify);
var readFile = thunkify(fs.readFile);
function* g() {
var f1 = yield readFile('fileA');
var f2 = yield readFile('fileB');
// ...
var fn = yield readFile('fileN');
}
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
run(g);
以上就能实现 Generator函数的自动执行
co模块是著名程序员TJ Holowaychuk发布的小工具,用于Generator函数的自动执行。
它的实现其实是集成了 thunk函数和 promise,其使用的前提是 yield命令后面只能是 Thunk函数 或 Promise对象
下面看是如何实现基于 Promise对象实现Generator的自动执行机制
先看原理,然后将其改装成一个自动执行的函数
# 首先把fs模块的readFile方法包装成一个 Promise对象
var fs =require('fs');
var readFile = function(fileName) {
return new Promise(function(resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) {
reject(error);
}
resolve(data);
})
})
};
function* g() {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
}
# 手动执行上面的Generator函数
var g = gen();
g.next().value.then(function(data) {
g.next(data).value.then(function(data) {
g.next(data);
});
});
从上面可以看出来,其实手动执行就是用 then
方法,层层添加回调函数。那么,我们就可以将这个用 then 方法的过程,封装成一个函数,变为自动执行器
function run(gen) {
var g = gen();
function next(data) {
var result = g.next(data);
if (result.done) {
return result.value;
}
result.value.then(function(data) {
next(data);
});
}
next();
}
run(gen);
上面就是 Promise实现 Generator自动执行的过程
async函数可以看做是Generator的语法糖
# 上面的一个例子
var gen = function* () {
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
}
asynce函数的写法就是
var asyncReadfile = async function() {
var f1 = await readFile('/etc/fstab');
var f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
}
async 相比于 generator有如下的几个好处
1) 内置执行器。 async
自带执行器,其调用过程和普通函数一样,并且会自动执行,而不会去调用 next
方法
2) 更好的语义
3) 更广的适用性
4)返回值是 Promise
对象,可以直接使用then
进行下一步操作
1) async
函数返回一个 Promise对象,内部的 return
语句返回的值,会成为 then
方法回调函数的参数
2) async
函数返回的 Promise对象,必须等到内部所有 await
命令的 Promise对象都执行完毕,才会发生状态改变,才能执行 then
方法指定的回调函数,有点类似于 Promise.all()
3) await
后面跟的是 Promise对象,如果不是,则使用 Promise.resolve
将其转为立即resolve的Promise对象
async函数的实现,就是将 Generator函数和自动执行器都包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
})
}
所有的 async
都可以写成上面的第二种形式,其中 spawn函数是自动执行器。spawn函数的实现,其实和上面的 run
大同小异
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
在以前,即 es5当中,使用构造函数创建新对象,一般都是采用构造函数模式和原型模式两种方法结合来创建一个对象
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
但是这对以前学c++或者Java等面向对象语言的人来说,用这种方式来构造对象会感到困惑,所以es6引入了 Class 这个概念。通过 class
关键字,定义类,其可以看作是一个语法糖,因为其绝大部分功能,在es5当中,都可以相对应的实现。
上面的构造函数的写法,用es6的class的写法,如下
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var p = new Point(2, 3)
表示方法和原来差不多,构造函数就是写在 constructor
当中,然后其余的函数就相当于是在 Point.prototype
上定义一样。有以下几个注意点
注意:
class
关键字有点类似于 function
new
命令创建实例,不使用new而直接调用函数,会报错!constructor
constructor
方法是类的默认方法,通过 new
命令生成对象实例时,自动调用该方法,如果没有显示定义 constructor
方法,一个空的 constructor
方法会被默认添加
constructor() {}
es5当中,使用构造函数的方式,因为 函数声明的方式可以实现变量提升,所以在es5当中,可以在声明之前就使用了构造函数,但class不行
new Foo(); // ReferenceError
class Foo {}
之所以不能让 class
关键字声明可以变量提升,与继承的实现机制有关,一定要保证子类在父类之后定义,如下面的代码,如果class能变量提升,那么将会报错
{
let Foo = class {};
class Bar extends Foo {}
}
因为let不能变量提升,如果class可以变量提升,即提到块级作用于的顶部,那么继承的 Foo 此时还没有定义,发生TDZ,会发生错误,所以 class 关键不能够变量提升!
与 函数一样,类也可以有 class表达式的定义方法
let MyClass = class Me {};
注意上面,这个类的名字是 myClass
而不是 Me
,Me
在此时,只能在class的内部代码当中使用,指代当前类
如果Me在内部不需要用到的话,完全可以省略 Me 这个名字,class表达式和函数表达式 类似!
一般采用Symbol值得方式来实现类当中的私有方法,因为class当中的所有方法都在 prototype
上面,都是可见的,所以只能采用一些特殊的方法来隐藏他,在es5当中,模块实现隐藏式在函数当中返回一个对象,对象当中都是特权函数,可以访问内部的变量、方法等,但是外部调用的时候,是无法修改函数内部的东西的。但在es6当中,不公开支持私有方法,变相私有,目的就是为了让别人不能得到
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) {
this[bar](baz);
}
// 私有方法
[bar](baz) {
return this[snaf] = baz;
}
// ...
};
类和模块的内部,默认就是严格模式
es5的继承和es6的继承有什么不同呢
es5的继承,是先新建子类的的实例对象的 this,再将父类的属性添加到子类上。而es6则先新建父类的实例对象的this,再通过子类的构造函数修饰 this
class之间可以通过 extends
关键字实现继承。
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
toString() {
return this.color + ' ' + super.toString();
}
}
这里使用了 extends
来来实现继承,ColorPoint 继承 Point的所有属性和方法,其中,在 constructor
当中,一定要调用 super
方法,否则在新建实例时会报错,这是因为子类是没有自己的 this
对象的,而是继承父类的 this
对象,然后对其进行加工,如果不调用 super
方法,子类就得不到 this
对象
如果子类没有显示声明 constructor
方法,则会默认添加如下的
constructor(...args) {
super(...args)
}
在es5当中,每一个对象都有 __proto__
属性,指向对应的构造函数的 prototype属性。Class作为构造函数的语法糖。构造函数同时有 prototype属性
和 __proto__
属性,因为总是存在两条链
__proto__
属性,表示构造函数的继承,总是指向父类。prototype
属性的 __proto__
属性,表示方法的继承,总是指向父类的 prototype
属性。
class A {
}
class B {
}
B.__proto__ === A; // true
B.prototype.__proto__ === A.prototype; // true
注意,存在如下几种特殊情况,其 构造函数 和 原型的 __proto__
分别指向
第一种情况,子类继承Object类
class A extends Object {
}
A.__proto__ === Object; // true
A.prototype.__proto__ === Object.prototype; // true
第二种情况,子类没有任何继承,即相当于一个正常函数
class A {
}
A.__proto__ === Function.prototype; // true
A.prototype.__proto__ === Object.prototype; // true
第三种情况,子类继承null
class A extends null
}
A.__proto__ === Function.prototype; // true
A.prototype.__proto__ === undefined // true
super
关键字,既可以当作函数使用,也可以当作对象使用。但是二者所表示的东西完全不同。
1) super
作为函数使用,其代表的是父类的构造函数
class A {}
class B extends A {
constructor() {
super();
}
}
super
在这里虽然代表了 父类A的构造函数,但是返回的确是子类B 的实例,即 super
内部的 this 指向的是B,因此,此时的 super
相当于 A.prototype.constructor.call(this)
,但是不调用的话,子类是没有 this 值的。
注意: super
作为函数使用的时候,只能在子类的构造函数当中使用
2) super
作为对象时,指向父类的原型对象
class A {
p() {
return 2;
}
}
class B extends A {
constructor() {
super();
console.log(super.p()); // 2
}
}
let b = new B();
注意:
1. 由于 super 作为对象的时候,其所代表的是父类的原型对象,所以是不能获取 父类实例上的方法或属性的。
class A {
constructor() {
this.p = 2;
this.method = function() {}
}
}
class B extends A {
constructor() {
super();
console.log(super.p)
}
}
let b = new B(); // undefined
2.super
调用父类的方法时,super
会绑定子类的 this
原生构造函数是指 js的内置构造函数,即内置对象。ECMAScript的原生构造函数大概有如下的这些
但是,在es5当中,是不可以继承原生构造函数的,即不能够自己定义一个构造函数,继承Array之类的,使其具有Array的所有特性。这是由于es5的继承机制所决定的,其无法访问到父类的属性和方法,只能继承其prototype上面的方法。
但是在es6当中,可以创建一个类,其继承于 原生构造函数
class MyArray extends Array {
}
var arr = new MyArray();
arr[0] = 12;
arr.length; // 1
这是一个很有用的功能,因为可以继承于原生构造函数,然后再自己进行额外的改造,获得一个新的数据结构类型,如下,就是实现了一个基于es5数组再添加一个历史版本的功能
class VersionedArray extends Array {
constructor() {
super();
this.history = [[]];
}
commit() {
this.history.push(this.slice());
}
revert() {
this.splice(0, this.length, ...this.history[this.history.length - 1]);
}
}
var x = new VersionedArray();
x.push(1);
x.push(2);
x // [1, 2]
x.history // [[]]
x.commit();
x.history // [[], [1, 2]]
x.push(3);
x // [1, 2, 3]
x.revert();
x // [1, 2]
注意:
继承原生构造函数当中,继承 Object
的子类,有一个行为差异
class NewObj extends Object{
constructor(){
super(...arguments);
}
}
var o = new NewObj({attr: true});
console.log(o.attr === true); // false
ES6改变了Object构造函数的行为,一旦发现Object方法不是通过 new Object()
这种形式调用,ES6规定Object构造函数会忽略参数
Class的静态方法即在class内部函数名前加上 static
关键字即可
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod();
// TypeError: foo.classMethod is not a function
其效果就是 定义的方法,不可以被实例所调用,只能由类本身或者 super
对象上调用
new
是从构造函数生成实例的命令。es6为 new
命令引入了一个 new.target
属性,用来判断当前调用的构造函数是哪一个。如果构造函数不是通过 new
命令调用的,new.target
会返回 undefined
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用new生成实例');
}
}
// 另一种写法
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error('必须使用new生成实例');
}
}
var person = new Person('张三'); // 正确
var notAPerson = Person.call(person, '张三'); // 报错
修饰器是一个函数,其用来改变类的行为。比如可以改变类的静态属性,或者改变类的某些属性。
修饰器对类的行为的改变,是代码编译时发生的,而不是运行时,这意味着,修饰器能在编译阶段就运行代码了,而不是等到执行的时候。
不过,这个只是ES7的一个提案,目前还没有在浏览器当中支持
其本质是编译时执行的函数
function testable(target) {
target.isTestable = true;
}
@testable
class MyTestableClass {}
console.log(MyTestableClass.isTestable); // true
class Person {
@nonenumerable
get kidCount() { return this.children.length; }
}
function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false;
return descriptor;
}
如果同一个类的属性有多个修饰器,其就会像剥洋葱一样,先从外到内进入,然后由内向外执行
function dec(id) {
console.log('evaluated', id);
return (target, property, descriptor) => console.log('executed', id);
}
class Example {
@dec(1)
@dec(2)
method() {}
}
// evaluated 1
// evaluated 2
// excuted 2
// excuted 1
修饰器只能作用于类 和 类的方法
因为函数存在函数声明提升,而这就会导致函数在修饰器定义之前,如下
var readOnly = require("some-decorator");
@readOnly
function foo() {
}
#实际执行过程当中
var readOnly;
@readOnly
function foo() {
}
readOnly = require("some-decorator");
一个第三方模块,提供了几个常见的修饰器,了解即可