最近读到了一篇JavaScript的文章,觉得不错。原本的中文翻译又404,所以我开始边读边翻译。这篇主要就是介绍js里面的一些非常基本但是又很重要的概念。

原文地址:http://dmitrysoshnikov.com/ecmascript/javascript-the-core/

翻译备注:因为有些英文翻译成中文会更加难懂,所以一些术语或者必要的地方,都是中文在前,后面紧随中括号中的英文。希望能让大家更好的读懂。

全文分为10个主要部分

本篇是ECMA-262-3 in detail系列的概述。每个章节都有一个更详细的内容链接,如果你觉得某个章节解释的不够过瘾,可以继续读一下每个章节对应的详细内容链接。

适合的读者:有经验的开发员,专业前端人员。

我们首先来看一下对象[Object]的概念,这也是ECMASript中最基本的概念。

An object

ECMAScript, 是一门高级抽象面向对象的语言,用以处理Objects. 当然,也有原生类型,但是必要时,也需要转换成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.

Object是一个属性的集合,并且都拥有一个单独的原型对象[prototype object]. 这个原型对象[prototype object]可以是一个object或者null值。

让我们举一个Object的例子。首先我们要清楚,一个Object的prototype是一个内部的[[prototype]]属性的引用。不过一般来说,我们会使用____ 下划线来代替双括号,例如__prototype__

看这段代码

var foo = {
  x10,
  y20
};

 

如上结构有两个显式的属性[explicit own properties]和一个自带隐式的 __proto__ 属性[implicit __proto__ property],也就是foo的prototype.

这个property有什么用处呢?我们来看一个原型链[prototype chain]的例子。

A prototype chain

Prototype对象也是对象类型的,也会有自己的prototypes。如果这个prototype仍然存在一个非空的prototype,那么这样一直搜寻下去,就形成了一个原型链[prototype chain]。

A prototype chain is a finite chain of objects which is used to implemented inheritance and shared properties.

原型链是由一系列用来继承和共享属性的对象组成有限的对象链。

考虑这样的一个情况。

有两个对象,只有一小部分的方法或者属性不同,其余的都是一样的。很明显,在一个好的设计模式中,我们会需要重用那部分相同的,而不是在每个对象中重复定义那些相同的方法或者属性。在基于类[class-based]的系统中,这些重用部分被称为类的继承 – 相同的部分放入class A,然后class B和class C从A继承,并且拥有各自的独特的东西。

ECMAScript没有类的概念。但是,重用[reuse]这个理念没什么不同(某些方面,甚至比class-更加灵活),可以由prototype chain原型链来实现。这种继承被称为delegation based inheritance-基于继承的委托,或者更通俗一些,叫做原型继承。

上述的Classes A,B,C,在ECMAScript中可以创建三个对象:a,b,c. 由a来负责b和c相同的部分,而b和c则负责各自不同的独特的东西。

var a = {
  x: 10,
  calculate: function (z) {
    return this.x + this.y + z
  }
};

var b = {
  y: 20,
  __proto__: a
};

var c = {
  y: 30,
  __proto__: a
};

// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80

这样看上去是不是很简单啦。b和c可以使用a中定义的calculate方法,这就是有原型链来[prototype chain]实现的。

我们来看一下原理:如果在对象b中找不到calculate方法(也就是对象b中没有这个calculate属性), 那么就会沿着原型链开始找。如果这个calculate方法在b的prototype中没有找到,那么就会沿着原型链找到a的prototype,一直遍历完整个原型链。记住,一旦找到,就返回第一个找到的属性或者方法。因此,第一个找到的属性成为继承属性。如果遍历完整个原型链,仍然没有找到,那么就会返回undefined.

注意一点,this这个值在一个继承机制中,仍然是指向它原本属于的对象,而不是从原型链上找到它时,它所属于的对象。例如,以上的例子,this.y是从b和c中获取的,而不是a。当然,你也发现了this.x是从a取的,因为是通过原型链机制找到的。

如果一个对象的prototype没有显示的声明过或者说定义过,那么__prototype__的默认值就是object.prototype, 而object.prototype也会有一个__prototype__, 这个就是原型链的终点了,被设置为null.

下面的图示就是表示了上述a,b,c的关系

我们再考虑一种情况。有时候,我们需要对象使用相同的或者相似的结构(例如相同的属性集),但是不同的值。在这种情况下,我们可以考虑使用构造函数[constructor function],是一种特定结构构造的对象。

Constructor

除了可以构造特定对象,构造函数[constructor]还有一个有用的功能 – 为一个新建对象创建一个原型对象。这个原型对象放置在构造函数的原型[ConstrutorFunction.prototype]中。

我们用构造函数重写一下上述的例子。

// a constructor function
function Foo(y) {
  // which may create objects
  // by specified pattern: they have after
  // creation own "y" property
  this.y = y;
}

// also "Foo.prototype" stores reference
// to the prototype of newly created objects,
// so we may use it to define shared/inherited
// properties or methods, so the same as in
// previous example we have:

// inherited property "x"
Foo.prototype.x = 10;

// and inherited method "calculate"
Foo.prototype.calculate = function (z) {
  return this.x + this.y + z;
};

// now create our "b" and "c"
// objects using "pattern" Foo
var b = new Foo(20);
var c = new Foo(30);

// call the inherited method
b.calculate(30); // 60
c.calculate(40); // 80

// let's show that we reference
// properties we expect

console.log(

  b.__proto__ === Foo.prototype, // true
  c.__proto__ === Foo.prototype, // true

  // also "Foo.prototype" automatically creates
  // a special property "constructor", which is a
  // reference to the constructor function itself;
  // instances "b" and "c" may found it via
  // delegation and use to check their constructor

  b.constructor === Foo, // true
  c.constructor === Foo, // true
  Foo.prototype.constructor === Foo // true

  b.calculate === b.__proto__.calculate, // true
  b.__proto__.calculate === Foo.prototype.calculate // true

);

上述的代码是如下的关系

 

上述的图示可以看出来,每一个object都有一个prototype. 构造函数Foo也拥有自己的__proto__, 也就是Function.prototype, 而Function.prototype的__proto__指向了Object.prototype. 因此,Foo.prototype只是一个显式的属性,也就是b和c的__proto__属性。

针对这个内容更完整和详细的可以在第七章ES3系列中找到。有两个部分: Chapter 7.1. OOP. The general theory(各种OOP的原理和它们与ECMAScript的对比),还有Chapter 7.2. OOP. ECMAScript implementation(阐述如何将OOP的概念引入到ECMAScript中)

现在,我们已经了解了基本的object原理,那么我们接下去来看看ECMAScript里面的程序执行环境[runtime program execution]. 这就是通常称为的“执行上下文堆栈”[execution context stack]。每一个元素都可以抽象的理解为object。你也许发现了,没错,在ECMAScript中,几乎处处都能看到object的身影。

Execution context stack

在ECMASscript中的代码有三种类型:global, function和eval。

每一种代码的执行都需要依赖自身的上下文。当然global的上下文可能涵盖了很多的function和eval的实例。函数的每一次调用,都会进入函数执行中的上下文,并且来计算函数中变量等的值。eval函数的每一次执行,也会进入eval执行中的上下文,判断应该从何处获取变量的值。

注意,一个function可能产生无限的上下文环境,因为一个函数的调用(甚至递归)都产生了一个新的上下文环境。

function foo(bar) {}

// call the same function,
// generate three different
// contexts in each call, with
// different context state (e.g. value
// of the "bar" argument)

foo(10);
foo(20);
foo(30);

一个执行上下文可以激活另一个上下文,就好比一个函数调用了另一个函数(或者全局的上下文调用了一个全局函数),然后一层一层调用下去。逻辑上来说,这种实现方式是栈,我们可以称之为上下文堆栈。

例如A上下文激活了B的上下文,那么A称为caller,B称为callee. 一个callee同时也可能是另一个callee的caller。(例如一个全局上下文中的function又一次调用了它的内部函数。)

当一个caller激活了一个callee,那么这个caller就会暂停它自身的执行,然后将控制权交给这个callee. 于是这个callee被放入堆栈,称为进行中的上下文[running/active execution context]. 当这个callee的上下文结束之后,会把控制权再次交给它的caller,然后caller会在刚才暂停的地方继续执行。在这个caller结束之后,会继续触发其他的上下文。一个callee可以用return或者例外退出(exit with an exception)结束它自身的上下文.

所有的ECMAScript的程序执行都可以看做是一个执行上下文堆栈[execution context (EC) stack]。堆栈的顶部就是处于激活状态的上下文。

 

 

当一段程序开始时,会先进入全局执行上下文环境[global execution context], 这个也是堆栈中最底部的元素。此全局程序会开始初始化,初始化生成必要的对象[objects]和函数[functions]. 在此全局上下文执行的过程中,它可能会激活一些方法(当然是已经初始化过的),然后进入他们的上下文环境,然后将新的元素加入堆栈。在这些初始化都结束之后,这个系统会等待一些事件(例如用户的鼠标点击等),会触发一些方法,然后进入一个新的上下文环境。

在这个图示中,一些方法的上下文称为EC1,全局的上下文称为 Global EC,我们看一下堆栈形式,演示了从全局上下文进入和推出EC1的过程。

这就是ECMAScript的执行系统如何管理代码执行的。

关于上下文环境的更多内容,可以在 Chapter 1. Execution context中找到。

就像我们刚刚讲过的,堆栈中每一个执行的上下文可以看做一个对象[object]。接街区,我们将看看它的结构和执行它的代码时状态类型(或者说属性)。

Execution context

一个执行的上下文可以抽象的理解为object。每一个执行的上下文都有一系列的属性(我们称为上下文状态),他们用来追踪关联代码的执行进度。这个图示就是一个context的结构

当然除了这三个必要的属性,一个上下文环境也会根据情况依赖其他的属性(状态)。

让我们仔细来看看这三个属性

Variable object

A variable object is a scope of data related with the execution context. It’s a special object associated with the context and which stores variables and function declarations are being defined within the context.

一个VO[variable object]是关联上下文执行环境的一系列数据。它是与上下文关系密切的一个特殊对象,存储了在上下文中定义的变量和函数声明。

注意:函数表达式[function expression](而不是函数声明[function declarations])是不包含在VO[variable object]里面的。

Variable Object是一个抽象的概念,某个方面来讲,它表示使用不同的object.以下简称VO. 例如,在global上下文中,variable object也是全局对象[global object]。(这就是我们可以通过全局对象的属性来指向全局变量)。

让我们看看下面例子中的全局执行上下文情况。

var foo = 10;

function bar() {} // function declaration, FD
(function baz() {}); // function expression, FE

console.log(
  this.foo == foo, // true
  window.bar == bar // true
);

console.log(baz); // ReferenceError, "baz" is not defined

 

如下图所示

可以看到,函数baz是一个函数表达式,所以它不在VO里面,所以会有一个ReferenceError的提示,因为此时是在全局环境下读取,也就是在函数表达式外部读取的。

注意,和其他的语言不同(例如c/c++),在ECMAScript中,只有函数functions会新建一个新的作用域。在一个函数中定义的变量和内部函数在外部是无法获取的,同时也不会影响全局的变量。

使用eval的话,我们通常会进入一个新的eval上下文环境。Eval会使用global VO,也可以使用caller的VO。

那么函数和它们的VO的情况呢,在一个函数的上下文中,一个VO代表了激活变量[activation object],以下简称AO.

Activation object

当一个函数被调用激活之后,这个称为AO(activation object)的特殊对象就被新建了.它涵盖了普通的参数和一个特殊的arguments对象(这是一个参数的映射表,并且有索引属性)。我们也可以认为:AO实际上就是函数上下文的VO.

例如,函数的VO实际上就是简单的VO,但是除了这些变量和函数定义,还有参数和arguments对象,这些就成为AO.

考虑下面的情况

function foo(x, y) {
  var z = 30;
  function bar() {} // FD
  (function baz() {}); // FE
}

foo(10, 20);

那么AO的图示如下所示:

同样道理,function expression不在AO的行列。

对于这个AO的详细内容可以通过Chapter 2. Variable object找到

我们接下去将第三个主要的对象。众所周知,在ECMAScript中,我们会用到内部函数[inner functions],在这些内部函数中,我们可能会引用它的父函数变量,或者全局的变量。我们把这些变量对象成为上下文作用域对象[scope object of the context]. 类似于上面讨论的原型链[prototype chain],我们在这里称为作用域链[scope chain]。

 

Scope chain

作用域链就是在代码上下文中,搜索标识符的过程中遇到的一系列对象

这个原理和原型链很类似,如果这个变量在自己的作用域中没有,那么它会寻找父级的,直到最顶层。

标示符[Identifiers]可以理解为变量名称、函数声明和普通参数。例如,当一个函数在自身函数体内需要引用一个变量,但是这个变量并没有在函数内部声明(或者也不是某个参数名),那么这个变量就可以称为自由变量[free variable].那么我们搜寻这些自由变量就需要用到作用域链。

在一般情况下,一个作用域链包括父级变量VO、函数自身的VO和AO。不过,有些情况下也会包含其它的对象,例如在执行期间,动态加入作用域链中的—例如with或者catch语句。

当处理一个变量,作用域链是会从AO开始搜索,然后一直搜索到顶端。和原型链非常类似。

var x = 10;

(function foo() {
  var y = 20;
  (function bar() {
    var z = 30;
    // "x" and "y" are "free variables"
    // and are found in the next (after
    // bar's activation object) object
    // of the bar's scope chain
    console.log(x + y + z);
  })();
})();

我们假设作用域链的对象联动是通过一个叫做__parent__的属性,它是指向作用域链的下一个对象。这可以在Rhino Code中测试一下这种流程,这种技术也确实在ES5环境中实现了(有一个称为outer链接).当然也可以用一个简单的数据来模拟这个模型。使用__parent__的概念,我们可以把上面的代码演示成如下的情况。(因此,父级变量是被存在函数的[[Scope]]属性中的)

在代码执行过程中,如果使用with或者catch语句就会改变作用域链。而这些对象都是一些简单对象,他们也会有原型链。这样的话,作用域链会从两个维度来搜寻。

  1. 首先在原本的作用域链
  2. 每一个链接点的作用域的链(如果这个链接点是有prototype的话)

我们再看下面这个例子

Object.prototype.x = 10;

var w = 20;
var y = 30;

// in SpiderMonkey global object
// i.e. variable object of the global
// context inherits from "Object.prototype",
// so we may refer "not defined global
// variable x", which is found in
// the prototype chain

console.log(x); // 10

(function foo() {

  // "foo" local variables
  var w = 40;
  var x = 100;

  // "x" is found in the
  // "Object.prototype", because
  // {z: 50} inherits from it

  with ({z: 50}) {
    console.log(w, x, y , z); // 40, 10, 30, 50
  }

  // after "with" object is removed
  // from the scope chain, "x" is
  // again found in the AO of "foo" context;
  // variable "w" is also local
  console.log(x, w); // 100, 40

  // and that's how we may refer
  // shadowed global "w" variable in
  // the browser host environment
  console.log(window.w); // 20

})();

我们就会有如下结构图示。注意,在我们去搜寻__parent__之前,首先会去__proto__的链接中。

注意,不是所有的全局对象都是由Object.prototype继承而来的。上述图示的情况可以在SpiderMonkey中测试。

我们会搜寻作用域链来寻找需要的变量。刚才也有提过,在一个上下文结束之后,它的状态和自身会销毁掉。同时,这个内部函数会返回到父级函数中。也许,这个返回函数稍后会再激活另一个上下文。如何保持一个自由变量仍然处于被激活中呢?理论上来说,有一个概念可以解决这个问题,称为闭包[Closure].它也是和作用域链有直接关系的。

 

Closure

在ECMAScript中,function[函数]是基本等级[first-class]的对象。这就意味着,函数可以作为另一个函数的参数(在这种情况下,称之为”funargs”, 也就是”functional arguments”的缩写).接收funargs的函数称为高级函数[higher-order function], 或者类似于数学中的运算符[operator]. 同样,函数也可以作为另一个函数的返回。把函数作为返回值的函数称为以函数为值的函数[function valued functions](或者functions with functional value)

关于上述的funargs和functional value,会引发一个叫做”Funarg problem”(或者”A problem of a functional argument)”。为了解决这个问题,我们引入了“闭包”。我们下面来详细讨论这两个问题(在ECMAScript中的要解决这两个问题,需要用到函数的一个[[Scope]]的属性)。

首先”funarg problem”的一个类型就是自下而上[”upward funarg problem”]. 当一个函数作为另一个函数的返回值时,并且使用了自由变量[free variable]的时候会发生。即便它的父级上下文环境已经结束了,它可以引用父级的变量,。这个内部函数在创建时就会将父级的作用域链保存在自己的作用域[[Scope]]中。当函数运行时,上下文环境的作用域量是由活跃变量[activation object]和它[[Scope]]属性组合而成。

Scope chain = Activation object + [[Scope]]

请再次注意这个很重要的点 – 在函数创建期间[creation moment],函数会将父级的作用域链保存起来,因为随后调用这个函数的时候使用的已经保存的作用域链来搜寻变量。

请看下面的函数

function foo() {
  var x = 10;
  return function bar() {
    console.log(x);
  };
}

// "foo" returns also a function
// and this returned function uses
// free variable "x"

var returnedFunction = foo();

// global variable "x"
var x = 20;

// execution of the returned function
returnedFunction(); // 10, but not 20

这种形式的作用域称为静态作用域[static/lexical scope]。上面的x变量就是在函数bar的[[Scope]]中搜寻到的。理论上来说,也会有动态作用域[dynamic scope], 也就是上述的x被解释为20,而不是10. 但是EMCAScript不使用动态作用域。

“funarg problem”的另一个类型就是自上而下[”downward funarg problem”].在这种情况下,父级的上下会存在,但是在判断一个变量值的时候会有多义性。也就是,这个变量究竟应该使用哪个作用域。是在函数创建时的作用域呢,还是在执行时的作用域呢?为了避免这种多义性,可以采用闭包,也就是使用静态作用域。

请看下面的例子

// global "x"
var x = 10;

// global function
function foo() {
  console.log(x);
}

(function (funArg) {

  // local "x"
  var x = 20;

  // there is no ambiguity,
  // because we use global "x",
  // which was statically saved in
  // [[Scope]] of the "foo" function,
  // but not the "x" of the caller's scope,
  // which activates the "funArg"

  funArg(); // 10, but not 20

})(foo); // pass "down" foo as a "funarg"

从上述的情况,我们似乎可以断定,在语言中,使用静态作用域是闭包的一个强制性要求。不过,在某些语言中,会提供动态和静态作用域的结合,可以允许开发员选择哪一种作用域。但是在ECMAScript中,只采用了静态作用域。所以ECMAScript完全支持使用[[Scope]]的属性。我们可以给闭包得出如下定义

A closure is a combination of a code block (in ECMAScript this is a function) and statically/lexically saved all parent scopes. Thus, via these saved scopes a function may easily refer free variables.

闭包是一系列代码块(在ECMAScript中是函数),并且静态保存所有父级的作用域。通过这些保存的作用域来搜寻到函数中的自由变量。

注意,其实每一个函数都在创建期间保存[[Scope]],所以理论上来说,在ECMAScript中所有的函数都是闭包。

还有一个很重要的点,几个函数可能含有相同的父级作用域(这是一个很普遍的情况,例如有好几个内部或者全局的函数)。在这种情况下,在[[Scope]]中存在的变量是会共享的。一个闭包中变量的变化,也会影响另一个闭包的。

function baz() {
  var x = 1;
  return {
    foo: function foo() { return ++x; },
    bar: function bar() { return --x; }
  };
}

var closures = baz();

console.log(
  closures.foo(), // 2
  closures.bar()  // 1
);

上述代码可以用这张图来表示

如果有这个特性的话,在一个循环中创建多个函数就会有关联了。在循环中创建函数,我们可能会遇到意想不到的情况,因为所有的函数都会共享同一个变量了。所有的函数都有同一个[[Scope]], 并且都指向了最后一次赋值的count。

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = function () {
    alert(k);
  };
}

data[0](); // 3, but not 0
data[1](); // 3, but not 1
data[2](); // 3, but not 2

有几种方法可以解决种鸽问题。一种就是在作用域链中提供一个新增的对象,例如增加一个函数。

var data = [];

for (var k = 0; k < 3; k++) {
  data[k] = (function (x) {
    return function () {
      alert(x);
    };
  })(k); // pass "k" value
}

// now it is correct
data[0](); // 0
data[1](); // 1
data[2](); // 2

如果对闭包和它的应用有兴趣的,可以去看看 Chapter 6. Closures.
如果对作用域链想要有更深入链接的,可以去看看 Chapter 4. Scope chain

接下去我们讨论下一个章节,也就是上下文环境的最后一个属性– this.

This value

A this value is a special object which is related with the execution context. Therefore, it may be named as a context object (i.e. an object in which context the execution context is activated).

this适合执行的上下文环境息息相关的一个特殊对象。因此,它也可以称为上下文对象[context object]

任何对象在上下文环境中都可以使用this。我需要澄清一个误区,在一些描述中总是认为this是一个变量对象的属性。请记住

a this value is a property of the execution context, but not a property of the variable object.

this是执行上下文环境的一个属性,而不是某个变量对象的属性

这个特点很重要,因为和变量不同,this是没有一个类似搜寻变量的过程。当你在代码中使用了this,这个 this的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻。this的值只取决中进入上下文时的情况。

顺便说一句,和ECMAScript不同,Python有一个self的参数,和this的情况差不多,但是可以在执行过程中被改变。在ECMAScript中,是不可以给this赋值的,因为,还是那句话,this不是变量。

在global context(全局上下文)中,this的值就是指全局这个对象,这就意味着,this值就是这个变量本身。

var x = 10;

console.log(
  x, // 10
  this.x, // 10
  window.x // 10
);

在函数上下文[function context]中,this会可能会根据每次的函数调用而成为不同的值.this会由每一次caller提供,caller是通过调用表达式[call expression]产生的(也就是这个函数如何被激活调用的)。例如,下面的例子中foo就是一个callee,在全局上下文中被激活。下面的例子就表明了不同的caller引起this的不同。

// the code of the "foo" function
// never changes, but the "this" value
// differs in every activation

function foo() {
  alert(this);
}

// caller activates "foo" (callee) and
// provides "this" for the callee

foo(); // global object
foo.prototype.constructor(); // foo.prototype

var bar = {
  baz: foo
};

bar.baz(); // bar

(bar.baz)(); // also bar
(bar.baz = bar.baz)(); // but here is global object
(bar.baz, bar.baz)(); // also global object
(false || bar.baz)(); // also global object

var otherFoo = bar.baz;
otherFoo(); // again global object

要更深入的了解问什么每次函数调用的时候,this都会改变,请看Chapter 3. This

Conslusion

到这里,我们基本上有一个简要的回顾。要详细的了解这一系列,需要一本书来阐述。我们没有涉及两个主要的概念:functions(function declaration 和 function expression)和evaluation strategy.这两个内容可以从Chapter 5. Functions and Chapter 8. Evaluation strategy.

If you have comments, questions or additions, I’ll be glad to discuss them in comments.
Good luck in studying ECMAScript!
Written by: Dmitry A. Soshnikov
Published on: 2010-09-02
Translated by: feifeipan

最后声明:如果翻译的内容有不妥或者需要改进的地方,请大家多多指出。

本文作者:小灰灰 转载请注明来自:携程设计委员会