编程风格
- 块级作用域
- 字符串
- 解构赋值
- 对象
- 数组
- 函数
- Map 结构
- Class
- 模块
- ESLint 的使用
块级作用域
(1)let 取代 var
ES6 提出了两个新的声明变量的命令:let和const。其中,let完全可以取代var,因为两者语义相同,而且let没有副作用。
'use strict';
if (true) {
let x = 'hello';
}
for (let i = 0; i < 10; i++) {
console.log(i);
}
全局常量和线程安全
在let和const之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。
const优于let有几个原因。一个是const可以提醒阅读程序的人,这个变量不应该改变;另一个是const比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对const进行优化,所以多使用const,有利于提高程序的运行效率,也就是说let和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 arr = [1, 2, 3, 4];
// bad
const first = arr[0];
const second = arr[1];
// good
const [first, second] = arr;
对象
单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
// 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 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。
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];
}
}
模块
首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import取代require。
// bad
const moduleA = require('moduleA');
const func1 = moduleA.func1;
const func2 = moduleA.func2;
// good
import { func1, func2 } from 'moduleA';
使用export取代module.exports。
// 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,如果模块有多个输出值,就不使用export default,export default与普通的export不要同时使用。
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
ESLint 的使用
ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。
首先,安装 ESLint。
$ npm i -g eslint
然后,安装 Airbnb 语法规则,以及 import、a11y、react 插件。
$ npm i -g eslint-config-airbnb
$ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
最后,在项目的根目录下新建一个.eslintrc文件,配置 ESLint。
{
"extends": "eslint-config-airbnb"
}
现在就可以检查,当前项目的代码是否符合预设的规则。
index.js文件的代码如下。
var unusued = 'I have no purpose!';
function greet() {
var message = 'Hello, World!';
alert(message);
}
greet();
使用 ESLint 检查这个文件,就会报出错误。
$ eslint index.js
index.js
1:1 error Unexpected var, use let or const instead no-var
1:5 error unusued is defined but never used no-unused-vars
4:5 error Expected indentation of 2 characters but found 4 indent
4:5 error Unexpected var, use let or const instead no-var
5:5 error Expected indentation of 2 characters but found 4 indent
✖ 5 problems (5 errors, 0 warnings)
上面代码说明,原文件有五个错误,其中两个是不应该使用var命令,而要使用let或const;一个是定义了变量,却没有使用;另外两个是行首缩进为 4 个空格,而不是规定的 2 个空格。
装饰器
- 类的装饰
- 方法的装饰
- 为什么装饰器不能用于函数?
- core-decorators.js
- 使用装饰器实现自动发布事件
- Mixin
- Trait
类的装饰
装饰器可以用来装饰整个类。
@testable
class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
上面代码中,@testable就是一个装饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestable。testable函数的参数target是MyTestableClass类本身。
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
下面是另外一个例子。
// mixins.js
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list)
}
}
// main.js
import { mixins } from './mixins'
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // 'foo'
上面代码通过装饰器mixins,把Foo对象的方法添加到了MyClass的实例上面。可以用Object.assign()模拟这个功能。
const Foo = {
foo() { console.log('foo') }
};
class MyClass {}
Object.assign(MyClass.prototype, Foo);
let obj = new MyClass();
obj.foo() // 'foo'
实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。
class MyReactComponent extends React.Component {}
export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
有了装饰器,就可以改写上面的代码。
@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}
相对来说,后一种写法看上去更容易理解。
方法的装饰
装饰器不仅可以装饰类,还可以装饰类的属性。
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
上面代码中,装饰器readonly用来装饰“类”的name方法。
装饰器函数readonly一共可以接受三个参数。
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
装饰器第一个参数是类的原型对象,上例是Person.prototype,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);第二个参数是所要装饰的属性名,第三个参数是该属性的描述对象。
另外,上面代码说明,装饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。
下面是另一个例子,修改属性描述对象的enumerable属性,使得该属性不可遍历。
class Person {
@nonenumerable
get kidCount() { return this.children.length; }
}
function nonenumerable(target, name, descriptor) {
descriptor.enumerable = false;
return descriptor;
}
下面的@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(this, arguments);
};
return descriptor;
}
const math = new Math();
// passed parameters should get logged now
math.add(2, 4);
上面代码中,@log装饰器的作用就是在执行原始的操作之前,执行一次console.log,从而达到输出日志的目的。
装饰器有注释的作用。
@testable
class Person {
@readonly
@nonenumerable
name() { return `${this.first} ${this.last}` }
}
从上面代码中,我们一眼就能看出,Person类是可测试的,而name方法是只读和不可枚举的。
下面是使用 Decorator 写法的组件,看上去一目了然。
@Component({
tag: 'my-component',
styleUrl: 'my-component.scss'
})
export class MyComponent {
@Prop() first: string;
@Prop() last: string;
@State() isVisible: boolean = true;
render() {
return (
<p>Hello, my name is {this.first} {this.last}</p>
);
}
}
如果同一个方法有多个装饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。
function dec(id){
console.log('evaluated', id);
return (target, property, descriptor) => console.log('executed', id);
}
class Example {
@dec(1)
@dec(2)
method(){}
}
// evaluated 1
// evaluated 2
// executed 2
// executed 1
上面代码中,外层装饰器@dec(1)先进入,但是内层装饰器@dec(2)先执行。
除了注释,装饰器还能用来类型检查。所以,对于类来说,这项功能相当有用。从长期来看,它将是 JavaScript 代码静态分析的重要工具。
使用装饰器实现自动发布事件
我们可以使用装饰器,使得对象的方法被调用时,自动发出一个事件。
const postal = require("postal/lib/postal.lodash");
export default function publish(topic, channel) {
const channelName = channel || '/';
const msgChannel = postal.channel(channelName);
msgChannel.subscribe(topic, v => {
console.log('频道: ', channelName);
console.log('事件: ', topic);
console.log('数据: ', v);
});
return function(target, name, descriptor) {
const fn = descriptor.value;
descriptor.value = function() {
let value = fn.apply(this, arguments);
msgChannel.publish(topic, value);
};
};
}
Mixin
在装饰器的基础上,可以实现Mixin模式。所谓Mixin模式,就是对象继承的一种替代方案,中文译为“混入”(mix in),意为在一个对象之中混入另外一个对象的方法。
请看下面的例子。
const Foo = {
foo() { console.log('foo') }
};
class MyClass {}
Object.assign(MyClass.prototype, Foo);
let obj = new MyClass();
obj.foo() // 'foo'
上面代码之中,对象Foo有一个foo方法,通过Object.assign方法,可以将foo方法“混入”MyClass类,导致MyClass的实例obj对象都具有foo方法。这就是“混入”模式的一个简单实现。
下面,我们部署一个通用脚本mixins.js,将 Mixin 写成一个装饰器。
export function mixins(...list) {
return function (target) {
Object.assign(target.prototype, ...list);
};
}
然后,就可以使用上面这个装饰器,为类“混入”各种方法。
import { mixins } from './mixins';
const Foo = {
foo() { console.log('foo') }
};
@mixins(Foo)
class MyClass {}
let obj = new MyClass();
obj.foo() // "foo"
通过mixins这个装饰器,实现了在MyClass类上面“混入”Foo对象的foo方法。
不过,上面的方法会改写MyClass类的prototype对象,如果不喜欢这一点,也可以通过类的继承实现 Mixin。
class MyClass extends MyBaseClass {
/* ... */
}
上面代码中,MyClass继承了MyBaseClass。如果我们想在MyClass里面“混入”一个foo方法,一个办法是在MyClass和MyBaseClass之间插入一个混入类,这个类具有foo方法,并且继承了MyBaseClass的所有方法,然后MyClass再继承这个类。
Trait
Trait 也是一种装饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。
下面采用traits-decorator这个第三方模块作为例子。这个模块提供的traits装饰器,不仅可以接受对象,还可以接受 ES6 类作为参数。
import { traits } from 'traits-decorator';
class TFoo {
foo() { console.log('foo') }
}
const TBar = {
bar() { console.log('bar') }
};
@traits(TFoo, TBar)
class MyClass { }
let obj = new MyClass();
obj.foo() // foo
obj.bar() // bar
上面代码中,通过traits装饰器,在MyClass类上面“混入”了TFoo类的foo方法和TBar对象的bar方法。
Trait 不允许“混入”同名方法。
import { traits, excludes } from 'traits-decorator';
class TFoo {
foo() { console.log('foo') }
}
const TBar = {
bar() { console.log('bar') },
foo() { console.log('foo') }
};
@traits(TFoo, TBar::excludes('foo'))
class MyClass { }
let obj = new MyClass();
obj.foo() // foo
obj.bar() // bar
上面代码使用绑定运算符(::)在TBar上排除foo方法,混入时就不会报错了。
另一种方法是为TBar的foo方法起一个别名。
import { traits, alias } from 'traits-decorator';
class TFoo {
foo() { console.log('foo') }
}
const TBar = {
bar() { console.log('bar') },
foo() { console.log('foo') }
};
@traits(TFoo, TBar::alias({foo: 'aliasFoo'}))
class MyClass { }
let obj = new MyClass();
obj.foo() // foo
obj.aliasFoo() // foo
obj.bar() // bar
上面代码为TBar的foo方法起了别名aliasFoo,于是MyClass也可以混入TBar的foo方法了。
alias和excludes方法,可以结合起来使用。
@traits(TExample::excludes('foo','bar')::alias({baz:'exampleBaz'}))
class MyClass {}
上面代码排除了TExample的foo方法和bar方法,为baz方法起了别名exampleBaz。
as方法则为上面的代码提供了另一种写法。
@traits(TExample::as({excludes:['foo', 'bar'], alias: {baz: 'exampleBaz'}}))
class MyClass {}