周元-TS与编译器

# 周元-TS与编译器

# TS

Typescript是 JavaScript 超集,包含类型系统,以及其他一些功能。

  • 类型系统、type-checking;
  • 类型(自动)推导、auto-completion;

Q: 为什么要用Typescript?

  1. 提升代码健壮性;
  2. 面向接口编程(代码自解释,并行开发);
  3. 静态检查可以提高开发效率;
  4. 减少开发时(人工推导带来的)认知负荷

# Enum 类型

枚举类型用于定义数值集合,使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。

// 枚举
enum Color {
    // 初始值默认为 0 其余的成员会会按顺序自动增长 可以理解为数组下标
    RED,
    PINK
}
const color: Color = Color.RED;
console.log(color); // 0

// 字符串枚举
enum Color {
  RED = "红色",
  PINK = "粉色",
  BLUE = "蓝色",
}

const pink: Color = Color.PINK;
console.log(pink); // 粉色
  1. 普通枚举:初始值默认为 0 其余的成员会会按顺序自动增长 可以理解为数组下标
  2. 字符串枚举
  3. 常量枚举

使用 const 关键字修饰的枚举,常量枚举与普通枚举的区别

const enum Color {
  RED,
  PINK,
  BLUE,
}

const color: Color[] = [Color.RED, Color.PINK, Color.BLUE];
console.log(color); //[0, 1, 2]

// 编译之后的js如下:
var color = [0 /* RED */, 1 /* PINK */, 2 /* BLUE */];
// 可以看到我们的枚举并没有被编译成js代码 只是把color这个数组变量编译出来了

# 基本类型

// 数组
const arr: number[] = [1,2,3];
const arr2: Array<number> = [1,2,3];


// 元组(tuple)类型
// 元组( Tuple )表示一个已知数量和类型的数组,可以理解为他是一种特殊的数组
const tuple: [number, string] = [1, "xianzao"];


// undefined和null
let a: undefined = undefined;
let b: null = null;

let str: string = 'xianzao';
// 默认情况下 null 和 undefined 是所有类型的子类型。 也就是说你可以把 null 和 undefined 赋值给其他类型。
str = null; // 编译正确
str = undefined; // 编译正确

// undefined 可以给 void 赋值
let c:void = undefined // 编译正确
let d:void = null // 编译错误


// any会跳过类型检查器对值的检查,任何值都可以赋值给any类型。
let value: any = 1;
value = "xianzao"; // 编译正确
value = []; // 编译正确
value = {};// 编译正确


// void 意思就是无效的, 一般只用在函数上,告诉别人这个函数没有返回值。
function sayHello(): void {
  console.log("hello xianzao");
}


// never 类型表示的是那些永不存在的值的类型。 例如never 类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型
// 异常:如果一个函数执行时抛出了异常,那么这个函数永远不存在返回值
function error(msg: string): never { // 编译正确
  throw new Error(msg); 
}
// 死循环:函数中执行无限循环的代码(死循环)
function loopForever(): never { // 编译正确
  while (true) {};
}


// unknown与any一样,所有类型都可以分配给unknown:
let value: unknown = 1;
value = "xianzao"; // 编译正确
value = false; // 编译正确
  • 任何类型的值可以赋值给any,同时any类型的值也可以赋值给任何类型。unknown 任何类型的值都可以赋值给它,但它只能赋值给unknown和any。

# object, Object 和 {} 类型

  • object 类型用于表示所有的非原始类型,即我们不能把 number、string、boolean、symbol等 原始类型赋值给 object。在严格模式下,null 和 undefined 类型也不能赋给 object。

  • Object 代表所有拥有 toString、hasOwnProperty 方法的类型 所以所有原始类型、非原始类型都可以赋给 Object(严格模式下 null 和 undefined 不可以)

  • {} 空对象类型和大 Object 一样 也是表示原始类型和非原始类型的集合

let object: object;
object = 1; // 报错
object = "a"; // 报错
object = true; // 报错
object = null; // 报错
object = undefined; // 报错
object = {}; // 编译正确


let bigObject: Object;
object = 1; // 编译正确
object = "a"; // 编译正确
object = true; // 编译正确
object = null; // 报错
ObjectCase = undefined; // 报错
ObjectCase = {}; // ok

# 其他类型

// 类
class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  sayHi(): void {
    console.log(`Hi, ${this.name}`);
  }
}


// 函数
function add(x: number, y: number): number {
  return x + y;
}
// 函数,返回数字类型
const add = function(x: number, y: number): number {
  return x + y;
}
// 可选参数
function add(x: number, y?: number): number {
  return y ? x + y : x;
}


// 接口定义函数
interface Add {
  (x: number, y: number): number;
}


// 在上下文中当类型检查器无法断定类型时,可以使用缀表达式操作符 ! 进行断言操作对象是非 null 和非 undefined 的类型,即x!的值不会为 null 或 undefined
let user: string | null | undefined;
console.log(user!.toUpperCase()); // 编译正确
console.log(user.toUpperCase()); // 错误


// 我们定义了变量, 没有赋值就使用,则会报错
let value:number
console.log(value); // Variable 'value' is used before being assigned.


// 联合类型用|分隔,表示取值可以为多种类型中的一种。
let status:string|number
status='xianzao'
status=1


// 交叉类型就是跟联合类型相反,用&操作符表示,交叉类型就是两个类型必须存在。
// 交叉类型取的多个类型的并集,但是如果key相同但是类型不同,则该key为never类型
interface IpersonA{
  name: string,
  age: number
}
interface IpersonB {
  name: string,
  gender: string
}
let person: IpersonA & IpersonB = { 
    name: "xianazo",
    age: 18,
    gender: "male"
};
  • 类型守卫:类型保护是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内。
  1. in关键字
  2. typeof 关键字
  3. instanceof
  4. 自定义类型保护的类型谓词
    function isNumber(num: any): num is number {
        return typeof num === 'number';
    }
    function isString(str: any): str is string{
        return typeof str=== 'string';
    }
    

# 接口

TypeScript 的核心原则之一是对值所具有的结构进行类型检查。 而接口的作用就是为这些类型命名和为你的代码或第三方代码定义数据模型

interface Person {
    readonly name: string; // 只读
    gender: string;
    age?: number; // 可选
    [prop: string]: any; // 索引签名:允许有其他的任意属性

}
let xianzao: Person = {
    name: 'xianzao',
    gender: 'boy'
};
  • 接口和类型别名都可以用来描述对象或函数的类型,只是语法不同。
type MyTYpe = {
  name: string;
  say(): void;
}

interface MyInterface {
  name: string;
  say(): void;
}



// type 使用 & 实现扩展
type MyType2 = MyType & {
  sex:string;
}

// interface 用 extends 来实现扩展
interface MyInterface2 extends MyInterface {
  sex: string;
}
  • type 和 interface
// type可以声明基本数据类型别名/联合类型/元组等,而interface不行;
// 基本类型别名
type UserName = string;
type UserName = string | number;
// 联合类型
type Animal = Pig | Dog | Cat;
type List = [string, boolean, number];


// interface能够合并声明,而type不行;
interface Person {
  name: string
}
interface Person {
  age: number
}
// 此时Person同时具有name和age属性

# 泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

  1. 泛型的语法是尖括号 <> 里面写类型参数,一般用 T 来表示第一个类型变量名称,其实它可以用任何有效名称来代替
  2. 泛型就像一个占位符一个变量,在使用的时候我们可以将定义好的类型像参数一样传入,原封不动的输出
// 函数的参数可以是任何值,返回值就是将参数原样返回,并且参数的类型是 string,函数返回类型就为 string
function getValue<T>(arg:T):T  {
  return arg;
}

// 使用
getValue<string>('xianzao'); // 定义 T 为 string 类型
// 或
getValue('xianzao') // 自动推导类型为 string


// 多个参数:可以引入希望定义的任何数量的类型变量
function getValue<T, U>(arg:[T,U]):[T,U] {
  return arg;
}
// 使用
const str = getValue(['xianzao', 18]);


// 可以使用extends关键字来对泛型进行约束
interface Lengthwise {
  length: number;
}
function getLength<T extends Lengthwise>(arg:T):T  {
  console.log(arg.length); 
  return arg;
}


// 在定义接口的时候指定泛型
interface KeyValue<T,U> {
  key: T;
  value: U;
}
const person1:KeyValue<string,number> = {
  key: 'xianzao',
  value: 18
}
const person2:KeyValue<number,string> = {
  key: 20,
  value: 'zaoxian'
}

// 泛型类型别名
type Cart<T> = { list: T[] } | T[];
let c1: Cart<string> = { list: ["1"] };
let c2: Cart<number> = [1];


/**
 * typeof: 关键词除了做类型保护,还可以从实现推出类型。
 */
//先定义变量,再定义类型
let p1 = {
  name: "xianzao",
  age: 18,
  gender: "male",
};
type People = typeof p1;
function getName(p: People): string {
  return p.name;
}
getName(p1);


/**
 * keyof: 可以用来获取一个对象接口中的所有 key 值。
 */
interface Person {
  name: string;
  age: number;
  gender: "male" | "female";
}
type PersonKey = keyof Person; //type PersonKey = 'name'|'age'|'gender';
function getValueByKey(p: Person, key: PersonKey) {
  return p[key];
}
let val = getValueByKey({ name: "xianzao", age: 18, gender: "male" }, "name");
console.log(val); // xianzao


/**
 * in: 用来遍历枚举类型
 */
type Keys = "a" | "b" | "c"
type Obj =  {
  [p in Keys]: any
} // -> { a: any, b: any, c: any }

# 内置工具类型

// Required: 将类型的属性变成必选
interface Person {
    name?: string,
    age?: number,
    hobby?: string[]
}
const user: Required<Person> = {
    name: "xianzao",
    age: 18,
    hobby: ["code"]
}


// Partial: 与 Required 相反,将所有属性转换为可选属性
interface Person {
    name: string,
    age: number,
}
type User = Partial<Person>
const xianzao: User={
  name:'xianzao'
} // 编译正确

# 编译器

编译器 compiler

代码 一种语言 => 另一种语言

less sass eslint webpack

  1. 词法分析
  2. 语法分析
  3. 代码转换
  4. 代码生成

# 词法分析(Lexical Analysis)

将文本分割成一个个的“token”。

例如:init、main、init、x、;、x、=、3、;、}等等。同时它可以去掉一些注释、空格、回车等等无效字符

  1. 使用正则进行词法分析: 不容易维护,性能不高

  2. 使用自动机进行词法分析

  • 有穷状态自动机(finite state machine):在有限个输入的情况下,在这些状态中转移并期望最终达到终止状态。

    • "确定有穷状态自动机”(DFA - Deterministic finite automaton): 在输入一个状态时,只得到一个固定的状态。DFA 可以认为是一种特殊的 NFA;
    • “非确定有穷自动机”(NFA - Non-deterministic finite automaton):当输入一个字符或者条件得到一个状态机的集合。JavaScript 正则采用的是 NFA 引擎

# 语法分析(Syntactic Analysis)

我们要解析一门语言,前提是这门语言有严格的语法规定的语言,定义语言的语法规格称为文法

语法分析的目的就是通过词法分析器拿到的token流 + 结合文法规则,通过一定算法得到一颗抽象语法树(AST)

token + 文法 -> AST

babel插件的原理就是:es6代码 → Babylon.parse → AST → babel-traverse → 新的AST → es5代码

自顶向下的分析方法 vs 自底向上的分析方法

  • 自底向上算法分析文法范围广,但实现难度大;
  • 自顶向下算法实现相对简单,并且能够解析文法的范围也不错,所以一般的compiler都是采用深度优先索引的方式。

# 代码转换(Transformation)

在得到AST后,我们一般会先将AST转为另一种AST,目的是生成更符合预期的AST,这一步称为代码转换。

AST -> AST

  1. 逻辑更清晰:将AST映射成中间代码表示,再映射成目标代码的工作分层进行,使编译算法更加清晰;
  2. 与机器无关:它作为中间语言可以为生成多种不同型号的目标机器码服务,易于移植

在转换阶段通常有两种形式:

  1. 同语言的AST转换;
  2. AST转换为新语言的AST;

这里有一种通用的做法是,对我们之前的AST从上至下的解析(称为traversal),然后会有个映射表(称为visitor),把对应的类型做相应的转换。

# 代码生成 (Code Generation)

在实际的代码处理过程中,可能会递归的分析(recursive)我们最终生成的AST,然后对于每种type都有个对应的函数处理;我们的目标代码会在这一步输出。

input => tokenizer => tokens; // 词法分析
tokens => parser => ast; // 语法分析,生成AST
ast => transformer => newAst; // 中间层代码转换
newAst => generator => output; // 生成目标代码

# 编译器实现

实现一个简单的Compiler:

/**
实现          input                   output

2 + 2        (add 2 2)               add(2, 2)
4 - 2        (subtract 4 2)          subtract(4, 2)
2 + (4 - 2)  (add 2 (subtract 4 2))  add(2, subtract(4, 2))
 */


/**
 * 大多数编译器分为三个主要阶段:解析、转换、
 * 和代码生成
 *
 * 1. *解析* 将原始代码转化为更抽象的代码
 * 代码的表示。
 *
 * 2. *转换* 采用这种抽象表示并进行操作
 * 无论编译器想要什么。
 *
 * 3. *代码生成*采用转换后的代码表示,并
 * 将其转换为新代码。
 */
  1. 词法分析实现:input => tokens
/**
 * input: (add 2 (subtract 4 2))
 * tokens: 
    [
 *     { type: 'paren',  value: '('        },
 *     { type: 'name',   value: 'add'      },
 *     { type: 'number', value: '2'        },
 *     { type: 'paren',  value: '('        },
 *     { type: 'name',   value: 'subtract' },
 *     { type: 'number', value: '4'        },
 *     { type: 'number', value: '2'        },
 *     { type: 'paren',  value: ')'        },
 *     { type: 'paren',  value: ')'        },
 *   ]
 */

function tokenizer(input) {
  let current = 0;
  let tokens = [];
  while (current < input.length) {
    let char = input[current];
    if (char === '(') {
      tokens.push({
        type: 'paren',
        value: '(',
      });
      current++;
      continue;
    }

    if (char === ')') {
      tokens.push({
        type: 'paren',
        value: ')',
      });
      current++;
      continue;
    }

    // 匹配空格
    let WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }


    // 匹配数字
    let NUMBERS = /[0-9]/;
    if (NUMBERS.test(char)) {
      let value = '';
      // 遍历判断是否是连续的数字,比如:234   
      while (NUMBERS.test(char)) {
        value += char;
        char = input[++current];
      }
      tokens.push({ type: 'number', value });
      continue;
    }

    // 字符串
    if (char === '"') {
      let value = '';
      char = input[++current];
      while (char !== '"') {
        value += char;
        char = input[++current];
      }
      char = input[++current];
      tokens.push({ type: 'string', value });
      continue;
    }

    // 英文字母
    let LETTERS = /[a-z]/i;
    if (LETTERS.test(char)) {
      let value = '';
      while (LETTERS.test(char)) {
        value += char;
        char = input[++current];
      }
      tokens.push({ type: 'name', value });
      continue;
    }

    // 否则报错
    throw new TypeError('I dont know what this character is: ' + char);
  }
  return tokens;
}
  1. 语法分析:tokens => ast
/**
 * AST:
 * {
 *     type: 'Program',
 *     body: [{
 *       type: 'CallExpression',
 *       name: 'add',
 *       params: [{
 *         type: 'NumberLiteral',
 *         value: '2',
 *       }, {
 *         type: 'CallExpression',
 *         name: 'subtract',
 *         params: [{
 *           type: 'NumberLiteral',
 *           value: '4',
 *         }, {
 *           type: 'NumberLiteral',
 *           value: '2',
 *         }]
 *       }]
 *     }]
 *  }
 */
function parser(tokens) {
  let current = 0;

  //   
  function walk() {
    let token = tokens[current];
    // 获取数字
    if (token.type === 'number') {
      current++;
      return {
        type: 'NumberLiteral', // 标记类型
        value: token.value,
      };
    }

    // 字符串
    if (token.type === 'string') {
      current++;
      return {
        type: 'StringLiteral',
        value: token.value,
      };
    }

    // 匹配左括号
    if (token.type === 'paren' && token.value === '(') {
      token = tokens[++current];
      let node = {
        type: 'CallExpression',
        name: token.value,
        params: [],
      };
      token = tokens[++current]; // 递增
      // 遍历,递归取出右括号里的内容,添加到node.params 
      while (token.type !== 'paren' || (token.type === 'paren' && token.value !== ')')) {
        node.params.push(walk());
        token = tokens[current];
      }
      current++;
      return node;
    }

    // 报错
    throw new TypeError(token.type);
  }

  let ast = {
    type: 'Program',
    body: [],
  };
  // 遍历添加   
  while (current < tokens.length) {
    ast.body.push(walk());
  }
  return ast; // 返回ast树
}
  1. 代码转换:AST => 新的AST

/**
 *  (add 2 (subtract 4 2))     =>        add(2, subtract(4, 2))
 * ----------------------------------------------------------------------------
 *   Original AST                     |   Transformed AST
 * ----------------------------------------------------------------------------
 *   {                                |   {
 *     type: 'Program',               |     type: 'Program',
 *     body: [{                       |     body: [{
 *       type: 'CallExpression',      |       type: 'ExpressionStatement',
 *       name: 'add',                 |       expression: {
 *       params: [{                   |         type: 'CallExpression',
 *         type: 'NumberLiteral',     |         callee: {
 *         value: '2'                 |           type: 'Identifier',
 *       }, {                         |           name: 'add'
 *         type: 'CallExpression',    |         },
 *         name: 'subtract',          |         arguments: [{
 *         params: [{                 |           type: 'NumberLiteral',
 *           type: 'NumberLiteral',   |           value: '2'
 *           value: '4'               |         }, {
 *         }, {                       |           type: 'CallExpression',
 *           type: 'NumberLiteral',   |           callee: {
 *           value: '2'               |             type: 'Identifier',
 *         }]                         |             name: 'subtract'
 *       }]                           |           },
 *     }]                             |           arguments: [{
 *   }                                |             type: 'NumberLiteral',
 *                                    |             value: '4'
 * ---------------------------------- |           }, {
 *                                    |             type: 'NumberLiteral',
 *                                    |             value: '2'
 *                                    |           }]
 *  (sorry the other one is longer.)  |         }
 *                                    |       }
 *                                    |     }]
 *                                    |   }
 * ----------------------------------------------------------------------------
 */


// 转换
// 通过监听visitor中类型进行处理
function traverser(ast, visitor) {
  
  // 处理数组
  function traverseArray(array, parent) {
    array.forEach(child => {
      traverseNode(child, parent);
    });
  }

  // 处理节点
  function traverseNode(node, parent) {
    // 获取visitor中定义的方法
    let methods = visitor[node.type];

    // 执行enter方法
    if (methods && methods.enter) {
      methods.enter(node, parent);
    }

    // 处理节点
    switch (node.type) {
      case 'Program': // 入口节点
        traverseArray(node.body, node);
        break;

      case 'CallExpression': // 函数调用节点
        traverseArray(node.params, node);
        break;

      case 'NumberLiteral': // 数字节点
      case 'StringLiteral': // 字符串节点
        break;

      default:
        throw new TypeError(node.type);
    }

    // 执行exit方法
    if (methods && methods.exit) {
      methods.exit(node, parent);
    }
  }

  traverseNode(ast, null);
}


// 转译器
function transformer(ast) {
  // 初始化newAst   
  let newAst = {
    type: 'Program',
    body: [],
  };

  // 将ast与newAst关联   
  ast._context = newAst.body;

  // 定义访问者,钩子函数,针对不同类型做不同处理 
  const visitor = {
    // 数字节点
    NumberLiteral: {
      enter(node, parent) {
        // newAst.body中注入节点
        parent._context.push({
          type: 'NumberLiteral',
          value: node.value,
        });
      },
    },
    // 字符串阶段
    StringLiteral: {
      enter(node, parent) {
        parent._context.push({
          type: 'StringLiteral',
          value: node.value,
        });
      },
    },
    // 函数调用节点 
    CallExpression: {
      //  传入旧的ast的值
      enter(node, parent) {
        let expression = {
          type: 'CallExpression', // 
          callee: { // 调用方法
            type: 'Identifier',
            name: node.name,
          },
          arguments: [], // 参数
        };

        // 将 parent._context 与 expression.arguments 关联
        node._context = expression.arguments;

        // 结束,输出
        if (parent.type !== 'CallExpression') {
          expression = {
            type: 'ExpressionStatement', // 表达式语句
            expression: expression,
          };
        }

        // newAst.body中注入节点
        parent._context.push(expression);
      },
    },
  }

  //  调用转换方法  
  traverser(ast, visitor);

  return newAst;
}

  1. 代码生成
function codeGenerator(node) {
  switch (node.type) {
    // 项目入口
    case 'Program':
      // 递归调用,换行处理  
      return node.body.map(codeGenerator).join('\n');

    // 表达式,完整的语句,分号分隔
    case 'ExpressionStatement':
      return (
        codeGenerator(node.expression) + ';'
      );

    // 函数调用
    case 'CallExpression':
      return codeGenerator(node.callee) + '(' + node.arguments.map(codeGenerator).join(', ') + ')';

    case 'Identifier':
      return node.name;

    case 'NumberLiteral':
      return node.value;

    case 'StringLiteral':
      return '"' + node.value + '"';

    default:
      throw new TypeError(node.type);
  }
}
  1. 调用
function compiler(input) {
  let tokens = tokenizer(input);
  let ast = parser(tokens);
  let newAst = transformer(ast);
  let output = codeGenerator(newAst);

  return output;
}

const input = '(add 2 (subtract 4 2))';
const output = compiler(input);  // add(2, substract(4, 2))

# TS原理解析

Typescript编译器主要分为以下五个关键部分:

  • Scanner 扫描器 (scanner.ts)
  • Parser 解析器 (parser.ts)
  • Binder 绑定器 (binder.ts)
  • Checker 检查器 (checker.ts)
  • Emitter 发射器 (emitter.ts)

编译器主要分了三条线路:

  1. 源代码 -> 扫描器 -> token流 -> 解析器 -> AST ->绑定器 -> Symbol(符号)
  2. AST -> 检查器 ~~ Symbol(符号) -> 类型检查
  3. AST -> 检查器 ~~ 发射器 -> js代码
  • 扫描器

ts扫描器的源代码均位于scanner.ts中。通过先前的流程图, 我们发现扫描器的作用就是将源代码生成token流。

扫描器通过对输入的源代码进行词法分析, 得到对应的SyntaxKind 即“token”。

  • 解析器

生成AST的过程其实就调用了一个createSourceFile 函数。

  • 绑定器

典型的 JavaScript 转换器只有以下流程:

源码 ~~扫描器~~> Tokens ~~解析器~~> AST ~~发射器~~> JavaScript

上述架构确实对于简化 TypeScript 生成 JavaScript 的理解有帮助,但缺失了一个关键功能,即 TypeScript 的语义系统。为了协助(检查器执行)类型检查,绑定器将源码的各部分连接成一个相关的类型系统,供检查器使用。绑定器的主要职责是创建符号(Symbols)

符号将 AST 中的声明节点与其它声明连接到相同的实体上。符号是语义系统的基本构造块。

  • *检查器

它就是根据我们生成AST上节点的声明起始节点的位置对传进来的字符串做位置类型语法等的校验与异常的抛出。

AST -> 检查器 ~~ Symbol(符号) -> 类型检查

  • 发射器

TypeScript 编译器提供了两个发射器:

  • emitter.ts: 它是 TS -> JavaScript 的发射器;
  • declarationEmitter.ts: 用于为 TypeScript 源文件(.ts) 创建声明文件

# 备注

  • tsconfig.json: ts规范;把里面的参数全部弄懂;

官网tsconfig配置 (opens new window)

可参考里面tsconfig.json的写法写一份配置规范~

# 讲义

# 其他

上次更新: 7/6/2023, 7:44:05 PM
最近更新
01
taro开发实操笔记
09-29
02
前端跨端技术调研报告
07-28
03
Flutter学习笔记
07-15
更多文章>