欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ES6读后总结

程序员文章站 2022-07-16 22:38:35
...

读阮一峰的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

  • 缺点:

    1. 无法编译时就报出变量未声明(因为全局变量可能是顶层对象创造的,是动态的)
    2. 可能无意间创建了全局变量
    3. 顶层对象的属性到处可以读写,不利于模块化编程
    4. window对象指的是浏览器的窗口对象,不大合适。
  • ES6规定,let,const,class命令声明的全局变量不属于顶层对象的属性。

  • 函数中的this在严格模式下指向的不是顶层对象而实undefined

想要在严格模式下返回全局对象的代码(如果用了CSP,Content Security Policy就无法使用)

new Function('return this')()
  • npm安装 system.global 可以统一顶层对象为global
  1. // ES6 模块的写法
    import shim from ‘system.global/shim’; shim();
    保证各种环境里面,global对象都是存在的。

  2. // 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, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;");

    // 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>&lt;script&gt;alert("abc")&lt;/script&gt; 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

正则方法

  1. match()
  2. replace()
  3. search()
  4. split()

u修饰符(unicode属性检验)

/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true
  1. 对于码点大于0xFFFF的 Unicode 字符,点字符不能识别,必须加上u修饰符。
  2. 大括号表示 Unicode 字符,需要加u修饰符
 /\u{61}/.test('a') // false
 /\u{61}/u.test('a') // true
  1. 加了之后量词能恢复正常
/????{2}/.test('????????') // false
/????{2}/u.test('????????') // true
  1. \S匹配所有非空白字符,也需要加u修饰符,才能识别大于0xFFFF的字符
/^\S$/.test('????') // false
/^\S$/u.test('????') // true
  1. 能识别相同字型,不会报错。
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的扩展

  1. 八进制要使用前缀 0o
  2. Number转化0b,0o为十进制
  3. Number.isFinite(),Number.isNaN() ,先调用Number转化为数值,再判断,与全局不同
  4. Number.parseInt(),Number.parseFloat() 移植入Number
  5. Number.isInteger
  6. Number.EPSILON, 表示 1 与大于 1 的最小浮点数之间的差。
  7. Number.isSafeInteger,Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER

Math的扩展

  1. Math.trunc() ,返回整数,去小数点后
  2. Math.sign() 判断正负
  3. Math.cbrt() 计算立方根
  4. Math.clz32() 转成32位,看前面几个0
  5. Math.imul() 传两个参,返回32位乘积,避免很大的数直接相乘低位不精确
  6. Math.fround() 返回一个数的32位单精度浮点数形式。
  7. Math.hypot() 返回所有参数的平方和的平方根。
  8. Math.expm1() 等同于 Math.exp(x) - 1
  9. Math.log1p() 等同于 Math.log(1 + x)
  10. Math.log10() 范围以10为底的x的对数。
  11. Math.log2() 范围以2为底的x的对数

指数运算符(**)

  • 右结合
// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512
  • 与Math.pow在特别大的运算中会有细微差别

函数的扩展

  1. 参数变量默认声明,不能用let或const再次声明
  2. 不能有同名参数
  3. 参数默认值惰性求值
  4. 通常情况下,定义了默认值的参数,应该是函数的尾参数。
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]
  1. 函数具有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 参数 (返回数组)

  1. 可以利用rest参数返回数组的特性, 直接对返回的数组进行排序等操作,而不是用Array.prototype.slice.call(arguments) 来操作。
  2. rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
  3. 函数的length属性,不包括 rest 参数。

严格模式

  1. 参数默认值、解构赋值、扩展运算符(rest)一旦使用,函数内部就不能显示设定严格模式,否则报错。 (在外部全局性的设定就可以,或者把函数包在一个无参数的立即执行函数中也行。)
  2. 因为严格模式需要适用于函数体和参数,但是只有运行到函数体才能知道是严格模式,所以产生bug。

name属性

返回函数名

  1. bind返回的函数,name属性值会加上bound前缀。

箭头函数

ES6 允许使用“箭头”(=>)定义函数。

  1. 如果返回对象,必须在外面加上圆括号,否则报错
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
  1. 只有一行语句,不需要返回值
let fn = () => void doesNotReturn();
  1. 不能作为构造函数,无法new
  2. 不可以使用arguments对象,可以用rest代替
  3. 不可以使用yield,不能用作generator函数
  4. 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的指向

箭头函数不使用场合

  1. 定义函数的方法时不适用,因为this不指向函数而指向全局对象
  2. 动态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);

扩展运算符的应用

  1. 复制数组
//ES5
const a1 = [1, 2];
const a2 = a1.concat();
//ES6
const a2 = [...a1]const [...a2] = a1
  1. 合并数组
[...arr1,...arr2,...arr3]
和concat一样是浅拷贝,如果数组中的对象属性或方法改变,会被影响。
  1. 与解构赋值结合
// ES5
a = list[0], rest = list.slice(1)
// ES6
[a, ...rest] = list
  1. 字符串 (能识别四个字节的unicode字符)
[...'hello']
// [ "h", "e", "l", "l", "o" ]

'x\uD83D\uDE80y'.length // 4
[...'x\uD83D\uDE80y'].length // 3
  1. 扩展运算符需要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]
  1. 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方法

主要的类数组:

  1. DOM操作返回的NodeList集合 (querySelectorAll等方法返回的集合)
  2. arguments对象

主要的iterable的对象

  1. 字符串
  2. Set
  3. 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。

对象的扩展

简写

  1. 允许对象中直接写变量。 此时属性名为变量名,属性值为变量值
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
};
  1. 方法简写
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的属性

  1. for…in
  2. Object.keys()
  3. JSON.stringify()
  4. 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常见用法

  1. 为对象添加属性
class Point {
    constructor(x,y){
        Object.assign(this,{x,y})
    }
}
  1. 为对象添加方法
Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    ···
  },
  anotherMethod() {
    ···
  }});
// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
  ···
};
SomeClass.prototype.anotherMethod = function () {
  ···
};
  1. 克隆对象
//先把origin中的原型属性赋予originProto的实例属性,然后根据originProto创建一个新的具有原型方法的target对象,传给assign。
function clone(origin) {
  let originProto = Object.getPrototypeOf(origin);
  return Object.assign(Object.create(originProto), origin);}
  1. 合并多个对象
const merge =
  (...sources) => Object.assign({}, ...sources);
  1. 为属性指定默认值

由于是浅拷贝 所以值最好是简单类型,不要指向一个对象。

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

前六种数据类型

  1. undefined
  2. null
  3. Boolean
  4. String
  5. Number
  6. 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实例的四个方法
  1. add(value):添加某个值,返回 Set 结构本身。
  2. delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  3. has(value):返回一个布尔值,表示该值是否为Set的成员。
  4. clear():清除所有成员,没有返回值。

Array.from可以将Set转化为数组

Array.from(new Set(array))
//可以去除数组重复成员

//这是直接将Set转化为数组的方法
[...set]
Set实例的四个遍历方法

Set的遍历顺序就是插入顺序

  1. keys():返回键名的遍历器
  2. values():返回键值的遍历器 (和keys方法完全一样,因为set没有键名)
  3. entries():返回键值对的遍历器 ([key,value]两个值完全一样)
  4. 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对对象的引用)
因为弱引用的原因,内部成员运行前后可能个数不一样,所以不可遍历

方法
  1. WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
  2. WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
  3. 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 是同一个键

属性与方法
  1. size
  2. set(key,value) 因为返回的是当前Map对象,可以链式写法
  3. get(key)
  4. has(key)
  5. delete(key)
  6. clear()

遍历方法

Map的遍历顺序是插入顺序

  1. keys()
  2. values()
  3. entries()
  4. 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
  1. 可以将Proxy对象设置成对象的属性
var object = { proxy: new Proxy(target, handler) };
  1. 可以将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

  1. 将Object的一些方法移植到Reflect上
  2. 修改一些Object的方法的返回结果,使其更合理
// 老写法
try {
  Object.defineProperty(target, property, attributes);
  // success
} catch (e) {
  // failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  // success
} else {
  // failure
}

//不抛出错误 ,而是返回false
  1. 让Object操作变为函数行为
// 老写法
'assign' in Object // true

// 新写法
Reflect.has(Object, 'assign') // true
  1. 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的缺点:

  1. 无法取消Promise,一旦新建就立即执行,中途无法取消
  2. 如果不设置回调函数,内部错误不会反应到外部
  3. 处于pending时,无法得知进展

所以如果某些事件不断反复发生,Stream模式比Promise好。

Generator

Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

两个特征:

  1. function关键字与函数名之间有一个星号
  2. 函数体内部使用yield表达式,定义不同的内部状态
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

yield表达式

由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

next的运行逻辑

  1. 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
  2. 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
  3. 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
  4. 如果该函数没有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函数的应用

  1. 异步操作的同步化表达
  • 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();
      }
    }
    
  1. 控制流管理
  • 多步操作

    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);
      }
    }
    
  1. 部署Iterator接口

利用 Generator 函数,可以在任意对象上部署 Iterator 接口。

  1. 作为数据结构

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 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:

  1. 函数体内外的数据交换
  2. 错误处理机制。
  • 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 函数的改进,体现在以下四点

  1. Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器
  2. asyncawait,比起星号和yield,语义更清楚了。
  3. co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值
  4. 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方法,处理前面可能出现的错误。

错误处理

  1. 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);
  });
}
  1. 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo();
let bar = await getBar();

//改成
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

这样同时触发可以缩短时间

  1. await必须在async函数中

  2. 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对象上

  • 类的属性名可以用表达式

  • 注意点:

    1. 类和模块的内部默认就是严格模式

    ​ 2. 不存在变量提升

    ​3. 有name属性

    1. 方法前加* 就表示该方法是Generator函数

    2. 类的方法内部如果含有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关键字

有两种使用情况

  1. 作为函数

    子类的构造函数必须执行一次super函数。

    super(),代表调用父类的构造函数。

class A {}

class B extends A {
  constructor() {
    super();
  }
}

//super()在这里相当于A.prototype.constructor.call(this)。
  1. 作为对象

    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不会在它的外层作用域引入变量
  • evalarguments不能被重新赋值
  • arguments不会自动反映函数参数的变化
  • 不能使用arguments.callee
  • 不能使用arguments.caller
  • 禁止this指向全局对象
  • 不能使用fn.callerfn.arguments获取函数调用的堆栈
  • 增加了保留字(比如protectedstaticinterface

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 };
  • 写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

模块的继承

  • 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>

渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。

deferasync的区别:

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.jsonmain字段指定的脚本。如果./foo/package.json不存在或者没有main字段,那么就会依次加载./foo/index.mjs./foo/index.js./foo/index.json./foo/index.node。如果以上四个文件还是都不存在,就会抛出错误。
  • Node 的import命令是异步加载,这一点与浏览器的处理方法相同。