JavaScript对象模型-执行模型

本文详细解析JavaScript的基本数据类型、对象、函数及其在数据类型实现、原型继承、对象创建过程、属性与方法继承等方面的核心概念。通过具体代码示例,揭示了JavaScript对象模型的内在逻辑和实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

数据类型

基本数据类型

基本数据类型是js语言最底层的实现。

简单数值类型: 有undefined, null, boolean, number和string。注意,描述中的英文单词在这里仅指数据类型的名称,并不特指js的全局对象n an, boolean, number, string等,它们在概念上的区别是比较大的。

对象: 一个无序属性的集合,这些属性的值为简单数值类型、对象或者函数。同上,这里的对象并不特指全局对象object。

函数: 函数是对象的一种,实现上内部属性[[class]]值为"function",表明它是函数类型,除了对象的内部属性方法外,还有[[construct]]、[[call]]、[[scope]]等内部属性。函数作为函数调用与构造器(使用new关键字创建实例对象)的处理机制不一样(function对象除外),内部方法[[construct]]用于实现作为构造器的逻辑,方法[[call]]实现作为函数调用的逻辑。同上,这里的函数并不特指全局对象function。

函数在js这个prototype语言中可以看作是面向对象语言的类,可以用它来构造对象实例。既然函数可以看作是类,所以每一个函数可以看作是一种扩展数据类型。

内置数据类型(内置对象)

function: 函数类型的用户接口。

object: 对象类型的用户接口。

boolean, number, string: 分别为这三种简单数值类型的对象包装器,对象包装在概念上有点类似c#中的box/unbox。

date, array, regexp: 可以把它们看作是几种内置的扩展数据类型。

首先,function, object, boolean, number, string, date, array, regexp等都是javascript语言的内置对象,它们都可以看作是函数的派生类型,例如number instanceof function为true,number instanceof object为true。在这个意义上,可以将它们跟用户定义的函数等同看待。

其次,它们各自可以代表一种数据类型,由js引擎用native code或内置的js代码实现,是暴露给开发者对这些内置数据类型进行操作的接口。在这个意义上,它们都是一种抽象的概念,后面隐藏了具体的实现机制。

在每一个提到number, function等单词的地方,应该迅速的在思维中将它们实例化为上面的两种情况之一。

数据类型实现模型描述

build-in *** data structure: 指js内部用于实现***类型的数据结构,这些结构我们基本上无法直接操作。

build-in *** object: 指js内置的number, string, boolean等这些对象,这是js将内部实现的数据类型暴露给开发者使用的接口。

build-in *** constructor: 指js内置的一些构造器,用来构造相应类型的对象实例。它们被包装成函数对象暴露出来,例如我们可以使用下面的方法访问到这些函数对象:

//passed in ff2.0, ie7, opera9.25, safari3.0.4

//access the build-in number constructor

var number = new number(123);

var numconstructor1 = number.constructor; //or

var numconstructor2 = new object(123).constructor;

//both numconstructor1 and numconstructor2 are the build-in number constructor

numconstructor1 == numconstructor2 //result: true

//access the build-in object constructor

var objconstructor1 = {}.constructor; //or

var objconstructor2 = new object().constructor;

//both objconstructor1 and objconstructor2 are the build-in object constructor

objconstructor1==objconstructor2 //result: true

具体实现上,上图中横向之间可能也存在关联,例如对于build-in data structure和constructor,function、 date、 array、 regexp等都可以继承object的结构而实现,但这是具体实现相关的事情了。

关于简单数值类型的对象化

这是一个细微的地方,下面描述对于boolean, string和number这三种简单数值类型都适用,以number为例说明。

js规范要求: 使用var num1=123;这样的代码,直接返回基本数据类型,就是说返回的对象不是派生自number和object类型,用num1 instanceof object测试为false;使用new关键字创建则返回number类型,例如var num2=new number(123); num2 instanceof number为true。

将number当作函数调用,返回结果会转换成简单数值类型。下面是测试代码:

//passed in ff2.0, ie7, opera9.25, safari3.0.4

var num1 = new number(123); //num1 derived from number & object

num1 instanceof number //result: true

num1 instanceof object //result: true

//convert the num1 from number type to primitive type, so it's no longer an instance of number or object

num1 = number(num1);

num1 instanceof number //result: false

num1 instanceof object //result: false

var num2 = 123; //num2 is a primitive type

num2 instanceof number //result: false

num2 instanceof object //result: false

虽然我们得到了一个简单数值类型,但它看起来仍然是一个js object对象,具有object以及相应类型的所有属性和方法,使用上基本没有差别,唯一不同之处是instanceof的测试结果。

prototype继承

prototype

每个对象都有一个[[prototype]]的内部属性,它的值为null或者另外一个对象。函数对象都有一个显示的prototype属性,它并不是内部[[prototype]]属性。不同的js引擎实现者可以将内部[[prototype]]属性命名为任何名字,并且设置它的可见性,只在js引擎内部使用。虽然无法在js代码中访问到内部[[prototype]](firefox中可以,名字为__proto__因为mozilla将它公开了),但可以使用对象的isprototypeof()方法进行测试,注意这个方法会在整个prototype链上进行判断。

使用obj.propname访问一个对象的属性时,按照下面的步骤进行处理(假设obj的内部[[prototype]]属性名为__proto__):

1. 如果obj存在propname属性,返回属性的值,否则

2. 如果obj.__proto__为null,返回undefined,否则

3. 返回obj.__proto__.propname

调用对象的方法跟访问属性搜索过程一样,因为方法的函数对象就是对象的一个属性值。

提示: 上面步骤中隐含了一个递归过程,步骤3中obj.__proto__是另外一个对象,同样将采用1, 2, 3这样的步骤来搜索propname属性。

例如下图所示,object1将具备属性prop1, prop2, prop3以及方法fn1, fn2, fn3。图中虚线箭头表示prototype链。

这就是基于prototype的继承和共享。其中object1的方法fn2来自object2,概念上即object2重写了object3的方法fn2。

javascript对象应当都通过prototype链关联起来,最顶层是object,即对象都派生自object类型。

类似c++等面向对象语言用类(被抽象了的类型)来承载方法,用对象(实例化对象)承载属性,prototype语言只用实例化的对象来承载方法和属性。本质区别是前者基于内存结构的描述来实现继承,后者基于具体的内存块实现。

对象创建过程

js中只有函数对象具备类的概念,因此要创建一个对象,必须使用函数对象。函数对象内部有[[construct]]方法和[[call]]方法,[[construct]]用于构造对象,[[call]]用于函数调用,只有使用new操作符时才触发[[construct]]逻辑。

var obj=new object(); 是使用内置的object这个函数对象创建实例化对象obj。var obj={};和var obj=[];这种代码将由js引擎触发object和array的构造过程。function fn(){}; var myobj=new fn();是使用用户定义的类型创建实例化对象。

new fn(args)的创建过程如下(即函数对象的[[construct]]方法处理逻辑,对象的创建过程)。另外函数对象本身的创建过程(指定义函数或者用function创建一个函数对象等方式)虽然也使用了下面的处理逻辑,但有特殊的地方,后面再描述。

1. 创建一个build-in object对象obj并初始化

2. 如果fn.prototype是object类型,则将obj的内部[[prototype]]设置为fn.prototype,否则obj的[[prototype]]将为其初始化值(即object.prototype)

3. 将obj作为this,使用args参数调用fn的内部[[call]]方法

3.1 内部[[call]]方法创建当前执行上下文

3.2 调用f的函数体

3.3 销毁当前的执行上下文

3.4 返回f函数体的返回值,如果f的函数体没有返回值则返回undefined

4. 如果[[call]]的返回值是object类型,则返回这个值,否则返回obj

注意步骤2中, prototype指对象显示的prototype属性,而[[prototype]]则代表对象内部prototype属性(隐式的)。

构成对象prototype链的是内部隐式的[[prototype]],而并非对象显示的prototype属性。显示的prototype只有在函数对象上才有意义,从上面的创建过程可以看到,函数的prototype被赋给派生对象隐式[[prototype]]属性,这样根据prototype规则,派生对象和函数的prototype对象之间才存在属性、方法的继承/共享关系。

用代码来做一些验证:

//passed in ff2.0, ie7, opera9.25, safari3.0.4

function fn(){}

//the value of implicit [[prototype]] property of those objects derived from fn will be assigned to fn.prototype

fn.prototype={ attr1:"aaa", attr2:"bbb"};

var obj=new fn();

document.write(obj.attr1 + "

"); //result: aaa

document.write(obj.attr2 + "

"); //result: bbb

document.write(obj instanceof fn); //result: true

document.write("

");

//i change the prototype of fn here, so by the algorithm of prototype the obj is no longer the instance of fn,

//but this won't affect the obj and its [[prototype]] property, and the obj still has attr1 and attr2 properties

fn.prototype={};

document.write(obj.attr1 + "

"); //result: aaa

document.write(obj.attr2 + "

"); //result: bbb

document.write(obj instanceof fn); //result: false

关于创建过程返回值的验证:

//passed in ff2.0, ie7, opera9.25, safari3.0.4

function fn(){

//according to step 4 described above,

//the new fn() operation will return the object { attr1: 111, attr2: 222 }, it's not an instance of fn!

return { attr1: 111, attr2: 222 };

}

fn.prototype={ attr1:"aaa", attr2:"bbb"};

var obj=new fn();

document.write(obj.attr1 + "

"); //result: 111

document.write(obj.attr2 + "

"); //result: 222

document.write(obj instanceof fn); //result: false

做个练习

经过上面的理解应,请写出下面这幅图的实现代码。图中cf是一个函数,cfp是cf的prototype对象,cf1, cf2, cf3, cf4, cf5都是cf的实例对象。虚线箭头表示隐式prototype关系,实线箭头表示显示prototype关系。

供参考的实现方案:

//passed in ff2.0, ie7, opera9.25, safari3.0.4

function cf(q1, q2){

this.q1=q1;

this.q2=q2;

}

cf.p1="p1 in cf";

cf.p2="p2 in cf";

function cfp(){

this.cfp1="cfp1 in cfp";

}

cf.prototype=new cfp();

var cf1=new cf("aaa", "bbb");

document.write(cf1.cfp1 + "

"); //result: cfp1 in cfp

document.write(cf1.q1 + "

"); //result: aaa

document.write(cf1.q2 + "

"); //result: bbb

本地属性与继承属性

对象通过隐式prototype链能够实现属性和方法的继承,但prototype也是一个普通对象,就是说它是一个普通的实例化的对象,而不是纯粹抽象的数据结构描述。所以就有了这个本地属性与继承属性的问题。

首先看一下设置对象属性时的处理过程。js定义了一组attribute,用来描述对象的属性property,以表明属性property是否可以在javascript代码中设值、被for in枚举等。

obj.propname=value的赋值语句处理步骤如下:

1. 如果propname的attribute设置为不能设值,则返回

2. 如果obj.propname不存在,则为obj创建一个属性,名称为propname

3. 将obj.propname的值设为value

可以看到,设值过程并不会考虑prototype链,道理很明显,obj的内部[[prototype]]是一个实例化的对象,它不仅仅向obj共享属性,还可能向其它对象共享属性,修改它可能影响其它对象。

用上面cf, cfp的示例来说明,实例对象cf1具有本地属性q1, q2以及继承属性cfp1,如果执行cf1.cfp1="",那么cf1就具有本地属性cfp1了,测试结果如下:

//passed in ff2.0, ie7, opera9.25, safari3.0.4

var cf1=new cf("aaa", "bbb");

var cf2=new cf(111, 222);

document.write(cf1.cfp1 + "

"); //result: cfp1 in cfp

document.write(cf2.cfp1 + "

"); //result: cfp1 in cfp

//it will result in a local property in cf1

cf1.cfp1="new value for cf1";

//changes on cf.prototype.cfp1 will affect cf2 but not cf1, because there's already a local property with

//the name cfp1 in cf1, but no such one in cf2

cf.prototype.cfp1="new value for cfp";

document.write(cf1.cfp1 + "

"); //result: new value for cf1

document.write(cf2.cfp1 + "

"); //result: new value for cfp

语义上的混乱?

还是使用上面cf, cfp示例的场景。

根据prototype的机制,我们可以说对象cf1, cf2等都继承了对象cfp的属性和方法,所以应该说他们之间存在继承关系。属性的继承/共享是沿着隐式prototype链作用的,所以继承关系也应当理解为沿着这个链。

我们再看instanceof操作,只有cf1 instanceof cf才成立,我们说cf1是cf的实例对象,cf充当了类的角色,而不会说cf1是cfp的实例对象,这样我们应当说cf1继承自cf? 但cf充当的只是一个第三方工厂的角色,它跟cf1之间并没有属性继承这个关系。

把cf, cfp看作一个整体来理解也同样牵强。

prototype就是prototype,没有必要强把javascript与面向对象概念结合起来,

javascript只具备有限的面向对象能力,从另外的角度我们可以把它看成函数语言、动态语言,所以它是吸收了多种语言特性的精简版。

对象模型

where are we?

1. 了解了javascript的数据类型,清楚了象number这样的系统内置对象具有多重身份: a)它们本身是一个函数对象,只是由引擎内部实现而已,b)它们代表一种数据类型,我们可以用它们定义、操作相应类型的数据,c)在它们背后隐藏了引擎的内部实现机制,例如内部的数据结构、各种被包装成了javascript对象的构造器等。

2. 了解了prototype机制,知道对象是如何通过它们继承属性和方法,知道了在创建对象过程中js引擎内部是如何设置prototype关系的。

接下来对用户自定义函数对象本身的创建过程进行了解之后,我们就可以对javascript的对象模型来一个整体性的overview了。

函数对象创建过程

javascript代码中定义函数,或者调用function创建函数时,最终都会以类似这样的形式调用function函数:var newfun=function(funargs, funbody); 。创建函数对象的主要步骤如下:

1. 创建一个build-in object对象fn

2. 将fn的内部[[prototype]]设为function.prototype

3. 设置内部的[[call]]属性,它是内部实现的一个方法,处理逻辑参考对象创建过程的步骤3

4. 设置内部的[[construct]]属性,它是内部实现的一个方法,处理逻辑参考对象创建过程的步骤1,2,3,4

5. 设置fn.length为funargs.length,如果函数没有参数,则将fn.length设置为0

6. 使用new object()同样的逻辑创建一个object对象fnproto

7. 将fnproto.constructor设为fn

8. 将fn.prototype设为fnproto

9. 返回fn

步骤1跟步骤6的区别为,步骤1只是创建内部用来实现object对象的数据结构(build-in object structure),并完成内部必要的初始化工作,但它的[[prototype]]、[[call]]、[[construct]]等属性应当为null或者内部初始化值,即我们可以理解为不指向任何对象(对[[prototype]]这样的属性而言),或者不包含任何处理(对[[call]]、[[construct]]这样的方法而言)。步骤6则将按照前面描述的对象创建过程创建一个新的对象,它的[[prototype]]等被设置了。

从上面的处理步骤可以了解,任何时候我们定义一个函数,它的prototype是一个object实例,这样默认情况下我们创建自定义函数的实例对象时,它们的prototype链将指向object.prototype。

另外,function一个特殊的地方,是它的[[call]]和[[construct]]处理逻辑一样。

javascript对象模型

红色虚线表示隐式prototype链。

这张对象模型图中包含了太多东西,不少地方需要仔细体会,可以写些测试代码进行验证。彻底理解了这张图,对javascript语言的了解也就差不多了。下面是一些补充说明:

1. 图中有好几个地方提到build-in function constructor,这是同一个对象,可以测试验证:

//passed in ff2.0, ie7, opera9.25, safari3.0.4

function==function.constructor //result: true

function==function.prototype.constructor //result: true

function==object.constructor //result: true

//function also equals to number.constructor, string.constructor, array.constructor, regexp.constructor, etc.

function fn(){}

function==fn.constructor //result: true

这说明了几个问题: function指向系统内置的函数构造器(build-in function constructor);function具有自举性;系统中所有函数都是由function构造。

2. 左下角的obj1, obj2...objn范指用类似这样的代码创建的对象: function fn1(){}; var obj1=new fn1();

这些对象没有本地constructor方法,但它们将从prototype链上得到一个继承的constructor方法,即fn.prototype.constructor,从函数对象的构造过程可以知道,它就是fn本身了。

右下角的obj1, obj2...objn范指用类似这样的代码创建的对象: var obj1=new object();或var obj1={};或var obj1=new number(123);或obj1=/\w+/;等等。所以这些对象prototype链的指向、从prototype链继承而来的constructor的值(指它们的constructor是build-in number constructor还是build-in object constructor等)等依赖于具体的对象类型。另外注意的是,var obj=new object(123);这样创建的对象,它的类型仍然是number,即同样需要根据参数值的类型来确定。

同样它们也没有本地constructor,而是从prototype链上获得继承的constructor方法,即build-in *** constructor,具体是哪一个由数据类型确定。

3. 关于图中prototype链的补充说明:

object.prototype是整个链的终结点,它的内部[[prototype]]为null。

所有函数的prototype链都指向function.prototype。

function的prototype链指向function.prototype,这是规范要求的,因为设计者将function设计为具有自举性。function的prototype链这样设计之后,function.constructor==function, function instanceof function都为true。另外function已经是最顶层的构造器,但function本身也是一个函数对象,它必然是由某个东西创建出来的,这样自举在语义上合情合理。

function.prototype的prototype链指向object.prototype,这也是规范强制要求的。首先function.prototype是function的一个实例对象(typeof function.prototype可以知道它是一个function,instanceof无法通过测试,因为prototype链在内部被额外设置了),所以按照prototype的规则,function.prototype的内部[[prototype]]值应当为function.prototype这个对象,即它的prototype链指向自己本身。这样一方面在prototype链上造成一个死循环,另一方面它本身成为了一个终结点,结果就是所有函数对象将不是派生自object了。加上这个强制要求之后,prototype链只有唯一的一个终结点。

4. 因为function.prototype是一个函数对象,所以它应当具有显示的prototype属性,即function.prototype.prototype,但只有firefox中可以访问到,ie、opera、safari都无法访问。所以图中用了个表示不存在的符号。

5. 用户自定义函数(user defined functions)默认情况下[[prototype]]值是object.prototype,即它的隐式prototype链指向object.prototype,所以图中就这样表示了,但并不代表总是这样,当用户设置了自定义函数的prototype属性之后,情况就不同了。

执行模型

执行上下文(execution context)简介

javascript代码运行的地方都存在执行上下文,它是一个概念,一种机制,用来完成javascript运行时作用域、生存期等方面的处理。执行上下文包括variable object、variable instatiation、scope/scope chain等概念,在不同的场景/执行环境下,处理上存在一些差异,下面先对这些场景进行说明。

函数对象分为用户自定义函数对象和系统内置函数对象,对于用户自定义函数对象将按照下面描述的机制进行处理,但内置函数对象与具体实现相关,ecma规范对它们执行上下文的处理没有要求,即它们基本不适合本节描述的内容。

执行的javascript代码分三种类型,后面会对这三种类型处理上不同的地方进行说明:

1. global code,即全局的、不在任何函数里面的代码,例如一个js文件、嵌入在html页面中的js代码等。

2. eval code,即使用eval()函数动态执行的js代码。

3. function code,即用户自定义函数中的函数体js代码。

基本原理

在用户自定义函数中,可以传入参数、在函数中定义局部变量,函数体代码可以使用这些入参、局部变量。背后的机制是什么样呢?

当js执行流进入函数时,javascript引擎在内部创建一个对象,叫做variable object。对应函数的每一个参数,在variable object上添加一个属性,属性的名字、值与参数的名字、值相同。函数中每声明一个变量,也会在variable object上添加一个属性,名字就是变量名,因此为变量赋值就是给variable object对应的属性赋值。在函数中访问参数或者局部变量时,就是在variable object上搜索相应的属性,返回其值。

一般情况下variable object是一个内部对象,js代码中无法直接访问。规范中对其实现方式也不做要求,因此它可能只是引擎内部的一种数据结构。

大致处理方式就这样,但作用域的概念不只这么简单,例如函数体中可以使用全局变量、函数嵌套定义时情况更复杂点。这些情况下怎样处理?javascript引擎将不同执行位置上的variable object按照规则构建一个链表,在访问一个变量时,先在链表的第一个variable object上查找,如果没有找到则继续在第二个variable object上查找,直到搜索结束。这就是scope/scope chain的大致概念。

下面是各个方面详细的处理。

global object

javascript的运行环境都必须存在一个唯一的全局对象-global object,例如html中的window对象。global object是一个宿主对象,除了作为javascript运行时的全局容器应具备的职责外,ecma规范对它没有额外要求。它包math、string、date、parseint等javascript中内置的全局对象、函数(都作为global object的属性),还可以包含其它宿主环境需要的一些属性。

variable object

上面简述了variable object的基本概念。创建variable object,将参数、局部变量设置为variable object属性的处理过程叫做variable instatiation-变量实例化,后面结合scope chain再进行详细说明。

global code

variable object就是global object,这是variable object唯一特殊的地方(指它是内部的无法访问的对象而言)。

var globalvariable = "www";

document.write(window.globalvariable); //result: www

上面代码在global code方式下运行,根据对variable object的处理,定义变量globalvariable时就会在global object(即window)对象上添加这个属性,所以输出是www这个值。

function code

variable object也叫做activation object(因为有一些差异存在,所以规范中重新取一个名字以示区别,global code/eval code中叫variable object,function code中就叫做activation object)。

每次进入函数执行都会创建一个新的activation object对象,然后创建一个arguments对象并设置为activation object的属性,再进行variable instantiation处理。

在退出函数时,activation object会被丢弃(并不是内存释放,只是可以被垃圾回收了)。

附arguments对象的属性:

length: 为实际传入参数的个数。注意,参考函数对象创建过程,函数对象上的length为函数定义时要求的参数个数;

callee: 为执行的函数对象本身。目的是使函数对象能够引用自己,例如需要递归调用的地方。

function fnname(...) { ... }这样定义函数,它的递归调用可以在函数体内使用fnname完成。var fn=function(...) { ... }这样定义匿名函数,在函数体内无法使用名字引用自己,通过arguments.callee就可以引用自己而实现递归调用。

参数列表: 调用者实际传入的参数列表。这个参数列表提供一个使用索引访问实际参数的方法。variable instantiation处理时会在activation object对象上添加属性,前提是函数声明时有指定参数列表。如果函数声明中不给出参数列表,或者实际调用参数个数与声明时的不一样,可以通过arguments访问各个参数。

arguments中的参数列表与activation object上的参数属性引用的是相同的参数对象(如果修改,在两处都会反映出来)。规范并不要求arguments是一个数组对象,下面是一个测试:

//passed in ff2.0, ie7, opera9.25, safari3.0.4

var argumentslike = { 0: "aaa", 1: 222, 2: "www", length: 3, callee: function() { } };

document.write(argumentslike[2] + "

"); //result: www

document.write(argumentslike[1] + "

"); //result: 222

//convert the argumentslike to an array object, just as we can do this for the arguments property

var array = [].slice.apply(argumentslike);

document.write(array instanceof array); //result: true

document.write("

");

document.write(array.reverse().join("|")); //result: www|222|aaa

eval code

variable object就是调用eval时当前执行上下文中的variable object。在global code中调用eval函数,它的variable object就是global object;在函数中调用eval,它的variable object就是函数的activation object。

//passed in ff2.0, ie7, opera9.25, safari3.0.4

function fn(arg){

var innervar = "variable in function";

eval(' \

var evalvar = "variable in eval"; \

document.write(arg + "

"); \

document.write(innervar + "

"); \

');

document.write(evalvar);

}

fn("arguments for function");

输出结果是:

arguments for function

variable in function

variable in eval

说明: eval调用中可以访问函数fn的参数、局部变量;在eval中定义的局部变量在函数fn中也可以访问,因为它们的varible object是同一个对象。

scope/scope chain

首先scope chain是一个类似链表/堆栈的结构,里面每个元素基本都是variable object/activation object。

其次存在执行上下文的地方都有当前scope chain,可以理解为scope chain就是执行上下文的具体表现形式。

global code

scope chain只包含一个对象,即global object。在开始javascript代码的执行之前,引擎会创建好这个scope chain结构。

function code

函数对象在内部都有一个[[scope]]属性,用来记录该函数所处位置的scope chain。

创建函数对象时,引擎会将当前执行环境的scope chain传给function的[[construct]]方法。[[construct]]会创建一个新的scope chain,内容与传入的scope chain完全一样,并赋给被创建函数的内部[[scope]]属性。在前面函数对象创建过程一节中,这个处理位于步骤4和5之间。

进入函数调用时,也会创建一个新的scope chain,包括同一个函数的递归调用,退出函数时这个scope chain被丢弃。新建的scope chain第一个对象是activation object,接下来的内容与内部[[scope]]上存储的scope chain内容完全一样。

eval code

进入eval code执行时会创建一个新的scope chain,内容与当前执行上下文的scope chain完全一样。

实例说明

scope chain的原理就上面这些,必须结合js代码的执行、variable instantiation的细节处理,才能理解上面这些如何产生作用,下面用一个简单的场景来综合说明。假设下面是一段javascript的global code:

var outervar1="variable in global code";

function fn1(arg1, arg2){

var innervar1="variable in function code";

function fn2() { return outervar1+" - "+innervar1+" - "+" - "+(arg1 + arg2); }

return fn2();

}

var outervar2=fn1(10, 20);

执行处理过程大致如下:

1. 初始化global object即window对象,variable object为window对象本身。创建scope chain对象,假设为scope_1,其中只包含window对象。

2. 扫描js源代码(读入源代码、可能有词法语法分析过程),从结果中可以得到定义的变量名、函数对象。按照扫描顺序:

2.1 发现变量outervar1,在window对象上添加outervar1属性,值为undefined;

2.2 发现函数fn1的定义,使用这个定义创建函数对象,传给创建过程的scope chain为scope_1。将结果添加到window的属性中,名字为fn1,值为返回的函数对象。注意fn1的内部[[scope]]就是scope_1。另外注意,创建过程并不会对函数体中的js代码做特殊处理,可以理解为只是将函数体js代码的扫描结果保存在函数对象的内部属性上,在函数执行时再做进一步处理。这对理解function code,尤其是嵌套函数定义中的variable instantiation很关键;

2.3 发现变量outervar2,在window对象上添加outervar2属性,值为undefined;

3. 执行outervar1赋值语句,赋值为"variable in global code"。

4. 执行函数fn1,得到返回值:

4.1 创建activation object,假设为activation_1;创建一个新的scope chain,假设为scope_2,scope_2中第一个对象为activation_1,第二个对象为window对象(取自fn1的[[scope]],即scope_1中的内容);

4.2 处理参数列表。在activation_1上设置属性arg1、arg2,值分别为10、20。创建arguments对象并进行设置,将arguments设置为activation_1的属性;

4.3 对fn1的函数体执行类似步骤2的处理过程:

4.3.1 发现变量innervar1,在activation_1对象上添加innervar1属性,值为undefine;

4.3.2 发现函数fn2的定义,使用这个定义创建函数对象,传给创建过程的scope chain为scope_2(函数fn1的scope chain为当前执行上下文的内容)。将结果添加到activation_1的属性中,名字为fn2,值为返回的函数对象。注意fn2的内部[[scope]]就是scope_2;

4.4 执行innervar1赋值语句,赋值为"variable in function code"。

4.5 执行fn2:

4.5.1 创建activation object,假设为activation_2;创建一个新的scope chain,假设为scope_3,scope_3中第一个对象为activation_2,接下来的对象依次为activation_1、window对象(取自fn2的[[scope]],即scope_2);

4.5.2 处理参数列表。因为fn2没有参数,所以只用创建arguments对象并设置为activation_2的属性。

4.5.3 对fn2的函数体执行类似步骤2的处理过程,没有发现变量定义和函数声明。

4.5.4 执行函数体。对任何一个变量引用,从scope_3上进行搜索,这个示例中,outervar1将在window上找到;innervar1、arg1、arg2将在activation_1上找到。

4.5.5 丢弃scope_3、activation_2(指它们可以被垃圾回收了)。

4.5.6 返回fn2的返回值。

4.6 丢弃activation_1、scope_2。

4.7 返回结果。

5. 将结果赋值给outervar2。

其它情况下scope chain、variable instantiation处理类似上面的过程进行分析就行了。

根据上面的实例说明,就可以解释下面这个测试代码的结果:

//passed in ff2.0, ie7, opera9.25, safari3.0.4

function fn(obj){

return {

//test whether exists a local variable "outervar" on obj

exists: object.prototype.hasownproperty.call(obj, "outervar"),

//test the value of the variable "outervar"

value: obj.outervar

};

}

var result1 = fn(window);

var outervar = "www";

var result2 = fn(window);

document.write(result1.exists + " " + result1.value); //result: true undefined

document.write("

");

document.write(result2.exists + " " + result2.value); //result: true www

result1调用的地方,outervar声明和赋值的语句还没有被执行,但是测试结果window对象已经拥有一个本地属性outervar,其值为undefined。result2的地方outervar已经赋值,所以window.outervar的值已经有了。实际使用中不要出现这种先使用,后定义的情况,否则某些情况下会有问题,因为会涉及到一些规范中没有提及,不同厂商实现方式上不一致的地方。

一些特殊处理

1. with(obj) { ... }这个语法的实现方式,是在当前的scope chain最前面位置插入obj这个对象,这样就会先在obj上搜索是否有相应名字的属性存在。其它类似的还有catch语句。

2. 前面对arguments对象的详细说明中,提到了对函数递归调用的支持问题,了解到了匿名函数使用arguments.callee来实现引用自己,而命名函数可以在函数体内引用自己,根据上面scope chain的工作原理我们还无法解释这个现象,因为这里有个特殊处理。

任何时候创建一个命名函数对象时,javascript引擎会在当前执行上下文scope chain的最前面插入一个对象,这个对象使用new object()方式创建,并将这个scope chain传给function的构造函数[[construct]],最终创建出来的函数对象内部[[scope]]上将包含这个object对象。创建过程返回之后,javascript引擎在object上添加一个属性,名字为函数名,值为返回的函数对象,然后从当前执行上下文的scope chain中移除它。这样函数对象的scope chain中第一个对象就是对自己的引用,而移除操作则确保了对函数对象创建处scope chain的恢复。

this关键字处理

执行上下文包含的另一个概念是this关键字。

global code中this关键字为global object;函数调用时this关键字为调用者,例如obj1.fn1(),在fn1中this对象为obj1;eval code中this关键字为当前执行上下文的variable object。

在函数调用时,javascript提供一个让用户自己指定this关键字值的机会,即每个函数都有的call、apply方法。例如:

fn1.call(obj1, arg1, arg2, ...)或者fn1.apply(obj1, argarray),都是将obj1作为this关键字,调用执行fn1函数,后面的参数都作为函数fn1的参数。如果obj1为null或undefined,则global object将作为this关键字的值;如果obj1不是object类型,则转化为object类型。它们之间的唯一区别在于,apply允许以数组的方式提供各个参数,而call方法必须一个一个参数的给。

前面的测试示例代码中有多处运用到了这个方法。例如window对象并没有hasownproperty方法,使用object.prototype.hasownproperty.call(window, "propertyname")也可以测试它是否拥有某个本地属性。

javascript中的闭包closures

示例:

//passed in ff2.0, ie7, opera9.25, safari3.0.4

function outer(){

var a="aaa";

var b="bbb";

return function(){ return a + " " + b; };

}

var inner=outer();

document.write(inner());

outer返回的是一个内嵌函数,内嵌函数使用了outer的局部变量a和b。照理outer的局部变量在返回时就超出了作用域因此inner()调用无法使用才对。这就是闭包closure,即函数调用返回了一个内嵌函数,而内嵌函数引用了外部函数的局部变量、参数等这些应当被关闭(close)了的资源。

根据前面scope chain的理解可以解释,返回的内嵌函数已经持有了构造它时的scope chain,虽然outer返回导致这些对象超出了作用域、生存期范围,但javascript使用自动垃圾回收来释放对象内存: 按照规则定期检查,对象没有任何引用才被释放。因此上面的代码能够正确运行。

关于使用closure时的内存泄漏、效率等问题,参考http://www.jibbering.com/faq/faq_notes/closures.html


======================================================
在最后,我邀请大家参加新浪APP,就是新浪免费送大家的一个空间,支持PHP+MySql,免费二级域名,免费域名绑定 这个是我邀请的地址,您通过这个链接注册即为我的好友,并获赠云豆500个,价值5元哦!短网址是http://t.cn/SXOiLh我创建的小站每天访客已经达到2000+了,每天挂广告赚50+元哦,呵呵,饭钱不愁了,\(^o^)/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值