编码规范

判空

  • object -> true
  • Undefined -> false
  • Null -> false
  • Booleans ->
  • Number -> +0,-0,NaN:false, 其他值:true
  • String -> 空字符串’’ : flase,其他值:true

简化判空操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bad
let a
if(a != null && typeof(a) != undefined && a != ''){
// a有内容时做一些处理
}

// good
if (!!a) {
// a有内容时做一些处理
}

or

if (a) {
// a有内容时做一些处理
}

注意:空对象 {}、空数组 [] 均会返回 true

let、var、const

  • 变量用let声明,常量用const声明,避免使用var
  • letconst都是块级作用域

避免不必要的三元表达式

1
2
3
4
5
6
7
8
9
// bad
const foo = a ? a : b
const bar = c ? true : false
const baz = c ? false : true

// good
const foo = a || b
const bar = !!c
const baz = !c

不要直接调用Object.prototype上的方法,比如,hasOwnPropertypropertyIsEnumerableisPrototypeOf

Object.prototype 上的方法可能会被错误的覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// bad 
console.log(object.hasOwnProperty(key))

// good
console.log(Object.prototype.hasOwnProperty.call(object, key))

let a = {
key: 1,
hasOwnProperty () {
return false
}
}

console.log(a.hasOwnProperty('key')) // false
console.log(Object.prototype.hasOwnProperty.call(a, 'key')) // true

callapplybind

每个函数都包含两个非继承而来的方法:call()apply()。这两个函数的用途都是在特定的作用域中调用函数,等同于设置函数体内this对象的值。

  • apply()方法接收两个参数,一个是函数运行的作用域,另一个是参数数组。第二个参数可以是Array类型,也可以是arguments对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function sum (a, b) {
return a + b
}

function callSum (a, b) {
return sum.apply(this, arguments)
}

function callSum2 (a, b) {
return sum.apply(this, [a, b])
}

console.log(callSum(1,2)) // 3
console.log(callSum2(1,2)) // 3
  • call()方法与apply()的作用相同,区别在于传递给函数的参数必须逐个列举出来
1
2
3
4
5
function callSum (a, b) {
return sum.call(this, a, b)
}

console.log(callSum(1,2)) // 3

应用:

1
2
3
4
5
6
7
8
9
10
11
12
13
window.color = 'red'
const obj = {
color: 'orange'
}

function sayColor () {
console.log(this.color)
}

sayColor() // red
sayColor.call(this) // red
sayColor.call(window) // red
sayColor.call(obj) // orange
  • bind()函数在 ES5 中定义的,这个方法会创建一个函数的实例,其this值会被绑定到传给bind()函数的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
window.color = 'red'
const obj = {
color: 'orange'
}

function sayColor () {
console.log(this.color)
}

const objSayColor = sayColor.bind(obj)

sayColor() // red
objSayColor() // orange

表达式解构

本质上,这种写法是一种”模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值,解构不成功变量的值等于 undefined。

  • 数组解构赋值
1
2
3
4
5
6
7
// 基本用法
let [a, b] = [1, 2, 3]
console.log(a, b) // 1 2

// 多维数组
let [c, [d]] = [1, [4, 5]]
console.log(c, d) // 1, 4

指定默认值

1
2
3
4
5
6
7
8
9
10
11
let [a, b] = [1]
console.log(a, b) // 1, undefined

let [a, b = 2] = [1]
console.log(a, b) // 1, 2

let [a, b = 2] = [1, undefined]
console.log(a, b) // 1, 2

let [a, b = 2] = [1, null]
console.log(a, b) // 1, null

注意:ES6 中使用严格相等运算符===判断是否相等,所以只有当一个数组成员严格等于 undefined 时默认值才会生效。例如,有一个数组成员为 null 则默认值会生效,匹配后的值为 null。

  • 对象解构赋值

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

1
2
let {key1, key2} = {key1: 'first key', key2: 'second key'}
console.log(key1, key2) // first key second key

嵌套对象解构

1
2
let {a, b: {c}} = {a: 1, b: {c: 3}}
console.log(a, c) // 1 3
  • 函数参数解构赋值
1
2
3
4
5
6
7
8
9
10
11
// bad
function getFullName(user) {
const firstName = user.firstName;
const lastName = user.lastName;

return `${firstName} ${lastName}`;
}
// good
function getFullName({ firstName, lastName }) {
return `${firstName} ${lastName}`;
}
  • 函数需返回多个值且不在关心顺序时,使用对象解构返回,而不是数组。
1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
function processInput(input) {
return [left, right, top, bottom];
}
// 需要考虑返回值的顺序
const [left, __, top] = processInput(input);

// good
function processInput(input) {
return { left, right, top, bottom };
}
// 不需要考虑顺序,需要哪个值取哪个
const { left, top } = processInput(input);

箭头函数

  • 几种常见写法
1
2
3
4
5
6
7
8
9
10
// 只有一个参数可省略括号
// 函数体值一行代码可省略大括号和return
1.x => x + 'world'
2.(x, y) => x + y
3.x => {
return x + 'world'
}
4.(x, y) => {
reutrn x + y
}
  • 箭头函数的上下文(this)会绑定为定义函数所在作用域的上下文
1
2
3
4
5
6
7
8
9
10
11
12
13
let obj = {
hello: 'object hello',
sayHello () {
const temp = () => {
return this.hello
}
return temp
}
}

window.hello = 'window hello'
window.sayHello = obj.sayHello()
window.sayHello() // object hello

上面的代码等价于:

1
2
3
4
5
6
7
8
9
10
let obj = {
hello: 'object hello',
sayHello () {
const temp = (function () {
return this.hello
}).bind(this)

return temp
}
}

注意:箭头函数对上下文的绑定是强制性的,无法通过applycall改变其上下文。

  • 使用匿名函数时,使用箭头函数定义。

箭头函数的上下文绑定特性,绑定的this通常都是我们希望的。

1
2
3
4
5
6
7
8
9
// bad
[1, 2, 3].map(function (x) {
return x * x
})

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

尽量使用高阶函数代替 for-infor-of

高阶函数相比 for-infor-of 具有更高的可读性,且减少了一些边界条件的考虑。

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
30
31
32
33
34
const numbers = [1, 2, 3, 4, 5]

// bad
let sum = 0
for (let num of numbers) {
sum += num
}
sum === 15

// good
let sum = 0
numbers.forEach((num) => {
sum += num
});
sum === 15

// best
const sum = numbers.reduce((total, num) => total + num, 0)
sum === 15

// bad
const increasedByOne = []
for (let i = 0; i < numbers.length; i++) {
increasedByOne.push(numbers[i] + 1)
}

// good
const increasedByOne = []
numbers.forEach((num) => {
increasedByOne.push(num + 1)
})

// best
const increasedByOne = numbers.map(num => num + 1)

模块化编程

在 ES6 之前,javaScript 一直没有模块体系,无法将一个大程序拆分成互相依赖的小文件,因此javaScript 社区制定了一些模块化加载方案,最主要的就是 CommonJSAMD 前者用于服务器,后者用于浏览器。ES6 在语言层面上实现了模块功能,而且相当简单完全可以取代 CommonJSAMD 规范。

ES6 模块

ES6 模块自动采用严格模式,不管是否在模块头部加上 "use strict"

export 命令

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

一个模块就是一个单独的文件,文件内定义的所有变量都是私有的,外部无法获取。如果希望外部读取模块的某个变量,只能通过 export 导出改变量。

例:

1
2
3
// moduleA.js
export let name = 'first name'
export let age = 18

除了上述写法一个一个导出变量,还有另外一种写法:

1
2
3
4
let name = 'first name'
let age = 19

export { name, age }

export 命令除了导出变量,还可以导出函数或类

1
2
3
export function sayHello () {
console.log('hello')
}

一般情况导出的变量就是本来定义的名字,如果希望重命名可以通过 as 关键字实现

1
2
3
let name = 'first name'

export { name as firstName }

import 命令

使用 export 定义了模块的接口后,其他 JS 文件可通过 import 命令引入这个模块

1
2
3
// main.js

import {name, age} from './moduleA.js'

import 命令接受一对大括号,在括号里指定要从其他模块导入的变量名,必须与模块中导出的名称相同。

如果想对 import 的变量重新命名,可通过 as 关键字实现:

1
import { name as firstName } from './moduleA.js'

import 命名是在编译阶段执行的,所以不能使用表达式和变量,这种在运行时才能得到结果的语法结构。

1
2
3
4
5
6
7
8
9
// 错误
import {'n' + 'ame'} from './moduleA.js'

// 错误
if (condition === 1) {
import {name} from './moduleA.js'
} else {
import {name} from './moduleB.js'
}

除了加载某个指定的输出值,还可通过 * 将模块的所有输出导出到一个对象上

1
2
3
4
import * as obj from './moduleA.js'

console.log(obj.name) // first name
console.log(obj.age) // 18

export default 命令

export default 为模块指定默认输出,一个文件或模块中只能有一个 export default

1
2
3
4
5
6
7
8
9
10
11
12
13
// export-default.js

export default function () {
console.log('hello')
}

// or

const sayHello = function () {
console.log('hello')
}

export default sayHello

import 引入模块默认输出是不需要大括号,且可以任意指定模块默认输出的名称,不需要知道原模块中输出的名称

1
2
3
import say from './export-default.js'

say() // hello

本质上 export default 就是输出一个叫做 default 的变量或者方法,所以export default 后面不能跟变量申明语句,直接导出变量表达式。

1
2
3
4
5
6
7
// 正确
export let a = 4
// 错误
export default let a = 4
// 正确
let a = 4
export default a

export 和 export default 区别

  • export与export default均可用于导出常量、函数、文件、模块等
  • 在一个文件或模块中,export、import可以有多个,export default仅有一个
  • 通过export方式导出,在导入时要加{ },export default则不需要
  • export能直接导出变量表达式,export default不行

CommonJS

CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如服务器和桌面应用。

CommonJS 是一种规范,为了解决 JavaScript 作用域问题而定义的模块形式。可以时每个模块在自身的命名空间中执行。模块必须通过module.exports到处对外的变量或接口,荣拓require()来导入模块。node.js 模块系统即参照 CommonJS 规范实现的。

浏览器不兼容 CommonJS 的根本原因是缺少四个 Node.js 的环境变量:

  • module
  • exports
  • require
  • global

要在浏览器环境兼容 CommonJS 可以使用一些转换工作,如 Browserify, Browserify是最常用的 CommonJS 格式转换工具。

例:

1
2
3
4
5
// moduleA.js
module.exports = {
a: 'a',
b: 'b'
}
1
2
3
// moduleB.js
const mod = require('./moduleA.js')
const aFromModuleA = mod.a

CommonJS 是同步加载模块

AMD

AMD 是为浏览器环境设计的,定义了一套 JavaScript 模块依赖异步加载标准,来解决同步加载的问题。

模块通过 define 函数定义在闭包中,格式如下:

1
define(id?: String, dependencies?: String[], factory: Function|Object)

id 是模块的名字,可选参数。dependencies 指定了所需要依赖的模块列表,是一个数组,也是可选参数。每个模块的输出将作为参数一次传入 factory。如果没有指定 dependencies 它的默认值是 ["require", "exports", "module"]

factory 是模块的具体实现,可以是一个函数或一个对象。如果是函数,那么函数的返回值就是模块的输出接口或值。

定义一个模块,依赖jQuery,如下:

1
2
3
define('myModule', ["jQuery"], function ($) {
$('body').text('hello world!')
})

在模块内部引用依赖

1
2
3
4
define('myModule', function (require) {
const $ = require('jQuery')
$('body').text('hello world')
})

具体应用可看 RequireJS

模块化编码建议

  • 总是使用import/export,不再使用 CommonJSAMD等模块化方案。
  • 不用通配符import

    1
    2
    3
    4
    5
    // bad
    import * as Module from './moduleA'

    // good
    import Module from './moduleA'
  • 从同一个 module import 多个值写在一行

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // bad
    import foo from 'foo'
    import { named1, named2 } from 'foo'

    // good
    import foo, { named1, named2 } from 'foo'

    // good
    import foo, {
    named1,
    named2,
    } from 'foo'
  • 只导出常量,不要导出可变绑定。

    1
    2
    3
    4
    5
    6
    7
    // bad
    let foo = 3;
    export { foo }

    // good
    const foo = 3;
    export { foo }

Promise

PromiseJavaScript 异步编程的一种解决方案,ES6 将其写入了语言标准,提供了原生的Promise对象。

Promise 设计上具有原子性,即只有两种状态,未开始和结束(成功和失败均是结束),也就是说 Promise 一但开始便不能中途取消,会得到一个结束状态成功或失败。未开始、成功、失败这三个状态分别用 pendingfulfilledrejected表示。

基本用法

Promise 构造函数接收一个函数参数,函数有两个参数resolverejectresolve 用于将Promise对象的状态从未开始 变为 成功reject 用于将Promise对象的状态从 未开始 变为 失败。要创建一个异步函数只需返回一个Promise对象即可。

如下,创建一个异步函数:

1
2
3
4
5
6
7
8
9
function fetchData() {
return new Promise((resolve, reject) => {
if (/*异步操作成功*/) {
resolve(value)
} else {
reject(error)
}
})
}

Promise 的参数建议用箭头函数来定义,利用箭头函数的上下文绑定将this绑定为函数所在的作用域,这通常是我们所期望的。

创建异步函数后,调用时可用.then()来添加resolve时的回调函数,.catch添加reject时的回调函数。resolve 中的参数会传递给.then()添加的回调函数,同理reject的参数亦会传递给.catch()添加的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function fetchData() {
return new Promise((resolve, reject) => {
if (/*异步操作成功*/) {
resolve(1)
} else {
reject('error message')
}
})
}

fetchData().then((response) => {
// success
console.log(response)
}).catch((error) => {
// fail
console.log(error)
})
// 1
// error message

注:resolvereject 并不会终止Promise的函数的执行。

1
2
3
4
5
6
7
8
new Promise((resolve, reject) => {
resolve(1);
console.log(2);
}).then(r => {
console.log(r);
});
// 2
// 1

链式调用

.then() 方法返回的是一个新的Promise对象(注意不是原来的那个),因此我们可以用链式写法添加多个.then()处理函数,多个.then()会添加顺序依次调用。.then()中传递参数给下一个.then()只需 return 对应的值即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function fetchData() {
return new Promise((resolve, reject) => {
if (true) {
resolve(1)
} else {
reject('error message')
}
})
}

fetchData().then((response) => {
// success
console.log(response)
return 2
}).then((res) => {
console.log(res)
}).catch((error) => {
// fail
console.log(error)
})
// 1
// 2

同理,可以链式添加多个.catch().catch() 中可以抛出错误被后续的.catch() 捕获。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function fetchData() {
return new Promise((resolve, reject) => {
if (false) {
resolve(1)
} else {
reject('error message')
}
})
}

fetchData().then((response) => {
// success
console.log(response)
}).catch((error) => {
// fail
console.log(error)
y + 1
}).catch((error) => {
console.log(error)
})
// error message
// ReferenceError: y is not defined

注:.catch()不仅会捕获reject的错误信息,.then()中出现错误一会被.catch()捕获。

Promise.prototype.finally()

.finally()方法用于添加不管Promise状态如何都会执行的操作。.finally()回调函数不接受任何参数,即无法知道Promise的状态是fulfilled还是rejected,故.finally()中的操作不应该包含于状态有关的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function fetchData() {
return new Promise((resolve, reject) => {
if (true) {
resolve(1)
} else {
reject('error message')
}
})
}

fetchData().then((response) => {
// success
console.log(response)
}).catch((error) => {
// fail
console.log(error)
}).finally(() => {
console.log('finally.........')
})
// 1
// finally.........

Promise.all()

Promise.all()方法用于将多个 Promise 实例包装成一个Promise实例。Promise.all()接收一个数组作为参数,数组中的元素必须是Promise实例,如果不是会调用Promise.resolve方法将参数转换为Promise实例,再做进一步的处理。

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

一个简单的例子如上,p1、p2、p3均为Promise实例,p的状态有以下两种情况:

  • p1、p2、p3 的状态均为fulfilled时,p 的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组传给 p 的.then()回调函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const p1 = new Promise((resolve, reject) => {
    resolve(1)
    }).then((res) => {
    return `message from p1 ${res}`
    })

    const p2 = new Promise((resolve, reject) => {
    resolve(2)
    }).then((res) => {
    return `message from p2 ${res}`
    })

    const p3 = new Promise((resolve, reject) => {
    resolve(3)
    }).then((res) => {
    return `message from p1 ${res}`
    })

    Promise.all([p1, p2, p3]).then((res) => {
    console.log(res)
    })
    // [ 1, 2, 3]
  • p1、p2、p3 中只要有一个状态为rejected,p 的状态就会变成 rejected,此时第一个reject的实例的返回值会传递给 p 的回调函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const p1 = new Promise((resolve, reject) => {
    resolve(1)
    }).then((res) => {
    return res
    })

    const p2 = new Promise((resolve, reject) => {
    reject(2)
    }).then((res) => {
    return res
    })

    const p3 = new Promise((resolve, reject) => {
    reject(3)
    }).then((res) => {
    return res
    })

    Promise.all([p1, p2, p3]).then((res) => {
    console.log(res)
    }).catch((error) => {
    console.log(error)
    })
    // 2

注:只有p1、p2、p3中reject的按个实例没有.catch()方法时才会调用Promise.all()catch方法。

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
const p1 = new Promise((resolve, reject) => {
resolve(1)
}).then((res) => {
return res
})

const p2 = new Promise((resolve, reject) => {
reject(2)
}).then((res) => {
return res
}).catch((error) => {
console.log(`error occure in p2 ${error}`)
})

const p3 = new Promise((resolve, reject) => {
reject(3)
}).then((res) => {
return res
})

Promise.all([p1, p2, p3]).then((res) => {
console.log(res)
}).catch((error) => {
console.log(error)
})
// error occure in p2 2
// 3

Promise.race()

Promise.race()Promise.all()类似,将多个 Promise 实例,包装成一个新的 Promise 实例。

不同之处在于,只要参数中有一个实例的状态改变了,Promise.race()的状态就会耕者改变,率先改变的 Promise 实例返回值传递给Promise.race()的回调函数。

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
30
const p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 2000)
}).then((res) => {
console.log('p1 resolved')
return res
})

const p2 = new Promise((resolve, reject) => {
setTimeout(() => resolve(2), 1000)
}).then((res) => {
console.log('p2 resolved')
return res
})

const p3 = new Promise((resolve, reject) => {
resolve(3)
}).then((res) => {
console.log('p3 resolved')
return res
})

Promise.all([p1, p2, p3]).then((res) => {
console.log(res)
}).catch((error) => {
console.log(error)
})
// p3 resolved
// 3
// p2 resolved
// p1 resolved