《ECMAScript6标准入门》第三版--读书笔记

2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。尽管在目前的工作中还没有使用 ES6,但是每项新技术出来总是忍不住想尝尝鲜,想知道 ES6 能为前端开发带来哪些变化?对自己的工作有哪些方面可以提升。刚好看到阮一峰的《ES6 标准入门》,便顺着这本书尝试着 ES6 的各种新特性。

ES6 的各种新特性的兼容性查询http://kangax.github.io/compat-table/es6/
尽管我们的浏览器还不一定完全支持 ES6 代码,我们可以使用 Babel 转码器,在这里我们使用命令行转码babel-cli,命令行$ npm install --global babel-cli安装 babel-cli

第二章 let 和 const 命令

let 命令

ES6 新增了 let 命令,用来声明变量。它的用法类似于 var ,但是所声明的变量,只在 let 命令所在的代码块内有效。let 不像 var 那样会发生“变量提升”现象。所以,变量一定要在声明后使用,否则报错。

{
  let a = 10;
  var b = 1;
}
a; // ReferenceError: a is not defined.
b; // 1

ES6 明确规定,如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
let 不允许在相同作用域内,重复声明同一个变量。let 实际上为 JavaScript 新增了块级作用域。 ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。

const 命令

const 声明一个只读的常量。一旦声明,常量的值就不能改变。
const 命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;
}

对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。 const 命令只是保证变量名指向的地址不变,并不保证该地址的数据不变

const foo = {};
foo.prop = 123;
foo.prop; //123

ES6 规定 var 命令和 function 命令声明的全局变量,依旧是全局对象的属性;let 命令、 const 命令、 class 命令声明的全局变量,不属于全局对象的属性。
ES5 只有两种声明变量的方式,var 和 function 命令,ES6 有 6 种方式,var,function,let,const,class,import

第三章 变量的解构赋值

数组的解构赋值

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo; // 1
bar; // 2
baz; // 3

解构赋值允许指定默认值。

[x, y = "b"] = ["a"]; // x='a', y='b'
[x, y = "b"] = ["a", undefined]; // x='a', y='b'

注意,ES6 内部使用严格相等运算符( === ),判断一个位置是否有值。所以,如果一个数组成员不严格等于 undefined ,默认值是不会生效的。

对象的解构赋值

var { foo, bar } = { foo: "aaa", bar: "bbb" };
foo; // "aaa"
bar; // "bbb"
var { foo: baz } = { foo: "aaa", bar: "bbb" };
baz; // "aaa"
foo; // error: foo is not defined

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

函数参数的解构赋值

[
  [1, 2],
  [3, 4],
].map(function ([a, b]) {
  return a + b;
});
//[3,7]

变量解构赋值用途

  1. 交换变量的值[x, y] = [y, x];
  2. 提取 JSON 数据
    var jsonData = {
        id: 42,
        status: "OK",
        data: [867, 5309]
    };
    let { id, status, data: number } = jsonData;
    console.log(id, status, number);// 42, "OK", [867, 5309]
  3. 函数参数的默认值
    jQuery.ajax = function (url, {
        async = true,
        beforeSend = function () {},
        cache = true,
        complete = function () {},
        crossDomain = false,
        global = true
    }) {
        // ... do stuff
    };

第四章 字符串的扩展

字符串的遍历器接口

es6 为字符串添加了遍历器接口,使得字符串可以由 for…of 循环遍历

for (let codePoint of "foo") {
  console.log(codePoint);
}

includes(), startsWith(), endsWith()

includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。

var s = "Hello world!";
s.startsWith("world", 6); // true
s.endsWith("Hello", 5); // true
s.includes("Hello", 6); // false

使用第二个参数 n 时, endsWith 的行为与其他两个方法有所不同。它针对前 n 个字符,而其他两个方法针对从第 n 个位置直到字符串结束。

repeat()

返回一个新字符串,表示将原字符串重复 n 次。

"hello".repeat(2); // "hellohello"

padStart(),padEnd()

padStart 用于头部补全, padEnd 用于尾部补全。

"x".padStart(5, "ab"); // 'ababx'
"x".padStart(4, "ab"); // 'abax'
"x".padEnd(5, "ab"); // 'xabab'
"x".padEnd(4, "ab"); // 'xaba'

padStart 和 padEnd 一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。

模板字符串

模板字符串(template string)是增强版的字符串,用反引号标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

var name = "Bob",
  time = "today";
`Hello ${name}, how are you ${time}?`;

第六章 数值的扩展

从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀 0 表示,ES6 进一步明确,要使用前缀 0o 表示。

Number.isFinite()

Number.isFinite() 用来检查一个数值是否非无穷(infinity)。

Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false

Number.isNaN()

Number.isNaN() 用来检查一个值是否为 NaN 。

Number.isNaN(NaN); // true
Number.isNaN(15); // false

它们与传统的全局方法 isFinite() 和 isNaN() 的区别在于,传统方法先调用 Number() 将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,非数值一律返回 false 。

Number.parseInt(), Number.parseFloat()

ES6 将全局方法 parseInt() 和 parseFloat() ,移植到 Number 对象上面,行为完全保持不变。

Number.isInteger()

Number.isInteger() 用来判断一个值是否为整数。需要注意的是,在 JavaScript 内部,整数和浮点数是同样的储存方法,所以 3 和 3.0 被视为同一个值。

安全整数和 Number.isSafeInteger()

JavaScript 能够准确表示的整数范围在 -2^53 到 2^53 之间(不含两个端点),超过这个范围,无法精确表示这个值。
ES6 引入了 Number.MAX_SAFE_INTEGER 和 Number.MIN_SAFE_INTEGER 这两个
常量,用来表示这个范围的上下限。
Number.isSafeInteger() 则是用来判断一个整数是否落在这个范围之内。

Math 对象的扩展

Math.trunc 方法用于去除一个数的小数部分,返回整数部分。
Math.sign 方法用来判断一个数到底是正数、负数、还是零。
Math.cbrt 方法用于计算一个数的立方根。
Math.fround 方法返回一个数的单精度浮点数形式。
Math.hypot 方法返回所有参数的平方和的平方根。

第七章 函数的扩展

通常情况下,定义了默认值的参数应该是函数的尾参数,

rest 参数

ES6 引入 rest 参数以用于获取函数的多余参数,这样就不需要使用 arguments 对象了

function add(...values) {
  let sum = 0;
  for (var val of values) {
    sum += val;
  }
  return sum;
}
add(2, 3, 5); //10

箭头函数

如果箭头函数不需要参数或需要多个参数,就使用圆括号代表参数部分。只有一个参数则可以不使用圆括号

var f = v => v

注意事项:

  1. 函数体内的 this 对象是定义时所在的对象,而不是使用时所在的对象
  2. 不可以当做构造函数
  3. 不可以使用 arguments,super, new.target,可以使用 rest 参数代替
  4. 不可以使用 yield 命令

绑定 this

函数绑定运算符是并排的双冒号,双冒号左边是一个对象,右边是一个函数,该运算符会自动将左边的对象作为上下文环境(this)绑定到右边的函数上
如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上

foo::bar
var method = ::object.foo

尾调用优化

尾调用是指某个函数最后一步是调用另一个函数,

function f(x){
    return g(x)
}

只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行尾调用优化

尾递归

递归非常耗费内存,因为需要同时保存成百上千个调用帧,很容易发生栈溢出错误,但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生栈溢出

function factorial(n,total){
    if(n===1) return total
    return fatorial(n-1,n*total)
}

function Fibonacci(n,ac1 = 1,ac2=2){
    if(n<=1){return ac2}
    return Fibonacci(n-1,ac2,ac1+ac2)}

柯里化

将多参数的函数转换成单参数的形式

function currying(fn,n){
    return function(m){
        return fn.call(this,m,n)
    }
}

const tailFactorial = currying(factorial,1)
tailFactorial(5)

ES6 的尾调用优化只有在严格模式下有效,正常模式下 arguments 和 caller 会跟踪函数的调用栈

在正常模式下可通过蹦床函数实现尾递归优化

function trampoline(f){
    while(f && f instranceof Function){
        f = f()  //只要f()执行后返回一个函数就继续执行
    }
    return f;
}

蹦床函数并不是真正的尾递归优化,下面的实现才是

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

第八章 数组的扩展

扩展运算符

(…) 可以将一个数组转化为逗号分隔的参数序列

console.log(...[1,2,3]) //1 2 3

扩展运算符可以代替数组的 apply 方法

Math.max(...[14,3,7])

可以合并数组

[1,2].concat(more)
//等同于
[1,2,...more]

Array.from()

Array.from 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的 arguments 对象。 Array.from 都可以将它们转为真正的数组。

// NodeList对象
let ps = document.querySelectorAll("p");
Array.from(ps).forEach(function (p) {
  console.log(p);
});
// arguments对象
function foo() {
  var args = Array.from(arguments);
  // ...
}

任何有 length 属性的对象,都可以通过 Array.from 方法转为数组,而这种情况扩展运算符无法转换
Array.from 还可以接受第二个参数,作用类似于数组的 map 方法,用来对每个元素进行处理,将处理后的值放入返回的数组

Array.of()

Array.of 方法用于将一组值,转换为数组。

Array.of(3, 11, 8); // [3,11,8]

数组实例的 copyWithin()

Array.prototype.copyWithin(target, start = 0, end = this.length)

  • target(必需):从该位置开始替换数据。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
[1, 2, 3, 4, 5].copyWithin(0, 3, 4);
// [4, 2, 3, 4, 5]

数组实例的 find()和 findIndex()

数组实例的 find 方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为 true 的成员,然后返回该成员。如果没有符合条件的成员,则返回 undefined 。

[1, 4, -5, 10].find((n) => n < 0);
// -5

数组实例的 findIndex 方法的用法与 find 方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回 -1 。

数组实例的 fill()

fill 方法使用给定值,填充一个数组。

["a", "b", "c"].fill(7); // [7, 7, 7]

数组实例的 entries(),keys()和 values()

ES6 提供三个新的方法—— entries() , keys() 和 values() ——用于遍历数组。唯一的区别是 keys() 是对键名的遍历、 values() 是对键值的遍历, entries() 是对键值对的遍历。
他们都返回一个遍历器对象,可用 for…of 循环遍历

includes()

该方法返回一个布尔值,标表示某个数组是否包含给定的值与字符串 includes 方法类似

[1,2,3].includes(2)  //true

相比 indexOf 方法
[NaN].indexOf(NaN) //-1
[NaN].includes(NaN) //true

第九章 对象的扩展

属性的简洁表示法

ES6 允许在对象之中,只写属性名,不写属性值。这时属性值等于属性名所代表的变量。

var Person = {
  name: "张三",
  //等同于birth: birth
  birth,
  // 等同于hello: function ()...
  hello() {
    console.log("我的名字是", this.name);
  },
};

Object.is()

ES5 比较两个值是否相等,只有两个运算符,== 和 === ,前者会自动转换数据类型没后者的 NaN 不等于自身,+0 等于-0
Object.is()则可以避免这个问题

Object.is(+0,-0) //false
Object.is(NaN,NaN) //true

Object.assign()

Object.assign 方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

var target = { a: 1 };
var source1 = { b: 2 };
var source2 = { c: 3 };
Object.assign(target, source1, source2);
target; // {a:1, b:2, c:3}

如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性
Object.assign 方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
Object.assign 方法有很多用处。

  1. 为对象添加属性
    class Point {
        constructor(x, y) {
            Object.assign(this, {x, y});
        }
    }
  2. 为对象添加方法
    Object.assign(SomeClass.prototype, {
        someMethod(arg1, arg2) {
        ···
        },
        anotherMethod() {
        ···
        }
    });
  3. 克隆对象
    function clone(origin) {
        return Object.assign({}, origin);
    }
    不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。
function clone(origin) {
  let originProto = Object.getPrototypeOf(origin);
  return Object.assign(Object.create(originProto), origin);
}
  1. 合并多个对象
    const merge = (target, ...sources) => Object.assign(target, ...sources);
  2. 为属性指定默认值
    const DEFAULTS = {
        logLevel: 0,
        outputFormat: 'html'
    };
    function processContent(options) {
        let options = Object.assign({}, DEFAULTS, options);
    }
ES6 属性的遍历 5 种方法
  1. for…in 循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
  2. Object.keys(obj)返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)。
  3. Object.getOwnPropertyNames(obj)返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)。
  4. Object.getOwnPropertySymbols(obj)返回一个数组,包含对象自身的所有 Symbol 属性。
  5. Reflect.ownKeys(obj)返回一个数组,包含对象自身的所有属性,不管是属性名是 Symbol 或字符串,也不管是否可枚举。

Object.setPrototypeOf()

该属性用来设置一个对象的 prototype 对象,返回参数对象本身

Object.setPrototypeOf(Object,prototype)

Object.getPrototypeOf()

该方法与 setPrototypeOf()方法配套,用于读取一个对象的 prototype 对象,参数如果不是对象会自动转为对象

function Rectangle() {
  // ...
}
const rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype
// true

Object.keys()

返回一个数组,成员是参数对象自身(不包含继承的)的所有可遍历属性的键名

Object.values()

Object.values 方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。

Object.entries()

Object.entries()方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。

Object.getOwnPropertyDescriptors()

ES5 的 Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象(descriptor)。
ES2017 引入了 Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。
该方法的引入目的,主要是为了解决 Object.assign()无法正确拷贝 get 属性和 set 属性的问题。

const source = {
  set foo(value) {
    console.log(value);
  }
};

const target1 = {};
Object.assign(target1, source);

Object.getOwnPropertyDescriptor(target1, 'foo')
// { value: undefined,
//   writable: true,
//   enumerable: true,
//   configurable: true }

第十章 Symbol

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入 Symbol 的原因。
注意,Symbol 函数前不能使用 new 命令,否则会报错。Symbol 值作为对象属性名时,不能用点运算符。

var mySymbol = Symbol();
var a = {};
a.mySymbol = "Hello!";
a[mySymbol]; // undefined
a["mySymbol"]; // "Hello!"

因为点运算符后面总是字符串,所以不会读取 mySymbol 作为标识名所指代的那个值,导致 a 的属性名实际上是一个字符串,而不是一个 Symbol 值。
Symbol 作为属性名,该属性不会出现在 for…in 、 for…of 循环中,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 返回。但是,它也不是私有属性,有一个 Object.getOwnPropertySymbols 方法,可以获取指定对象的所有 Symbol 属性名。
Reflect.ownKeys 方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

let obj = {
  [Symbol("my_key")]: 1,
  enum: 2,
  nonEnum: 3,
};
Reflect.ownKeys(obj); // [Symbol(my_key), 'enum', 'nonEnum']

Symbol.for(),Symbol.keyFor()

有时,我们希望重新使用同一个 Symbol 值, Symbol.for 方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

Symbol.for("bar") === Symbol.for("bar"); // true
Symbol("bar") === Symbol("bar"); // false
var s1 = Symbol.for("foo");
Symbol.keyFor(s1); // "foo"
var s2 = Symbol("foo");
Symbol.keyFor(s2); // undefined

第十一章 Set 和 Map 数据结构

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set 本身是一个构造函数,用来生成 Set 数据结构。

var s = new Set();
[2, 3, 5, 4, 5, 2, 2].map((x) => s.add(x));
for (let i of s) {
  console.log(i);
}
// 2 3 5 4

可用于数组去重

[...new set(array)]

Array.from(new set(1,2,3,4)) Array.from 可以将 set 结构转为数组

Set 实例的属性和方法

  1. Set.prototype.constructor :构造函数,默认就是 Set 函数。
  2. Set.prototype.size :返回 Set 实例的成员总数。
  3. add(value) :添加某个值,返回 Set 结构本身。
  4. delete(value) :删除某个值,返回一个布尔值,表示删除是否成功。
  5. has(value) :返回一个布尔值,表示该值是否为 Set 的成员。
  6. clear() :清除所有成员,没有返回值。

遍历操作

  1. keys() :返回键名的遍历器。
  2. values() :返回键值的遍历器。
  3. entries() :返回所有成员的遍历器。
  4. forEach() :遍历 Map 的所有成员。
    也可以直接使用 for…of 遍历 set 结构

Map 结构的目的和基本用法

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键
Map 原生提供三个遍历器生成函数和一个遍历方法。

  1. keys() :返回键名的遍历器。
  2. values() :返回键值的遍历器。
  3. entries() :返回所有成员的遍历器。
  4. forEach() :遍历 Map 的所有成员。

第 12 章 Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

proxy 的实例方法

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • 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, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

第 13 章 Reflect

Reflect 对象的设计目的有这样几个。
1) 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。
2) 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc)则会返回 false。
3) 让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如 name in obj 和 delete obj[name],而 Reflect.has(obj, name)和 Reflect.deleteProperty(obj, name)让它们变成了函数行为。
4)Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。

Reflect 静态方法

Reflect.apply(target, 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)

第 14 章 Promise 对象

Promise 是异步编程的一种解决方案,可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数

基本用法

var promise = new Promise(function(resolve, reject) {
    // ... some code
    if (/* 异步操作成功 */){
        resolve(value);
    } else {
        reject(error);
    }
});

Primoise 新建后就会立即执行
Promise 实例生成以后,可以用 then 方法分别指定 Resolved 状态和 Reject 状态的回调函数。

var getJSON = function (url) {
  var promise = new Promise(function (resolve, reject) {
    var client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();
    function handler() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    }
  });
  return promise;
};
getJSON("/posts.json").then(
  function (json) {
    console.log("Contents: " + json);
  },
  function (error) {
    console.error("出错了", error);
  }
);

Promise.prototype.then()

then 方法可以接受两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为 Resolved 时调用,第二个回调函数是 Promise 对象的状态变为 Reject 时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受 Promise 对象传出的值作为参数。
then 方法返回的是一个新的 Promise 实例(注意,不是原来那个 Promise 实例)。因此可以采用链式写法,即 then 方法后面再调用另一个 then 方法。

Promise.prototype.catch()

Promise.prototype.catch 方法是 .then(null, rejection) 的别名,用于指定发生错误时的回调函数。
一般来说,不要在 then 方法里面定义 Reject 状态的回调函数(即 then 的第二个参数),总是使用 catch 方法。

Promise.all()

promise.all 方法将用于将多个 promise 实例包装成一个新的 promise 实例

const p = Promise.all([p1, p2, p3]);

p 的状态由 p1、p2、p3 决定,分成两种情况。
(1)只有 p1、p2、p3 的状态都变成 fulfilled,p 的状态才会变成 fulfilled,此时 p1、p2、p3 的返回值组成一个数组,传递给 p 的回调函数。
(2)只要 p1、p2、p3 之中有一个被 rejected,p 的状态就变成 rejected,此时第一个被 reject 的实例的返回值,会传递给 p 的回调函数。

Promise.race()

const p = Promise.race([p1, p2, p3]);

上面代码中,只要 p1、p2、p3 之中有一个实例率先改变状态,p 的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给 p 的回调函数。

第 15 章 lterator 和 for…of 循环

Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令 for…of 循环,Iterator 接口主要供 for…of 消费
ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被 for…of 循环遍历
原生具备 Iterator 接口的数据结构如下。

Array
Map
Set
String
TypedArray
函数的 arguments 对象
NodeList 对象

第 18 章 async 函数

ES2017 标准引入了 async 函数,使得异步操作变得更加方便
async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);

async 函数返回一个 Promise 对象
async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数。
async 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态。抛出的错误对象会被 catch 方法回调函数接收到

async 函数返回的 Promise 对象,必须等到内部所有 await 命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到 return 语句或者抛出错误。也就是说,只有 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(v=>console.log(v))
// "ECMAScript 2017 Language Specification"

错误处理

如果 await 后面的异步操作出错,那么等同于 async 函数返回的 Promise 对象被 reject

async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('出错了');
  });
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// Error:出错了

防止出错的方法,也是将其放在 try…catch 代码块之中

async function f() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出错了');
    });
  } catch(e) {
  }
  return await('hello world');
}

使用注意点

第一点,前面已经说过,await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中

async function myFunction() {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err);
  });
}

第二点,多个 await 命令后面的异步操作,如果不存在继发关系,最好让它们同时触发

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

第三点,await 命令只能用在 async 函数之中,如果用在普通函数,就会报错

第 19 章 Class 基本语法

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }
  toString() {
    return "(" + this.x + ", " + this.y + ")";
  }
}

上面代码定义了一个“类”,可以看到里面有一个 constructor 方法,这就是构造方法,而 this 关键字则代表实例对象。也就是说,ES5 的构造函数 Point ,对应 ES6 的 Point 类的构造方法。
由于类的方法都定义在 prototype 对象上面,所以类的新方法可以添加在 prototype 对象上面。 Object.assign 方法可以很方便地一次向类添加多个方法。

class Point {
  constructor() {
    // ...
  }
}
Object.assign(Point.prototype, {
  toString() {},
  toValue() {},
});

另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。

constructor 方法

constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有 constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。constructor 方法默认返回实例对象(即 this ),完全可以指定返回另外一个
对象。

this 的指向

类的方法内部如果含有 this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错

class Logger {
  printName(name = 'there') {
    this.print(`Hello ${name}`);
  }

  print(text) {
    console.log(text);
  }
}

const logger = new Logger();
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined

上面代码中,printName 方法中的 this,默认指向 Logger 类的实例。但是,如果将这个方法提取出来单独使用,this 会指向该方法运行时所在的环境(由于 class 内部是严格模式,所以 this 实际指向的是 undefined),从而导致找不到 print 方法而报错
解决办法
在构造方法中绑定 this

class Logger {
  constructor() {
    this.printName = this.printName.bind(this);
  }

  // ...
}

另一种解决方法是使用箭头函数

class Logger {
  constructor() {
    this.printName = (name = 'there') => {
      this.print(`Hello ${name}`);
    };
  }

  // ...
}

还有一种解决方法是使用 Proxy,获取方法的时候,自动绑定 this

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());

Class 的静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function

注意,如果静态方法包含 this 关键字,这个 this 指的是类,而不是实例
父类的静态方法,可以被子类继承。

new.target 属性

new 是从构造函数生成实例对象的命令。ES6 为 new 命令引入了一个 new.target 属性,该属性一般用在构造函数之中,返回 new 命令作用于的那个构造函数。如果构造函数不是通过 new 命令或 Reflect.construct()调用的,new.target 会返回 undefined,因此这个属性可以用来确定构造函数是怎么调用的。

function Person(name) {
  if (new.target !== undefined) {
    this.name = name;
  } else {
    throw new Error('必须使用 new 命令生成实例');
  }
}

第 20 章 Class 的继承

Class 之间可以通过 extends 关键字实现继承

class ColorPoint extends Point {}

另一个需要注意的地方是,在子类的构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则会报错。 这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用 super 方法,子类就得不到 this 对象。
Class 不存在变量提升(hoist),这一点与 ES5 完全不同。

new Foo(); // ReferenceError
class Foo {}

Object.getPrototypeOf()

Object.getPrototypeOf 方法可以用来从子类上获取父类。
因此,可以使用这个方法判断,一个类是否继承了另一个类。

super 关键字

super 这个关键字,有两种用法,含义不同。

  1. 作为函数调用时(即 super(…args) ), super 代表父类的构造函数。
  2. 作为对象调用时(即 super.prop 或 super.method() ),在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

ES6 规定,在子类普通方法中通过 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 作为对象,用在静态方法之中,这时 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

类的 prototype 属性和proto属性

(1)子类的proto属性,表示构造函数的继承,总是指向父类。

(2)子类 prototype 属性的proto属性,表示方法的继承,总是指向父类的 prototype 属性。

class A {
}

class B extends A {
}

B.__proto__ === A // true
B.prototype.__proto__ === A.prototype // true

这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(proto属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype 属性)是父类的原型对象(prototype 属性)的实例。

编程风格

  1. let 取代 var
  2. 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
  3. 函数的参数如果是对象的成员,优先使用解构赋值。
  4. 单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
    const a = { k1: v1, k2: v2 };
    const b = {
      k1: v1,
      k2: v2,
    };
  5. 数组
    使用扩展运算符(…)拷贝数组。const itemsCopy = [...items];
    使用 Array.from 方法,将类似数组的对象转为数组。
    const foo = document.querySelectorAll(".foo");
    const nodes = Array.from(foo);
  6. 函数
    立即执行函数可以写成箭头函数的形式。
    (() => {
      console.log("Welcome to the Internet.");
    })();
    使用默认值语法设置函数参数的默认值。
  7. 总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。

文章目录

  1. 第二章 let 和 const 命令
    1. let 命令
    2. const 命令
  2. 第三章 变量的解构赋值
    1. 数组的解构赋值
    2. 对象的解构赋值
    3. 函数参数的解构赋值
    4. 变量解构赋值用途
  3. 第四章 字符串的扩展
    1. 字符串的遍历器接口
    2. includes(), startsWith(), endsWith()
    3. repeat()
    4. padStart(),padEnd()
    5. 模板字符串
  4. 第六章 数值的扩展
    1. Number.isFinite()
    2. Number.isNaN()
    3. Number.parseInt(), Number.parseFloat()
    4. Number.isInteger()
    5. 安全整数和 Number.isSafeInteger()
    6. Math 对象的扩展
  5. 第七章 函数的扩展
    1. rest 参数
    2. 箭头函数
    3. 绑定 this
    4. 尾调用优化
    5. 尾递归
    6. 柯里化
  6. 第八章 数组的扩展
    1. 扩展运算符
    2. Array.from()
    3. Array.of()
    4. 数组实例的 copyWithin()
    5. 数组实例的 find()和 findIndex()
    6. 数组实例的 fill()
    7. 数组实例的 entries(),keys()和 values()
    8. includes()
  7. 第九章 对象的扩展
    1. 属性的简洁表示法
    2. Object.is()
    3. Object.assign()
      1. ES6 属性的遍历 5 种方法
    4. Object.setPrototypeOf()
    5. Object.getPrototypeOf()
    6. Object.keys()
    7. Object.values()
    8. Object.entries()
    9. Object.getOwnPropertyDescriptors()
  8. 第十章 Symbol
    1. Symbol.for(),Symbol.keyFor()
  9. 第十一章 Set 和 Map 数据结构
    1. Set 实例的属性和方法
    2. 遍历操作
    3. Map 结构的目的和基本用法
  10. 第 12 章 Proxy
    1. proxy 的实例方法
  11. 第 13 章 Reflect
    1. Reflect 静态方法
  12. 第 14 章 Promise 对象
    1. 基本用法
    2. Promise.prototype.then()
    3. Promise.prototype.catch()
    4. Promise.all()
    5. Promise.race()
  13. 第 15 章 lterator 和 for…of 循环
  14. 第 18 章 async 函数
    1. 错误处理
    2. 使用注意点
  15. 第 19 章 Class 基本语法
    1. constructor 方法
    2. this 的指向
    3. Class 的静态方法
    4. new.target 属性
  16. 第 20 章 Class 的继承
    1. Object.getPrototypeOf()
    2. super 关键字
    3. 类的 prototype 属性和proto属性
  17. 编程风格