js模型
第三章 对象基础(对象模型)
3.1对象创建过程
JS中只有函数对象具备类的概念,因此要创建一个对象,必须使用函数对象。函数对象内部有[[Construct]]方法和[[Call]]方法,[[Construct]]用于构造对象,[[Call]]用于函数调用,只有使用new操作符时才触发[[Construct]]逻辑。var obj=new Object();(对象创建表达式) 是使用内置的Object这个函数对象创建实例化对象obj。var obj={};(对象初始化表达式)和var obj=[];(数组初始化表达式)这种代码将由JS引擎生成Objec、Array的实例,生成效率高,并不调用Object和Array的构造函数。function Fn(){}; var myObj=new Fn();(对象创建表达式)是使用用户定义的类型创建实例化对象。
new Fn(args)的创建过程如下(即函数对象的[[Construct]]方法处理逻辑,对象的创建过程)。另外函数对象本身的创建过程(指定义函数或者用Function创建一个函数对象等方式)虽然也使用了下面的处理逻辑,但有特殊的地方,后面再描述。
对象创建过程([[Construc]](args))
new Fn(args):使用new操作符时触发Fn 的[[Construct]]逻辑,创建对象的步骤如下:
- 创建一个build-in object对象obj(实例对象);
(并完成内部必要的初始化,它的[[Class]]、[[Extensible]]、[[Prototype]]等属性应当为null或者内部初始化值。)
- 设置obj的内部属性[[Class]]为”Object”,或者其它本地类型,例如:”Number”、”String”、
“Boolean”等。这是由构造函数的类型来定的。
3. 设置obj的内部属性[[Extensible]]为 true。
4. 如果Fn.prototype是object引用对象类型,则将obj的内部[[Prototype]]设置为Fn.prototype,否则obj的[[Prototype]]将为其初始化值(即Object.prototype)
5. 将obj作为this,使用args参数调用Fn的内部[[Call]]方法。(this.[[Call]](args))
5.1 创建内部[[Call]]方法的当前执行环境(当前执行环境压入执行环境栈)
(函数体内function定义的方法的[[Scope]]静态作用域复制到[[Call]]的作用域链,在作用域链的前端添加[[Call]]的活动对象;this引用obj)
5.2 调用Fn的函数体
3.3 销毁当前的执行环境(当前执行环境弹出执行环境栈)
3.4 返回Fn函数体的返回值,如果Fn的函数体没有返回值则返回undefined
6. 如果返回值是object引用对象类型,则返回这个值;否则(返回值是基本类型)则返回obj
注意步骤4中, prototype指Fn对象显示的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 + "<br />"); //result: aaa
document.write(obj.attr2 + "<br />"); //result: bbb
// fn.prototype.constructor == fn; //result: false
// obj.[[ prototype]] == fn.prototype; //result: true
//因此,obj继承了fn.prototype的constructor ,
// obj. constructor à obj.[[ prototype]]
// à fn.prototype à fn.prototype. constructor -> fn
document.write(obj instanceof fn); //result: true
document.write("<br />");
//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 + "<br />"); //result: aaa
document.write(obj.attr2 + "<br />"); //result: bbb
document.write(obj instanceof fn); //result: false
// fn.prototype.constructor == Object; //result: true
// obj.[[ prototype]] == fn.prototype; //result: true
//因此,obj继承了fn.prototype的constructor ,
// obj. constructor à obj.[[ prototype]]
à fn.prototype à fn.prototype. constructor -> Object
document.write(obj instanceof Object); //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 + "<br />"); //result: 111
document.write(obj.attr2 + "<br />"); //result: 222
document.write(obj instanceof fn); //result: false
3.2 自定义函数对象创建过程(类/函数/构造函数)
函数标识符:
- 符合语法
- 有意义
- 简洁
- 符合JavaScript的命名规范
- 使用function关键字定义函数对象
1)函数声明语句
如果函数声明只用于调用,则采用camel命名规范
function Func (a){document.write(a + "<br />");}
如果函数声明用于new,则采用Pascal命名规范
function func(a){document.write(a + "<br />");}
2)函数声明表达式
匿名函数声明表达式
var test = function func (a){document.write(a + "<br />");}
命名函数声明表达式
var test = function (a){document.write(a + "<br />");}
- 使用Function类定义函数对象
具有动态性,它允许在程序运行期间动态地创建函数。
语法:var 函数名 = new Function("形式参数1","形式参数2",...,"形式参数n","函数体");
语法:var 函数名 = Function("形式参数1","形式参数2",...,"形式参数n","函数体");
Function类的一个特殊地方,是它的[[Call]]和[[Construct]]处理逻辑一样。
new Function生成的函数的[[Scope]]永远都只包含window对象。
自定义函数(/类/构造函数)对象创建过程
JavaScript代码中使用关键词function定义函数,或者调用Function创建函数对象,其步骤如下:
- 创建一个build-in object对象fn
(并完成内部必要的初始化,它的 [[Prototype]] 、[[Class]]、[[Extensible]]、 [[Call]]、[[Construct]]等内部隐式属性应当为null或者内部初始化值。)
- 设置fn的内部隐式属性[[Class]]为” Function”。
3. 设置fn的内部隐式属性[[Extensible]]为true。
4. 设置fn的内部隐式属性[[Prototype]]为Function.prototype
5. 设置fn的内部隐式属性[[Call]],它是内部实现的一个方法(包含JS代码),处理逻辑参考对象创建过程的步骤5
6. 设置fn的内部隐式属性[[Construct]],它是内部实现的一个方法,处理逻辑参考对象创建过程的步骤1,2,3,4,5,6。
7. 设置fn的内部隐式属性[[Scope]]为:
7.1 使用function关键字声明的函数声明语句创建的函数对象,其[[Scope]]静态作用域复制当前执行环境的作用域链。
function Func (a){document.write(a + "<br />");}
function func (a){document.write(a + "<br />");}
7.2 使用function关键字声明的匿名函数表达式创建的函数对象,其[[Scope]]静态作用域复制当前执行环境的作用域链。
var test = function (a){document.write(a + "<br />");}
7.3 使用function关键字声明的命名函数表达式创建的函数对象,其[[Scope]]静态作用域复制当前执行环境的作用域链;在[[Scope]]静态作用域的前端添加一个Object类的实例;在该实例上添加一个属性,名字为函数名func,值为返回的函数对象fn。
var test = function func (a){document.write(a + "<br />");}
7.4 new Function()、Function()函数定义的函数对象的[[Scope]]静态作用域复制全局执行环境的作用域链
var fn = new Function("形式参数1","形式参数2",...,"形式参数n","函数体");
var fn = Function("形式参数1","形式参数2",...,"形式参数n","函数体");
8. 设置fn.length为函数形参数量,如果函数没有参数,则将fn.length设置为0
9. 在函数对象fn内部设置形参列表。
10. 使用new Object()同样的逻辑创建一个Object类的实例对象fnProto
11. 将fnProto.constructor设为fn
原型的构造器属性引用构造函数本身,对编码者来说意义不大,它仅是语法规范的一种实现。
12. 将fn.prototype设为fnProto
13. 返回fn
步骤1跟步骤10的区别为,步骤1只是创建内部用来实现object复杂对象的数据结构(build-in object structure),并完成内部必要的初始化工作,但它的[[Prototype]]、[[Call]]、[[Construct]]等属性应当为null或者内部初始化值,即我们可以理解为不指向任何对象(对[[Prototype]]这样的属性而言),或者不包含任何处理(对[[Call]]、[[Construct]]这样的方法而言)。步骤10则将按照前面描述的对象创建过程创建Object类的一个新对象,它的[[Prototype]]等被设置为Object.protorype。
从上面的处理步骤可以了解,任何时候我们定义一个函数,它的prototype是一个Object类的实例,这样默认情况下我们创建自定义函数的实例对象时,它们的Prototype链将指向Object.prototype。
另外,Function或者Array类的一个特殊地方,是它的[[Call]]和[[Construct]]处理逻辑一样。而通常fn的[[Construct]]调用[[Call]],参见对象创建过程5。
3.3Function构造函数/Function类型
(相当于java的class反射类或者C#的type反射类)
3.4JavaScript对象模型
红色虚线表示隐式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属性之后,情况就不同了。
Javascript引擎的内置对象/类是由ECMAScript实现提供的、与宿主无关,这些对象在程序执行之前就已经存在了,具有特殊的数据结构或Native Code ;其数据结构或Native Code 的行为与我们自定义的对象/函数不同;只能通过内置类的API来操作内置类的实例。即内置类或浏览器的数据结构只能通过API来操作;我们自定义的对象内部没有这些特殊的数据结构。