JavaScript:ES6的一些习惯

ECMAScript 6 入门
这篇文章不错,下面的只是随手记。

块级作用域

  • 能用let的地方,就不要用var,可以解决i异常的问题。

  • 少用立即执行函数表达式(IIFE)

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

  • 允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。

  • const的作用域与let命令相同:只在声明所在的块级作用域内有效。

  • const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。提倡多用const,安全性高。

  • const一般用于基本类型,对象类型要慎用,可能达不到预期的效果。因为本质是地址不变,地址指向的内容不能保证不变。

  • 对象的冻结,应该用Object.freeze方法,采用递归的方法,将对象和属性都冻结。

let constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};
  • let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。多用这些关键字来声明变量,减少对window顶层对象的影响。

解构赋值

主要用在数组和对象上,比较方便。以下几个方面推荐使用:

  • 输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");
  • 遍历Map结构
var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}
// first is hello
// second is world

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}
  • 提取JSON数据
let jsonData = {
  id: 42,
  status: "OK",
  data: [867, 5309]
};

let { id, status, data: number } = jsonData;  // data是模式匹配,number是变量

console.log(id, status, number);
// 42, "OK", [867, 5309]
  • 从函数返回多个值
// 返回一个数组

function example() {
  return [1, 2, 3];
}
let [a, b, c] = example();

// 返回一个对象

function example() {
  return {
    foo: 1,
    bar: 2
  };
}
let { foo, bar } = example();
  • 函数参数的默认值
jQuery.ajax = function (url, {
  async = true,
  beforeSend = function () {},
  cache = true,
  complete = function () {},
  crossDomain = false,
  global = true,
  // ... more config
}) {
  // ... do stuff
};
  • 交换变量的值
let x = 1;
let y = 2;

[x, y] = [y, x];

Unicode 表示法

  • 将将码点放入大括号,可以表示4字节的Unicode字符
'\z' === 'z'     // true
'\172' === 'z'   // true
'\x7A' === 'z'   // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true

'\u{1F680}' === '\uD83D\uDE80'  // true 这是老的表示方法,现状可以不用了,直接用{}方便
  • String.fromCharCode方法,用于从码点返回对应字符;(类方法)codePointAt方法会正确返回32位的UTF-16字符的码点(实例方法)。这两个方法是一对的,支持4字节的Unicode字符。
var s = '𠮷a';
s.codePointAt(0).toString(16) // "20bb7";𠮷
s.codePointAt(2).toString(16) // "61" ; a;参数序号仍然不对,这是缺点

String.fromCodePoint(0x20BB7)    // "𠮷"
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'    // true
  • 字符串增加了遍历器接口,可以被for...of循环遍历,并且可以识别大于0xFFFF的码点(4字节),传统的for循环无法识别这样的码点。
var text = String.fromCodePoint(0x20BB7);

for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "
// " "

for (let i of text) {
  console.log(i);
}
// "𠮷"

字符串新增方法

  • 判断包含,开头,结尾。以前只有indexOf,现在有新方法
    includes():返回布尔值,表示是否找到了参数字符串。
    startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
    endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。
var s = 'Hello world!';

s.startsWith('Hello') // true
s.endsWith('!')       // true
s.includes('o')       // true
  • repeat方法返回一个新字符串,表示将原字符串重复n次。
'x'.repeat(3)     // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0)     // ""
  • 模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。这个在格式化输出的时候很有用,推荐使用。
// 普通字符串
`In JavaScript '\n' is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

console.log(`string text line 1
string text line 2`);

// 字符串中嵌入变量
var name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
  • String.raw()转义字符串,可以少写一个\;也可以理解为输出原来的样子,用在模板字符串,format格式化输出中。
String.raw `Hi\n!`;                 
// "Hi\\n!",这里得到的不是 Hi 后面跟个换行符,而是跟着 \ 和 n 两个字符

String.raw `Hi\u000A!`;             
// "Hi\\u000A!",同上,这里得到的会是 \、u、0、0、0、A 6个字符,
// 任何类型的转义形式都会失效,保留原样输出,不信你试试.length

let name = "Bob";
String.raw `Hi\n${name}!`;             
// "Hi\\nBob!",内插表达式还可以正常运行

String.raw({raw: "test"}, 0, 1, 2); 
// "t0e1s2t",我认为你通常不需要把它当成普通函数来调用
// String.raw({ raw: ['t','e','s','t'] }, 0, 1, 2);
// String.raw({raw: ['Hi ', '!']}, 5);  // "Hi 5!"
// 这个比较难理解,不要这样用

Number新增特性

  • ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)0o(或0O)表示。
0b111110111 === 503 // true
0o767 === 503 // true
  • 如果要将0b和0o前缀的字符串数值转为十进制,要使用Number方法。
Number('0b111');  // 7
Number('0o10');   // 8
  • Number.isFinite()用来检查一个数值是否为有限的(finite)
Number.isFinite(15);           // true
Number.isFinite(0.8);          // true
Number.isFinite(NaN);          // false
Number.isFinite(Infinity);     // false
Number.isFinite(-Infinity);    // false
Number.isFinite('foo');        // false
Number.isFinite('15');         // false
Number.isFinite(true);         // false
  • Number.isNaN()用来检查一个值是否为NaN
Number.isNaN(NaN)             // true
Number.isNaN(15)              // false
Number.isNaN('15')            // false
Number.isNaN(true)            // false
Number.isNaN(9/NaN)           // true
Number.isNaN('true'/0)        // true
Number.isNaN('true'/'true')   // true
  • ES6将全局方法parseInt()parseFloat(),移植到Number对象上面,行为完全保持不变。这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。
// ES5的写法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45

// ES6的写法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45
  • Number.isInteger()用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值。
Number.isInteger(25)   // true
Number.isInteger(25.0) // true;这个要注意
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false
  • ES6Number对象上面,新增一个极小的常量Number.EPSILON。引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。但是如果这个误差能够小于Number.EPSILON,我们就可以认为得到了正确结果。
0.1 + 0.2
// 0.30000000000000004

0.1 + 0.2 - 0.3
// 5.551115123125783e-17

5.551115123125783e-17.toFixed(20)
// '0.00000000000000005551'
  • ES6引入了Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// true
Number.MAX_SAFE_INTEGER === 9007199254740991
// true

Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
// true
Number.MIN_SAFE_INTEGER === -9007199254740991
// true

Number.isSafeInteger('a')       // false
Number.isSafeInteger(null)      // false
Number.isSafeInteger(NaN)       // false
Number.isSafeInteger(Infinity)  // false
Number.isSafeInteger(-Infinity) // false

Number.isSafeInteger(3)    // true
Number.isSafeInteger(1.2)  // false
Number.isSafeInteger(9007199254740990) // true
Number.isSafeInteger(9007199254740992) // false

Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER)     // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER)     // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false
  • Math.trunc方法用于去除一个数的小数部分,返回整数部分。
Math.trunc(4.1)      // 4
Math.trunc(4.9)      // 4
Math.trunc(-4.1)     // -4
Math.trunc(-4.9)     // -4
Math.trunc(-0.1234)  // -0

// 对于非数值,Math.trunc内部使用Number方法将其先转为数值。
Math.trunc('123.456')
// 123

// 对于空值和无法截取整数的值,返回NaN。
Math.trunc(NaN);      // NaN
Math.trunc('foo');    // NaN
Math.trunc();         // NaN
  • Math.sign方法用来判断一个数到底是正数、负数、还是零。
Math.sign(-5)        // -1;负数
Math.sign(5)         // +1;正数
Math.sign(0)         // +0
Math.sign(-0)        // -0
Math.sign(NaN)       // NaN
Math.sign('foo');    // NaN
Math.sign();         // NaN
  • Math.cbrt方法用于计算一个数的立方根。
Math.cbrt(-1) // -1
Math.cbrt(0)  // 0
Math.cbrt(1)  // 1
Math.cbrt(2)  // 1.2599210498948734

// 对于非数值,Math.cbrt方法内部也是先使用Number方法将其转为数值。
Math.cbrt('8')     // 2
Math.cbrt('hello') // NaN
  • Math.hypot方法返回所有参数的平方和的平方根。
Math.hypot(3, 4);        // 5;勾三股四玄五
Math.hypot(3, 4, 5);     // 7.0710678118654755
Math.hypot();            // 0
Math.hypot(NaN);         // NaN
Math.hypot(3, 4, 'foo'); // NaN
Math.hypot(3, 4, '5');   // 7.0710678118654755
Math.hypot(-3);          // 3
  • ES2016 新增了一个指数运算符(**)。指数运算符可以与等号结合,形成一个新的赋值运算符(**=)
2 ** 2 // 4
2 ** 3 // 8

let a = 1.5;
a **= 2;
// 2.25;等同于 a = a * a; 

let b = 4;
b **= 3;
// 64; 等同于 b = b * b * b;

Array新增特性

  • Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。这个方法很有用,数组有map方法,这个可是函数式编程的重点内容啊。
let arrayLike = {
    '0': 'a',
    '1': 'b',
    '2': 'c',
    length: 3
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
let arr2 = Array.from(arrayLike);    // ['a', 'b', 'c']

// 实际应用中,常见的类似数组的对象是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);
  // ...
}

// 只要是部署了Iterator接口的数据结构,Array.from都能将其转为数组。
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']

let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']

// 所谓类似数组的对象,本质特征只有一点,即必须有length属性。
Array.from({ length: 3 });
// [ undefined, undefined, undefined ]

// Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

// Array.from()的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种Unicode字符,可以避免JavaScript将大于\uFFFF的Unicode字符,算作两个字符的bug。
function countSymbols(string) {
  return Array.from(string).length;
}
  • 扩展运算符(...)也可以将某些数据结构转为数组。这个也是比较方便的,可以多用。比如,函数定义直接写成function foo(...args) {}
// arguments对象
function foo() {
  var args = [...arguments];
}

// NodeList对象
[...document.querySelectorAll('div')]
  • Array.of方法用于将一组值,转换为数组。基本上可以用来替代Array()new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。这个函数可以多用用。
Array.of()          // []
Array.of(undefined) // [undefined]
Array.of(1)         // [1]
Array.of(1, 2)      // [1, 2]
  • 数组实例的find()和findIndex(),用来查找元素,可以和原先的indexOf()按照使用场景,灵活使用。
// find 返回元素
[1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;
}) // 10

// findeIndex返回下标
[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

// indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。
[NaN].indexOf(NaN)
// -1

[NaN].findIndex(y => Object.is(NaN, y))
// 0
  • fill方法使用给定值,填充一个数组。在初始化数组的时候需要用,减少一些for循环
['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]
  • entries(),keys()和values()——用于遍历数组,结合for...of一起使用
for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let elem of ['a', 'b'].values()) {
  console.log(elem);
}
// 'a'
// 'b'

for (let [index, elem] of ['a', 'b'].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"
  • Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。该方法属于ES7,但Babel转码器已经支持。chrom浏览器可以使用。
    有这个之后,检查元素的存在与否,语义就更直观了,推荐使用。
[1, 2, 3].includes(2);     // true
[1, 2, 3].includes(4);     // false
[1, 2, NaN].includes(NaN); // true
  • 数组的空位指,数组的某一个位置没有任何值。注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。
0 in [undefined, undefined, undefined] // true
0 in [, , ,]                           // false
  • ES6则是明确将空位转为undefined。由于空位的处理规则非常不统一,所以建议避免出现空位。统一用undefined代替空位是个好习惯。
// Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。
Array.from(['a',,'b'])
// [ "a", undefined, "b" ]

// 扩展运算符(...)也会将空位转为undefined。
[...['a',,'b']]
// [ "a", undefined, "b" ]

// entries()、keys()、values()、find()和findIndex()会将空位处理成undefined。
// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]

// keys()
[...[,'a'].keys()] // [0,1]

// values()
[...[,'a'].values()] // [undefined,"a"]

// find()
[,'a'].find(x => true) // undefined

// findIndex()
[,'a'].findIndex(x => true) // 0

函数新增特性

  • ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
  • 参数默认值可以与解构赋值的默认值,结合起来使用。
function fetch(url, { body = '', method = 'GET', headers = {} }) {
  console.log(method);
}

fetch('http://example.com', {})
// "GET"

fetch('http://example.com')
// 报错

上面的写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。

function fetch(url, { method = 'GET' } = {}) {
  console.log(method);
}

fetch('http://example.com')
// "GET"
  • 通常情况下,定义了默认值的参数,应该是函数的尾参数。

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

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
  • 注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。函数的length属性,不包括rest参数。
// 报错
function f(a, ...b, c) {
  // ...
}

(function(a) {}).length  // 1
(function(...a) {}).length  // 0
(function(a, ...b) {}).length  // 1
  • 扩展运算符(spread)是三个点(...)。它好比rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。该运算符主要用于函数调用。
function push(array, ...items) {
  array.push(...items);
}

function add(x, y) {
  return x + y;
}

var numbers = [4, 38];
add(...numbers) // 42

// 扩展运算符与正常的函数参数可以结合使用,非常灵活。
function f(v, w, x, y, z) { }
var args = [0, 1];
f(-1, ...args, 2, ...[3]);
  • 由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。
// ES5的写法
Math.max.apply(null, [14, 3, 77])

// ES6的写法
Math.max(...[14, 3, 77])

// 等同于
Math.max(14, 3, 77);
// ES5的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES6的写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
arr1.push(...arr2);
  • 函数的name属性,返回该函数的函数名。
function foo() {}
foo.name // "foo"
  • ES6允许使用“箭头”(=>)定义函数。
// 正常函数写法
var result = values.sort(function (a, b) {
  return a - b;
});

// 箭头函数写法
var result = values.sort((a, b) => a - b);
  • 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
    递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
// 一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。
function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

// 如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120

ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。

  • ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

  • ES2017允许函数的最后一个参数有尾逗号(trailing comma)。这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);

对象扩展

  • 属性、函数简洁表示法:keyvalue一样的话,只写一遍
var ms = {};

function getItem (key) {
  return key in ms ? ms[key] : null;
}

function setItem (key, value) {
  ms[key] = value;
}

function clear () {
  ms = {};
}

module.exports = { getItem, setItem, clear };
// 等同于
module.exports = {
  getItem: getItem,
  setItem: setItem,
  clear: clear
};
  • 属性名表达式,用[]包起来
var lastWord = 'last word';

var a = {
  'first word': 'hello',
  [lastWord]: 'world'
};

a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"
  • Object.is用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
  • 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}
  • 扩展运算符可以用于合并两个对象。
let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);

Module 的加载实现

浏览器加载

  • 要加入type="module"属性,效果等同于打开了<script>标签的defer属性。
<script type="module" src="foo.js"></script>
<!-- 等同于 -->
<script type="module" src="foo.js" defer></script>

ES6 模块与 CommonJS 模块的差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

  • CommonJS 模块是运行时加载,ES6模块是编译时输出接口。

ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
ES6的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";
ES6模块之中,顶层的this指向undefined,即不应该在顶层代码使用this。

export 命令

一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。

  • 推荐的做法是在文件最后集中导出,使用者查看更方便
// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};
  • 也可以直接在导出变量的前面加export关键字,这样做比较省事
// profile.js
export var firstName = 'Michael';
export var lastName = 'Jackson';
export var year = 1958;
  • export命令除了输出变量,还可以输出函数或类(class)。通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。
function v1() { ... }
function v2() { ... }

export {
  v1 as streamV1,
  v2 as streamV2,
  v2 as streamLatestVersion
};

import 命令

  • 通常的做法是用一个{}导出具体的变量名或者函数名。只需要导出要用的函数或者变量,用不到的就不用导出了。
// main.js
import {firstName, lastName, year} from './profile';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}
  • 如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。
import { lastName as surname } from './profile';
  • 除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。这种写法不推荐。
// circle.js

export function area(radius) {
  return Math.PI * radius * radius;
}

export function circumference(radius) {
  return 2 * Math.PI * radius;
}
// main.js
import * as circle from './circle';

console.log('圆面积:' + circle.area(4));
console.log('圆周长:' + circle.circumference(14));
  • 为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。需要注意的是,这时import命令后面,不使用大括号。默认导出的函数名或者类名都不起效果,名字由使用者在外面自己起,相当于匿名的函数或者类。这个有点像ES5的习惯,在有限的场景使用,不推荐。
// export-default.js
export default function () {
  console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
  • 如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。
export { foo, bar } from 'my_module';

// 等同于
import { foo, bar } from 'my_module';
export { foo, bar };

跨模块常量

  • 如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。
// constants.js 模块
export const A = 1;
export const B = 3;
export const C = 4;

// test1.js 模块
import * as constants from './constants';
console.log(constants.A); // 1
console.log(constants.B); // 3

// test2.js 模块
import {A, B} from './constants';
console.log(A); // 1
console.log(B); // 3
  • 如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。
// constants/db.js
export const db = {
  url: 'http://my.couchdbserver.local:5984',
  admin_username: 'admin',
  admin_password: 'admin password'
};

// constants/user.js
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];

// constants/index.js
export {db} from './db';
export {users} from './users';

// main.js使用的时候,直接加载index.js就可以了。
import {db, users} from './index';

Set

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

  • Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]

// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
  • Set 结构的实例有以下属性。
Set.prototype.constructor:构造函数,默认就是Set函数。
Set.prototype.size:返回Set实例的成员总数。
  • Set 实例的方法
add(value):添加某个值,返回Set结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为Set的成员。
clear():清除所有成员,没有返回值。
  • 下面是一个对比,看看在判断是否包括一个键上面,Object结构和Set结构的写法不同。
// 对象的写法
const properties = {
  'width': 1,
  'height': 1
};

if (properties[someName]) {
  // do something
}

// Set的写法
const properties = new Set();

properties.add('width');
properties.add('height');

if (properties.has(someName)) {
  // do something
}
  • Array.from方法可以将 Set 结构转为数组。这就提供了去除数组重复成员的另一种方法。
function dedupe(array) {
  return Array.from(new Set(array));
}

dedupe([1, 1, 2, 3]) // [1, 2, 3]
  • 如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。
// 方法一
let set = new Set([1, 2, 3]);
set = new Set([...set].map(val => val * 2));
// set的值是2, 4, 6

// 方法二
let set = new Set([1, 2, 3]);
set = new Set(Array.from(set, val => val * 2));
// set的值是2, 4, 6
  • 扩展运算符(...)可以展开set为数组,也展示了一种去除数组重复成员的方法。
// 去除数组的重复成员
[...new Set(array)]

Map

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

  • size属性返回 Map 结构的成员总数。
const map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
  • set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
const m = new Map();

m.set('edition', 6)        // 键是字符串
m.set(262, 'standard')     // 键是数值
m.set(undefined, 'nah')    // 键是 undefined

set方法返回的是当前的Map对象,因此可以采用链式写法。

let map = new Map()
  .set(1, 'a')
  .set(2, 'b')
  .set(3, 'c');
  • get方法读取key对应的键值,如果找不到key,返回undefined。
const m = new Map();

const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数

m.get(hello)  // Hello ES6!
  • has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
const m = new Map();

m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');

m.has('edition')     // true
m.has('years')       // false
m.has(262)           // true
m.has(undefined)     // true
  • delete方法删除某个键,返回true。如果删除失败,返回false。
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined)     // true

m.delete(undefined)
m.has(undefined)       // false
  • clear方法清除所有成员,没有返回值。
let map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
map.clear()
map.size // 0
  • Map 结构原生提供三个遍历器生成函数和一个遍历方法。
    keys():返回键名的遍历器。
    values():返回键值的遍历器。
    entries():返回所有成员的遍历器。
    forEach():遍历 Map 的所有成员。
const map = new Map([
  ['F', 'no'],
  ['T',  'yes'],
]);

for (let key of map.keys()) {
  console.log(key);
}
// "F"
// "T"

for (let value of map.values()) {
  console.log(value);
}
// "no"
// "yes"

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"

// 或者
for (let [key, value] of map.entries()) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value);
}
// "F" "no"
// "T" "yes"

Class 的基本语法

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

//定义类
class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }

}

var point = new Point(2, 3);

point.toString() // (2, 3)
  • 与 ES5 一样,在“类”的内部可以使用get和set关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。
class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'
  • 如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。
class Foo {
  constructor(...args) {
    this.args = args;
  }
  * [Symbol.iterator]() {
    for (let arg of this.args) {
      yield arg;
    }
  }
}

for (let x of new Foo('hello', 'world')) {
  console.log(x);
}
// hello
// world
  • 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo {
  static classMethod() {
    return 'hello';
  }
}

Foo.classMethod() // 'hello'

var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
  • 类的实例属性可以用等式,写入类的定义之中。
class MyClass {
  myProp = 42;

  constructor() {
    console.log(this.myProp); // 42
  }
}
  • 类的静态属性只要在上面的实例属性写法前面,加上static关键字就可以了。
class MyClass {
  static myStaticProp = 42;

  constructor() {
    console.log(MyClass.myStaticProp); // 42
  }
}
  • Class 可以通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。
class Point {
}

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

编程风格

  • let 取代 var;
    在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。
// bad
var a = 1, b = 2, c = 3;

// good
const a = 1;
const b = 2;
const c = 3;

// best
const [a, b, c] = [1, 2, 3];
  • 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
// bad
const a = "foobar";
const b = 'foo' + a + 'bar';

// acceptable
const c = `foobar`;

// good
const a = 'foobar';
const b = `foo${a}bar`;
const c = 'foobar';

解构赋值

  • 使用数组成员对变量赋值时,优先使用解构赋值。
const arr = [1, 2, 3, 4];

// bad
const first = arr[0];
const second = arr[1];

// good
const [first, second] = arr;
  • 函数的参数如果是对象的成员,优先使用解构赋值。
// bad
function getFullName(user) {
  const firstName = user.firstName;
  const lastName = user.lastName;
}

// good
function getFullName(obj) {
  const { firstName, lastName } = obj;
}

// best
function getFullName({ firstName, lastName }) {
}
  • 如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。
// bad
function processInput(input) {
  return [left, right, top, bottom];
}

// good
function processInput(input) {
  return { left, right, top, bottom };
}

const { left, right } = processInput(input);

对象

  • 单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
// bad
const a = { k1: v1, k2: v2, };
const b = {
  k1: v1,
  k2: v2
};

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

// if reshape unavoidable
const a = {};
Object.assign(a, { x: 3 });

// good
const a = { x: null };
a.x = 3;
  • 如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。
// bad
const obj = {
  id: 5,
  name: 'San Francisco',
};
obj[getKey('enabled')] = true;

// good
const obj = {
  id: 5,
  name: 'San Francisco',
  [getKey('enabled')]: true,
};
  • 对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。
var ref = 'some value';

// bad
const atom = {
  ref: ref,

  value: 1,

  addValue: function (value) {
    return atom.value + value;
  },
};

// good
const atom = {
  ref,

  value: 1,

  addValue(value) {
    return atom.value + value;
  },
};

数组

  • 使用扩展运算符(...)拷贝数组。
// bad
const len = items.length;
const itemsCopy = [];
let i;

for (i = 0; i < len; i++) {
  itemsCopy[i] = items[i];
}

// good
const itemsCopy = [...items];
  • 使用Array.from方法,将类似数组的对象转为数组。
const foo = document.querySelectorAll('.foo');
const nodes = Array.from(foo);

函数

  • 立即执行函数可以写成箭头函数的形式。
(() => {
  console.log('Welcome to the Internet.');
})();
  • 那些需要使用函数表达式的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了this。
// bad
[1, 2, 3].map(function (x) {
  return x * x;
});

// good
[1, 2, 3].map((x) => {
  return x * x;
});

// best
[1, 2, 3].map(x => x * x);
  • 箭头函数取代Function.prototype.bind,不应再用self/_this/that绑定 this。
// bad
const self = this;
const boundMethod = function(...params) {
  return method.apply(self, params);
}

// acceptable
const boundMethod = method.bind(this);

// best
const boundMethod = (...params) => method.apply(this, params);
  • 所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。
// bad
function divide(a, b, option = false ) {
}

// good
function divide(a, b, { option = false } = {}) {
}
  • 不要在函数体内使用arguments变量,使用rest运算符(...)代替。因为rest运算符显式表明你想要获取参数,而且arguments是一个类似数组的对象,而rest运算符可以提供一个真正的数组。
// bad
function concatenateAll() {
  const args = Array.prototype.slice.call(arguments);
  return args.join('');
}

// good
function concatenateAll(...args) {
  return args.join('');
}
  • 使用默认值语法设置函数参数的默认值。
// bad
function handleThings(opts) {
  opts = opts || {};
}

// good
function handleThings(opts = {}) {
  // ...
}

Map结构

注意区分Object和Map,只有模拟现实世界的实体对象时,才使用Object。如果只是需要key: value的数据结构,使用Map结构。因为Map有内建的遍历机制。

let map = new Map(arr);

for (let key of map.keys()) {
  console.log(key);
}

for (let value of map.values()) {
  console.log(value);
}

for (let item of map.entries()) {
  console.log(item[0], item[1]);
}

Class

  • 总是用Class,取代需要prototype的操作。因为Class的写法更简洁,更易于理解。
// bad
function Queue(contents = []) {
  this._queue = [...contents];
}
Queue.prototype.pop = function() {
  const value = this._queue[0];
  this._queue.splice(0, 1);
  return value;
}

// good
class Queue {
  constructor(contents = []) {
    this._queue = [...contents];
  }
  pop() {
    const value = this._queue[0];
    this._queue.splice(0, 1);
    return value;
  }
}
  • 使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。
// bad
const inherits = require('inherits');
function PeekableQueue(contents) {
  Queue.apply(this, contents);
}
inherits(PeekableQueue, Queue);
PeekableQueue.prototype.peek = function() {
  return this._queue[0];
}

// good
class PeekableQueue extends Queue {
  peek() {
    return this._queue[0];
  }
}

模块

  • 使用import取代require。
// bad
const moduleA = require('moduleA');
const func1 = moduleA.func1;
const func2 = moduleA.func2;

// good
import { func1, func2 } from 'moduleA';
  • 使用export取代module.exports
    如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export default,export default与普通的export不要同时使用。
// commonJS的写法
var React = require('react');

var Breadcrumbs = React.createClass({
  render() {
    return <nav />;
  }
});

module.exports = Breadcrumbs;

// ES6的写法
import React from 'react';

class Breadcrumbs extends React.Component {
  render() {
    return <nav />;
  }
};

export default Breadcrumbs;
  • 不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
// bad
import * as myObject './importModule';

// good
import myObject from './importModule';
  • 如果模块默认输出一个函数,函数名的首字母应该小写。
function makeStyleGuide() {
}

export default makeStyleGuide;
  • 如果模块默认输出一个对象,对象名的首字母应该大写。
const StyleGuide = {
  es6: {
  }
};

export default StyleGuide;

ESLint的使用

  • 安装ESLint。
$ npm i -g eslint
  • 安装Airbnb语法规则。
$ npm i -g eslint-config-airbnb
  • 在项目的根目录下新建一个.eslintrc文件,配置ESLint。
{
  "extends": "eslint-config-airbnb"
}
  • 使用ESLint检查文件。
$ eslint xxx.js
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容

  • 一、ES6简介 ​ 历时将近6年的时间来制定的新 ECMAScript 标准 ECMAScript 6(亦称 ...
    一岁一枯荣_阅读 5,961评论 8 25
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,152评论 0 4
  • 人生能有几个十年?即使能够活到百岁,也正如杨绛先生所说的那样:“少年贪玩,青年迷恋爱情,壮年汲汲于成名成家,暮年自...
    方素衣阅读 607评论 3 50
  • 作为子女,要学规矩,其实这两句话全完是从夫人哪里学到的,去年确定了恋爱关系,大年初三,就去夫人家了,一家人对于我的...
    可乐少年c阅读 1,622评论 0 2
  • 每年的毕业季,都会让人感触颇深。你我不约而同想到自己的毕业时光 。我们真的是有默契,不是吗? 毕业嘛,多少都会有伤...
    亦大乎阅读 337评论 0 0