运行时

本文共--字 阅读约--分钟 | 浏览: -- Last Updated: 2021-05-21

TypeScript 只存在于程序设计阶段,在运行前,会被编译成JavaScript代码,所以TypeScript的运行时就是JavaScript的运行时,编译之后的代码会在服务端(node.js)或客户端(浏览器)执行,然后就可能遇到运行时的问题;

环境

运行时环境是开发一个应用首先考虑到的事情,一旦编译完成,TypeScript就可能被不同的JavaScript引擎执行,主要就是浏览器,也可能是Node.js或RingoJS环境中运行的服务端程序或桌面应用。

所以需要谨慎使用那些仅在特定运行时环境才可用的变量,例如,浏览器中的 DOM 和 BOM 对象,又比如Node.js中的process。

另外一点需要把同一 JavaScript 运行时的不同版本考虑在内,我们的应用常常需要兼容多个浏览器或多个版本的 Node.js。

处理这些问题的比较好的实践就是添加一个逻辑,在使用一个特性前,先检测一个这个特性是否存在,有一个很好用的库,Modernizr

运行时的一些概念

帧 frame

一个帧是一个连续的工作单元,在JavaScript函数被调用时,运行时环境就会在栈中创建一个帧,帧里面保存了特殊函数的参数和局部变量。当函数返回时,帧就被栈中推出。

function foo(b) {
  var a = 12;
  return a + b + 35;
}

function bar(x) {
  var m = 4;
  return foo(m * x);
}

bar(21);

// 当bar被执行时,运行时就会创建一个包含bar的参数和所有局部变量的帧,会被添加到栈顶。
// bar 中调用了 foo,那么就又会创建一个新的帧添加到栈顶
// 然后 foo 执行完毕,栈顶部的帧就被移除
// 之后 bar 执行完毕,对应的帧也同样被移除

// 如果 foo 中又调用了 bar,就会形成死循环,导致栈溢出错误

栈 stack

栈包含了一个信息在执行时的所有步骤(帧),栈的数据结构是一个后进先出的对象集合,因此新的帧,总是被添加在最上面。也因此事件循环会从上至下地处理栈中的帧,单帧所依赖的其他的帧,将会被添加在此帧的上面,以保证它从栈中可以获取到依赖的信息。

队列 queue

队列中包含一个待执行信息的列表,每一个信息都于一个函数相互联系,当栈为空时,队列中的一条信息就会被取出并且处理。处理的过程为调用该信息所关联的函数,然后将此帧添加到栈的顶部,当栈再次为空时,本次信息处理即视为结束;

堆 heap

堆是一个内存存储空间,它不关注内部存储的内容的保存顺序,堆中保存了所有正在被使用的变量和对象。同时也保存了一些当前作用域已经不会再使用到,但还没有被垃圾回收的帧。

事件循环

并发是指同一时间有两个或更多的操作一起执行,由于运行时单线程的原因,事件循环内的信息都是线性执行的,每当一个函数被调用,队列中就被加入一个新的信息,如果栈是空的,那么函数就会立即执行,当所有的帧都被加入栈中之后,栈便从上至下一个个清楚(执行)这些帧,最后栈被清空,然后下一个信息将会被处理。

使用事件循环的好处是执行顺序是非常容易预测且容易追踪的,另一个好处就是,在事件循环内可以进行非阻塞 I/O 操作,意味着当一个应用在等待 I/O 操作的执行结果时,它还可以处理其他事情,比如处理用户输入。

事件循环的一个特点是,当一个信息需要大量的事件来处理时,应用会变得无响应,一个好的做法是,保持每个信息尽量简短,可能的话,将一个信息函数分割为多个小函数。

this 操作符

在JavaScript中,this的值通常由它所属的函数被调用的方式来决定,它的值不能在执行时通过赋值操作来设置,并且同一个函数以不同的方式被调用,其this值也可能不同;

全局上下文的this: 在全局中this操作符总是指向全局对象,在浏览器中,window对象即是全局对象;

函数上下文中的this:函数中的this操作符,指向函数的调用者。

call、apply、bind:可以使用这些方法来设置函数内部的this操作符的值,funcA.apply(真正的调用者, args)

一旦使用bind方法为一个函数内的this操作符进行了绑定,那么就不能再apply或者call去再次覆盖它。

在构造函数中,this指向对象的原型

原型

实例属性与类属性

1、可以在类方法中访问类属性

// 类
function Math() {}

// 类属性
Math.PI = '3.1415926';

// 类方法
Math.fn = function (radius) {
  return radius * radius * this.PI;
  // this 指向 Math
}

2、可以在实例方法中访问类属性

// 类
function Math() {}

// 类属性
Math.PI = '3.1415926';

// 实例方法
Math.prototype.fn = function (radius) {
  // 使用 this.constructor (返回对象构造函数的引用)访问类属性
  return radius * radius * this.constructor.PI;

  // 因为 this 指向 new 出来的实例。 实例上没有 constructor
  // 所以 this.constructor == Math.prototype.constructor
  // Math.prototype.constructor = Math
}

3、不可以在类属性或者方法中访问实例属性或方法;

function Math() {
  // 实例属性
  this.PI = '3.1415';
}

// 类方法
Math.fn = function (radius) {
  return radius * radius * this.PI; // this.PI是 undefined
  // this 指向 Math 
}

继承

TypeScript 实现的 extends

var __extends = this.__extends || function(d, b) {
  for(var p in b) {
    // for in 遍历对象的实例,会迭代对象的实例属性
    // 当遍历一个构造函数时,将会迭代类属性
    // 这里场景用来遍历一个构造函数(类),即 子类 继承 父类的类属性和方法
    if(b.hasOwnProperty(p)) {
      d[p] = b][p];
    }
  }
  // 以下就是一个原型式继承 让子类原型对象 继承 父类的原型对象
  function __() {
    this.constructor = d; // 修复constructor的指向
  }

  __.prototype = b.prototype;
  d.prototype = new __();
  // new __().__proto__ == b.prototype
}

当实际转换 extends 关键字的时候

class A extends B {
  email: string = 'abc@123.com';
  // public age 相当于就是 this.age = age;
  constructor (name: string, public age: number) {
    super(name)
  }
}

// 转换成类似以下代码,就完成了extends关键字的寄生式组合继承
var A = /** @class */ (function (_super) {
    __extends(A, _super); // 原型式继承
    function A(name, age) {
        var _this = _super.call(this, name) || this; // 调用父类的构造函数
        _this.age = age;
        _this.email = 'abc@123.com';
        return _this;
    }
    return A;
}(B));

闭包

闭包和静态变量

// counter.ts
class Counter {
  private static _COUNTER = 0;

  constructor() {};

  private _changeBy(val: number) {
    Counter._COUNTER += val;
  }

  public increment() {
    this._changeBy(1);
  }

  public value() {
    return Counter._COUNTER;
  }
}

// 会被转换成
var Counter =(function() {
  function Counter () {};

  Counter.prototype._changeBy = function(val) {
    Counter._COUNTER += val;
  }

  Counter.prototype.increment = function(val) {
    this._changeBy(1);
  }

  Counter.prototype.value = function(val) {
    return Counter._COUNTER;
  }

  Counter._COUNTER = 0; // 静态变量被声明成了类属性,而不是实例属性,因为它可以被所有实例共享。
  return Counter;
})()

闭包和私有成员

闭包函数可以访问到在创建的字面作用域上持续存在的变量,这些变量并不是函数原型或函数体的一部分,而是闭包上下文中的一部分,由于我们不能直接访问闭包上下文,那么该上下文中的变量就可以用来模拟私有成员

TypeScript由于性能问题,并没有在运行时使用闭包模拟私有成员,不管我们添加和删除private修饰符,生成的JavaScript都不会有变化,这意味着在运行时,私有成员变成了公开成员,但是如果我们视图访问一个私有成员,TypeScript 编译器会在编译时抛错。

但是,使用闭包来模拟私有成员是完全可行的。

function makeCounter() {
  // 闭包上下文
  var _COUNTER = 0;

  function changeBy(val) {
    _COUNTER += val;
  }

  function Counter() {};

  Counter.prototype.increment = function () {
  };

  Counter.prototype.value = function () {
    return _COUNTER;
  };

  return new Counter();
}

当调用makeCounter函数时,一个新的闭包上下文被创建,所以每个实例都有独立的上下文(_COUNTER变量和 changeBy方法),由于_COUNTERchangeBy不能被直接访问,所以可以说其为私有成员。

var c1 = makeCounter();
console.log(c1.value); // 0
c1.increment();
console.log(c1.value); // 1
console.log(c1._COUNTER); // undefined;
c1.changeBy(); // error, changeBy is not function