人要向前进步,就必须要学会接受批评和不同意见。

ES6相比5,有了很大的进步:在严格模式下,开发者受到了适当而必要的约束;新增了几种数据类型(map、set)和函数能力(Generator、迭代器);进一步强化了JavaScript的特点(promise、proxy);并且让JavaScript能适用于更大型的程序开发(modules、class)。
一个提案只要能进入Stage 2(Stage 2–Draft(草案阶段)),就差不多肯定会包括在以后的正式标准里面。

node的版本管理工具nvm、npm的仓库管理工具nrm、node --v8-options是用来查看可用的v8虚拟机选项的,要使用这些选项直接加在node后面、node –harmony is a shortcut to enable all the harmony features。

块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。

ES5只有两种声明变量的方法:var命令和function命令。ES6除了添加let和const命令,另外两种声明变量的方法:import命令和class命令。所以,ES6一共有6种声明变量的方法。

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。如果解构不成功,变量的值就等于undefined。默认值生效的条件是,对象的属性值严格等于undefined。对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。
对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

各种对象的扩展:

1、字符串扩展(编码水深、手动实现各编码间的转换)

  • Unicode只规定了每个字符的码点(code point),到底用什么样的字节序表示这个码点,就涉及到编码方法。
  • 最前面的65536个字符位,称为基本平面(缩写BMP)。所有最常见的字符都放在这个平面,这是Unicode最先定义和公布的一个平面。剩下的字符都放在辅助平面(缩写SMP)。在UTF-16编码中,一个辅助平面的字符,被拆成两个基本平面的字符表示。(‘\u{1F680}’ === ‘\uD83D\uDE80’)
  • JavaScript语言采用Unicode字符集,但是只支持一种编码方法。这种编码既不是UTF-16,也不是UTF-8,更不是UTF-32。(UTF-16与UCS-2的关系:两者的关系简单说,就是UTF-16取代了UCS-2,或者说UCS-2整合进了UTF-16)
  • 由于JavaScript只能处理UCS-2编码,造成所有字符在这门语言中都是2个字节,如果是4个字节的字符,会当作两个双字节的字符处理。JavaScript的字符函数都受到这一点的影响,无法返回正确结果。(ES5要解决这个问题:必须对码点做一个判断,然后手动调整)
  • 注意,fromCodePoint方法定义在String对象上,而codePointAt方法定义在字符串的实例对象上。
  • *.java (utf-8/gbk/...) -> *.class (utf-8) -> memory (utf-16)。javac编译中有参数可以指定源代码的编码 -encoding Specify character encoding used by source files 所以源码用其他编码都是可以的,最常见的是使用UTF-8。

  • 模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。1、如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中 2、模板字符串中嵌入变量,需要将变量名写在${}之中,大括号内部可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性。
  • 最佳实践:只允许使用单引号包裹普通字符串,禁止使用双引号。

2、正则扩展

  • 字符串对象共有4个方法,可以使用正则表达式:match()、replace()、search()和split()。ES6将这4个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。

3、数值扩展

  • ES6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

4、数组扩展

  • Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。只要是部署了Iterator接口的数据结构,Array.from都能将其转为数组。(所谓类似数组的对象,本质特征只有一点,即必须有length属性)
  • fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
  • 扩展运算符(…)也可以将某些数据结构转为数组,扩展运算符背后调用的是遍历器接口(Symbol.iterator)。

5、函数扩展

  • ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。参数默认值可以与解构赋值的默认值,结合起来使用。通常情况下,定义了默认值的参数,应该是函数的尾参数。
  • ES6引入rest参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
  • 扩展运算符(spread)是三个点(…)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。该运算符主要用于函数调用。
  • ES6允许使用“箭头”(=>)定义函数。如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。箭头函数使得表达更加简洁。
  • 默认的prototype对象可以被用户创建的对象替换掉。这样做的话,我们必须重复javascript运行时在幕后对默认的prototype对象所做的那样去手动设置constructor属性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function foo() { } ; var f1 = new foo();  
    f1.constructor === foo.prototype.constructor === foo
    //replace the default prototype object
    foo.prototype = new Object();
    //now we have:
    f1.constructor === foo.prototype.constructor === Object
    //so now we say:
    foo.prototype.constructor = foo
    //all is well again
    f1.constructor === foo.prototype.constructor === foo
  • 每一个的prototype对象自己都是由Object()函数(默认情况下)创建而来,所以prototype有它自己的原型那就是Object.prototype。Function.constructor === Function,也就是说:Function是它自己的构造函数!

  • Function.prototype.toString是一个内置的函数,它有别于另外一个内置的函数:Object.prototype.toString,即覆盖了Object.prototype上的toString方法。Function.prototype.toString返回的是函数体。Object.prototype.toString返回"[object type]",其中type是对象的类型。可以通过toString()来获取每个对象的类型。为了每个对象都能通过Object.prototype.toString()来检测,需要以Function.prototype.call()或者Function.prototype.apply()的形式来调用,传递要检查的对象作为第一个参数,称为thisArg。

    1
    2
    3
    4
    5
    var toString = Object.prototype.toString;

    toString.call(new Date); // [object Date]
    toString.call(new String); // [object String]
    toString.call(Math); // [object Math]
  • 只要new表达式之后的constructor返回(return)一个引用对象(数组,对象,函数等),都将覆盖new创建的匿名对象,如果返回(return)一个原始类型(无return时其实为return原始类型undefined),那么就返回new创建的匿名对象。

  • 在javascript中,如果试着改变一个属性的值,那么对应的setter将被执行。setter经常和getter连用以创建一个伪属性。不可能在具有真实值的属性上同时拥有一个setter器。({ x: ..., set x(v) { }}是不允许的)。

6、对象扩展

  • 属性的简洁表示法:ES6允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。即ES6允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值。(注意,简洁写法的属性名总是字符串)。
  • Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。(Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用)。
  • 对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。(格式:{configurable:xxx,enumerable:xxx,value:xxx,writable:xxx})。
  • 属性的可枚举性:描述对象的enumerable属性,称为”可枚举性“,如果该属性为false,就表示某些操作会忽略当前属性。目前有四个操作会忽略enumerable为false的属性。1、for...in循环:只遍历对象自身的和继承的可枚举的属性 2、Object.keys():返回对象自身的所有可枚举的属性的键名 3、JSON.stringify():只串行化对象自身的可枚举的属性 4、Object.assign():忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。这四个操作之中,前三个是ES5就有的,最后一个Object.assign()是ES6新增的。其中,只有for...in会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。
  • 实际上,引入“可枚举”(enumerable)这个概念的最初目的,就是让某些属性可以规避掉for...in操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的toString方法,以及数组的length属性,就通过“可枚举性”,从而避免被for…in遍历到。总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替。
  • 属性的遍历:ES6一共有5种方法可以遍历对象的属性。for…in、Object.keys(obj)、Object.getOwnPropertyNames(obj)、Object.getOwnPropertySymbols(obj)、Reflect.ownKeys(obj) 以上的5种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。1、首先遍历所有数值键,按照数值升序排列 2、其次遍历所有字符串键,按照加入时间升序排列 3、最后遍历所有Symbol键,按照加入时间升序排列。
  • proto属性(前后各两个下划线):标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。
  • get/set访问器不是对象的属性,而是属性的特性。大家一定要分清楚。特性只有内部才用,因此在javaScript中不能直接访问他们。为了表示特性是内部值用两队中括号括起来表示如[[Value]]。

Symbol

  • ES5的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是ES6引入Symbol的原因。
  • Symbol值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的Symbol类型。凡是属性名属于Symbol类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
  • 注意,Symbol值作为对象属性名时,不能用点运算符。因为点运算符后面总是字符串。同理,在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号之中。
  • 除了定义自己使用的Symbol值以外,ES6还提供了11个内置的Symbol值,指向语言内部使用的方法。 (对象的Symbol.iterator属性,指向该对象的默认遍历器方法。对象进行for…of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器)

Set和Map数据结构

  • ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set函数可以接受一个数组(或者具有iterable接口的其他数据结构)作为参数,用来初始化。(去除数组的重复成员:[…new Set(array)])
  • Set实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。由于Set结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。 (扩展运算符…内部使用for…of循环)
  • 如果想在遍历操作中,同步改变原来的Set结构,目前没有直接的方法。利用原Set结构映射出一个新的结构,然后赋值给原来的Set结构。(let set = new Set([1, 2, 3]);set = new Set([…set].map(val => val * 2));)
  • WeakSet结构与Set类似,也是不重复的值的集合。但是,它与Set有两个区别。首先,WeakSet的成员只能是对象,而不能是其他类型的值。其次,WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于WeakSet之中。
  • JavaScript的对象(Object),本质上是键值对的集合(Hash结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。为了解决这个问题,ES6提供了Map数据结构。也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现。如果你需要“键值对”的数据结构,Map比Object更合适。
  • Map如果读取一个未知的键,则返回undefined。Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。
  • 需要特别注意的是,Map的遍历顺序就是插入顺序。Map与其他数据结构的互相转换。

Iterator和for…of循环

  • 遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。(Iterator接口主要供for...of消费,当使用for...of循环遍历某种数据结构时,该循环会自动去寻找Iterator接口)
  • ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。(遍历器生成函数-->遍历器(对象)-->该对象具备next、return、throw等方法)
  • ES6的有些数据结构原生具备Iterator接口(比如数组),即不用任何处理,就可以被for…of循环遍历。另外一些数据结构没有(比如对象)。原生具备Iterator接口的数据结构如下:Array、Map、Set、String、TypedArray、函数的arguments对象、NodeList对象。(let arr = [‘a’, ‘b’, ‘c’];let iter = arr[Symbol.iterator]();iter.next();)
  • 如果Symbol.iterator方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。

    1
    2
    typeof "hello"[Symbol.iterator] //function   
    typeof "hello"[Symbol.iterator]() //object
  • 调用Iterator接口的场合:1、解构赋值,对数组和Set结构进行解构赋值时,会默认调用Symbol.iterator方法 2、扩展运算符…也会调用默认的Iterator接口 3、yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口

  • Symbol.iterator方法的最简单实现:使用Generator函数(只要用yield命令给出每一步的返回值即可,感觉模拟next?)。
  • JavaScript原有的for…in循环,只能获得对象的键名,不能直接获取键值。ES6提供for…of循环,允许遍历获得键值。
  • 遍历Set结构和Map结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set结构遍历时,返回的是一个值,而Map结构遍历时,返回的是一个数组,该数组的两个成员分别为当前Map成员的键名和键值。
  • for…of循环与其他遍历语法的比较。

Proxy和Reflect

  • Proxy用于修改某些操作的默认行为,Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。(Proxy有重载的感觉,即用自己的定义覆盖了语言的原始定义)
  • ES6原生提供Proxy构造函数,用来生成Proxy实例。var proxy = new Proxy(target, handler);1、target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为 2、注意,要使得Proxy起作用,必须针对Proxy实例进行操作,而不是针对目标对象进行操作 3、如果handler没有设置任何拦截,那就等同于直接通向原对象 4、对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果 5、get方法可以继承,receiver参数会绑定target上的get函数的this对象 6、如果目标对象是函数,那么还有两种额外操作可以拦截:apply、construct
  • Proxy.revocable方法返回一个可取消的Proxy实例。Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
  • Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API(均是静态方法)。Reflect对象的设计目的有这样几个:1、将Object对象的一些明显属于语言内部的方法,放到Reflect对象上 2、修改某些Object方法的返回结果,让其变得更合理 3、让Object操作都变成函数行为 4、Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法,这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。
  • 观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //Web服务的客户端
    const service = createWebService('http://example.com/data');

    service.employees().then(json => {
    const employees = JSON.parse(json);
    // ···
    });

    function createWebService(baseUrl) {
    return new Proxy({}, {
    get(target, propKey, receiver) {
    return () => httpGet(baseUrl+'/' + propKey);
    }
    });
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//使用Proxy实现观察者模式
const person = observable({
name: '张三',
age: 20
});

function print() {
console.log(`${person.name}, ${person.age}`)
}

observe(print);
person.name = '李四';

const queuedObservers = new Set(); //定义了一个Set集合,所有观察者函数都放进这个集合

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set}); //observable函数返回原始对象的代理,拦截赋值操作

function set(target, key, value, receiver) { //拦截函数set之中,会自动执行所有观察者
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}

Generator函数

  • Generator函数是ES6提供的一种异步编程解决方案,执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。Generator函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
  • Generator函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。另外需要注意,yield表达式只能用在Generator函数里面,用在其他地方都会报错(句法错误SyntaxError)。遍历器对象本身也具有Symbol.iterator属性,执行后返回自身。(英语中,generator这个词是“生成器”的意思;yield是“产出”的意思)
  • yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
  • for…of循环可以自动遍历Generator函数时生成的Iterator对象。(这里需要注意,一旦next方法的返回对象的done属性为true,for…of循环就会中止,且不包含该返回对象,所以return语句返回的,不包括在for…of循环之中)
  • throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。注意,不要混淆遍历器对象的throw方法和全局的throw命令。后者只能被函数体外的catch语句捕获。如果Generator函数内部没有部署try…catch代码块,那么throw方法抛出的错误,将被外部try…catch代码块捕获。如果Generator函数内部和外部,都没有部署try…catch代码块,那么程序将报错,直接中断执行。一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象,即JavaScript引擎认为这个Generator已经运行结束了。
  • 如果在Generator函数内部,调用另一个Generator函数,默认情况下是没有效果的。这时就需要用到yield 表达式,用来在一个Generator函数里面执行另一个Generator函数。实际上,任何数据结构只要有Iterator接口,就可以被yield遍历。

Promise对象和async函数

  • 承诺promise–(触发)–>未来–(实现)–>异步操作(异步操作代码)–(分离、回调)–>回调函数(处理异步结果代码) (同步方式写异步代码,解决回调地狱) console.dir(Promise) 充分利用Event Loop模型 改变代码的执行顺序
  • Promise是异步编程的一种解决方案,它由社区最早提出和实现,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。ES6规定,Promise对象是一个构造函数,用来生成Promise实例。Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由JavaScript引擎提供,不用自己部署。(源码中resolve/reject是异步操作,可理解为setTimeout,但是micotask类型) Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。
  • resolve函数的作用是:将Promise对象的状态从“未完成”变为“成功”(即从pending变为resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是:将Promise对象的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
  • Promise.prototype.then():Promise实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。可以用then方法分别指定resolved状态和rejected状态的回调函数。then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
  • promise的then方法接受两个参数:promise.then(onFulfilled, onRejected) 1、调用时机:实践中要确保onFulfilled和onRejected方法异步执行,且应该在then方法被调用的那一轮事件循环之后的新执行栈中执行 2、调用要求:onFulfilled和onRejected必须被作为函数调用(即没有this值) (总之确保onFulfilled/onRejected异步执行)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //Promise新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。 
    let promise = new Promise(function(resolve, reject) {
    console.log('Promise');
    resolve();
    });

    promise.then(function() {
    console.log('resolved.');
    });

    console.log('Hi!');

    // Promise
    // Hi!
    // resolved
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// getJSON是对XMLHttpRequest对象的封装,用于发出一个针对JSON数据的HTTP请求,并且返回一个Promise对象。需要注意的是,在getJSON内部,resolve函数和reject函数调用时,都带有参数。
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});

return promise;
};

getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
  • 一般来说,不要在then方法里面定义reject状态的回调函数(即then的第二个参数),总是使用catch方法。它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。
  • 有时需要将现有对象转为Promise对象,Promise.resolve方法就起到这个作用。Promise.resolve方法的参数分成四种情况。
  • resolve方法的参数若是promise类型或者then回调函数返回的是promise类型,则此时以该promise类型的状态决定后续then的执行情况。
  • then方法必须返回一个promise对象。promise2 = promise1.then(onFulfilled, onRejected);如果onFulfilled或者onRejected返回一个值x,则运行Promise解决过程:[[Resolve]](promise2, x)。Promise解决过程是一个抽象的操作,其需输入一个promise和一个值,我们表示为:[[Resolve]](promise, x)。
    1、如果promise和x指向同一对象,以TypeError为据因拒绝执行promise;
    2、如果x为Promise,则使promise接受x的状态;(如果x处于等待态,promise需保持为等待态直至x被执行或拒绝;如果x处于执行态,用相同的值执行promise;如果x处于拒绝态,用相同的据因拒绝promise)
    3、如果x不为对象或者函数,以x为参数执行promise;
  • Promise对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。Promise内部的错误不会影响到Promise外部的代码,通俗的说法就是“Promise会吃掉错误”。finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。该方法是ES2018引入标准的。
  • Promise.all():const p = Promise.all([p1, p2, p3]); //p的状态由p1、p2、p3决定,分成两种情况:1、只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled 2、只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected。
  • 所谓”异步”,简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。JavaScript语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。回调函数的英语名字callback,直译过来就是”重新调用”。
  • 回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。Promise对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。Promise的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了。Promise的最大问题是代码冗余,原来的任务被Promise包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。
  • “协程”(coroutine),它的运行流程大致如下:第一步,协程A开始执行。第二步,协程A执行到一半,进入暂停,执行权转移到协程B;第三步,(一段时间后)协程B交还执行权;第四步,协程A恢复执行。协程A就是异步任务,因为它分成两段(或多段)执行。协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。
  • Generator函数是协程在ES6的实现,最大特点就是可以交出函数的执行权(即暂停执行)。整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。
  • Generator函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。next返回值的value属性,是Generator函数向外输出数据;next方法还可以接受参数,向Generator函数体内输入数据。Generator函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。
  • 虽然Generator函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。co模块用于Generator函数的自动执行。co模块可以让你不用编写Generator函数的执行器。co函数返回一个Promise对象,因此可以用then方法添加回调函数。
  • ES2017标准引入了async函数,使得异步操作变得更加方便。async函数是什么?一句话,它就是Generator函数的语法糖。进一步说,async函数完全可以看作多个异步操作,包装成的一个Promise对象,而await命令就是内部then命令的语法糖。
  • async函数对Generator函数的改进,体现在以下四点。1、内置执行器。Generator函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器 2、更好的语义。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果 3、更广的适用性。async函数的await命令后面,可以是Promise对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作) 4、返回值是Promise。
  • async函数的实现原理,就是将Generator函数和自动执行器,包装在一个函数里。async函数用法:async函数返回一个Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
  • async函数有多种使用形式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 函数声明
    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 () => {};
  • 只要一个await语句后面的Promise变为reject,那么整个async函数都会中断执行。如果有多个await命令,可以统一放在try...catch结构中。

Class的基本语法和Class的继承

  • ES6的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。(注意:1、定义“类”的方法的时候,前面不需要加上function这个关键字 2、ES6的类,完全可以看作构造函数的另一种写法。 class Point {} typeof Point // “function” Point === Point.prototype.constructor // true 上面代码表明,类的数据类型就是函数,类本身就指向构造函数 3、构造函数的prototype属性,在ES6的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面 4、类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式)
  • constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。类必须使用new调用,否则会报错。
  • 与函数一样,类也可以使用表达式的形式定义。采用Class表达式,可以写出立即执行的Class。类不存在变量提升(hoist),这一点与ES5完全不同。
  • this的指向:类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。(constructor() { this.printName = this.printName.bind(this);})
  • 私有方法是常见需求,但ES6不提供。与私有方法一样,ES6不支持私有属性。目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。
  • 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。父类的静态方法,可以被子类继承。静态方法也是可以从super对象上调用的。
  • ES6明确规定,Class内部只有静态方法,没有静态属性。目前有一个静态属性的提案,对实例属性和静态属性都规定了新的写法。
  • new是从构造函数生成实例对象的命令。ES6为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。(注意,在函数外部,使用new.target会报错)
  • Class可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。子类必须在constructor方法中调用super方法,否则新建实例时会报错。如果子类没有定义constructor方法,这个方法会被默认添加。
  • super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。第一种情况,super作为函数调用时,代表父类的构造函数。ES6要求,子类的构造函数必须执行一次super函数。第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。 ES6规定,通过super调用父类的方法时,方法内部的this指向当前的子类实例。(由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字)
  • 类的prototype属性和proto属性:大多数浏览器的ES5实现之中,每一个对象都有proto属性,指向对应的构造函数的prototype属性。Class作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。1、子类的proto属性,表示构造函数的继承,总是指向父类 2、子类prototype属性的proto属性,表示方法的继承,总是指向父类的prototype属性
  • 原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript的原生构造函数大致有下面这些:Boolean()、Number()、String()、Array()、Date()、Function()、RegExp()、Error()、Object()
    //ES5中这种最简单的继承,实质上就是将子类的原型设置为父类的实例

    //ES6和ES5的继承是一模一样的,只是多了class和extends ,ES6的子类和父类,子类原型和父类原型,通过proto连接

修饰器

  • 修饰器:许多面向对象的语言都有修饰器(Decorator)函数,用来修改类的行为。目前,有一个提案将这项功能,引入了ECMAScript。修饰器本质就是编译时执行的函数
  • 类的修饰:修饰器是一个对类进行处理的函数。修饰器函数的第一个参数,就是所要修饰的目标类。
  • 方法的修饰:修饰器第一个参数是类的原型对象,下面的&#64log修饰器,可以起到输出日志的作用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //log修饰器的作用就是在执行原始的操作之前,执行一次console.log,从而达到输出日志的目的。
    class Math {
    @log
    add(a, b) {
    return a + b;
    }
    }

    function log(target, name, descriptor) {
    var oldValue = descriptor.value;

    descriptor.value = function() {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(null, arguments);
    };

    return descriptor;
    }

    const math = new Math();

    // passed parameters should get logged now
    math.add(2, 4);
  • 如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。从长期来看,修饰器将是JavaScript代码静态分析的重要工具。

  • 修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。
  • core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。如:@autobind、@readonly、@override、@deprecate、@suppressWarnings

Module的语法和Module的加载实现

  • ES6模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量(编译时加载、静态加载)。CommonJS和AMD模块,都只能在运行时确定这些东西(运行时加载、动态加载)。
  • export命令实质是,在接口名与模块内部变量之间,建立了一一对应的关系。export命令可以出现在模块的任何位置,只要处于模块顶层就可以。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //下面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。
    // 写法一
    export var m = 1;
    // 写法二
    var m = 1;
    export {m};
    // 写法三
    var n = 1;
    export {n as m};

    //下面两种写法都会报错,因为没有提供对外的接口。
    // 报错
    export 1;
    // 报错
    var m = 1;
    export m;
  • export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。这一点与CommonJS规范完全不同。CommonJS模块输出的是值的缓存,不存在动态更新。(与数据类型有关?)

    1
    2
    3
    //下面代码输出变量foo,值为bar,500毫秒之后变成baz。
    export var foo = 'bar';
    setTimeout(() => foo = 'baz', 500);
  • 为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出(这时import命令后面,不使用大括号)。export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // modules.js
    function add(x, y) {
    return x * y;
    }
    export {add as default};
    // 等同于
    // export default add;

    // app.js
    import { default as foo } from 'modules';
    // 等同于
    // import foo from 'modules';
  • import命令中大括号里面的变量名,必须与被导入模块对外接口的名称相同。import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。(1、import命令具有提升效果,会提升到整个模块的头部,首先执行 2、由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构)
    import语句会执行所加载的模块,因此可以有下面的写法。 import 'lodash'; 上面代码仅仅执行lodash模块,但是不输入任何值。 如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。import除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

  • export与import的复合写法:如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。(模块的继承、跨模块常量)
  • 有一个提案,建议引入import()函数,完成动态加载。import()返回一个Promise对象。import()类似于Node的require方法,区别主要是前者是异步加载,后者是同步加载。
  • defer与async的区别是:defer要等到整个页面在内存中正常渲染结束(DOM结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。
  • 浏览器加载ES6模块,也使用<script>标签,但是要加入type=”module”属性。浏览器对于带有type=”module”的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。如果网页有多个<script type=”module”>,它们会按照在页面出现的顺序依次执行。<script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。一旦使用了async属性,<script type=”module”>就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。
  • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在ES6模块之中。
  • JS引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用(对它进行重新赋值会报错)。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6的import有点像Unix系统的“符号连接”,原始值变了,import加载的值也会跟着变。
  • Node对ES6模块的处理比较麻烦,因为它有自己的CommonJS模块格式,与ES6模块格式是不兼容的。目前的解决方案是,将两者分开,ES6模块和CommonJS采用各自的加载方案。Node要求ES6模块采用.mjs后缀文件名。 Node的import命令是异步加载,这一点与浏览器的处理方法相同。 ES6模块应该是通用的,同一个模块不用修改,就可以用在浏览器环境和服务器环境。
  • ES6模块加载CommonJS模块:CommonJS模块的输出都定义在module.exports这个属性上面。Node的import命令加载CommonJS模块,Node会自动将module.exports属性,当作模块的默认输出,即等同于export default xxx。
  • CommonJS模块加载ES6模块:CommonJS模块加载ES6模块,不能使用require命令,而要使用import()函数。ES6模块的所有输出接口,会成为输入对象的属性。
  • CommonJS模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。ES6处理“循环加载”与CommonJS有本质的不同。ES6模块是动态引用,如果使用import从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

ArrayBuffer

  • ArrayBuffer对象、TypedArray视图和DataView视图是JavaScript操作二进制数据的一个接口。这些对象早就存在,属于独立的规格,ES6将它们纳入了ECMAScript规格,并且增加了新的方法。它们都是以数组的语法处理二进制数据,所以统称为二进制数组。 (这个接口的原始设计目的,与WebGL项目有关)
  • 二进制数组由三类对象组成—ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。TypedArray视图:共包括9种类型的视图;DataView视图:可以自定义复合格式的视图。简单说,ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据
  • 很多浏览器操作的API,用到了二进制数组操作二进制数据,下面是其中的几个:File API、XMLHttpRequest、Fetch API、Canvas、WebSockets。(二进制数组并不是真正的数组,而是类似数组的对象)
  • ArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。
  • ArrayBuffer也是一个构造函数,可以分配一段可以存放数据的连续内存区域。ArrayBuffer构造函数的参数是所需要的内存大小(单位字节)。每个字节的值默认都是0。TypedArray视图,与DataView视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。TypedArray视图的构造函数,除了接受ArrayBuffer实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值。
  • 如果想从任意字节开始解读ArrayBuffer对象,必须使用DataView视图,因为TypedArray视图只提供9种固定的解读格式。(TypedArray的构造函数有多种用法)
  • TypedArray数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。 (ArrayBuffer与字符串的互相转换)
  • TypedArray数组也可以转换回普通数组,1、const normalArray = […typedArray]; 2、const normalArray = Array.from(typedArray); 3、const normalArray = Array.prototype.slice.call(typedArray)。 (普通数组的操作方法和属性,对TypedArray数组完全适用)
  • 字节序:所有个人电脑几乎都是小端字节序,所以TypedArray数组内部也采用小端字节序读写数据,或者更准确的说,按照本机操作系统设定的字节序读写数据。这并不意味大端字节序不重要,事实上,很多网络设备和特定的操作系统采用的是大端字节序。这就带来一个严重的问题:如果一段数据是大端字节序,TypedArray数组将无法正确解析,因为它只能处理小端字节序!为了解决这个问题,JavaScript引入DataView对象,可以设定字节序。
  • 溢出:TypedArray数组的溢出处理规则,简单来说,就是抛弃溢出的位,然后按照视图类型进行解释。
  • TypedArray数组的所有构造函数,都有一个静态方法of,用于将参数转为一个TypedArray实例。静态方法from接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的TypedArray实例。
  • DataView视图提供更多操作选项,而且支持设定字节序。本来,在设计目的上,ArrayBuffer对象的各种TypedArray视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。
  • DataView实例提供8个方法读取内存、8个方法写入内存。
  • 二进制数组的应用(大量的Web API用到了ArrayBuffer对象和它的视图对象):1、AJAX–传统上,服务器通过AJAX操作只能返回文本数据,即responseType属性默认为text。XMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob。 2、Canvas:网页Canvas元素输出的二进制像素数据,就是TypedArray数组。 3、WebSocket:WebSocket可以通过ArrayBuffer,发送或接收二进制数据。 4、Fetch API:Fetch API取回的数据,就是ArrayBuffer对象。 5、File API:如果知道一个文件的二进制数据类型,也可以将这个文件读取为ArrayBuffer对象。

编程风格和ECMAScript规格

  • 使用扩展运算符(…)拷贝数组。使用Array.from方法,将类似数组的对象转为数组。
  • 那些需要使用函数表达式的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了this。
  • 注意区分Object和Map,只有模拟现实世界的实体对象时,才使用Object。如果只是需要key: value的数据结构,使用Map结构。因为Map有内建的遍历机制。
  • 总是用Class,取代需要prototype的操作。因为Class的写法更简洁,更易于理解。
  • 如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export default,export default与普通的export不要同时使用。
  • 规格文件是计算机语言的官方标准,详细描述语法规则和实现方法。规格是解决问题的“最后一招”。

Comments

去留言
2018-02-26

⬆︎TOP