汇编语言的优点是能用来写任何东西,汇编语言的缺点是你不会用它来写任何东西
能被web平台实现的最终一定会被实现在web平台上
所谓的Atwood's Law是这样的:
Any application that can be written in JavaScript, will eventually be written in JavaScript.
这句话的出处是Jeff Atwood在2007年写的博客《The Principle of Least Power》,Jeff Atwood是Stack Overflow的联合创始人。
如何评价Atwood's Law呢?
一方面,他的预测在某种程度上确实对了,这10多年来,JS的发展确实非常惊人,现在的我们可以用JS来做各种事情,比如写网页、写APP、写小程序、写APP、写后端,写IOT;JS的生态系统也异常繁荣与开放,GitHub与NPM有着各种工具与框架;从2015年开始,ECMAScript每年会发布一个新的版本,增加新的特性;Node.js的更新也非常稳定,基本上按照预定的日期发布新的版本,优化性能,增加特性...我们无法在10多年前对JS的发展有这样的判断,但是从目前的情况来看,JS的前景还是非常好的。
另一方面,JS真的无所不能吗?它可以替代其他编程语言吗?作为JS开发者,我觉得这显然是不可能的。每种编程语言都有自己擅长的事情,或者说每种语言都有自己的一亩三分地,比如Python在AI领域如日中天,现在风头貌似盖过了其他所有语言;Java在后端领域依然是霸主,使用Node.js写后端的依然寥寥无几,这一点从招聘网站就能看出来;Golang在底层基础架构领域也非常受欢迎,业界炙手可热的工具比如Docker与Kuberntes用的都是Go....用JS替代Python/Java/Golang等语言在各自领域的作用,基本上是不可能的,也没有必要,这都用不着去分析技术细节的优劣,人家用得好好的,咱有必要去重复造轮子吗?
------------------------------------------------------------------------------
上一篇所谓终结篇有个小缺憾,是引入了vue.js和element-ui两个外部源src
今天学习了一下ES6的知识,把这两个依赖也去掉了,实现了用纯原生Javascript ES6规范的脚本来渲染电子发票文件。
HTML 部分全部用
Javascript部分无任何外部引用,共约24000行代码
样式表
HTML 部分:
Javascript部分:
样式表
部署在Tomcat上的web单页应用大小809K
效果:
------------------------------------------------------------------------------
ECMAScript 发展史
1998 年 ECMAScript 2.0 发布
1999 年 ECMAScript 3.0 发布
2007 年 提出了 ECMAScript 4.0 因为太激进被各大浏览器厂商及互联网公司联合抵制,最终没有发布。
2008 年 放弃4.0计划,改为3.1优化计划,只做了小部分更新,我们现在写的最早的版本大概就是 3.1 版本了。
2009 年 发布了ES5
2011 年 修正了ES5 发布了 ES5.1 这个标准是目前国标版本,也就是说所有的2011年之后的浏览器必须支持的版本。
2015 年 经过 2013 年多次波折和延期发布,终于发布了ES6 版本。
因为ES6版本发生了加大的变更,同时太多的想法也在慢慢计划和实施,所以ECMAScript 从2015年起, 每年都会更新一版。
ES2015 以后的版本我们通常称之为ES6 +
ES6的主要新特性:
声明变量的方法
ES5只有两种声明变量的方法:var
命令和function
命令。
ES6新增了let
命令,用来声明变量。它的用法类似于var
,但是所声明的变量,只在let
命令所在的代码块内有效。
ES6新增了const
命令,const
声明一个只读的常量。一旦声明,常量的值就不能改变。
ES6除了添加let
和const
命令,另外两种声明变量的方法:import
命令和class
命令。所以,ES6一共有6种声明变量的方法。
Class语法
JavaScript语言的传统方法是通过构造函数,定义并生成新对象。下面是一个例子。
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
上面这种写法跟传统的面向对象语言(比如C++和Java)差异很大,很容易让新学习这门语言的程序员感到困惑。
ES6提供了更接近传统语言的写法,引入了Class(类)这个概念,作为对象的模板。通过class
关键字,可以定义类。基本上,ES6的class
可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用ES6的“类”改写,就是下面这样。
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
上面代码定义了一个“类”,可以看到里面有一个constructor
方法,这就是构造方法,而this
关键字则代表实例对象。也就是说,ES5的构造函数Point
,对应ES6的Point
类的构造方法。
Point
类除了构造方法,还定义了一个toString
方法。注意,定义“类”的方法的时候,前面不需要加上function
这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
import语法
ES6的Class只是面向对象编程的语法糖,升级了ES5的构造函数的原型链继承的写法,并没有解决模块化问题。Module功能就是为了解决这个问题而提出的。
历史上,JavaScript一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如Ruby的require
、Python的import
,甚至就连CSS都有@import
,但是JavaScript任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。
在ES6之前,社区制定了一些模块加载方案,最主要的有CommonJS和AMD两种。前者用于服务器,后者用于浏览器。ES6在语言规格的层面上,实现了模块功能,而且实现得相当简单,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。
ES6模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS和AMD模块,都只能在运行时确定这些东西。比如,CommonJS模块就是对象,输入时必须查找对象属性。
// CommonJS模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat, exists = _fs.exists, readfile = _fs.readfile;
上面代码的实质是整体加载fs
模块(即加载fs
的所有方法),生成一个对象(_fs
),然后再从这个对象上面读取3个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
ES6模块不是对象,而是通过export
命令显式指定输出的代码,输入时也采用静态命令的形式。
// ES6模块
import { stat, exists, readFile } from 'fs';
上面代码的实质是从fs
模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”,即ES6可以在编译时就完成模块加载,效率要比CommonJS模块的加载方式高。当然,这也导致了没法引用ES6模块本身,因为它不是对象。
由于ES6模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽JavaScript的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。
除了静态加载带来的各种好处,ES6模块还有以下好处。
不再需要UMD模块格式了,将来服务器和浏览器都会支持ES6模块格式。目前,通过各种工具库,其实已经做到了这一点。
将来浏览器的新API就能用模块格式提供,不再必要做成全局变量或者
navigator
对象的属性。不再需要对象作为命名空间(比如
Math
对象),未来这些功能可以通过模块提供。
浏览器使用ES6模块的语法如下。
<script type="module" src="foo.js">script>
变量的解构赋值
ES6允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
以前,为变量赋值,只能直接指定值。
var a = 1;
var b = 2;
var c = 3;
ES6允许写成下面这样。
var [a, b, c] = [1, 2, 3];
上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。
本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
ES6 异步操作和Async函数
异步编程对JavaScript语言太重要。Javascript语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可。
ES6诞生以前,异步编程的方法,大概有下面四种。
回调函数
事件监听
发布/订阅
Promise 对象
ES6将JavaScript异步编程带入了一个全新的阶段,ES7的Async
函数更是提出了异步编程的终极解决方案。
所谓"异步",简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。
相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。
回调函数
JavaScript语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。它的英语名字callback,直译过来就是"重新调用"。
Promise
回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。假定读取A文件之后,再读取B文件,代码如下。
fs.readFile(fileA, function (err, data) {
fs.readFile(fileB, function (err, data) {
// ...
});
});
不难想象,如果依次读取多个文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。这种情况就称为"回调函数噩梦"(callback hell)。
Promise就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。采用Promise,连续读取多个文件......
Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。
Promise 的最大问题是代码冗余,原来的任务被Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。
那么,有没有更好的写法呢?......
ECMAScript 6 字符串的扩展
ECMAScript 6 正则的扩展
ECMAScript 6 数值的扩展
ECMAScript 6 数组的扩展
ECMAScript 6 函数的扩展
ECMAScript 6 对象的扩展
ECMAScript 6 Symbol
ECMAScript 6 Proxy和Reflect
ECMAScript 6 二进制数组
ECMAScript 6 Set和Map数据结构
ECMAScript 6 Iterator和for...of循环
ECMAScript 6 Generator 函数
ECMAScript 6 Promise对象
ECMAScript 6 异步操作和Async函数
ECMAScript 6 修饰器(Decorator)
ES6的编程风格:
块级作用域
(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);
}
上面代码如果用var
替代let
,实际上就声明了两个全局变量,这显然不是本意。变量应该只在其声明的代码块内有效,var
命令做不到这一点。
var
命令存在变量提升效用,let
命令没有这个问题。
'use strict';
if(true) {
console.log(x); // ReferenceError
let x = 'hello';
}
上面代码如果使用var
替代let
,console.log
那一行就不会报错,而是会输出undefined
,因为变量声明提升到代码块的头部。这违反了变量先声明后使用的原则。
所以,建议不再使用var
命令,而是使用let
命令取代。
(2)全局常量和线程安全
在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];
const
声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。
所有的函数都应该设置为常量。
长远来看,JavaScript可能会有多线程的实现(比如Intel的River Trail那一类的项目),这时let
表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。
字符串
静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
// 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,
};
上面代码中,对象obj
的最后一个属性名,需要计算得到。这时最好采用属性表达式,在新建obj
的时候,将该属性与其他属性定义在一起。这样一来,所有属性就在一个地方定义了。
另外,对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。
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];
}
}
模块
首先,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';
const Breadcrumbs = React.createClass({
render() {
return <nav />;
}});
export default Breadcrumbs
如果模块只有一个输出值,就使用export default
,如果模块有多个输出值,就不使用export default
,不要export default
与普通的export
同时使用。
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
// bad
import * as myObject './importModule';
// good
import myObject from './importModule';
如果模块默认输出一个函数,函数名的首字母应该小写。
function makeStyleGuide() {
}
export default makeStyleGuide;
如果模块默认输出一个对象,对象名的首字母应该大写。
const StyleGuide = {
es6: {
}
};
export default StyleGuide;