TypeScript

前言

Any application that can be written in JavaScript, will eventually be written in JavaScript.

JavaScript

变量

变量是对“值”的具名引用。变量就是为“值”起名,然后引用这个名字,就等同于引用这个值。变量的名字就是变量名。

1
var a = 1;

上面的代码先声明变量a,然后在变量a与数值1之间建立引用关系,称为将数值1“赋值”给变量a。以后,引用变量名a就会得到数值1。最前面的var,是变量声明命令。它表示通知解释引擎,要创建一个变量a。如果只是声明变量而没有赋值,则该变量的值是undefined。undefined是一个特殊的值,表示“无定义”。

变量提升

JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升。

1
2
console.log(a);
var a = 1;

上面代码首先使用console.log方法,在控制台(console)显示变量a的值。这时变量a还没有声明和赋值,所以这是一种错误的做法,但是实际上不会报错。因为存在变量提升,真正运行的是下面的代码。

1
2
3
var a;
console.log(a);
a = 1;

最后的结果是显示undefined,表示变量a已声明,但还未赋值。

区块

JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。对于var命令来说,JavaScript 的区块不构成单独的作用域(scope)。

1
2
3
4
{
var a = 1;
}
a; // 1

上面代码在区块内部,使用var命令声明并赋值了变量a,然后在区块外部,变量a依然有效,区块对于var命令不构成单独的作用域,与不使用区块的情况没有任何区别。在 JavaScript 语言中,单独使用区块并不常见,区块往往用来构成其他更复杂的语法结构,比如for、if、while、function等。

注意,对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。

条件语句

在 JavaScript 中,truthy(真值)指的是在布尔值上下文中,转换后的值为真的值。所有值都是真值,除非它们被定义为 假值(即除 false、0、””、null、undefined 和 NaN 以外皆为真值)。

1
2
3
4
5
6
7
8
9
if (false)
if (null)
if (undefined)
if (0)
if (0n)
if (NaN)
if ('')
if ("")
if (``)

null 和 undefined 的区别:null是一个表示“空”的对象,转为数值时为0;undefined是一个表示”此处无定义”的原始值,转为数值时为NaN。

数值

JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1与1.0是相同的,是同一个数。

1
1 === 1.0 // true

这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)。容易造成混淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把64位浮点数,转成32位整数,然后再进行运算。由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心。

1
2
0.1 + 0.2 === 0.3 // false
0.3 / 0.1 // 2.9999999999999996

NaN是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合。NaN不等于任何值,包括它本身。需要注意的是,NaN不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number,使用typeof运算符可以看得很清楚。

1
2
typeof NaN  // 'number'
NaN === NaN // false

Base64编码

JavaScript 原生提供两个 Base64 相关的方法:

  • btoa():任意值转为 Base64 编码
  • atob():Base64 编码转为原来的值
    1
    2
    3
    4
    5
    6
    var string = 'Hello World!';
    btoa(string); // "SGVsbG8gV29ybGQh"
    atob('SGVsbG8gV29ybGQh'); // "Hello World!"
    btoa('你好'); // 报错
    btoa(encodeURIComponent('你好')); // "JUU0JUJEJUEwJUU1JUE1JUJE"
    decodeURIComponent(atob('JUU0JUJEJUEwJUU1JUE1JUJE')); // "你好"

注意,这两个方法不适合非 ASCII 码的字符,会报错。要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法。

对象

什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。

1
2
3
4
var obj = {
foo: 'Hello',
bar: 'World'
};

如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量。如果取消某一个变量对于原对象的引用,不会影响到另一个变量。但是,这种引用只局限于对象,如果两个变量指向同一个原始类型的值。那么,变量这时都是值的拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var o1 = {};
var o2 = o1;

o1.a = 1;
o2.a; // 1

o1 = 1;
o2; // {a: 1}

var x = 1;
var y = x;

x = 2;
y; // 1

对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句?

1
{ foo: 123 }

JavaScript 引擎读到上面这行代码,会发现可能有两种含义。第一种可能是,这是一个表达式,表示一个包含foo属性的对象;第二种可能是,这是一个语句,表示一个代码区块,里面有一个标签foo,指向表达式123。

为了避免这种歧义,JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块。

1
{ console.log(123) } // 123

上面的语句是一个代码块,而且只有解释为代码块,才能执行。如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。

1
2
({ foo: 123 }) // 正确
({ console.log(123) }) // 报错

for…in循环用来遍历一个对象的全部属性,使用时需要注意:

  • 它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。
  • 它不仅遍历对象自身的属性,还遍历继承的属性。
    如果继承的属性是可遍历的,那么就会被for…in循环遍历到。但是,一般情况下,都是只想遍历对象自身的属性,所以使用for…in的时候,应该结合使用hasOwnProperty方法,在循环内部判断一下,某个属性是否为对象自身的属性。
    1
    2
    3
    4
    5
    6
    var person = { name: '老张' };
    for (var key in person) {
    if (person.hasOwnProperty(key)) {
    console.log(key); // name
    }
    }

对象的继承:大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现(ES6 引入了基于 class 的继承语法)。JavaScript 通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。这个问题的解决方法,就是 JavaScript 的原型对象(prototype)。

prototype 属性的作用:JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。JavaScript 规定,每个函数都有一个prototype属性,指向一个对象。

1
2
function f() {}
typeof f.prototype // "object"

注意,原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。原型对象的作用就是定义所有实例对象共享的属性和方法,这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Animal(name) {
this.name = name;
}
Animal.prototype.color = 'white';

var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');

cat1.color // 'white'
cat2.color // 'white'

Animal.prototype.color = 'yellow';

cat1.color // "yellow"
cat2.color // "yellow"

constructor 属性:prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。

1
2
3
4
5
6
function P() {}
P.prototype.constructor === P // true
var p = new P();
p.constructor === P // true
p.hasOwnProperty('constructor') // false
p.constructor === P.prototype.constructor // true

函数

调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数。

1
2
3
4
var add = function(x, y) {
return x + y;
};
add(1, 1); // 2

上面代码中,函数名后面紧跟一对圆括号,就会调用这个函数。

JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民。

JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部。所以,下面的代码不会报错。

1
2
3
f();

function f() {}

表面上,上面代码好像在声明之前就调用了函数f。但是实际上,由于“变量提升”,函数f被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript 就会报错。

1
2
3
4
5
6
f();
var f = function (){};
// 上面的代码等同于下面的形式
var f;
f(); // TypeError: undefined is not a function
f = function () {};

函数作用域:作用域(scope)指的是变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。ES6 又新增了块级作用域。

  • 函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取;
  • 在函数内部定义的变量称为局部变量(local variable),外部无法读取;
  • 函数内部定义的变量,会在该作用域内覆盖同名全局变量;

注意,对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量。与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部。

1
2
3
4
5
6
7
8
9
10
11
12
function foo(x) {
if (x > 100) {
var tmp = x - 100;
}
}
// 等同于
function foo(x) {
var tmp;
if (x > 100) {
tmp = x - 100;
}
}

数组

本质上,数组属于一种特殊的对象。typeof运算符会返回数组的类型是object。

只要是数组,就一定有length属性。该属性是一个动态的值,等于键名中的最大整数加上1。清空数组的一个有效方法,就是将length属性设为0。如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object)。

1
2
3
4
5
6
var obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3
};

典型的“类似数组的对象”是函数的arguments对象,以及大多数 DOM 元素集,还有字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// arguments对象
function func() { return arguments }
var arrayLike = func('a', 'b');

arrayLike[0] // 'a'
arrayLike.length // 2
arrayLike instanceof Array // false

// DOM元素集
var elts = document.getElementsByTagName('h3');
elts.length // 3
elts instanceof Array // false

// 字符串
'abc'[1] // 'b'
'abc'.length // 3
'abc' instanceof Array // false

数组的slice方法可以将“类似数组的对象”变成真正的数组。

1
var arr = Array.prototype.slice.call(arrayLike);

除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过call()把数组的方法放到对象上面。注意,这种方法比直接使用数组原生的forEach要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach方法。

1
2
3
Array.prototype.forEach.call(arrayLike, function(value, index) {
console.log(index + ' : ' + value);
});

ECMAScript

let & const

var命令会发生“变量提升”现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。使用let命令所声明的变量,只在let命令所在的代码块内有效。

1
2
3
4
5
6
7
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

const声明一个只读的常量。一旦声明,常量的值就不能改变。const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。

1
2
3
4
5
6
7
8
const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

Symbol

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入 Symbol 的原因。

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值,它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

数据类型 symbol 是一种基本数据类型,该类型的性质在于这个类型的值可以用来创建匿名的对象属性。该数据类型通常被用作一个对象属性的键值(当你想让它是私有的时候)。例如,symbol 类型的键存在于各种内置的 JavaScript 对象中。同样,自定义类也可以这样创建私有成员。symbol 数据类型具有非常明确的目的,并且因为其功能性单一的优点而突出;一个 symbol 实例可以被赋值到一个左值变量,还可以通过标识符检查类型,这就是它的全部特性。

当一个 Symbol 包装器对象作为一个属性的键时,这个对象将被强制转换为它包装过的 symbol 值:

1
2
3
4
var sym = Symbol("foo");
var obj = {[sym]: 1};
obj[sym]; // 1
obj[Object(sym)]; // still 1

Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例:

1
2
3
4
5
6
7
8
/**
*
* @param target 要使用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
* @param handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为
*
* @returns A proxy instance.
*/
const p = new Proxy(target, handler)

Proxy 对象的所有用法,都是上面这种形式,不同的只是 handler 参数的写法。其中,new Proxy() 表示生成一个 Proxy 实例,target 参数表示所要拦截的目标对象,handler 参数也是一个对象,用来定制拦截行为。

1
2
3
4
5
6
7
8
9
10
const p = new Proxy({}, {
get: function(obj, prop) {
return prop in obj ? obj[prop] : 37;
}
});
p.a = 1;
p.b = undefined;

console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37

注意,要使得 Proxy 起作用,必须针对 Proxy 实例进行操作,而不是针对目标对象进行操作。

下面是 Proxy 支持的拦截操作一览,一共 13 种:

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.fooproxy['foo']
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

Reflect

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个:

  1. Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

  2. 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 老写法
    try {
    Object.defineProperty(target, property, attributes);
    // success
    } catch (e) {
    // failure
    }

    // 新写法
    if (Reflect.defineProperty(target, property, attributes)) {
    // success
    } else {
    // failure
    }
  3. Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

    1
    2
    3
    4
    5
    // 老写法
    'assign' in Object // true

    // 新写法
    Reflect.has(Object, 'assign') // true
  4. Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Proxy(target, {
    set: function(target, name, value, receiver) {
    // 采用 Reflect.set 方法将值赋值给对象的属性,确保完成原有的行为,然后再添加额外的操作
    var success = Reflect.set(target, name, value, receiver);
    if (success) {
    console.log('property ' + name + ' on ' + target + ' set to ' + value);
    }
    return success;
    }
    });

Class

ES6 提供了更接近传统语言的写法,引入了 Class 这个概念,作为对象的模板。通过class关键字,可以定义类。ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的 class 写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

1
2
3
4
5
6
7
8
9
10
11
12
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}

toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
typeof Point; // "function"
Point === Point.prototype.constructor; // true

构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Point {
constructor() {
}

toString() {
}
}

// 等同于
Point.prototype = {
constructor() {},
toString() {}
};

由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。

1
2
3
4
5
6
7
8
class Point {
constructor(){
}
}

Object.assign(Point.prototype, {
toString(){}
});

另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable),这一点与 ES5 的行为不一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// ES6 Class
class Point {
constructor(x, y) {
}

toString() {
}
}
Object.keys(Point.prototype); // []
Object.getOwnPropertyNames(Point.prototype); // ["constructor", "toString"]

// ES5 Function
var Point = function (x, y) {
};
Point.prototype.toString = function() {
};
Object.keys(Point.prototype); // ["toString"]
Object.getOwnPropertyNames(Point.prototype); // ["constructor", "toString"]

Generator 函数

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个 Iterator 对象,返回的 Iterator 对象可以依次遍历 Generator 函数内部的每一个状态。

形式上,Generator 函数就是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

1
2
3
4
5
6
7
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}

var hw = helloWorldGenerator();

上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是 Iterator 对象。

下一步,必须调用 Iterator 对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

1
2
3
4
5
6
7
hw.next(); // { value: 'hello', done: false }

hw.next(); // { value: 'world', done: false }

hw.next(); // { value: 'ending', done: true }

hw.next(); // { value: undefined, done: true }

Async 函数

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。async 函数是什么?一句话,它就是 Generator 函数的语法糖。

1
2
3
4
5
6
7
8
9
10
11
12
// Generator 函数
const fun = function* () {
const res = yield fetch('https://api.github.com/orgs/nodejs');
console.log(res);
return res;
};
// Async 函数
const fun = async function () {
const res = await fetch('https://api.github.com/orgs/nodejs');
console.log(res);
return res;
};

一比较就会发现,async函数就是将Generator函数的星号替换成async,将yield替换成await,仅此而已(async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果);进一步说,async函数完全可以看作多个异步操作包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

Module

在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

1
2
3
4
5
6
7
8
// CommonJS模块
let { stat, exists, readFile } = require('fs');

// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;

上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取三个方法。这种加载称为运行时加载,因为只有运行时才能得到这个对象,导致完全没办法在编译时做静态优化。

ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

1
2
// ES6模块
import { stat, exists, readFile } from 'fs';

上面代码的实质是从fs模块加载三个方法,其他方法不加载。这种加载称为编译时加载或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。

1
2
3
4
5
6
7
(function(msg) {
console.info(msg);
}('func run...'));

export default {
name: 'AppModule'
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html>
<head>
<title>ES2015</title>
</head>
<body>
<h1>ECMAScript modules in browsers</h1>
<script type="module">
import AppModule from './AppModule.js'; // func run...
console.log(AppModule); // {name: "AppModule"}
</script>
</body>
</html>

Decorator

装饰器(Decorator)是一种与类(class)相关的语法,用来注释或修改类和类方法。装饰器是一种函数(写成@+函数名),它可以放在类和类方法的定义前面。

1
2
3
4
5
6
7
8
9
10
@testable
class MyTestableClass {
// ...
}

function testable(target) {
target.isTestable = true;
}

MyTestableClass.isTestable; // true

上面代码中,@testable 就是一个装饰器。它修改了 MyTestableClass这 个类的行为,为它加上了静态属性isTestable。testable 函数的参数 target 是 MyTestableClass 类本身。

基本上,装饰器的行为就是下面这样:

1
2
3
4
5
6
7
@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

也就是说,装饰器是一个对类进行处理的函数。装饰器函数的第一个参数,就是所要装饰的目标类

1
2
3
function testable(target) {
// ...
}

上面代码中,testable函数的参数target,就是会被装饰的类。

如果觉得一个参数不够用,可以在装饰器外面再封装一层函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
function testable(isTestable) {
return function(target) {
target.isTestable = isTestable;
}
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

上面代码中,装饰器 testable 可以接受参数,这就等于可以修改装饰器的行为。

注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数

前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的prototype对象操作。

1
2
3
4
5
6
7
8
9
function testable(target) {
target.prototype.isTestable = true;
}

@testable
class MyTestableClass {}

let obj = new MyTestableClass();
obj.isTestable // true

上面代码中,装饰器函数testable是在目标类的prototype对象上添加属性,因此就可以在实例上调用。

TypeScript

TypeScript 中的类除了实现了所有 ES6 中的类的功能以外,还添加了一些新的用法。

修饰符

TypeScript 可以使用三种访问修饰符:publicprivateprotected

  • public 修饰的属性或方法是公有的,可以在任何地方被访问到,默认所有的属性和方法都是 public
  • private 修饰的属性或方法是私有的,不能在声明它的类的外部访问
  • protected 修饰的属性或方法是受保护的,它和 private 类似,区别是它在子类中也是允许被访问的

readonly修饰符

可以使用 readonly 关键字将属性设置为只读的,只读属性必须在声明时或构造函数里被初始化。

1
2
3
4
5
6
class AuthController {
private readonly authService: AuthService;
public constructor(authService: AuthService) {
this.authService = authService;
}
}

参数属性

修饰符和 readonly 可以使用在构造函数参数中,等同于在类中定义该属性同时给该属性赋值,使代码更简洁。

1
2
3
4
5
6
7
8
9
/**
* 使用参数属性可以方便地把声明和赋值合并至一处
*/
class AuthController {
// private readonly authService: AuthService;
constructor(private readonly authService: AuthService) {
// this.authService = authService;
}
}

抽象类

抽象类是供其他类继承的基类,抽象类不允许被实例化,抽象类中的抽象方法必须在子类中被实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract class Animal {
public name;
public constructor(name) {
this.name = name;
}
public abstract sayHi();
}

class Cat extends Animal {
public sayHi() {
console.log(`Meow, My name is ${this.name}`);
}
}

let cat = new Cat('Tom');

接口

在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。在TypeScript里,我们只会去关注值的外形,只在两个类型内部的结构兼容那么这两个类型就是兼容的。这就允许我们在实现接口时候只要保证包含了接口要求的结构就可以,而不必明确地使用 implements 语句。

1
2
3
4
5
6
7
8
9
10
11
12
interface Person {
firstName: string;
lastName: string;
}

function greeter(person: Person) {
return "Hello, " + person.firstName + " " + person.lastName;
}

let user = { firstName: "Jane", lastName: "User" };

document.body.innerHTML = greeter(user);

装饰器

随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。若要启用实验性的装饰器特性,你必须在命令行或 tsconfig.json 里启用 experimentalDecorators 编译器选项。

装饰器是一种特殊类型的声明,它能够被附加到类声明、方法、访问符、属性或参数上。装饰器使用 @expression 这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。

参考链接