[Javascript] The Core

sddtc 于 2018-06-24 发布

This article covers ES2017+ runtime system.

丨- Object
丨- Prototype
丨- Class
丨- Execution context

Object

ECMAScript 是一种面向对象的语言, 以 prototype 为基本组织形式, 核心表现形式为 object.

Def. 1: Object: An object is a collection of properties, and has a single prototype object. The prototype may be either an object or the null value.

对象是一组 properties 的集合, 有唯一的 prototype 对象, 该对象要么是 null, 要么是另外一个对象.
例如:

let point = {
  x: 10,
  y: 20
};

这样, 我们创建了一个对象, 它包含两个显式的属性(x, y)与一个隐式的属性__proto__, 这个隐式属性的引用与point关联
prototype objects 常常在ECMAScriptdynamic dispatch机制方面用来实现继承的特性

Prototype

每个对象在创建后便拥有prototype属性, 如果没有显式的赋值, 对象会拥有一个default prototype.

Def. 2: Prototype: A prototype is a delegation object used to implement prototype-based inheritance.(原型是用于实现基于原型的继承的委托对象)

prototype 可以被__proto__显式的赋值, 或者通过Object.create方法.
例如:

// Base object.
let point = {
  x: 10,
  y: 20,
};
 
// Inherit from `point` object.
let point3D = {
  z: 30,
  __proto__: point,
};
 
console.log(
  point3D.x, // 10, inherited
  point3D.y, // 20, inherited
  point3D.z  // 30, own
);

任何对象都可以作为其它对象的prototype, 每个对象亦可以拥有自己的prototype. 如果对象有prototype并且非空, 以此类推可称之为prototype chain.

Def. 3: Prototype chain: A prototype chain is a finite chain of objects used to implement inheritance and shared properties.

a prototype chain的规则很简单, 如果object A没有属性x, 那么会查找object Aprototype有没有属性x、 查找object Aprototypeprototype、 以此类推. 直到遍历完整个chain.
从技术上讲,这种机制被称为dynamic dispatchdynamic delegation

Def. 4: Delegation: a mechanism used to resolve a property in the inheritance chain. The process happens at runtime, hence is also called dynamic dispatch.

若遍历完整个prototype chain依然没有找到属性x, 那么程序会返回undefined.
例如:

// An "empty" object.
let empty = {};
 
console.log(
 
  // function, from default prototype
  empty.toString,
   
  // undefined
  empty.x,
 
);

我们会看到, 当创建一个对象时, 就算是空对象它也永远不是真正的empty, 因为它总会从Object.prototype上继承些什么. 除非我们显式的将它的prototype置为null:
例如:

// Doesn't inherit from anything.
let dict = Object.create(null);
 
console.log(dict.toString); // undefined

The dynamic dispatch机制允许继承链的完全可变性,提供更改委托对象的能力:
例如:

let protoA = {x: 10};
let protoB = {x: 20};
 
// Same as `let objectC = {__proto__: protoA};`:
let objectC = Object.create(protoA);
console.log(objectC.x); // 10
 
// Change the delegate:
Object.setPrototypeOf(objectC, protoB);
console.log(objectC.x); // 20

尽管__proto__时至今日已经标准化, 使用方便容易理解, 但我们仍然倾向于使用API来操作对象的prototype, 例如方法Object.create, Object.getPrototypeOf, Object.setPrototypeOf.
我们学习了Object.prototype, 知道它的优势是可以在多个对象间共享, 基于这个原则, 让我们来看看JS里面的"class".

Class

当多个对象拥有统一的行为和相同的初始状态时, 他们便可以当成一类对象.

Def. 5: Class: A class is a formal abstract set which specifies initial state and behavior of its objects.

例如:

let letter = {
  getNumber() {
    return this.number;
  }
};
 
let a = {number: 1, __proto__: letter};
let b = {number: 2, __proto__: letter};
// ...
let z = {number: 26, __proto__: letter};
 
console.log(
  a.getNumber(), // 1
  b.getNumber(), // 2
  z.getNumber(), // 26
);

这种方式虽然可以实现需求, 但是显得很麻烦, 而类正是为了解决这个问题(语法糖), 构造方法也有同样的效果, 只是在这里类看起来更加的适合.
例如:

class Letter {
  constructor(number) {
      this.number = number;
  }

  getNumber() {
      return this.number;
  }
}

let letterA = new Letter(1);
let letterB = new Letter(2);
//
let letterZ = new Letter(26);

console.log(
  letterA.getNumber(),
  letterB.getNumber(),
  letterZ.getNumber()
);

从技术上来讲, "class" = 构造方法 + prototype的组合实现. 使用构造方法来创建对象, 该对象会自动被赋予prototype的属性, 该属性存储在<ConstructorFunction>.prototype.

Def. 6: Constructor: A constructor is a function which is used to create instances, and automatically set their prototype.

在以前, 开发人员用以下方式来实现类似于构造方法的功能:
例如:

function LegacyLetter(number) {
  this.number = number;
}

LegacyLetter.prototype.getNumber = function() {
  return this.number;
}

let legacyA = new LegacyLetter(1);
let legacyB = new LegacyLetter(2);
let legacyZ = new LegacyLetter(26);

console.log(
  legacyA.getNumber(),
  legacyB.getNumber(),
  legacyZ.getNumber(),
)

虽然创建单层构造函数非常简单, 但父类的继承模式需要更多的模板. 目前, 这个模板作为一个实现细节隐藏起来: 这正是我们在JavaScript中创建一个类时发生的情况.
Note 任何对象的实际原型总是__proto__引用. 而构造函数的显式原型属性只是对其实例原型的引用; 从实例中它仍然被__proto__引用. 具体细节在此
到目前为止, 我们已经理解了ECMAScript的对象相关的知识, 接下来让我们学习更深一层的知识: JS runtime system

Execution context

ECMAScript定义了execution context来执行和追踪JS code. execution context在逻辑上用一个栈来表示.

Def. 7: Execution context: An execution context is a specification device that is used to track the runtime evaluation of the code.

ECMAScript代码有几种类型:the global code, function code, eval code, and module code; 每种都在其execution context中进行评估. 不同的代码类型及其适当的对象可能会影响execution context的结构: 例如, generator functions将其生成器对象保存在execution context中.
例如一个递归函数如下所示:

function recursive(flag) {
  if(flag === 2) {
      return;
  }
  recursive(++flag);
}

recursive(0);

当函数被调用时, 一个新的execution context被初始化出来, 并被放入栈中, 此时它是一个activeexecution context. 当函数执行完返回时, 它的context从栈中退出.
context如果还调用了其它context, 我们称之为caller, 被调用的那一方我们称之为callee. 在上面的例子里, 递归函数recursive既是caller也是callee.

Def. 8: Execution context stack: An execution context stack is a LIFO structure used to maintain control flow and order of execution.

the global context永远在栈的最底层, 它是在执行任何其他context之前创建的.
一般情况下, context的代码可运行至完成, 但正如我们上面提到的那样, 某些对象(如generators)可能会违反堆栈的LIFO顺序. generators可以暂停其running context, 并在完成之前将其从堆栈中移除. 一旦generator再次激活, 其context恢复并再次被压入堆栈:
例如:

function *gen() {
  yield 1;
  return 2;
}

let g = gen();

console.log(
  g.next().value, // 1
  g.next().value, // 2
);

接下来, 我们将讨论execution context的重要组成部分; 特别是我们应该看到ECMAScript运行时如何管理variables storage, and scopes created by nested blocks of a code. 这是一个通用概念, 用于JS存储数据, 并通过闭包机制解决Funarg问题.