严格地说,这个题目是不够严谨的。JavaScript和ES6之间是版本的区别而并不是两种不同的事物,但是我们往往用ES6表示新一代的JS,所以简单的说是JavaScript和新版JS和其他与JS相似的变体间的比较与区别。
JavaScript
这里的JavaScript指的是我们使用的最初的简单的JS脚本语言,下面我们对语法和特点做一些回顾。
-
能够改变并控制页面中的所有HTML元素、属性、CSS样式,并对事件作出响应
-
window.alert()弹出警告框、document.write()写入文档、innerHTML写入元素、console.log()控制台打印
-
用分号来结束语句是可选的,但是字符串换行需加反斜杠,不建议这样做
-
最好不要使用JS进行小数的四则运算操作,因为精度的问题可能结果不准确,最好先转换为整数
-
注释方式与JAVA相同,两个斜杠单行,斜杠星号多行注释
-
任何事物均为对象,包括函数;弱类型变量,无需声明也可使用,变量可以被赋予任何类型,包括函数
-
函数可以声明,也可作为表达式被赋给一个变量,使用此变量加括号即可调用
-
一个函数返回另一个内部嵌套的函数,在某种情况下形成函数闭包,函数闭包解决了变量作用域的问题
-
用===表示绝对相等,即类型和内容均相同。例如:5==‘5’返回true,但5===‘5’返回false
-
需要注意的是null==undefined为true,但null===undefined为false
-
可以用+号附在数字前,确保是一个数字类型,否则用Number()函数和String()函数做类型转换
-
JSON英文全称JavaScript Object Notation,冒号键值对,中间用逗号分开
ES6
let与const
使用一个var做变量声明(抑或者不声明)这种方法带来阅读和维护难度,增加let和const虽然可能使JS变得复杂,但在可用性上来讲是一种进步。
-
let作用域仅限于块内,也就是距离最近的一个大括号
-
暂时性死区:只要块内有let变量,则块外的变量不能影响此变量
-
const类似JAVA中的final,固定值不可变需要在开始的时候就进行初始化
-
let与const变量在全局时,并不属于window的顶层变量
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
解构赋值
JavaScript增加了解构赋值,这和python中的赋值方式很相似,本质上,这是一种模式的匹配,只要模式相似就会为相对应的变量进行赋值。这种方法有点类似我们初中数学中解同系数的二元二次方程组(在初中限于知识是无法求解二元二次方程组的,但是通过系数相同,能推测出x和y就是另外式子中的数字)。
解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
-
数组解构
let [ , , third] = ["foo", "bar", "baz"]; // third="baz"
let [a, [b], d] = [1, [2, 3], 4]; // a=1 b=2 d=4
let [x, y = 'b'] = ['a']; // x='a', y='b'
-
对象解构
let { bar, foo:foa } = { foo: "aaa", bar: "bbb" }; // foa="aaa" bar="bbb"
let { log, sin, cos } = Math; //将Math的方法,赋到3个变量
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr; // first=1 last=3
-
字符串解构
const [a, b, c] = 'hello'; // a="h" b="e" c="l"
-
函数参数解构
function add([x, y]){
return x + y;
}
add([1, 2]); // 3,本来参数应该是一个arr,但是传入时被解构
变量的解构赋值用途很多:
-
交换两个变量内容
-
从函数返回多个值。从函数返回具有多个属性的对象,多个属性可以被作为多个返回值被解构赋给其他变量
-
函数参数的定义更加灵活,可以用更有意义的形参名称描述传入的参数
-
快速提取JSON中数据
-
为函数参数设置默认值
-
快速遍历map和set结构
[x, y] = [y, x];
function example() {
return {
foo: 1,
bar: 2
};
}
let { foo, bar } = example();
function f([x, y, z]) { ... }
f([1, 2, 3]); // 如果不能解构,参数要写arr,描述不形象
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
global = true
} = {}) {
// ... do stuff
};
const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map) {
console.log(key + " is " + value);
}
模版字符串
我们经常要在JS中插入html字符串,用来对当前的文档进行修改,但是为了符合JS的格式,这部分html字符串需要多次转译和连接。在ES6中我们只需要使用模版字符串就可以方便的处理这个问题。
-
如果在模板字符串中需要使用反引号,则前面要用反斜杠转义
-
如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中
-
模板字符串能嵌套
let name = "Bob", time = "today";
`Hello ${name},
how are you ${time} ${fn()}?`
基本函数新功能
-
参数可以设置默认值。这说起来并没有什么稀奇,但是这种方法能解决我们看文档才能了解的——“哪些参数是可选的”这件事情。一般的,我们习惯将这种可选参数放在参数列表的末尾
-
函数的length属性可以返回没有指定默认值的参数个数
-
rest参数可以获取多余的参数形成一个数组,类似于JAVA中的变长参数
-
name属性返回此函数的函数名
function log(x, y = 'World') {
console.log(x, y);
}
(function (a, b, c = 5) {}).length // 2
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
function foo() {} // foo.name="foo"
箭头函数
箭头函数类似于JAVA中的lambda表达式,是一种函数的缩写方式,如果你对lambda表达式有了解,那么你会对箭头函数非常熟悉。箭头函数可以与变量解构结合使用。
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
属性缩略表示
ES6 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。简单的说就是将变量名作为属性名,那么变量的值直接作为属性值,在写的时候仅写变量名即可。
const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}
// 等同于
const baz = {foo: foo};
function f(x, y) {
return {x, y};
}
// 等同于
function f(x, y) {
return {x: x, y: y};
}
const o = {
method() {
return "Hello!";
}
};
// 等同于
const o = {
method: function() {
return "Hello!";
}
};
除此之外,属性不仅能使用冒号来创建,使用一对中括号加冒号的组合也是可以的。这样增加了灵活性,而且有些情况只能这样来设置属性。
let obj = {
[propKey]: true,
['a' + 'bc']: 123
};
Symbol
对象的属性名是很容易重复的,这样会造成很多问题,为了解决唯一性。ES6中新增了Symbol类型,此类型的每一个对象都是唯一的。通过函数生成,并且在生成的时候可以传入一个字符串作为标识(即使两个Symbol传入相同的字符串也不是同一个,字符串只用做标识)。
let s = Symbol(); // typeof s "symbol"
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
当使用Symbol作为对象属性的时候需要方括号,否则会认为是一个字符串做属性名。
let a = {};
a[mySymbol] = 'Hello!';
let a = {
[mySymbol]: 'Hello!'
};
let obj = {
[s]: function (arg) { ... }
};
Symbol如何使用呢?有的时候我们需要一个变量作为标识符,用来辨识某些条件,但是此辨识符的具体值我们并不关心,此时Symbol就派上了用场。
set和map
set和map类似于JAVA中的Stream流的操作,其中forEeach、map等操作基本相同。JAVA中Stream和lambda与函数式接口配合使用,在ES6中set和map与箭头函数配合使用。
-
set中元素不重复,且set中的key和value指的是同一个,即value
-
我们可以用map和set生成新的map,原来的值将自动复制到新map中(set视为value-value)
-
set和map的遍历顺序是插入顺序,先遍历先插入的值
-
set中值的类型、map中的键类型并不需要是相同的
let set = new Set(
['red', 'green', 'blue']
);
[...new Set(array)] // 去除数组的重复成员
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
Class
ES6 的类,完全可以看作构造函数的另一种写法。类的类型就是函数,类本身就指向自己的构造函数。
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var p = new Point(1,2);
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
class Point {
constructor() {
// ...
}
toString() {
// ...
}
toValue() {
// ...
}
}
// 等同于
Point.prototype = {
constructor() {},
toString() {},
toValue() {},
};
下面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是Me,Me只在 Class 的内部代码可用,指代当前类。实际上,如果Me不使用,是可以省略的。
const MyClass = class Me {
getClassName() {
return Me.name;
}
};
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
对于类的私有方法,我们一般约定在方法名前加上下划线来表示私有,但是这并不是保险的,只是我们的约定。
class Widget {
// 公有方法
foo (baz) {
this._bar(baz);
}
// 私有方法
_bar(baz) {
return this.snaf = baz;
}
// ...
}
与 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'
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
-
父类的静态方法,可以被子类继承。
class Foo {
static classMethod() {
return 'hello';
}
}
Foo.classMethod() // 'hello'
var foo = new Foo();
foo.classMethod()
// TypeError: foo.classMethod is not a function
new.target属性。new是从构造函数生成实例对象的命令。ES6为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。
function Person(name) {
if (new.target !== undefined) {
this.name = name;
} else {
throw new Error('必须使用 new 命令生成实例');
}
}
-
Class可以通过extends关键字实现继承
-
子类必须在constructor方法中调用super方法,否则新建实例时会报错
-
Object.getPrototypeOf方法可以用来从子类上获取父类,可以使用这个方法判断,一个类是否继承了另一个类。
export
-
export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值
-
为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出
export var firstName = 'Michael';
var firstName = 'Michael';
var lastName = 'Jackson';
export {firstName, lastName};
export function multiply(x, y) {
return x * y;
};
import {multiply} from 'multiply';
function foo() {
console.log('foo');
}
export default foo;
import foo from 'foo';
import _, { each, each as forEach } from 'lodash';
export default function (obj) {
// ···
}
export function each(obj, iterator, context) {
// ···
}
export { each as forEach };
import
-
import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口
-
如果加载是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块
-
由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构
-
如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次
import {firstName, lastName, year} from './profile.js';
import { lastName as surname } from './profile.js';
import * as circle from './circle';
promise
Promise简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
promise.then(function(value) {
// success
}, function(error) {
// failure
});
下面是一个用Promise对象实现的 Ajax 操作的例子。
const getJSON = function(url) {
const promise = new Promise(function(resolve, reject){
const handler = function() {
if (this.readyState !== 4) {
return;
}
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setRequestHeader("Accept", "application/json");
client.send();
});
return promise;
};
getJSON("/posts.json").then(function(json) {
console.log('Contents: ' + json);
}, function(error) {
console.error('出错了', error);
});
TypeScrpit
我们为什么要使用TypeScript?
-
类型系统实际上是最好的文档,大部分的函数看看类型的定义就可以知道如何使用了
-
可以在编译阶段就发现大部分错误,这总比在运行时候出错好
-
增强了编辑器和IDE的功能,包括代码补全、接口提示、跳转到定义、重构等
-
TypeScript是JavaScript的超集,.js文件可以直接重命名为.ts即可
-
即使TypeScript编译报错,也可以生成JavaScript文件
-
兼容第三方库,即使第三方库不是用TypeScript写的,也可以编写单独的类型文件供TypeScript读取
下面我们主要介绍各种JavaScript的类型和结构在TypeScript中的写法。
基本数据类型
-
如果声明类型是any,则可以被赋值为任何类型,并且对这个any类型的值进行任何操作,返回的值的类型也都是any
-
如果定义的时候没有赋值,不管之后有没有赋值,都会被推断成 any 类型而完全不被类型检查。如果定义的时候被赋值,则会被自动认定为相应类型的值
-
可以被声明为联合类型,表示取值可以为多种类型中的一种。但是如果一个值被声明为联合类型,它只能访问这些类型中共有的属性或方法。比如,一个变量被声明为string|number,则不能访问length,因为number没有这个属性。解决的方法是,先将变量赋值,这样变量会自动变成当前值的类型,再调用属性或方法就不会被联合类型限制
-
通过断言的方法(值 as 类型),将联合类型的变量的类型指定为一个更具体的类型
let isDone: boolean = false;
let sum: number = 0;
let substr: string = 'hello';
function funcName(): void {
// do something...
}
let myFavoriteNumber: any = 'seven';
let something; // 被识别为any类型
something = 'seven';
something = 7;
let myFavoriteNumber = 'seven'; // 因为赋值了,被认定为string类型
myFavoriteNumber = 7;
function getLength(something: string | number): number {
return something.length; //报错
}
let myFavoriteNumber: string | number;
myFavoriteNumber = 'seven';
console.log(myFavoriteNumber.length); // 5
myFavoriteNumber = 7;
console.log(myFavoriteNumber.length); // 编译时报错
function getLength(something: string | number): number {
if ((something as string).length) {
return (<string>something).length;
} else {
return something.toString().length;
}
}
对象类型
-
在TypeScript中,我们使用接口(Interfaces)来定义对象的类型。定义了类型之后就可以像其他类型(number、string)一样去做其他变量的类型
-
我们在属性的冒号前加上问号,表示这是一个可选属性,有没有均可
-
我们用[propName: string]: any;表示可以添加一个任意类型的属性,需要注意的是,一旦定义了任意属性,那么确定属性和可选属性都必须是它的子属性
-
用readonly定义只读属性,类似const,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候
interface Person {
readonly id: number;
name: string;
age?: number;
[propName: string]: any;
}
let tom: Person = {
id: 89757,
name: 'Tom',
age: 25
};
数组类型
-
最简单的方法是使用「类型 + 方括号」来表示数组,数组中如果有多个类型,应该使用联合类型,否则报错
-
也可以使用数组泛型来表示数组
-
一个比较常见的做法是,用 any 表示数组中允许出现任意类型:
let fibonacci: number[] = [1, 1, 2, 3, 5];
let fibonacci: Array<number> = [1, 1, 2, 3, 5];
let list: any[] = ['Xcat Liu', 25, { website: 'http://xcatliu.com' }];
函数类型
-
与接口中的可选属性类似,我们用 ? 表示可选的参数,也允许给函数的参数添加默认值。TypeScript会将添加了默认值的参数识别为可选参数,可选参数后面不允许再出现必须参数了
-
ES6中的声誉参数可以用一个数组表示
-
写多个类型的函数进行重载,实现最后的函数
function sum(x: number, y: number): number {
return x + y;
}
let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
return x + y;
};
function buildName(firstName: string, lastName?: string) {
// ...
}
function buildName(firstName: string, lastName: string = 'Cat') {
// ...
}
function push(array: any[], ...items: any[]) {
// ...
}
function reverse(x: number): number;
function reverse(x: string): string;
function reverse(x: number | string): number | string {
if (typeof x === 'number') {
return Number(x.toString().split('').reverse().join(''));
} else if (typeof x === 'string') {
return x.split('').reverse().join('');
}
}
声明文件
-
当使用第三方库时,我们需要引用它的声明文件。在使用到的文件的开头,用三斜线指令表示引用了声明文件
// jQuery.d.ts
declare var jQuery: (string) => any;
/// <reference path="./jQuery.d.ts" />
jQuery('#foo');
类型别名
-
类型别名用来给一个类型起个新名字
-
字符串字面量类型用来约束取值只能是某几个字符串中的一个
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
} else {
return n();
}
}
type EventNames = 'click' | 'scroll' | 'mousemove';
function handleEvent(ele: Element, event: EventNames) {
// do something
}
handleEvent(document.getElementById('hello'), 'scroll'); // 没问题
handleEvent(document.getElementById('world'), 'dbclick'); // 报错
类与接口
-
TypeScript可以使用三种访问修饰符,分别是public、private和protected
-
可以定义abstract抽象类
-
可以创建接口,关键字和对象的类型一样,但是功能类似于JAVA,接口可以继承
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
sayHi(): string {
return `My name is ${this.name}`;
}
}
let a: Animal = new Animal('Jack');
console.log(a.sayHi()); // My name is Jack
泛型
-
泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性
function createArray<T>(length: number, value: T): Array<T> {
let result: T[] = [];
for (let i = 0; i < length; i++) {
result[i] = value;
}
return result;
}
createArray<string>(3, 'x'); // ['x', 'x', 'x']