Fork me on GitHub

《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的写法更简洁,更易于理解。
posted @ 2017-04-06 21:55  Jesse131  阅读(1071)  评论(0编辑  收藏  举报