从JavaScript到ES6再到TypeScript

版权声明:本文为博主原创文章,转载请注明原地址,谢谢 https://blog.csdn.net/QuinnNorris/article/details/84852242

严格地说,这个题目是不够严谨的。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']

猜你喜欢

转载自blog.csdn.net/QuinnNorris/article/details/84852242