[关闭]
@xiaoqq 2016-08-29T12:07:58.000000Z 字数 16599 阅读 1092

ECMAScript 6 学习笔记

JavaScript


一、ECMAScript 6 简介

ECMAScript和JavaScript的关系:前者是后者的规格,后者是前者的一种实现(另外的ECMAScript方言还有Jscript和ActionScript)。日常场合,这两个词是可以互换的。

Babel转码器:

Babel是一个广泛使用的ES6转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。这意味着,你可以用ES6的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。

Babel的配置文件是.babelrc

Babel提供babel-cli工具,用于命令行转码

二、let和const命令

let

  1. let用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。
  2. for循环的计数器,就很合适使用let命令
  3. let不存在变量提升

    1. console.log(foo); // 输出undefined
    2. console.log(bar); // 报错ReferenceError
    3. var foo = 2;
    4. let bar = 2;
  4. 暂时性死区(temporal dead zone,简称TDZ)
    暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

    1. var tmp = 123;
    2. if (true) {
    3. tmp = 'abc'; // ReferenceError
    4. let tmp;
    5. }
  5. 不允许重复声明

块级作用域

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

  2. ES5严格模式下,不允许在块级作用域下声明函数。而ES6中可以,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。

    考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

Const命令

const声明一个只读的常量。一旦声明,常量的值就不能改变。

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

const与let类似,声明不提升,不可重复声明,只在块级作用域内有效。

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

三、变量的解构赋值

数组解析结构赋值

ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。

  1. \\ES5
  2. var a = 1;
  3. var b = 2;
  4. var c = 3;
  5. \\ES6
  6. var [a, b, c] = [1, 2, 3];

默认值

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

  1. var [foo = true] = [];
  2. foo // true
  3. [x, y = 'b'] = ['a']; // x='a', y='b'
  4. [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

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

对象的结构赋值

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

  1. var { bar, foo } = { foo: "aaa", bar: "bbb" };
  2. foo // "aaa"
  3. bar // "bbb"
  4. var { baz } = { foo: "aaa", bar: "bbb" };
  5. baz // undefined

字符串的解构赋值

  1. const [a, b, c, d, e] = 'hello';
  2. a // "h"
  3. b // "e"
  4. c // "l"
  5. d // "l"
  6. e // "o"

数值和字符串的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

  1. let {toString: s} = 123;
  2. s === Number.prototype.toString // true
  3. let {toString: s} = true;
  4. s === Boolean.prototype.toString // true

函数的解构赋值

函数的参数也可以使用解构赋值。

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

圆括号的问题

ES6的规则是,只要有可能导致解构的歧义,就不得使用圆括号。

用途

  1. 交换变量的值:[x, y] = [y, x];
  2. 从函数返回多个值: var [a, b, c] = example();
  3. 函数参数的定义
  4. 提取JSON格式的数据
  5. 指定函数参数的默认值
  6. 遍历Map解构

    1. var map = new Map();
    2. map.set('first', 'hello');
    3. map.set('second', 'world');
    4. for (let [key, value] of map) {
    5. console.log(key + " is " + value);
    6. }
    7. // first is hello
    8. // second is world
  7. 输入模块的指定方法

    1. const { SourceMapConsumer, SourceNode } = require("source-map");

四、字符串的扩展

TODOs...

includes(), startsWith(), endsWith()

传统上,JavaScript只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6又提供了三种新方法。

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

repeat()

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

  1. 'x'.repeat(3) // "xxx"
  2. 'hello'.repeat(2) // "hellohello"
  3. 'na'.repeat(0) // ""

padStart(),padEnd()

ES7推出了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart用于头部补全,padEnd用于尾部补全。

  1. 'x'.padStart(5, 'ab') // 'ababx'
  2. 'x'.padStart(4, 'ab') // 'abax'
  3. 'x'.padEnd(5, 'ab') // 'xabab'
  4. 'x'.padEnd(4, 'ab') // 'xaba'

模板字符串

ES6引入了模板字符串:

  1. $('#result').append(`
  2. There are <b>${basket.count}</b> items
  3. in your basket, <em>${basket.onSale}</em>
  4. are on sale!
  5. `);

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

五、正则的扩展

TODOs...

六、数值的扩展

二进制和八进制表示法

ES6提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。

Number.isFinite(), Number.isNaN()

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

Number.isInteger()

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

Number.EPSILON

ES6在Number对象上面,新增一个极小的常量Number.EPSILON。引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。

Math扩展

Math.trunc()

Math.trunc方法用于去除一个数的小数部分,返回整数部分。

Math.sign()

Math.sign方法用来判断一个数到底是正数、负数、还是零。它会返回五种值:+1/-1/+0/-0/NaN

七、数组的扩展

Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。

  1. let arrayLike = {
  2. '0': 'a',
  3. '1': 'b',
  4. '2': 'c',
  5. length: 3
  6. };
  7. // ES5的写法
  8. var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
  9. // ES6的写法
  10. let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

Array.of()

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

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

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

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

TODOs...

八、函数的扩展

函数参数的默认值

ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。

  1. function log(x, y = 'World') {
  2. console.log(x, y);
  3. }
  4. log('Hello') // Hello World
  5. log('Hello', 'China') // Hello China
  6. log('Hello', '') // Hello

注意事项:

  1. 与解构赋值默认值结合使用

    1. function foo({x, y = 5}) {
    2. console.log(x, y);
    3. }
    4. foo({}) // undefined, 5
    5. foo({x: 1}) // 1, 5
    6. foo({x: 1, y: 2}) // 1, 2
    7. foo() // TypeError: Cannot read property 'x' of undefined

    如果函数foo调用时参数不是对象,变量x和y就不会生成,从而报错。如果参数对象没有y属性,y的默认值5才会生效。

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

  3. 函数的length属性
    指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真

  4. 作用域:函数参数的作用域规则与其他的是一样的

rest参数

ES6引入rest参数(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

  1. // arguments变量的写法
  2. function sortNumbers() {
  3. return Array.prototype.slice.call(arguments).sort();
  4. }
  5. // rest参数的写法
  6. const sortNumbers = (...numbers) => numbers.sort();

注意,rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
函数的length属性,不包括rest参数。

扩展运算符

扩展运算符(spread)是三个点(...)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。

  1. console.log(...[1, 2, 3]) // 1 2 3
  2. // ES5的写法
  3. Math.max.apply(null, [14, 3, 77])
  4. // ES6的写法
  5. Math.max(...[14, 3, 77])

应用

  1. 合并数组 [1, 2, ...more]
  2. 与解构赋值结合 [a, ...rest] = list
  3. 函数的返回值。JavaScript的函数只能返回一个值,如果需要返回多个值,只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。
  4. 字符串,将字符串展开成数组:[...'hello'] // [ "h", "e", "l", "l", "o" ]
  5. 实现了Iterator接口的对象

    1. var nodeList = document.querySelectorAll('div');
    2. var array = [...nodeList];
  6. Map和Set结构,Generator函数

箭头函数

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

注意:

  1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

    1. function foo() {
    2. setTimeout(() => {
    3. console.log('id:', this.id);
    4. }, 100);
    5. }
    6. var id = 21;
    7. foo.call({ id: 42 });
    8. // id: 42
    9. // 如果对于一般的函数,返回的是21
  2. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

  3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。

  4. 不可以使用yield命令,因此箭头函数不能用作Generator函数。

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

箭头函数还有一个功能,就是可以很方便地改写λ演算。

  1. // λ演算的写法
  2. fix = λf.(λx.fv.x(x)(v)))(λx.fv.x(x)(v)))
  3. // ES6的写法
  4. var fix = f => (x => f(v => x(x)(v)))
  5. (x => f(v => x(x)(v)));

函数绑定

ES7提出了函数绑定运算符::,用来取代callapplybind调用。

  1. foo::bar;
  2. // 等同于
  3. bar.bind(foo);
  4. foo::bar(...arguments);
  5. // 等同于
  6. bar.apply(foo, arguments);

如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

  1. var method = obj::obj.foo;
  2. // 等同于
  3. var method = ::obj.foo;
  4. let log = ::console.log;
  5. // 等同于
  6. var log = console.log.bind(console);

尾调用优化

尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

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

九、对象的扩展

属性的简洁表示法

ES6允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

  1. var foo = 'bar';
  2. var baz = {foo};
  3. baz // {foo: "bar"}
  4. // 等同于
  5. var baz = {foo: foo};
  6. function f(x, y) {
  7. return {x, y};
  8. }
  9. // 等同于
  10. function f(x, y) {
  11. return {x: x, y: y};
  12. }
  13. f(1, 2) // Object {x: 1, y: 2}

属性名表达式

ES6允许字面量定义对象时,用表达式作为对象的属性名,即把表达式放在方括号内。

  1. let propKey = 'foo';
  2. let obj = {
  3. [propKey]: true,
  4. ['a' + 'bc']: 123
  5. };

方法的name属性

函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。
如果使用了取值函数,则会在方法名前加上get。如果是存值函数,方法名的前面会加上set。

Object.is()

Object.is用来处理“Same-value equality”(同值相等)算法,基本等同于===,有两点不同:
一是+0不等于-0,二是NaN等于自身。

  1. +0 === -0 //true
  2. NaN === NaN // false
  3. Object.is(+0, -0) // false
  4. Object.is(NaN, NaN) // true

Object.assign()

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

Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
Object.assign可以用来处理数组,但是会把数组视为对象。

用途:

  1. 为对象添加属性

    1. class Point {
    2. constructor(x, y) {
    3. Object.assign(this, {x, y});
    4. }
    5. }
  2. 为对象添加方法
  3. 克隆对象
  4. 合并多个对象
  5. 为属性指定默认值

属性的可枚举性

Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

属性的遍历

  1. for...in循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。
  2. Object.keys(obj)
  3. Object.getOwnPropertyNames(obj)
  4. Object.getOwnPropertySymbols(obj)返回一个数组,包含对象自身的所有Symbol属性。
  5. Reflect.ownKeys(obj)返回一个数组,包含对象自身的所有属性,不管是属性名是Symbol或字符串,也不管是否可枚举。

__proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()

__proto__它本质上是一个内部属性,而不是一个正式的对外的API,只是由于浏览器广泛支持,才被加入了ES6。

对象扩展运算符

Rest解构赋值

  1. let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
  2. x // 1
  3. y // 2
  4. z // { a: 3, b: 4 }

十、Symbol

Symbol是JavaScript的第七种数据类型

十一、Proxy和Reflect

Proxy概述

Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程

Proxy可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

Reflect概述

Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新API。Reflect对象的设计目的有这样几个。

  1. 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和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)让它们变成了函数行为。

    1. // 老写法
    2. 'assign' in Object // true
    3. // 新写法
    4. Reflect.has(Object, 'assign') // true
  4. Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。

十二、二进制数组

二进制数组(ArrayBuffer对象、TypedArray视图和DataView视图)是JavaScript操作二进制数据的一个接口。这个接口的原始设计目的,与WebGL项目有关。

它很像C语言的数组,允许开发者以数组下标的形式,直接操作内存,大大增强了JavaScript处理二进制数据的能力,使得开发者有可能通过JavaScript与操作系统的原生接口进行二进制通信。

二进制数组由三类对象组成。

  1. ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。

  2. TypedArray视图:共包括9种类型的视图,比如Uint8Array(无符号8位整数)数组视图, Int16Array(16位整数)数组视图, Float32Array(32位浮点数)数组视图等等。

  3. DataView视图:可以自定义复合格式的视图,比如第一个字节是Uint8(无符号8位整数)、第二、三个字节是Int16(16位整数)、第四个字节开始是Float32(32位浮点数)等等,此外还可以自定义字节序。

用到了二进制数组的浏览器API

  1. AJAX
    传统上,服务器通过AJAX操作只能返回文本数据,即responseType属性默认为text。XMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。

    1. xhr.onreadystatechange = function () {
    2. if (req.readyState === 4 ) {
    3. var arrayResponse = xhr.response;
    4. var dataView = new DataView(arrayResponse);
    5. var ints = new Uint32Array(dataView.byteLength / 4);
    6. xhrDiv.style.backgroundColor = "#00FF00";
    7. xhrDiv.innerText = "Array is " + ints.length + "uints long";
    8. }
    9. }
  2. Canvas
    网页Canvas元素输出的二进制像素数据,就是TypedArray数组。

    1. var canvas = document.getElementById('myCanvas');
    2. var ctx = canvas.getContext('2d');
    3. var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    4. var uint8ClampedArray = imageData.data;
  3. WebSocket
    WebSocket可以通过ArrayBuffer,发送或接收二进制数据。

  4. Fetch API
    Fetch API取回的数据,就是ArrayBuffer对象。

    1. fetch(url)
    2. .then(function(request){
    3. return request.arrayBuffer()
    4. })
    5. .then(function(arrayBuffer){
    6. // ...
    7. });
  5. File API
    如果知道一个文件的二进制数据类型,也可以将这个文件读取为ArrayBuffer对象。

    1. var fileInput = document.getElementById('fileInput');
    2. var file = fileInput.files[0];
    3. var reader = new FileReader();
    4. reader.readAsArrayBuffer(file);
    5. reader.onload = function () {
    6. var arrayBuffer = reader.result;
    7. // ···
    8. };

十三、Set和Map数据结构

Set

ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

Set函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。

Set结构的实例有四个遍历方法,可以用于遍历成员。

  1. let set = new Set(['red', 'green', 'blue']);
  2. for (let item of set.keys()) {
  3. console.log(item);
  4. }
  5. // red
  6. // green
  7. // blue
  8. for (let item of set.values()) {
  9. console.log(item);
  10. }
  11. // red
  12. // green
  13. // blue
  14. for (let item of set.entries()) {
  15. console.log(item);
  16. }
  17. // ["red", "red"]
  18. // ["green", "green"]
  19. // ["blue", "blue"]

Map

JavaScript的对象(Object),本质上是键值对的集合(Hash结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键

  1. var m = new Map();
  2. var o = {p: "Hello World"};
  3. m.set(o, "content")
  4. m.get(o) // "content"
  5. m.has(o) // true
  6. m.delete(o) // true
  7. m.has(o) // false

Map的遍历方法与Set相同

Map与其他数据结构的互相转换

  1. Map转为数组:Map转为数组最方便的方法,就是使用扩展运算符(...)

    1. let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
    2. [...myMap]
    3. // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
  2. 数组转为Map:将数组转入Map构造函数,就可以转为Map。

    1. new Map([[true, 7], [{foo: 3}, ['abc']]])
    2. // Map {true => 7, Object {foo: 3} => ['abc']}
  3. Map转为对象:如果所有Map的键都是字符串,它可以转为对象。

    1. function strMapToObj(strMap) {
    2. let obj = Object.create(null);
    3. for (let [k,v] of strMap) {
    4. obj[k] = v;
    5. }
    6. return obj;
    7. }
    8. let myMap = new Map().set('yes', true).set('no', false);
    9. strMapToObj(myMap)
    10. // { yes: true, no: false }
  4. 对象转为Map

    1. function objToStrMap(obj) {
    2. let strMap = new Map();
    3. for (let k of Object.keys(obj)) {
    4. strMap.set(k, obj[k]);
    5. }
    6. return strMap;
    7. }
    8. objToStrMap({yes: true, no: false})
    9. // [ [ 'yes', true ], [ 'no', false ] ]
  5. Map转为JSON
  6. JSON转为Map

十四、Iterator和for...of循环

十八、Class

ES6中的class只是可以看做一个语法糖即可。

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

  2. 类的构造函数,不使用new是没法调用的,会报错。这是它跟普通构造函数的一个主要区别。

  3. 与ES5一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)

  4. Class**不存在变量提升**(hoist),这一点与ES5完全不同。

    1. new Foo(); // ReferenceError
    2. class Foo {}
  5. 与函数一样,Class也可以使用表达式的形式定义。

  6. ES6不提供私有方法,只能通过变通方法模拟实现。a) 在命名上加以区别;b) 另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的;c) 还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。

    1. const bar = Symbol('bar');
    2. const snaf = Symbol('snaf');
    3. export default subclassFactory({
    4. // 共有方法
    5. foo (baz) {
    6. this[bar](baz);
    7. }
    8. // 私有方法
    9. [bar](baz) {
    10. return this[snaf] = baz;
    11. }
    12. // ...
    13. });
  7. 类和模块的内部,默认就是严格模式。

  8. name属性总是返回紧跟在class关键字后面的类名。

Class的继承

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

  1. class ColorPoint extends Point {}

在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。

  1. class Point {
  2. constructor(x, y) {
  3. this.x = x;
  4. this.y = y;
  5. }
  6. }
  7. class ColorPoint extends Point {
  8. constructor(x, y, color) {
  9. this.color = color; // ReferenceError
  10. super(x, y);
  11. this.color = color; // 正确
  12. }
  13. }

super关键字有两种用法:

原生构造函数的继承

ES6可以自定义原生数据结构(比如Array、String等)的子类,这是ES5无法做到的。因此可以在原生数据结构的基础上,定义自己的数据结构。

Class的静态方法

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

new.target属性

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

Mixin模式的实现

Mixin模式指的是,将多个类的接口“混入”(mix in)另一个类。它在ES6的实现如下。

  1. function mix(...mixins) {
  2. class Mix {}
  3. for (let mixin of mixins) {
  4. copyProperties(Mix, mixin);
  5. copyProperties(Mix.prototype, mixin.prototype);
  6. }
  7. return Mix;
  8. }
  9. function copyProperties(target, source) {
  10. for (let key of Reflect.ownKeys(source)) {
  11. if ( key !== "constructor"
  12. && key !== "prototype"
  13. && key !== "name"
  14. ) {
  15. let desc = Object.getOwnPropertyDescriptor(source, key);
  16. Object.defineProperty(target, key, desc);
  17. }
  18. }
  19. }

上面代码的mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。

  1. class DistributedEdit extends mix(Loggable, Serializable) {
  2. // ...
  3. }

二十、Module

在ES6之前,主要有CommonJS和AMD两种规范,前者基于服务器,后者基于浏览器。NodeJS就是CMD的规范。

ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。

严格模式

ES6的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

export/import命令

模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

  1. // profile.js
  2. var firstName = 'Michael';
  3. var lastName = 'Jackson';
  4. var year = 1958;
  5. export {firstName, lastName, year};

export输出的变量就是本来的名字,但是可以使用as关键字重命名。

export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

  1. // 报错
  2. export 1;
  3. // 报错
  4. var m = 1;
  5. export m;

正确的写法:

  1. // 写法一
  2. export var m = 1;
  3. // 写法二
  4. var m = 1;
  5. export {m};
  6. // 写法三
  7. var n = 1;
  8. export {n as m};

import大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

  1. // main.js
  2. import {firstName, lastName, year} from './profile';
  3. function setName(element) {
  4. element.textContent = firstName + ' ' + lastName;
  5. }

模块的整体加载

除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

export default命令

其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

  1. // export-default.js
  2. export default function () {
  3. console.log('foo');
  4. }
  5. // import-default.js
  6. import customName from './export-default';
  7. customName(); // 'foo'

export default也可以用来输出类。

  1. // MyClass.js
  2. export default class { ... }
  3. // main.js
  4. import MyClass from 'MyClass';
  5. let o = new MyClass();

模块的继承

export * from 'xxx';表示再输出xxx模块的所有属性和方法

ES6模块加载的实质

ES6模块加载的机制,与CommonJS模块完全不同。CommonJS模块输出的是一个值的拷贝,而ES6模块输出的是值的引用。

CommonJS模块输出的是被输出值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。

ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令import时,不会去执行模块,而是只生成一个动态的只读引用等到真的需要用到时,再到模块里面去取值,换句话说,ES6的输入有点像Unix系统的“符号连接”,原始值变了,import输入的值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

循环加载

“循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。
通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。

二十一、编程风格

  1. let取代var

  2. 在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量

  3. 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。

  4. 使用数组成员对变量赋值时,优先使用解构赋值。
    函数的参数如果是对象的成员,优先使用解构赋值

    1. // bad
    2. function getFullName(user) {
    3. const firstName = user.firstName;
    4. const lastName = user.lastName;
    5. }
    6. // good
    7. function getFullName(obj) {
    8. const { firstName, lastName } = obj;
    9. }
    10. // best
    11. function getFullName({ firstName, lastName }) {
    12. }

    如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。

    1. // bad
    2. function processInput(input) {
    3. return [left, right, top, bottom];
    4. }
    5. // good
    6. function processInput(input) {
    7. return { left, right, top, bottom };
    8. }
    9. const { left, right } = processInput(input);
  5. 对象:单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾

    1. // bad
    2. const a = { k1: v1, k2: v2, };
    3. const b = {
    4. k1: v1,
    5. k2: v2
    6. };
    7. // good
    8. const a = { k1: v1, k2: v2 };
    9. const b = {
    10. k1: v1,
    11. k2: v2,
    12. };
  6. 对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。

    1. // bad
    2. const a = {};
    3. a.x = 3;
    4. // if reshape unavoidable
    5. const a = {};
    6. Object.assign(a, { x: 3 });
    7. // good
    8. const a = { x: null };
    9. a.x = 3;
  7. 数组:使用扩展运算符(...)拷贝数组。使用Array.from方法,将类似数组的对象转为数组。

  8. 函数:立即执行函数可以写成箭头函数的形式。简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。

    所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。

    不要在函数体内使用arguments变量,使用rest运算符(...)代替。因为rest运算符显式表明你想要获取参数,而且arguments是一个类似数组的对象,而rest运算符可以提供一个真正的数组。

    1. // bad
    2. function concatenateAll() {
    3. const args = Array.prototype.slice.call(arguments);
    4. return args.join('');
    5. }
    6. // good
    7. function concatenateAll(...args) {
    8. return args.join('');
    9. }

    使用默认值语法设置函数参数的默认值。

    1. // bad
    2. function handleThings(opts) {
    3. opts = opts || {};
    4. }
    5. // good
    6. function handleThings(opts = {}) {
    7. // ...
    8. }
  9. Map结构:注意区分Object和Map,只有模拟现实世界的实体对象时,才使用Object。如果只是需要key: value的数据结构,使用Map结构。因为Map有内建的遍历机制。

  10. Class:总是用Class,取代需要prototype的操作。因为Class的写法更简洁,更易于理解。使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。

  11. 首先,Module语法是JavaScript模块的标准写法,坚持使用这种写法。使用import取代require。使用export取代module.exports。

    如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export default,不要export default与普通的export同时使用。

    如果模块默认输出一个函数,函数名的首字母应该小写。如果模块默认输出一个对象,对象名的首字母应该大写。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注