ES6读后总结
读阮一峰的ECMAScript 6 入门的读书笔记,原文链接为
http://es6.ruanyifeng.com/#README
ES6
let (块级作用域)
- let只在命令所在的代码块内有效
- for循环适合用let
- let不会变量提升
- let存在暂时性死区(TDZ),在声明变量之前,该变量都是无法使用的(如let x= x就会报错)
- 因为let的存在,typeof不再是一个百分百不会报错的操作,在TDZ中也会报错。
- let不允许重复声明
// 报错
function func() {
let a = 10;
var a = 1;}
// 报错
function func() {
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
- E55不允许在块级作用域(if,try)中函数声明(函数表达式声明倒是可以),而ES6允许。
- ES6的函数声明类似于let不会提升
const
- 声明常量,不可改变
- 和let一样属于块级作用域
- 也存在暂时性死区(TDZ),声明不可提升
- const只保证变量指向的地址的值不变。因为变量直接指向简单类型的数据(数值、字符串、布尔值),所以保证简单类型的数据不变。而变量只指向一个地址,指向复合类型数据(对象和数组),所以无法保证复合类型的数据不变。
- 如果想要将对象冻结,则使用Object.freeze方法
const foo = Object.freeze({})
- Object.keys(obj) 能讲对象中的key以数组的方式返回
顶层对象的属性
-
浏览器:window,self
Node:global
Web Worker: self -
缺点:
- 无法编译时就报出变量未声明(因为全局变量可能是顶层对象创造的,是动态的)
- 可能无意间创建了全局变量
- 顶层对象的属性到处可以读写,不利于模块化编程
- window对象指的是浏览器的窗口对象,不大合适。
-
ES6规定,let,const,class命令声明的全局变量不属于顶层对象的属性。
-
函数中的this在严格模式下指向的不是顶层对象而实undefined
想要在严格模式下返回全局对象的代码(如果用了CSP,Content Security Policy就无法使用)
new Function('return this')()
- npm安装 system.global 可以统一顶层对象为global
-
// ES6 模块的写法
import shim from ‘system.global/shim’; shim();
保证各种环境里面,global对象都是存在的。 -
// ES6 模块的写法
import getGlobal from ‘system.global’;
const global = getGlobal();
上面代码将顶层对象放入变量global。
变量的解构赋值
let [a, b, c] = [1, 2, 3];
可以从数组中提取值,按照对应位置,对变量赋值。
这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3
let [ , , third] = ["foo", "bar", "baz"];
third // "baz"
let [x, , y] = [1, 2, 3];
x // 1
y // 3
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []
ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。
let [x = 1] = [undefined];
x // 1
let [x = 1] = [null];
x // null
Map与Object的区别
https://*.com/questions/18541940/map-vs-object-in-javascript
map便于以for…of 遍历对象,以及能直接获取对象的size。
字符串
codePointAt
- codePointAt与 for…of 方法结合能正确识别32位的UTF-16字符。
let s = '????a';for (let ch of s) {
console.log(ch.codePointAt(0).toString(16));}
// 20bb7
// 61
- codePointAt 是测试一个字符由两个字节还是四个字节组成的最简单的方法
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;}
is32Bit("????") // true
is32Bit("a") // false
String.fromCodePoint()
- 能逆翻译成UTF-16编码,2,4个字节的都能翻译成。
String.fromCodePoint(0x20BB7)
// "????"
for…of 遍历字符串
普通遍历的方法无法识别4个字节的字符串,会拆分成2个。
let text = String.fromCodePoint(0x20BB7);
for (let i = 0; i < text.length; i++) {
console.log(text[i]);}
// " "
// " "
for (let i of text) {
console.log(i);}
// "????"
includes(),startsWith(),endsWith()
includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
repeat()
- 参数小数向下取整
- 参数为(-1,0]返回空
- 参数(-∞,-1] 报错
padStart(),padEnd() 字符串补全
- 如果省略第二个参数,则默认使用空格补全
模板字符串
- 反引号配合${}
- trim()消除模板字符串中的换行与空格
- 模板字符串可以嵌套
rest参数
- function f(a,…values){} values中包括所有传入没命名的参数,是一个数组
标签模板(tagged template)
模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能
- 模板字符串中如果有变量的话就不能简单的传入函数了,而是先处理为多个参数,再调用函数。
let a = 5;let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);
- “标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
(在往页面中添加用户输入的数据时可能是这种代码,造成页面结构破坏。
let message =
SaferHTML`<p>${sender} has sent you a message.</p>`;
function SaferHTML(templateData) {
let s = templateData[0];
for (let i = 1; i < arguments.length; i++) {
let arg = String(arguments[i]);
// Escape special characters in the substitution.
s += arg.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// Don't escape special characters in the template.
s += templateData[i];
}
return s;
}
let sender = '<script>alert("abc")</script>'; // 恶意代码
let message = SaferHTML`<p>${sender} has sent you a message.</p>`;
message
// <p><script>alert("abc")</script> has sent you a message.</p>
String.raw()
作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。
第一个参数为具有raw属性的对象,后面的参数不限数量。
String.raw({ raw: 'test' }, 0, 1, 2);
// 't0e1s2t'
String.raw`Hi\n${2+3}!`;
// 返回 "Hi\\n5!"
正则的扩展
- ES6允许这种写法
var regex = new RegExp(/xyz/, 'i');
new RegExp(/abc/ig, 'i').flags
// "i" 第二个参数会覆盖第一个参数的ig
正则方法
- match()
- replace()
- search()
- split()
u修饰符(unicode属性检验)
/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true
- 对于码点大于0xFFFF的 Unicode 字符,点字符不能识别,必须加上u修饰符。
- 大括号表示 Unicode 字符,需要加u修饰符
/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
- 加了之后量词能恢复正常
/????{2}/.test('????????') // false
/????{2}/u.test('????????') // true
- \S匹配所有非空白字符,也需要加u修饰符,才能识别大于0xFFFF的字符
/^\S$/.test('????') // false
/^\S$/u.test('????') // true
- 能识别相同字型,不会报错。
String.fromCodePoint('0x004B')
//"K"
String.fromCodePoint('0x212A')
//"K"
'K' === 'K'
//false
/[a-z]/i.test('\u212A') // false
/[a-z]/iu.test('\u212A') // true
/[a-z]/i.test('\u004B') // true
/[a-z]/iu.test('\u004B') // true
所有正则实例对象新增了unicode属性
表示是否设置了u修饰符
const r1 = /hello/;
const r2 = /hello/u;
r1.unicode // false
r2.unicode // true
y修饰符 (sticky属性检验)
粘连(sticky)修饰符
必须保证下一个匹配的位置紧接着上一个。
(必须和g联用,不然只返回第一个)
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
sticky属性
var r = /hello\d/y;
r.sticky // true
ES5的source属性与ES6的flags属性
// ES5 的 source 属性
// 返回正则表达式的正文
/abc/ig.source
// "abc"
// ES6 的 flags 属性
// 返回正则表达式的修饰符
/abc/ig.flags
// 'gi'
s修饰符(也称dotAll模式)
让正则表达式中 , . 能匹配 行终止符(\n,\r,行分隔符,段分隔符)
后行断言
// 都想要匹配x
//这是先行断言 /x(?=y)/或者/x(?!y)/ x需要在y前面
/\d+(?=%)/.exec('100% of US presidents have been male') // ["100"]
/\d+(?!%)/.exec('that’s all 44 of them') // ["44"]
//这是后行断言 /(?<=y)x/ 或者/(?<!y)x/ 需要回头看y (和检索的方向相反,所以较难实现,到ES2018才出来)
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill') // ["100"]
/(?<!\$)\d+/.exec('it’s is worth about €90') // ["90"]
- 后行断言从右往左进行贪婪模式
/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"]
/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]
- 因为后行断言从后往前扫描,所以\1 需要放在小括号定义的左边。(注:此处\1代表的就是小括号包裹的内容)
var RegExp = /^(123)(456)\2\1$/;这个正则表达式匹配到的字符串就是123456456123
/(?<=(o)d\1)r/.exec('hodor') // null
/(?<=\1d(o))r/.exec('hodor') // ["r", "o"]
Unicode 属性类 \p{…}和\P{…} (匹配希腊字母什么的方法)
允许正则表达式匹配符合 Unicode 某种属性的所有字符。
\p{Number} 匹配罗马数字 等等
const regexGreekSymbol = /\p{Script=Greek}/u;
regexGreekSymbol.test('π') // true
具体名匹配 在圆括号中加上 ?<组名> 然后groups属性中找
//传统
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj[1]; // 1999
const month = matchObj[2]; // 12
const day = matchObj[3]; // 31
//具体名匹配
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const matchObj = RE_DATE.exec('1999-12-31');
const year = matchObj.groups.year; // 1999
const month = matchObj.groups.month; // 12
const day = matchObj.groups.day; // 31
具体名匹配之后,将匹配生成的对象利用解构赋值能快速为变量赋值
let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
one // foo
two // bar
replace()时,可以用$<组名> 引用
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
'2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
// '02/01/2015'
正则表达式中引用"具体组匹配",使用\k<组名>
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
RE_TWICE.test('abc!abc') // true
RE_TWICE.test('abc!ab') // false
也可以照常使用\1 ,\2这种方法
string.matchAll(regex)
- 返回的是遍历器不是数组。
- 遍历器也可以用for…of循环取出。
- 因为返回的是遍历器,所以比较节省资源
- 遍历器很容易转化为数组
// 转为数组方法一
[...string.matchAll(regex)]
// 转为数组方法二
Array.from(string.matchAll(regex));
数值的扩展
都是为了促进模块化,减少全局方法
Number的扩展
- 八进制要使用前缀 0o
- Number转化0b,0o为十进制
- Number.isFinite(),Number.isNaN() ,先调用Number转化为数值,再判断,与全局不同
- Number.parseInt(),Number.parseFloat() 移植入Number
- Number.isInteger
- Number.EPSILON, 表示 1 与大于 1 的最小浮点数之间的差。
- Number.isSafeInteger,Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER
Math的扩展
- Math.trunc() ,返回整数,去小数点后
- Math.sign() 判断正负
- Math.cbrt() 计算立方根
- Math.clz32() 转成32位,看前面几个0
- Math.imul() 传两个参,返回32位乘积,避免很大的数直接相乘低位不精确
- Math.fround() 返回一个数的32位单精度浮点数形式。
- Math.hypot() 返回所有参数的平方和的平方根。
- Math.expm1() 等同于 Math.exp(x) - 1
- Math.log1p() 等同于 Math.log(1 + x)
- Math.log10() 范围以10为底的x的对数。
- Math.log2() 范围以2为底的x的对数
指数运算符(**)
- 右结合
// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512
- 与Math.pow在特别大的运算中会有细微差别
函数的扩展
- 参数变量默认声明,不能用let或const再次声明
- 不能有同名参数
- 参数默认值惰性求值
- 通常情况下,定义了默认值的参数,应该是函数的尾参数。
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 = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
作用域
函数声明时的独立作用域
var x = 1;
function f(x, y = x) {
console.log(y);}
f(2) // 2
//这种行为只在声明初始化的时候形成单独的作用域
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);}
f() // 1
-
f调用时, y=x形成一个独立的作用域,x么密友定义就指向外层的x。所以内部无法影响结果。
-
参数默认值可以给一个报错函数,不给就报错
function throwIfMissing() {
throw new Error('Missing parameter');}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;}
foo()
// Error: Missing parameter
- 可以将参数默认值设为undefined,表明这个参数是可以省略的。
rest 参数 (返回数组)
- 可以利用rest参数返回数组的特性, 直接对返回的数组进行排序等操作,而不是用Array.prototype.slice.call(arguments) 来操作。
- rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
- 函数的length属性,不包括 rest 参数。
严格模式
- 参数默认值、解构赋值、扩展运算符(rest)一旦使用,函数内部就不能显示设定严格模式,否则报错。 (在外部全局性的设定就可以,或者把函数包在一个无参数的立即执行函数中也行。)
- 因为严格模式需要适用于函数体和参数,但是只有运行到函数体才能知道是严格模式,所以产生bug。
name属性
返回函数名
- bind返回的函数,name属性值会加上bound前缀。
箭头函数
ES6 允许使用“箭头”(=>)定义函数。
- 如果返回对象,必须在外面加上圆括号,否则报错
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
- 只有一行语句,不需要返回值
let fn = () => void doesNotReturn();
- 不能作为构造函数,无法new
- 不可以使用arguments对象,可以用rest代替
- 不可以使用yield,不能用作generator函数
- this对象是定义时的对象,不是使用时的对象
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);}
var id = 21;
foo.call({ id: 42 });
// id: 42
因为箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。
7.箭头函数因为没有this,不能用call(), apply(),bind()改变this的指向
箭头函数不使用场合
- 定义函数的方法时不适用,因为this不指向函数而指向全局对象
- 动态this不应该使用。 (比如click事件中的this指向的是全局对象,而不是被点击对象)
双冒号运算符 ::
函数绑定运算符,用来取代call、apply、bind
document :: function ;
//等同于
function.bind(document)
document :: function (...values)
//等同于
function.apply(document,values)
尾调用(Tail Call)[尾调用优化]
某个函数的最后一步是调用另一个函数。
函数调用会在内存中形成‘调用记录’(也叫调用帧,call frame),保存调用位置和内部变量等信息。
如果上一个调用函数还没有结束,则会在下一个调用函数上方保存上一个函数的调用位置和变量信息,一步步则形成一个“调用栈”(call stack),这样会增加负担
如果尾调用的话则不会保存上一个调用函数的调用帧
尾递归
递归非常耗费内存,容易“栈溢出”(哈哈,stack overflow),而尾递归只存在一个调用帧,则不会“栈溢出”
//复杂度O(n)
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);}
factorial(5) // 120
//复杂度O(1)
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);}
factorial(5, 1) // 120
斐波那契数列利用尾递归
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
尾调用优化只在严格模式下开启!正常模式无效
因为在正常模式下,函数内部有两个变量可以跟踪函数的调用栈。
func.arguments:返回调用时函数的参数。
func.caller:返回调用当前函数的那个函数。
严格模式禁用这两个变量 。(不过不能在函数内部声明严格模式的同时用箭头函数哦!)
正常模式下进行尾递归优化
function tco(f) {
var value;
var active = false;
var accumulated = [];
return function accumulator() {
accumulated.push(arguments);
if (!active) {
active = true;
while (accumulated.length) {
value = f.apply(this, accumulated.shift());
}
active = false;
return value;
}
};}
var sum = tco(function(x, y) {
if (y > 0) {
return sum(x + 1, y - 1)
}
else {
return x
}});
sum(1, 100000)
// 100001
ES6允许函数的参数最后存在尾逗号
使得版本管理系统不会显示多余的变动
数组的扩展
扩展运算符( … )
和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>]
替代apply方法
用扩展运算符替代apply将数组转化为函数的参数。
// ES5 的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的写法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];f(...args);
扩展运算符的应用
- 复制数组
//ES5
const a1 = [1, 2];
const a2 = a1.concat();
//ES6
const a2 = [...a1]
或
const [...a2] = a1
- 合并数组
[...arr1,...arr2,...arr3]
和concat一样是浅拷贝,如果数组中的对象属性或方法改变,会被影响。
- 与解构赋值结合
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
- 字符串 (能识别四个字节的unicode字符)
[...'hello']
// [ "h", "e", "l", "l", "o" ]
'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3
- 扩展运算符需要iterator接口,如果没有可以自己设置,如
Number.prototype[Symbol.iterator] = function*() {
let i = 0;
let num = this.valueOf();
while (i < num) {
yield i++;
}}
console.log([...5]) // [0, 1, 2, 3, 4]
- Map,Set和Generator函数都可以使用扩展运算符
Map和Set具有Iterator接口。
Generator 运行后会返回遍历器对象。
//Map
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],]);
let arr = [...map.keys()]; // [1, 2, 3]
//Generator
const go = function*(){
yield 1;
yield 2;
yield 3;};
[...go()] // [1, 2, 3]
Array.from()
可以将类数组(array-like object)和可遍历(iterable)的对象(包括Set和Map)转化为数组。
可以接收第二个参数(函数),用来将每个元素进行处理。类似于map方法
主要的类数组:
- DOM操作返回的NodeList集合 (querySelectorAll等方法返回的集合)
- arguments对象
主要的iterable的对象
- 字符串
- Set
- Map
一个应用是将字符串转化为Unicode字符,能正确识别4个字节的unicode字符
function countSymbols(string) {
return Array.from(string).length;}
Array.of() — 用来代替Array()或new Array()
将一组值转化为数组,主要为了弥补构造函数Array()的不足
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
数组实例的copyWithin()
target(必需):从该位置开始替换数据。如果为负值,表示倒数。
start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
Array.prototype.copyWithin(target, start = 0, end = this.length)
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
数组实例的find() 和 findIndex()
都能发现NaN,弥补了数组indexOf方法的不足
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
find()
不符合返回undefined ,符合返回第一个
三个参数:当前的值、当前的位置、原数组
[1, 4, -5, -10,-100].find((value,index,arr) => value < 0)
// -5
findIndex()
不符合返回-1,符合返回第一个的位置
三个参数:当前的值、当前的位置、原数组
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;}) // 2
find和findIndex都能接受第二个参数,绑定回调函数中this的对象。
//重要!!!
function f(v){
return v > this.age;}let person = {name: 'John', age: 20};[10, 12, 26, 15].find(f, person); // 26
数组实例fill()
fill方法使用给定值,填充数组
第二第三个参数指定起始与结束位置
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
fill使用对象的话只填充对象的地址,所以是浅拷贝
数组实例 entries() keys() values()
用于遍历数组,返回一个遍历器对象,可以用for…of遍历
数组实例 includes()
返回布尔值,判定是否包含。
类似于字符串的Includes方法
第二个参数为起始位置
比indexOf的优点在于indexOf内部使用===判断,会误判NaN
[NaN].indexOf(NaN)
// -1
[NaN].includes(NaN)
// true
数组实例 flat(),flatMap()
flat
将嵌套的数组拉平,返回一个新数组.
只会拉平第一层,如果向多层需要传入层数,默认为1,可以传Infinity
[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]
flatMap
对原数组的每一个成员先执行类似于map的方法,然后flat()
只能展开一层数组
函数也接受3个参数,当前成员,当前位置,原数组
// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]
数组的空位
空位不是undefined ,是有值的
0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false
ES6,Array.from将空位转化成undefined。
扩展运算符也会将空位转为undefined
copyWithin()也会一起拷贝
fill视空位为正常的位置
for…of也会遍历空位
entries()、keys()、values()、find()和findIndex()会将空位处理成undefined。
对象的扩展
简写
- 允许对象中直接写变量。 此时属性名为变量名,属性值为变量值
function f(x, y) {
return {x, y};}
f(1, 2) // Object {x: 1, y: 2}
//CommonJS 模块输出时就适合简写
module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
};
- 方法简写
const o = {
method() {
return "Hello!";
}};
属性名表达式
对象的属性名可以用表达式
let propKey = 'foo';
let obj = {
[propKey]: true,
['a' + 'bc']: 123};
属性名表达式如果传入对象的话会把对象转为字符串 [object Object] !!!!!
方法的name属性
方法的name属性,返回方法名。 像函数的name属性返回函数名一样
有getter和setter的对象的特例
const obj = {
get foo() {},
set foo(x) {}};
obj.foo.name
// TypeError: Cannot read property 'name' of undefined
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');
descriptor.get.name // "get foo"
descriptor.set.name // "set foo"
Object.getOwnPropertyDescriptor
对象的属性的描述对象
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
以下操作会忽略enumerable为false的属性
- for…in
- Object.keys()
- JSON.stringify()
- Object.assign()
其中只有for…in会返回继承的属性,其他都忽略。
ES6规定,所有Class的原型的方法都是不可枚举的
Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable
// false
属性的遍历
能遍历对象属性的方法
方法名 | 遍历内容 |
---|---|
for…in | 对象自身和继承的可枚举属性(不含Symbol属性) |
Object.keys(obj) | 返回对象自身(不含继承)所有可枚举属性的键名(不含Symbol属性) |
Object.getOwnPropertyNames(obj) | 和Object.keys一样,但能遍历不可枚举的属性 |
Object.getOwnPropertySymbols(obj) | 返回对象自身所有Symbol属性的键名 |
Reflect.ownKeys(obj) | (最全)返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。 |
super
指向当前对象的原型对象
只能用在对象的方法中,其他地方都会报错
super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。
目前必须用对象方法的简写法super才能被识别
//这样简写
const obj = {
foo: 'world',
find() {
return super.foo;
}};
对象的解构赋值
扩展运算符的解构赋值不能继承原型对象的属性
扩展运算符用于提出对象的所有可遍历属性,拷贝到当前对象之中。
对象的扩展运算符等同于使用Object.assign()
完全拷贝一个对象(包括原型)
// 写法一(非浏览器环境不一定部署)
const clone1 = {
__proto__: Object.getPrototypeOf(obj),
...obj
};
// 写法二
const clone2 = Object.assign(
Object.create(Object.getPrototypeOf(obj)),
obj
);
// 写法三
const clone3 = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj))
对象的新增方法
Object.is() 同值相等算法
与“===”的不同之处在于补足了缺点。
认为+0和-0不同, 认为NaN与NaN相同
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.assign()
存在同名属性时,后面的属性会覆盖前面的
只拷贝对象属性和symbol属性,不拷贝继承也不拷贝不可枚举的属性
浅拷贝
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
需要可枚举属性
const v1 = 'abc';
const v2 = true;
const v3 = 10;
const obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
//可以用Object函数查看
Object(true) // {[[PrimitiveValue]]: true}
Object(10) // {[[PrimitiveValue]]: 10}
Object('abc') // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
可以处理数组,将数组视为对象,属性名0、1、2的对象
Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
Object.assign常见用法
- 为对象添加属性
class Point {
constructor(x,y){
Object.assign(this,{x,y})
}
}
- 为对象添加方法
Object.assign(SomeClass.prototype, {
someMethod(arg1, arg2) {
···
},
anotherMethod() {
···
}});
// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
···
};
SomeClass.prototype.anotherMethod = function () {
···
};
- 克隆对象
//先把origin中的原型属性赋予originProto的实例属性,然后根据originProto创建一个新的具有原型方法的target对象,传给assign。
function clone(origin) {
let originProto = Object.getPrototypeOf(origin);
return Object.assign(Object.create(originProto), origin);}
- 合并多个对象
const merge =
(...sources) => Object.assign({}, ...sources);
- 为属性指定默认值
由于是浅拷贝 所以值最好是简单类型,不要指向一个对象。
const DEFAULTS = {
logLevel: 0,
outputFormat: 'html'};
function processContent(options) {
options = Object.assign({}, DEFAULTS, options);
console.log(options);
// ...
}
Object.getOwnPropertyDescriptors()
返回对象属性(非继承属性)的描述对象
主要为了解决Object.assign()无法正确拷贝get和set属性
因为Object.assign方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法。
Object.assign不能正确拷贝get,set属性的解决方法 (使用Object.getOwnPropertyDescriptors()方法配合Object.defineProperties()方法)
const source = {
set foo(value) {
console.log(value);
}};
const target2 = {};
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
Object.getOwnPropertyDescriptor(target2, 'foo')
// { get: undefined,
// set: [Function: set foo],
// enumerable: true,
// configurable: true }
也可以克隆对象(浅拷贝)
//Object.create第二个参数需要是描述对象,用来赋予实例属性
const shallowClone = (obj) => Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj));
__proto__
调用的实际上是Object.prototype.__proto__
推荐用Object.setPrototypeOf() 和 Object.getPrototypeOf()
Object.entries的妙用
将Object转化为Map解构
将key和value以数组的形式输出
const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }
Object.fromEntries()
Object.entries的你操作
将Map转回为Object
Object.fromEntries([
['foo', 'bar'],
['baz', 42]])
// { foo: "bar", baz: 42 }
可以配合URLSearchParams对象,将查询字符转为对象。
Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }
Symbol
前六种数据类型
- undefined
- null
- Boolean
- String
- Number
- Object
每一个Symbol值都是不相等的
不能使用new命令。因为Symbol不是对象,而实一个原始类型的值
可以传入字符串,表示描述
Symbol 值不能与其他类型的值进行运算,会报错。
Symbol可以toString或String()转化为字符串
Symbol也可以转化为布尔值 Boolean()
作为属性名
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
不能用点运算符,因为点运算符后面总是字符串,会将其识别为字符串
再对象内部定义必须放在方括号之中,不然也被认为是字符串
不会被发现 | 会被发现 |
---|---|
for…in,for…of,Object.keys(),Object.getOwnPropertyNames()、JSON.stringify() | Object.getOwnPropertySymbols |
当然用 Reflect.ownKeys 可以获取所有的。
Symbol.for() ,Symbol.keyFor()
Symbol.for()
能够重复使用同一个值。
Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。
Symbol.for("bar") === Symbol.for("bar")
// true
Symbol("bar") === Symbol("bar")
// false
Symbol.keyFor()
//必须用Symbol.for()等级过的Symbol才能用Symbol.keyFor()寻找
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"
let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined
Symbol.for登记一定是全局环境的,在其他地方取到的是同一个值
模块的Singleton模式
Singleton 模式指的是调用一个类,任何时候返回的都是同一个实例。
但是返回的实例一般都放在顶层对象中,容易被在其他模块中修改。
防止这种情况可以用Symbol.for()
// mod.js
const FOO_KEY = Symbol.for('foo');
function A() {
this.foo = 'hello';}
if (!global[FOO_KEY]) {
global[FOO_KEY] = new A();}
module.exports = global[FOO_KEY];
// 引用
global[Symbol.for('foo')] = { foo: 'world' };
const a = require('./mod.js');
内置的Symbol值
1. Symbol.hasInstance (配合instanceof)
在调用instanceof时触发
class MyClass {
[Symbol.hasInstance](foo) {
return foo instanceof Array;
}}
[1, 2, 3] instanceof new MyClass() // true
2. Symbol.isConcatSpreadable (配合concat)
设置后决定用concat方法时是否可以展开
let arr1 = ['c', 'd'];['a', 'b'].concat(arr1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr1[Symbol.isConcatSpreadable] // undefined
let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;['a', 'b'].concat(arr2, 'e') // ['a', 'b', ['c','d'], 'e']
3. Symbol.species
解决 instanceof 有多个父类的问题
调用该属性指定的构造函数
class MyArray extends Array {
static get [Symbol.species]() { return Array; }}
const a = new MyArray();
const b = a.map(x => x);
b instanceof MyArray // false
b instanceof Array // true
4. Symbol.match (配合match方法)
执行str.match(obj)方法时调用
String.prototype.match(regexp)
// 等同于
regexp[Symbol.match](this)
class MyMatcher {
[Symbol.match](string) {
return 'hello world'.indexOf(string);
}}
'e'.match(new MyMatcher()) // 1
5.Symbol.replace (调用replace时调用)
const x = {};
x[Symbol.replace] = (...s) => console.log(s);
'Hello'.replace(x, 'World') // ["Hello", "World"]
6.Symbol.search (配合search方法)
String.prototype.search(regexp)
// 等同于
regexp[Symbol.search](this)
class MySearch {
constructor(value) {
this.value = value;
}
[Symbol.search](string) {
return string.indexOf(this.value);
}}'foobar'.search(new MySearch('foo')) // 0
7.Symbol.split (配合split方法)
class MySplitter {
constructor(value) {
this.value = value;
}
[Symbol.split](string) {
let index = string.indexOf(this.value);
if (index === -1) {
return string;
}
return [
string.substr(0, index),
string.substr(index + this.value.length)
];
}}
'foobar'.split(new MySplitter('foo'))
// ['', 'bar']
'foobar'.split(new MySplitter('bar'))
// ['foo', '']
'foobar'.split(new MySplitter('baz'))
// 'foobar'
8.Symbol.iterator (配合for…of循环)
对象的Symbol.iterator属性,指向对象的默认遍历器方法
const myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;};
[...myIterable] // [1, 2, 3]
class Collection {
*[Symbol.iterator]() {
let i = 0;
while(this[i] !== undefined) {
yield this[i];
++i;
}
}}
let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;
for(let value of myCollection) {
console.log(value);}
// 1
// 2
9.Symbol.toPrimitive (对象被转为原始类型时调用)
有三种模式
Number:该场合需要转成数值
String:该场合需要转成字符串
Default:该场合可以转成数值,也可以转成字符串
let obj = {
[Symbol.toPrimitive](hint) {
switch (hint) {
case 'number':
return 123;
case 'string':
return 'str';
case 'default':
return 'default';
default:
throw new Error();
}
}};
2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'
10.Symbol.toStringTag (调用toString方法时运用)
可以定制[object Object]或[object Array]中object后面的那个字符串。
// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
// 例二
class Collection {
get [Symbol.toStringTag]() {
return 'xxx';
}}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"
11.Symbol.unscopables (指定使用with关键字时哪些会被排除)
// 没有 unscopables 时
class MyClass {
foo() { return 1; }}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 1
}
// 有 unscopables 时
class MyClass {
foo() { return 1; }
get [Symbol.unscopables]() {
return { foo: true };
}}
var foo = function () { return 2; };
with (MyClass.prototype) {
foo(); // 2
}
Set和Map数据解构
Set
构造函数
类似于数组,但没有重复的值
可接受一个数组(或具有iterable接口的数据结构)
用的是“Same-value-zero equality”,认为NaN等于自身
Set没有键名 ,只有键值(或者说键名和键值是同一个值)
//比较实用的案例
const set = new Set(document.querySelectorAll('div'));
set.size // 56
//用于去除字符串中的重复字符
两个对象总是不相等的
let set = new Set();
set.add({});set.size // 1
set.add({});set.size // 2
Set实例的四个方法
- add(value):添加某个值,返回 Set 结构本身。
- delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
- has(value):返回一个布尔值,表示该值是否为Set的成员。
- clear():清除所有成员,没有返回值。
Array.from可以将Set转化为数组
Array.from(new Set(array))
//可以去除数组重复成员
//这是直接将Set转化为数组的方法
[...set]
Set实例的四个遍历方法
Set的遍历顺序就是插入顺序
- keys():返回键名的遍历器
- values():返回键值的遍历器 (和keys方法完全一样,因为set没有键名)
- entries():返回键值对的遍历器 ([key,value]两个值完全一样)
- forEach():使用回调函数遍历每个成员
实现并集,交集和差集
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}
WebSet
WeakSet的成员只能是对象
WeakSet的参数可以接受数组,类数组(具有Iterable接口的对象都行)
弱引用 (垃圾回收极致不考虑WeakSet对对象的引用)
因为弱引用的原因,内部成员运行前后可能个数不一样,所以不可遍历
方法
- WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
- WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
- WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
Map
各种类型的值都可以当作键
Map可以接受数组(以及任何具有Iterator接口的、双元素数组的数据解构)作为参数,实际算法如下
接受的数组需要是[[1,2],[3,4]] 这样的双元素数组
const items = [
['name', '张三'],
['title', 'Author']];
const map = new Map();
items.forEach(
([key, value]) => map.set(key, value));
只有对同一个对象的引用,Map 结构才将其视为同一个键。
// 这两个值内存地址不一样
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
0和-0是一个键
NaN 是同一个键
属性与方法
- size
- set(key,value) 因为返回的是当前Map对象,可以链式写法
- get(key)
- has(key)
- delete(key)
- clear()
遍历方法
Map的遍历顺序是插入顺序
- keys()
- values()
- entries()
- forEach()
Map解构的默认遍历器接口是entries方法
扩展运算符能快速将Map转化为数组,然后运用数组的map和filter方法
Map自身也有forEach方法,可接受第二个参数,绑定this
与其他数据解构的互相转换
转换目标 | 方法 | 逆方法 |
---|---|---|
数组 | 扩展运算符(…) | 传入Map构造函数 |
对象 | strMapToObj(方法见下面) | objToStrMap |
JSON | strMapToJson或mapToArrayJson | jsonToStrMap |
//如果键名非字符串,会被转化为字符串,会有损
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;}
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;}
//Map键名都是字符串,可以转化为对象JSON
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));}
//Map 的键名有非字符串,这时可以选择转为数组 JSON。
function mapToArrayJson(map) {
return JSON.stringify([...map]);}
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));}
WeakMap (典型场合,以DOM节点作为键名)
只接受对象作为键名
弱引用 ( 弱引用的只是键名,不是键值。键值是正常引用)
const wm = new WeakMap();let key = {};let obj = {foo: 1};
wm.set(key, obj);
obj = null;
wm.get(key)
// Object {foo: 1}
也没有遍历操作和size属性
只有四个方法,get(),set(),has(),delete()
适合用于部署私有属性,删除实例后,就会随之消失,不会造成内存泄漏
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
if (counter < 1) return;
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}}
const c = new Countdown(2, () => console.log('DONE'));
c.dec()
c.dec()
// DONE
Proxy
用于修改操作的默认行为,是一种"元编程"(meta programming)
在目标对象前进行拦截,可以对外节访问过滤和改写
ES6提供的Proxy构造函数
//写法
var proxy = new Proxy(target, handler);
//例子
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}});
proxy.time // 35
proxy.name // 35
proxy.title // 35
- 可以将Proxy对象设置成对象的属性
var object = { proxy: new Proxy(target, handler) };
- 可以将Proxy对象设置成原型对象
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}});
let obj = Object.create(proxy);
obj.time // 35
//访问不存在的对象属性的话会进入原型对象中寻找。
//可以拦截多个操作
var handler = {
get: function(target, name) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
apply: function(target, thisBinding, args) {
return args[0];
},
construct: function(target, args) {
return {value: args[1]};
}};
var fproxy = new Proxy(function(x, y) {
return x + y;}, handler);
fproxy(1, 2) // 1
new fproxy(1, 2) // {value: 2}
fproxy.prototype === Object.prototype // true
fproxy.foo === "Hello, foo" // true
Proxy支持的handler拦截操作
方法名 | 作用 |
---|---|
get(target, propKey, receiver) | 拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。 |
set(target, propKey, value, receiver) | 拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。 需要返回true |
has(target, propKey) | 拦截propKey in proxy的操作,返回一个布尔值。(对for…in不生效) |
deleteProperty(target, propKey) | 拦截delete proxy[propKey]的操作,返回一个布尔值。 |
ownKeys(target) | 拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。 返回的数组只能是字符串或Symbol值 。 |
getOwnPropertyDescriptor(target, propKey) | 拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。 |
defineProperty(target, propKey, propDesc) | 拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。 |
preventExtensions(target) | 拦截Object.preventExtensions(proxy),返回一个布尔值。 |
getPrototypeOf(target) | 拦截Object.getPrototypeOf(proxy),返回一个对象。 |
isExtensible(target) | 拦截Object.isExtensible(proxy),返回一个布尔值。 |
setPrototypeOf(target, proto) | 拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 |
apply(target, ctxobj, args) | 拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。 |
construct(target, args) | 拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。必须返回一个对象 |
receiver指原始读操作的那个对象 target指传入的对象
const proxy = new Proxy({}, {
get: function(target, property, receiver) {
return receiver;
}});
const d = Object.create(proxy);
d.a === d // true
如果属性不可配置且不可写,proxy读此属性会报错
严格模式下 set代理需要返回true,否则会报错
如果obj对象禁止扩展,则用has拦截就会报错
var obj = { a: 10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop) {
return false;
}});
'a' in p // TypeError is thrown
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
一种情况是,目标对象不允许直接访问,必须代理访问,访问结束就要收回代理权.
this问题
使用proxy代理后 this指向的就是proxy而不是target
用bind绑定原始对象就好了
Reflect
- 将Object的一些方法移植到Reflect上
- 修改一些Object的方法的返回结果,使其更合理
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
//不抛出错误 ,而是返回false
- 让Object操作变为函数行为
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
- Reflect对象的方法与Proxy对象的方法一一对应
**拦截target对象的方法 ,采用Reflect方法,确保完成原有的行为后,再部属别的功能 **
Proxy(target, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target,name, value, receiver);
if (success) {
console.log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}});
另一个例子
var loggedObj = new Proxy(obj, {
get(target, name) {
console.log('get', target, name);
return Reflect.get(target, name);
},
deleteProperty(target, name) {
console.log('delete' + name);
return Reflect.deleteProperty(target, name);
},
has(target, name) {
console.log('has' + name);
return Reflect.has(target, name);
}});
Reflect的13个静态方法
Reflect.apply(target, thisArg, args)
//等于 Function.prototype.apply.call(func, thisArg, args)
Reflect.construct(target, args)
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
Reflect.defineProperty(target, name, desc)
Reflect.deleteProperty(target, name)
Reflect.has(target,name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target,name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
使用Proxy实现观察者模式
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});
function set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
Promise
异步编程的一种解决方案
一个保存着某个未来菜会结束的事件的容器(通常是指异步操作)
只有异步操作的结果才能决定Promise的对象状态 所以命名Promise
Promise对象有以下两个特点
特点 | 描述 |
---|---|
对象的状态不受外界影响 | 有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。 |
一旦状态改变,就不会再变 | Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。 |
Promise的缺点:
- 无法取消Promise,一旦新建就立即执行,中途无法取消
- 如果不设置回调函数,内部错误不会反应到外部
- 处于pending时,无法得知进展
所以如果某些事件不断反复发生,Stream模式比Promise好。
Generator
Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
两个特征:
function
关键字与函数名之间有一个星号- 函数体内部使用
yield
表达式,定义不同的内部状态
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
yield表达式
由于 Generator 函数返回的遍历器对象,只有调用
next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
next的运行逻辑
- 遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。- 下一次调用
next
方法时,再继续往下执行,直到遇到下一个yield
表达式。- 如果没有再遇到新的
yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。- 如果该函数没有
return
语句,则返回的对象的value
属性值为undefined
。需要注意的是,
yield
表达式后面的表达式,只有当调用next
方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
- yield 与return的区别在于每次遇到
yield
,函数暂停执行,下一次再从该位置继续向后执行,而return
语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return
语句,但是可以执行多次(或者说多个)yield
表达式。 - Generator 函数可以不用
yield
表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
console.log('执行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
//此函数只有在调用next方法时才会执行
- yield表达式只能在Generator函数中使用
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
a.forEach(function (item) {
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
});
};
for (var f of flat(arr)){
console.log(f);
}
//这里在forEach的方法里使用了,forEach的参数是一个普通函数,用了yield就会报错
//所以应该改成for循环
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
var length = a.length;
for (var i = 0; i < length; i++) {
var item = a[i];
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
}
};
for (var f of flat(arr)) {
console.log(f);
}
// 1, 2, 3, 4, 5, 6
- yield表达式如果放在另一个表达式中,必须放在圆括号里面
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
- yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
与iterator的关系
任意一个对象的
Symbol.iterator
方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的
Symbol.iterator
属性,从而使得该对象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有
Symbol.iterator
属性,执行后返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
next方法的参数
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
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 }
g.next(true) // { value: 0, done: false }
通过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 }
//在这个方法中不停的输入参数改变yield的返回值
// 5+12*2+13 = 42
for…of循环
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
//利用Generator函数和for...of循环,实现斐波那契数列
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}
//可以为任意对象用Generator增加一个接口,然后用for...of遍历
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
//方法一:(较麻烦)
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
//方法二:(直接加到Symbol.iterator属性上)
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
出了for…of以外,扩展运算符,解构赋值和Array.from()方法调用的其实都是遍历器接口。
这意味着他们都能将Generator函数返回的对象作为参数
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 扩展运算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循环
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
Generator.prototype.throw()
Generator函数返回的对象都有throw方法,可以在Generator外部抛出错误,然而却能在Generator函数内部捕获
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
//内部只捕获一次
-
throw方法可以接受一个参数,这个参数会被Generator函数体里的catch方法接收,所以抛出Error对象比较好
-
遍历器对象用throw方法抛出的错误要被内部捕获,必须至少执行过一次
next
方法。因为执行一次next方法才启动执行Generator函数的内部代码 -
throw
方法被捕获以后,会附带执行下一条yield
表达式。也就是说,会附带执行一次next
方法。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b
g.next() // c
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个
yield
表达式,可以只用一个try...catch
代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次catch
语句就可以了。
函数体外捕获函数内部的错误
Generator 函数体内抛出的错误,也可以被函数体外的
catch
捕获。
function* foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err);
}
Generator.prototype.return()
Generator 函数返回的遍历器对象除了next,throw还有一个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 函数内部有
try...finally
代码块,且正在执行try
代码块,那么return
方法会推迟到finally
代码块执行完再执行。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
next()、throw()、return() 的共同点
它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换
yield
表达式。
yield* 表达式
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';
}
-
如果
yield
表达式后面跟的是一个遍历器对象,需要在yield
表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*
表达式。 -
yield*
后面的 Generator 函数(没有return
语句时),等同于在 Generator 函数内部,部署一个for...of
循环。
// 如果`yield*`后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
//不加星号返回的是整个数组
//任何数据结构只要有iterator接口,都可以被yield* 遍历
- 如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
- yield* 的另外一个例子
function* genFuncWithReturn() {
yield 'a';
yield 'b';
console.log('test')
return 'The result';
}
function* logReturned(genObj) {
let result = yield* genObj;
console.log(result);
}
[...logReturned(genFuncWithReturn())]
// test
// The result
// 值为 [ 'a', 'b' ]
-
yield*
命令可以很方便地取出嵌套数组的所有成员。
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
作为对象属性的Generator函数
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
//等价于
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
Generator 函数的this
Generator返回的总是遍历器对象,而不是
this
对象。
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
既可以用
next
方法,又可以获得正常的this
的变通的方法
- 生成一个空对象
- 使用
call
方法绑定 Generator 函数内部的this
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
- 如果想要将两个统一的话可以将obj换成F.prototype
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
- 如果再将F改成构造函数,就可以new了
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
Generator与状态机
反复改变状态可以用Generator实现
var clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};
半协程的Generator函数
Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用
yield
表达式交换控制权。
Generator函数的应用
- 异步操作的同步化表达
-
Generator 函数的暂停执行的效果,意味着可以把异步操作写在
yield
表达式里面,等到调用next
方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield
表达式下面,反正要等到调用next
方法时再执行。function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } var loader = loadUI(); // 加载UI loader.next() // 卸载UI loader.next()
-
Generator函数与Ajax的结合
function* main() { var result = yield request("http://some.url"); var resp = JSON.parse(result); console.log(resp.value); } function request(url) { makeAjaxCall(url, function(response){ it.next(response); }); } var it = main(); it.next()
-
Generator函数与异步操作(读取文件结合)
function* numbers() { let file = new FileReader("numbers.txt"); try { while(!file.eof) { yield parseInt(file.readLine(), 10); } } finally { file.close(); } }
- 控制流管理
-
多步操作
function* longRunningTask(value1) { try { var value2 = yield step1(value1); var value3 = yield step2(value2); var value4 = yield step3(value3); var value5 = yield step4(value4); // Do something with value4 } catch (e) { // Handle any error from step1 through step4 } } //然后使用一个函数按次序执行所有步骤 scheduler(longRunningTask(initialValue)); function scheduler(task) { var taskObj = task.next(task.value); // 如果Generator函数未结束,就继续调用 if (!taskObj.done) { task.value = taskObj.value scheduler(task); } }
- 部署Iterator接口
利用 Generator 函数,可以在任意对象上部署 Iterator 接口。
- 作为数据结构
Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
for (task of doStuff()) {
// task是一个函数,可以像回调函数那样使用它
}
Generator函数的异步应用
Node 约定,回调函数的第一个参数,必须是错误对象
err
(如果没有错误,该参数就是null
)因为执行分成两段,第一段执行完以后,任务所在的上下文环境就已经结束了。在这以后抛出的错误,原来的上下文环境已经无法捕捉,只能当作参数,传入第二段。
- Promise 的写法只是回调函数的改进,使用
then
方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。
协程(coroutine)
协程有点像函数,又有点像线程。它的运行流程大致如下。
- 第一步,协程
A
开始执行。- 第二步,协程
A
执行到一半,进入暂停,执行权转移到协程B
。- 第三步,(一段时间后)协程
B
交还执行权。- 第四步,协程
A
恢复执行。
协程的Generator函数实现
Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用
yield
语句注明。
Generator 函数的数据交换和错误处理
Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:
- 函数体内外的数据交换
- 错误处理机制。
-
next
返回值的 value 属性,是 Generator 函数向外输出数据;next
方法还可以接受参数,向 Generator 函数体内输入数据。 - Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。
异步任务的封装
真实案例
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
执行这段代码的方法如下:
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。
Thunk 函数
Thunk 函数是自动执行 Generator 函数的一种方法。是传名调用。
“传值调用”(call by value):即在进入函数体之前,就计算x + 5
的值(等于 6),再将这个值传入函数f
。C 语言就采用这种策略。
“传名调用”(call by name):即直接将表达式x + 5
传入函数体,只在用到它的时候求值。Haskell 语言采用这种策略。
编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。
//Thunk 函数转换器
// ES5版本
var Thunk = function(fn){
return function (){
var args = Array.prototype.slice.call(arguments);
return function (callback){
args.push(callback);
return fn.apply(this, args);
}
};
};
// ES6版本
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
使用上面的转换器,生成fs.readFile
的 Thunk 函数。
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
例子:
function f(a, cb) {
cb(a);
}
const ft = Thunk(f);
ft(1)(console.log) // 1
Thunkify 模块
var thunkify = require('thunkify');
var fs = require('fs');
var read = thunkify(fs.readFile);
read('package.json')(function(err, str){
// ...
});
源码:
function thunkify(fn) {
return function() {
var args = new Array(arguments.length);
var ctx = this;
for (var i = 0; i < args.length; ++i) {
args[i] = arguments[i];
}
return function (done) {
var called;
args.push(function () {
if (called) return;
called = true;
done.apply(null, arguments);
});
try {
fn.apply(ctx, args);
} catch (err) {
done(err);
}
}
}
};
Thunk函数的自动流程管理
Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
function* g() {
// ...
}
run(g);
async函数
Generator函数的语法糖
async
函数就是将 Generator 函数的星号(*
)替换成async
,将yield
替换成await
,仅此而已。
async
函数对 Generator 函数的改进,体现在以下四点
- Generator 函数的执行必须靠执行器,所以才有了
co
模块,而async
函数自带执行器 -
async
和await
,比起星号和yield
,语义更清楚了。 -
co
模块约定,yield
命令后面只能是 Thunk 函数或 Promise 对象,而async
函数的await
命令后面,可以是 Promise 对象和原始类型的值 -
async
函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。可以用then
方法指定下一步的操作。
async
函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
async 函数有多种使用形式。
// 函数声明
async function foo() {}
// 函数表达式
const foo = async function () {};
// 对象的方法
let obj = { async foo() {} };
obj.foo().then(...)
// Class 的方法
class Storage {
constructor() {
this.cachePromise = caches.open('avatars');
}
async getAvatar(name) {
const cache = await this.cachePromise;
return cache.match(`/avatars/${name}.jpg`);
}
}
const storage = new Storage();
storage.getAvatar('jake').then(…);
// 箭头函数
const foo = async () => {};
返回Promise对象
async
函数内部return
语句返回的值,会成为then
方法回调函数的参数。
async function f() {
return 'hello world';
}
f().then(v => console.log(v))
// "hello world"
async
函数内部抛出错误,会导致返回的 Promise 对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到。
async function f() {
throw new Error('出错了');
}
f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出错了
Promise 对象的状态变化
只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"
await 命令
如果await后面不是Promise对象就直接返回对应的值
async function f() {
// 等同于
// return 123;
return await 123;
}
f().then(v => console.log(v))
// 123
await
命令后面是一个thenable
对象(即定义then
方法的对象),那么await
会将其等同于 Promise 对象
class Sleep {
constructor(timeout) {
this.timeout = timeout;
}
then(resolve, reject) {
const startTime = Date.now();
setTimeout(
() => resolve(Date.now() - startTime),
this.timeout
);
}
}
(async () => {
const actualTime = await new Sleep(1000);
console.log(actualTime);
})();
//这个实例不是 Promise 对象,但是因为定义了then方法,await会将其视为Promise处理。
任何一个await
语句后面的 Promise 对象变为reject
状态,那么整个async
函数都会中断执行。
可以将第一个await
放在try...catch
结构里面,这样不管这个异步操作是否成功,第二个await
都会执行。
另一种方法是await
后面的 Promise 对象再跟一个catch
方法,处理前面可能出现的错误。
错误处理
- await后面的Promise对象可能是reject,所以最好放在try…catch命令中
async function myFunction() {
try {
await somethingThatReturnsAPromise();
} catch (err) {
console.log(err);
}
}
// 另一种写法
async function myFunction() {
await somethingThatReturnsAPromise()
.catch(function (err) {
console.log(err);
});
}
- 多个
await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();
//改成
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
这样同时触发可以缩短时间
-
await必须在async函数中
-
async 函数可以保留运行堆栈。
const a = () => { b().then(() => c()); };
等到
b()
运行结束,可能a()
早就运行结束了,b()
所在的上下文环境已经消失了。如果b()
或c()
报错,错误堆栈将不包括a()
。现在将这个例子改成
async
函数。const a = async () => { await b(); c(); }; //b()运行的时候,a()是暂停执行,上下文环境都保存着。一旦b()或c(),错误堆栈将包括a()。
async函数的实现原理
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
Class
-
类的所有方法都定义在类的
prototype
属性上面。 -
由于类的方法都定义在
prototype
对象上面,所以类的新方法可以添加在prototype
对象上面。Object.assign
方法可以很方便地一次向类添加多个方法。 -
prototype
对象的constructor
属性,直接指向“类”的本身 -
类的内部所有定义的方法,都是不可枚举的(non-enumerable),ES5可以
-
一个类必须有
constructor
方法,如果没有显式定义,一个空的constructor
方法会被默认添加。 -
constructor
方法默认返回实例对象(即this
),完全可以指定返回另外一个对象。 -
get和set函数设置在原型对象Descriptor对象上
-
类的属性名可以用表达式
-
注意点:
- 类和模块的内部默认就是严格模式
2. 不存在变量提升
3. 有name属性
-
方法前加* 就表示该方法是Generator函数
-
类的方法内部如果含有
this
,它默认指向类的实例。一旦单独使用该方法,很可能报错。解决方法: // 方法1:在构造方法中绑定this,这样就不会找不到print方法了。 class Logger { constructor() { this.printName = this.printName.bind(this); } // ... } //方法2:使用箭头函数法 class Logger { constructor() { this.printName = (name = 'there') => { this.print(`Hello ${name}`); }; } // ... } //方法3:使用Proxy function selfish (target) { const cache = new WeakMap(); const handler = { get (target, key) { const value = Reflect.get(target, key); if (typeof value !== 'function') { return value; } if (!cache.has(value)) { cache.set(value, value.bind(target)); } return cache.get(value); } }; const proxy = new Proxy(target, handler); return proxy; } const logger = selfish(new Logger());
静态方法
加上
static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。- 如果静态方法包含
this
关键字,这个this
指的是类,而不是实例 - 父类的静态方法,可以被子类继承。
静态属性
因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。
只能在外部定义。
私有方法和属性
ES6 不提供,只能通过变通方法模拟实现
用symbol定义属性名比较好
new.target属性
如果构造函数不是通过
new
命令调用的,new.target
会返回undefined
这个属性可以用来确定构造函数是怎么调用的。
-
子类继承父类时,
new.target
会返回子类。 -
利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。
class Shape { constructor() { if (new.target === Shape) { throw new Error('本类不能实例化'); } } } class Rectangle extends Shape { constructor(length, width) { super(); // ... } } var x = new Shape(); // 报错 var y = new Rectangle(3, 4); // 正确
Class的继承
Class 可以通过
extends
关键字实现继承
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
-
super
关键字,它在这里表示父类的构造函数,用来新建父类的this
对象 - 子类必须在
constructor
方法中调用super
方法,否则新建实例时会报错。
ES5 的继承,实质是先创造子类的实例对象
this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this
上面(所以必须先调用super
方法),然后再用子类的构造函数修改this
。
//如果子类没有添加constructor方法
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
-
Object.getPrototypeOf
方法可以用来从子类上获取父类。
super关键字
有两种使用情况
-
作为函数
子类的构造函数必须执行一次
super
函数。super()
,代表调用父类的构造函数。
class A {}
class B extends A {
constructor() {
super();
}
}
//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();
-
子类普通方法中通过
super
调用父类的方法时,方法内部的this
指向当前的子类实例。class A { constructor() { this.x = 1; } print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } m() { super.print(); } } let b = new B(); b.m() // 2 //实际上执行的是super.print.call(this)
-
由于
this
指向子类实例,所以如果通过super
对某个属性赋值,这时super
就是this
,赋值的属性会变成子类实例的属性。class A { constructor() { this.x = 1; } } class B extends A { constructor() { super(); this.x = 2; super.x = 3; console.log(super.x); // undefined console.log(this.x); // 3 } } let b = new B();
-
如果
super
作为对象,用在静态方法之中,这时super
将指向父类,而不是父类的原型对象。class Parent { static myMethod(msg) { console.log('static', msg); } myMethod(msg) { console.log('instance', msg); } } class Child extends Parent { static myMethod(msg) { super.myMethod(msg); } myMethod(msg) { super.myMethod(msg); } } Child.myMethod(1); // static 1 var child = new Child(); child.myMethod(2); // instance 2
-
在子类的静态方法中通过
super
调用父类的方法时,方法内部的this
指向当前的子类,而不是子类的实例。class A { constructor() { this.x = 1; } static print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } static m() { super.print(); } } B.x = 3; B.m() // 3
-
使用
super
的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。class A {} class B extends A { constructor() { super(); console.log(super); // 报错 } } //像这样用super.valueOf()救恩表明super是一个对象,就不会报错 class A {} class B extends A { constructor() { super(); console.log(super.valueOf() instanceof B); // true } } let b = new B();
-
类的prototype和__proto__属性
类同时有
prototype
属性和__proto__
属性,因此同时存在两条继承链。
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2)子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
class A {
}
class B extends A {
}
B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true
原因是类的继承是用Object.setPrototypeOf实现的
Object.setPrototypeOf = function (obj, proto) {
obj.__proto__ = proto;
return obj;
}
class A {
}
class B {
}
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);
const b = new B();
实例的 proto 属性
子类实例的
__proto__
属性的__proto__
属性,指向父类实例的__proto__
属性。
var p1 = new Point(2, 3);
var p2 = new ColorPoint(2, 3, 'red');
p2.__proto__ === p1.__proto__ // false
p2.__proto__.__proto__ === p1.__proto__ // true
通过子类实例的__proto__.__proto__
属性,可以修改父类实例的行为。
p2.__proto__.__proto__.printName = function () {
console.log('Ha');
};
p1.printName() // "Ha"
原生构造函数的继承
原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
ES5中,子类无法获得原生构造函数的内部属性,通过
Array.apply()
或者分配给原型对象都不行。原生构造函数会忽略apply
方法传入的this
,也就是说,原生构造函数的this
无法绑定,导致拿不到内部属性。
ES6 允许继承原生构造函数定义子类
class MyArray extends Array {
constructor(...args) {
super(...args);
}
}
var arr = new MyArray();
arr[0] = 12;
arr.length // 1
arr.length = 0;
arr[0] // undefined
Mixin 模式的实现
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。
简单实现:
const a = {
a: 'a'
};
const b = {
b: 'b'
};
const c = {...a, ...b}; // {a: 'a', b: 'b'}
更完整的实现:
function mix(...mixins) {
class Mix {}
for (let mixin of mixins) {
copyProperties(Mix.prototype, mixin); // 拷贝实例属性
copyProperties(Mix.prototype, Reflect.getPrototypeOf(mixin)); // 拷贝原型属性
}
return Mix;
}
function copyProperties(target, source) {
for (let key of Reflect.ownKeys(source)) {
if ( key !== "constructor"
&& key !== "prototype"
&& key !== "name"
) {
let desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(target, key, desc);
}
}
}
上面代码的mix
函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。
class DistributedEdit extends mix(Loggable, Serializable) {
// ...
}
Module
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器
ES6 模块不是对象,而是通过
export
命令显式指定输出的代码,再通过import
命令输入。
ES6模块的好处
- 不再需要
UMD
模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。 - 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者
navigator
对象的属性。 - 不再需要对象作为命名空间(比如
Math
对象),未来这些功能可以通过模块提供。
严格模式
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上
"use strict";
严格模式主要有以下限制。
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with
语句 - 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop
,会报错,只能删除属性delete global[prop]
-
eval
不会在它的外层作用域引入变量 -
eval
和arguments
不能被重新赋值 -
arguments
不会自动反映函数参数的变化 - 不能使用
arguments.callee
- 不能使用
arguments.caller
- 禁止
this
指向全局对象 - 不能使用
fn.caller
和fn.arguments
获取函数调用的堆栈 - 增加了保留字(比如
protected
、static
和interface
)
export命令
export
命令用于规定模块的对外接口export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。
export命令的写法:
//写法1:
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
//写法2:
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
可以用as关键字重命名
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
export必须提供对外的接口,直接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);
//0.5秒后输出的值改变
export必须位于顶层对象
import命令
-
import
命令输入的变量都是只读的,因为它的本质是输入接口。 -
如果
a
是一个对象,改写a
的属性是允许的!(不推荐) -
import
后面的from
指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js
后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。 -
import
命令具有提升效果,会提升到整个模块的头部,首先执行。 -
由于
import
是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。 -
import
语句会执行所加载的模块import 'lodash'; //会执行,但是不输入任何值 import 'lodash'; import 'lodash'; //只执行一次
export default命令
使用
import
命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。export default可以指定默认输出,如果指定函数名在外部也视为匿名函数
export default function () {
console.log('foo');
}
//import的时候就可以指定任意名字
import customName from './export-default';
customName(); // 'foo'
本质上,
export default
就是输出一个叫做default
的变量或方法,然后系统允许你为它取任意名字。
export {add as default};
// 等同于
// export default add;
import { default as foo } from 'modules';
// 等同于
// import foo from 'modules';
- export default 只是输出一个叫做
default
的变量,所以它后面不能跟变量声明语句。 - 和export不一样 export default后面就可以直接跟值。
export和import的复合写法
import可以和export合并
export { foo, bar } from 'my_module';
// 可以简单理解为
import { foo, bar } from 'my_module';
export { foo, bar };
- 写成一行以后,
foo
和bar
实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo
和bar
。
模块的继承
-
export *
命令会忽略circle
模块的default
方法
跨模块常量
const
声明的常量只在当前代码块有效。
以下方法可以跨模块使用
// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;
// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3
// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
可以专门简一个目录,放常量
// constants/db.js
export const db = {
url: 'http://my.couchdbserver.local:5984',
admin_username: 'admin',
admin_password: 'admin password'
};
// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
//在index中合并
// constants/index.js
export {db} from './db';
export {users} from './users';
//使用的时候直接加载index
// script.js
import {db, users} from './constants/index';
Module 的加载实现
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到
<script>
标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。
<script src="path/to/myModule.js" defer></script>
<script src="path/to/myModule.js" async></script>
渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。
defer
与async
的区别:
defer
要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;
async
一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
defer
是“渲染完再执行”,async
是“下载完就执行”。另外,如果有多个defer
脚本,会按照它们在页面出现的顺序加载,而多个async
脚本是不能保证加载顺序的。
ES6模块加载规则
浏览器加载 ES6 模块,也使用<script>
标签,但是要加入type="module"
属性。
<script type="module" src="./foo.js"></script>
浏览器对于带有
type="module"
的<script>
,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>
标签的defer
属性。
<script type="module" src="./foo.js"></script>
<!-- 等同于 -->
<script type="module" src="./foo.js" defer></script>
如果网页有多个<script type="module">
,它们会按照在页面出现的顺序依次执行。
如果使用了sasync属性,就是只要加载完成就执行
<script type="module" src="./foo.js" async></script>
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明
use strict
。 - 模块之中,可以使用
import
命令加载其他模块(.js
后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export
命令输出对外接口。 - 模块之中,顶层的
this
关键字返回undefined
,而不是指向window
。也就是说,在模块顶层使用this
关键字,是无意义的。 - 同一个模块如果加载多次,将只执行一次。
ES6与CommonJS的区别
它们有两个重大差异。
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
node加载
Node 要求 ES6 模块采用
.mjs
后缀文件名。也就是说,只要脚本文件里面使用import
或者export
命令,那么就必须采用**.mjs
**后缀名!!!!!
- 目前,Node 的
import
命令只支持加载本地模块(file:
协议),不支持加载远程模块。 - 如果模块名不含路径,那么
import
命令会去node_modules
目录寻找这个模块。 - 如果脚本文件省略了后缀名,比如
import './foo'
,Node 会依次尝试四个后缀名:./foo.mjs
、./foo.js
、./foo.json
、./foo.node
如果这些脚本文件都不存在,Node 就会去加载./foo/package.json
的main
字段指定的脚本。如果./foo/package.json
不存在或者没有main
字段,那么就会依次加载./foo/index.mjs
、./foo/index.js
、./foo/index.json
、./foo/index.node
。如果以上四个文件还是都不存在,就会抛出错误。 - Node 的
import
命令是异步加载,这一点与浏览器的处理方法相同。
上一篇: servlet中使用seam组件