javascript 原型链范围链的相关知识,翻译

原网址是http://dmitrysoshnikov.com/ecmascript/javascript-the-core/

翻译的很牵强,一部分是谷歌翻译帮忙的。 

但是自己大概是看懂了,这篇文章真的很有帮助

 

一个对象原型链构造执行上下文执行上下文变量对象激活对象范围链闭包This总结

这篇笔记是对 “ECMA-262-3 in detail” 这一系列文章的概述. 以上11个部分每一个部分都包含对应内容的页面跳转,当你点击时,你可以跳转到对应界面进行阅读以便获得更深层次的理解。

目标读者:经验丰富的程序员,专业人士。

首先,我们来考虑对象的概念,这是ECMAScript的基础。

一个对象

ECMAscript,是一个高度抽象的面向对象的语言用来处理对象,它也有原语,但是在需要的时候,同样可以转换成对象。

一个对象是一个有很多属性的集合并且含有一个原型对象。它的原型对象可能是一个对象或者是一个null值。

让我们来看一个有关对象的基本示例吧,对象的原型是由内部的[[Prototype]]属性所引用的。但是,在本图中,我们将使用__<internal-property>__ 下划线表示法,而不是双括号表示法,特别是对于原型对象__proto__.;

这是代码:

var foo = {
    x:10,
    y:20
}; foo = {
    x:10,
    y:20
};

我们有两个显示的自有属性和一个隐藏的继承属性(prototype),这个继承属性指向foo的原型

​ 图一,一个对象和它的原型。

这些属性都需要什么东西呢?让我们来考虑一下原型链概念再来回答这个问题吧。

原型链

原型对象也是简单的对象并且也许原型对象也有他们的原型,如果一个原型对象对其原型有一个非空引用等,则称它为原型链。(我理解的是A是B的prototypr,B是A的constructor,那他们就构成了原型链)

一个原型链是一个被用作实现继承和共享属性功能的有穷的对象链。

考虑下面这种情况,我们有两个对象,他们大多部分是互相相同的,只有一小部分不同。很显然,为了一个合理的设计良好的系统,我们应当重用这些相似的代码(函数)而不是在每一个单独的对象里重复造轮子。在面向对象继承类的编程系统中,这种代码重用的问题方式被称为面向对象的继承。你把相似的函数功能放到一个classA中,并提供从A中继承的class B和class C以及有自己的一些小的功能添加。

ECMAScript 没有关于类的文本,但是,代码重用的文本方式其实并没有多大的不同,甚至,在某些方面上,他可能比面向对象的继承类的方式更灵活。ECMAScript采用原型链。这种继承被称为基于代理的继承。或者,更加贴切ECMAScript,基于原型的继承。

类似于类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); 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); 

是不是特别的简单?我们发现类b和类c 有权利去访问在a里面定义的方法。这完全是通过原型链实现的。

这个原理是非常简单的:如果在一个对象在自身没有发现特定的原型或者方法(对象没有一个自有属性),那么它就会尝试在原型链中寻找这个原型或者方法,那么就会继续考虑原型的原型,然后一直沿着原型链向上找。换句话说,这整个原型链(在解析面向对象继承类的方法时,在哪里,我们是通过class链)。基于类的继承是完全相同的。使用第一个在原型链中找到的相同名字的函数或者方法,因此,找到的属性叫做继承属性。如果一个属性在整个原型链中没有被找到。那么就返回一个undefined的值。

但是需要注意的是,使用继承方法的this的值被设置为调用它的对象而不是原型链上呗找到的属性。患处华说,在上面的那个例子中。 This.y的值来自b和c而不是来自a。但是,this.x来自a。并且是通过原型链机制。(就是从自身开始向上搜寻。自身搜得到就不需要用原型的值,搜不到就用原型的值)

如果一个对象并没有被规定一个原型,那么原型的默认值 object.prototype将会被采用。 object本身也有一个原型,这个原型是原型链的最后,是null。

下面这个图片展示了a,b,c三个对象之间的继承等级关系。

​ 图二 原型链

注意:ES5使用Object.create函数为基于原型的继承规范了一种替代方法:


var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});

你可以在 appropriate chapter 得到更多有关新的ES5提供的APIS的信息。

ES6也指定了有关原型的标准,并且他可以被用作初始化对象。

我们经常需要具有相同或者相似构造结构的对象(换句话说,设置有想要的属性)但是他们有不同的状态值,在这种情况,我们也许就需要使用构造函数去创造对象按照我们想要的方式。

构造

除了通过指派模式创建对象之外。构造函数提供了另一个有用的东西。它对没一个新创建的对象自动的设置了一个原型链。这个原型对象被保存在ConstructorFunction.prototype 原型中。

例如,我们可以通过使用构造函数来重新之前的例子创建object b和object c。因此,对象a就扮演者原型的身份。


// 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
 
);// 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
 
);

 

代码也许会展示出下面这种关系。

​ 图三,构造对象和对象之间的关系

这个图片再次说明了每个对象都有一个原型,构造函数Foo也有原型,它是Function.prototype, 并且它通过它的 __proto__ 属性再次引用到Object.prototype ,因此,在这里我们再强调一遍。`FOO.prototype仅仅是FOO的一个显式属性,它指向b和c的原型。

在形式上,如果考虑分类的概念,(我们刚刚已经分类了新的单独的东西-Foo),就可以称构造函数和原型对象的组合为类。实际上,例如 Python的 first-class 动态类具有与属性/方法解析完全相同的实现。 从这个观点来看,Python的类只是ECMAScript中用于基于委托的继承的语法。

注意:在ES6中,“类”的概念是标准化的,并且如上所述在构造函数之上完全实现为语法糖。 从这个角度来看,原型链变成了基于类的继承的实现细节:


class Foo {
  constructor(name) {
    this._name = name;
  }
 
  getName() {
    return this._name;
  }
}
 
class Bar extends Foo {
  getName() {
    return super.getName() + ' Doe';
  }
}
 
var bar = new Bar('John');
console.log(bar.getName()); // John Doeclass Foo {
  constructor(name) {
    this._name = name;
  }
 
  getName() {
    return this._name;
  }
}
 
class Bar extends Foo {
  getName() {
    return super.getName() + ' Doe';
  }
}
 
var bar = new Bar('John');
console.log(bar.getName()); // John Doe

有关此主题的完整和详细的解释可以在ES3系列的第7章中找到。 有两部分:[第7.1章。面向对象编程的 一般理论],您可以在其中找到各种面向对象编程的范例和文体学的描述,以及它们与ECMAScript的比较,以及[Chapter7.2 面向对象的ECMAScript实现]专门用于ECMAScript中的面向对象。

现在,我们已经了解了基本的对象方面的概念了,让我们一起看看EVMAScript运行时候程序的上下文,也就是所谓的执行上下文,这里面没一个元素都是抽象的,它们也可以表示为一个对象,是的,在ECMAScript中,几乎所有的地方都可以使用对象的概念。

执行上下文

在ECMAScript的代码中有三种类型,全局代码,函数代码和eval代码。没一个代码都在它们的执行上下文中被评估。只有一个全局上下文,但是可能有很多函数和eval的执行上下文的实例。每调用一次函数,都将进入函数执行上下文并评估函数代码类型。 每次调用eval函数,进入eval 执行上下文并评估其代码。

注意,一个函数可能无限生成上下文集合,因为每次对函数的调用,哪怕是调用自己。也会产生一个带有新上下文的新上下文。

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);
​ 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);
​

一个执行上下文可能会唤醒另一个上下文,换句话说,一个函数调用拎一个函数(或者全局上下文调用一个全局函数)等等。从逻辑上来讲,这是作为一个堆栈来实现的,所以被叫做堆栈的执行上下文。

唤醒别的执行上下文的上下文呗称作调用者,被唤醒的叫做被调用者。一个被调用者在用一个时间也许是其它被调用者的调用者,换句话来说,一个函数被全局上下文调用的同时有调用了一些内部函数。

当一个执行者唤醒调用者的时候,执行者暂停执行并将控制流传递给被调用者。被调用者被推进栈中同时变成活动上下文。当被调用者上下文结束之后,它会将控制权还给执行者,并且调用者的上下文的评估继续进行(其可以激活其他上下文)直到其结束,依此类推。 被调用者可以返回或退出异常。 抛出但未捕获的异常可能会退出(从堆栈弹出)一个或多个上下文。

即所有的ECMAScript项目运行时都作为执行上下文的堆栈呈现,其中该堆栈的顶部是活动的上下文。

​ 图四 一个执行上下文堆栈

当项目开始时,他输入一个全局上下文,他是堆栈的第一个元素。然后全局代码提供了一些初始化,创建对象和函数。在执行全局上下文的时候,也许会唤醒其它已经创建的代码。这些函数正在等待进入执行上下文,并且推入到堆栈中等等,在初始化结束之后,运行系统就需要等待事件的发生了(比如用户的鼠标点击)来激活某些函数,并能进入一个新的执行上下文。

在下一个图片中,有一些函数上下文作为es1,并将全局上下文作为global ec,我们从ec1中输入或退出时进行一下堆栈的操作。

​ 图五,执行上下文堆栈的改变。

这正是ECMAScript的运行时系统如何管理代码的执行。

更多有关ECMAScript中执行上下文的消息需要再 Chapter 1. Execution context.中查询。

正如我们所说,每一个堆栈中的执行上下文都会被当做对象来呈现,让我们来看看他们的结构并且看看执行他们的代码需要哪些属性。

执行上下文

执行上下文一般被抽象的当做一个简单的对象来呈现。每一个执行上下文都设置了必要的属性(我们称作上下文状态)来跟踪相关算法的执行进度。在下一张图中展示上下文的结构。

 

​ 图六,执行上下文的结构

除了这三类必须的属性(一个变量的对象,一个this值,一个范围链)一个执行上下文可能有其它的状态,这就取决于具体实现了。

让我们仔细考虑一下这些执行上下文的重要属性。

 

变量对象

一个变量对象时执行上下文相关数据的容器。这是一个在上下文中定义的存储变量和函数特殊对象。

注意到,变量对象时不包含函数表达式的额(这就不同于函数声明了)

变量对象时一个抽象的概念。不同于上下文类型。物理结构上,以使用不同的对象来展示。比如,在全局上下文类型中就是全局对象它本身,这就是为什么我们可以通过全局对象的原型名去指向全局变量。

让我们考虑一下以下这个全局上下文中的例子


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 definedvar 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这个函数声明不被变量对象包括,这就是为什么。当我们在函数本身以外调用它会发生一个指向错误。

注意,在和其他如c、c++等语言进行对比的时候,ECMAScript创建一个新的范围,在函数范围内部定义的变量和函数在外部是不可见的,所以不会污染全局变量。

使用 eval 我们也可以进入一个新的eval的执行上下文,但是,eval使用其他的全局变量对象,或者一个调用者的一个变量对象比如调用eval的函数。

什么事函数和他们的变量对象呢。在函数上下文中,一个变了对象被标识为激活对象。

激活对象

当一个函数(被调用者)被执行者激活的时候,一个特殊的激活对象就被创建了。它充满了形式参数和特殊的arguments对象(是一个包含索引的形势参数的映射),这个激活对象在函数上下文中被用作变量对象。

换句话说,一个函数变量对象也是一个简单的变量对象,但是除了变量和函数声明。他还存储着形式参数和arguments对象并被称为激活对象。

考虑下面的例子。


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

我们在foo函数上下文中,有以下的激活对象。

​ 图八,激活对象

但是baz的函数声明并没有没包括到激活对象中。

有关变量和函数声明的提升完整的解释可以在 Chapter 2. Variable object.找到

注意,在ES5中,变量对象和激活对象的概念被合并到词法环境模型中,具体可以在 appropriate chapter.中查询。

我们将进入下一环节,众所周知,在ECMAScript中,我们可以使用内部函数,并且在这些内部函数中,我们可以引用父级函数的变量和全局上下文中的变量。如果我们声明一个变量对象作为上下文的范围变量。就像我们之前声明的原型链。存在所谓的范围链。

范围链

范围链是一个对象的列表,搜索标志服出现在上下文的代码中。

这个规则和原型链一样的简单和相似,如果一个变量没有在他自己的范围里找到(在自己的变量/激活对象),它将会不断地去他的父级变量对象中寻找。

关于上下文,标识符是,变量的名字,函数的声明,形式参数等等,当一个函数在其代码中引用的不是一个局部变量(或一个局部函数或者形式参数)这些的变量被称为自由变量。当搜索这些自由变量时正好适合一个范围链。

在通常情况,一个范围链是一个他父级变量对象和函数自己变量对象的集合。但是,这个范围链也许也会包含其他的对象。在执行上下文中被添加到范围链中动态添加到作用链的对象,比如with对象和特殊捕获的对象

当搜索标识符时,就从范围链的激活对象开始寻找,(如果标识符没有在它自身的激活对象中找到)直到范围链的头部,就像原型链一样。


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);
  })();
})();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__属性来假设范围链对象的链接,它指向链中的下一个对象。 这种方法可以在real Rhino code中测试并且这种技术正好用于 [ES5 词汇环境](在那里它被命名为“外部”链接)。 范围链的另一种表示可能是一个简单的数组。 使用__parent__概念,我们可以用下图来表示上面的例子(因此父变量对象保存在函数的[[Scope]]属性中):

 

​ 图九 范围链

在代码执行时,范围链可以使用with语句和catch子句对象来扩充。 由于这些对象是简单的对象,它们可能有原型(和原型链)。 这个事实导致范围链查找是二维的:(1)首先考虑一个范围链链接,然后(2)在每个范围链的链接上 - 进入链接的原型链深度(如果 当然有一个原型)。

看下面这个例子。


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
 
})();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__链):

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值