JavaScript面向对象的支持(下)

来源:岁月联盟 编辑:zhuzhu 时间:2009-01-15

  八、JavaScript面向对象的支持

  4. 实例和实例引用

  在.NET Framework对CTS(Common Type System)约定“一切都是对象”,并分为“值类型”和“引用类型”两种。其中“值类型”的对象在转换成“引用类型”数据的过程中,需要进行一个“装箱”和“拆箱”的过程。在JavaScript也有同样的问题。我们看到的typeof关键字,返回以下六种数据类型:

  "number"、"string"、"boolean"、"object"、"function" 和 "undefined"。我们也发现JavaScript的对象系统中,有String、Number、Function、Boolean这四种对象构造器。那么,我们的问题是:如果有一个数字A,typeof(A)的结果,到底会是'number'呢,还是一个构造器指向function Number()的对象呢?

  // 关于JavaScript的类型的测试代码

  function getTypeInfo(V) {
  return (typeof V == 'object' ? 'Object, construct by '+V.constructor
   : 'Value, type of '+typeof V);
  }var A1 = 100;
var A2 = new Number(100);
document.writeln('A1 is ', getTypeInfo(A1), '<BR>');
document.writeln('A2 is ', getTypeInfo(A2), '<BR>');
document.writeln([A1.constructor === A2.constructor, A2.constructor === Number]);

  测试代码的执行结果如下:

A1 is Value, type of number
A2 is Object, construct by function Number() { [native code] }
true,true

  我们注意到,A1和A2的构造器都指向Number.这意味着通过constructor属性来识别对象,(有时)比typeof更加有效.因为"值类型数据"A1作为一个对象来看待时,与A2有完全相同的特性.

  --除了与实例引用有关的问题.

  参考JScript手册,我们对其它基础类型和构造器做相同考察,可以发现:

  - 基础类型中的undefined、number、boolean和string,是“值类型”变量

  - 基础类型中的array、function和object,是“引用类型”变量

  - 使用new()方法构造出对象,是“引用类型”变量下面的代码说明“值类型”与“引用类型”之间的区别:

  // 关于JavaScript类型系统中的值/引用问题

  var str1 = 'abcdefgh', str2 = 'abcdefgh';
  var obj1 = new String('abcdefgh'), obj2 = new String('abcdefgh');document.writeln([str1==str2, str1===str2], '<br>');
document.writeln([obj1==obj2, obj1===obj2]);

  测试代码的执行结果如下:

-----------
true, true
false, false
-----------

  我们看到,无论是等值运算(==),还是全等运算(===),对"对象"和"值"的理解都是不一样的.

  更进一步的理解这种现象,我们知道:

  - 运算结果为值类型,或变量为值类型时,等值(或全等)比较可以得到预想结果 - (即使包含相同的数据,)不同的对象实例之间是不等值(或全等)的 - 同一个对象的不同引用之间,是等值(==)且全等(===)的但对于String类型,有一点补充:根据JScript的描述,两个字符串比较时,只要有一个是值类型,则按值比较.这意味着在上面的例子中,代码"str1==obj1"会得到结果true.而全等(===)运算需要检测变量类型的一致性,因此"str1===obj1"的结果返回false.

  JavaScript中的函数参数总是传入值参,引用类型(的实例)是作为指针值传入的.因此函数可以随意重写入口变量,而不用担心外部变量被修改.但是,需要留意传入的引用类型的变量,因为对它方法调用和属性读写可能会影响到实例本身.--但,也可以通过引用类型的参数来传出数据.

  最后补充说明一下,值类型比较会逐字节检测对象实例中的数据,效率低但准确性高;而引用类型只检测实例指针和数据类型,因此效率高而准确性低.如果你需要检测两个引用类型是否真的包含相同的数据,可能你需要尝试把它转换成"字符串值"再来比较.

  6. 函数的上下文环境--------只要写过代码,你应该知道变量是有"全局变量"和"局部变量"之分的.绝大多数的JavaScript程序员也知道下面这些概念:

  // JavaScript中的全局变量与局部变量var v1 = '全局变量-1';
v2 = '全局变量-2';
function foo() {
 v3 = '全局变量-3';
 var v4 = '
只有在函数内部并使用var定义的,才是局部变量';
}
按照通常对语言的理解来说,不同的代码调用函数,都会拥有一套独立的局部变量。

  因此下面这段代码很容易理解:

// JavaScript的局部变量
//---------------------------------------------------------
  function MyObject() {

  var o = new Object; this.getValue = function() {
  return o;
 }
}
var obj1 = new MyObject();
var obj2 = new MyObject();
document.writeln(obj1.getValue() == obj2.getValue());

  结果显示false,表明不同(实例的方法)调用返回的局部变量"obj1/obj2"是不相同.

  变量的局部、全局特性与OOP的封装性中的"私有(private)"、"公开(public)"具有类同性.因此绝大多数资料总是以下面的方式来说明JavaScript的面向对象系统中的"封装权限级别"问题:

  // JavaScript中OOP封装性

  function MyObject() {

  // 1. 私有成员和方法

  var private_prop = 0;
  var private_method_1 = function() {
  // ...
  return 1
  }
  function private_method_2() {
  // ...
  return 1
  }

  // 2. 特权方法

  this.privileged_method = function () {
  private_prop++;
  return private_prop + private_method_1() + private_method_2();
  } 

  // 3. 公开成员和方法

  this.public_prop_1 = '';
  this.public_method_1 = function () {
  // ...
  }
  }

  // 4. 公开成员和方法(2)

MyObject.prototype.public_prop_1 = '';
MyObject.prototype.public_method_1 = function () {
 // ...
}
var obj1 = new MyObject();
var obj2 = new MyObject();
document.writeln(obj1.privileged_method(), '<br>');
document.writeln(obj2.privileged_method());

  在这里,"私有(private)"表明只有在(构造)函数内部可访问,而"特权(privileged)"是特指一种存取"私有域"的"公开(public)"方法."公开(public)"表明在(构造)函数外可以调用和存取.

  除了上述的封装权限之外,一些文档还介绍了其它两种相关的概念:

  - 原型属性:Classname.prototype.propertyName = someValue - (类)静态属性:Classname.propertyName = someValue 然而,从面向对象的角度上来讲,上面这些概念都很难自圆其说:JavaScript究竟是为何、以及如何划分出这些封装权限和概念来的呢?

  ——因为我们必须注意到下面这个例子所带来的问题:

//---------------------------------------------------------
// JavaScript中的局部变量
//---------------------------------------------------------
function MyFoo() {
 var i;
 MyFoo.setValue = function (v) {
   i = v;
 }
 MyFoo.getValue = function () {
   return i;
 }
}
MyFoo();
var obj1 = new Object();
var obj2 = new Object();

  // 测试一

  MyFoo.setValue.call(obj1, 'obj1');
  document.writeln(MyFoo.getValue.call(obj1), '<BR>');

  // 测试二

  MyFoo.setValue.call(obj2, 'obj2');
  document.writeln(MyFoo.getValue.call(obj2));
  document.writeln(MyFoo.getValue.call(obj1));
  document.writeln(MyFoo.getValue());

  在这个测试代码中,obj1/obj2都是Object()实例.我们使用function.call()的方式来调用setValue/getValue,使得在MyFoo()调用的过程中替换this为obj1/obj2实例.

  然而我们发现"测试二"完成之后,obj2、obj1以及function MyFoo()所持有的局部变量都返回了"obj2".--这表明三个函数使用了同一个局部变量.

  由此可见,JavaScript在处理局部变量时,对"普通函数"与"构造器"是分别对待的.这种处理策略在一些JavaScript相关的资料中被解释作"面向对象中的私有域"问题.而事实上,我更愿意从源代码一级来告诉你真相:这是对象的上下文环境的问题.--只不过从表面看去,"上下文环境"的问题被转嫁到对象的封装性问题上了.

  (在阅读下面的文字之前,)先做一个概念性的说明:

  - 在普通函数中,上下文环境被window对象所持有 - 在"构造器和对象方法"中,上下文环境被对象实例所持有在JavaScript的实现代码中,每次创建一个对象,解释器将为对象创建一个上下文环境链,用于存放对象在进入"构造器和对象方法"时对function()内部数据的一个备份.

  JavaScript保证这个对象在以后再进入"构造器和对象方法"内部时,总是持有该上下文环境,和一个与之相关的this对象.由于对象可能有多个方法,且每个方法可能又存在多层嵌套函数,因此这事实上构成了一个上下文环境的树型链表结构.而在构造器和对象方法之外,JavaScript不提供任何访问(该构造器和对象方法的)上下文环境的方法.

  简而言之:

  - 上下文环境与对象实例调用"构造器和对象方法"时相关,而与(普通)函数无关 - 上下文环境记录一个对象在"构造函数和对象方法"内部的私有数据 - 上下文环境采用链式结构,以记录多层的嵌套函数中的上下文由于上下文环境只与构造函数及其内部的嵌套函数有关,重新阅读前面的代码:

//---------------------------------------------------------
// JavaScript中的局部变量
//---------------------------------------------------------
function MyFoo() {
 var i;
 MyFoo.setValue = function (v) {
   i = v;
 }
 MyFoo.getValue = function () {
   return i;
 }
}
MyFoo();
var obj1 = new Object();
MyFoo.setValue.call(obj1, 'obj1');

  我们发现setValue()的确可以访问到位于MyFoo()函数内部的"局部变量i",但是由于setValue()方法的执有者是MyFoo对象(记住函数也是对象),因此MyFoo对象拥有MyFoo()函数的唯一一份"上下文环境".

  接下来MyFoo.setValue.call()调用虽然为setValue()传入了新的this对象,但实际上拥有"上下文环境"的仍旧是MyFoo对象.因此我们看到无论创建多少个obj1/obj2,最终操作的都是同一个私有变量i.

  全局函数/变量的"上下文环境"持有者为window,因此下面的代码说明了"为什么全局变量能被任意的对象和函数访问":

//---------------------------------------------------------
// 全局函数的上下文
//---------------------------------------------------------
/*
function Window() {
*/
 var global_i = 0;
 var global_j = 1;
 function foo_0() {
 }
 function foo_1() {
 }
/*
}
window = new Window();
*/

  因此我们可以看到foo_0()与foo_1()能同时访问global_i和global_j。接下来的推论是,上下文环境决定了变量的“全局”与“私有”。而不是反过来通过变量的私有与全局来讨论上下文环境问题。更进一步的推论是:JavaScript中的全局变量与函数,本质上是window对象的私有变量与方法。而这个上下文环境块,位于所有(window对象内部的)对象实例的上下文环境链表的顶端,因此都可能访问到。用“上下文环境”的理论,你可以顺利地解释在本小节中,有关变量的“全局/局部”作用域的问题,以及有关对象方法的封装权限问题。事实上,在实现JavaScript的C源代码中,这个“上下文环境”被叫做“JSContext”,并作为函数/方法的第一个参数传入。

  ——如果你有兴趣,你可以从源代码中证实本小节所述的理论。另外,《JavaScript权威指南》这本书中第4.7节也讲述了这个问题,但被叫做“变量的作用域”。然而重要的是,这本书把问题讲反了。

  ——作者试图用“全局、局部的作用域”,来解释产生这种现象的“上下文环境”的问题。因此这个小节显得凌乱而且难以自圆其说。不过在4.6.3小节,作者也提到了执行环境(execution context)的问题,这就与我们这里说的“上下文环境”是一致的了。然而更麻烦的是,作者又将读者引错了方法,试图用函数的上下文环境去解释DOM和ScriptEngine中的问题。但这本书在“上下文环境链表”的查询方式上的讲述,是正确的而合理的。只是把这个叫成“作用域”有点不对,或者不妥。八、JavaScript面向对象的支持

  7. JavaScript面向对象的支持的补充内容

  1). 类型系统

  我们前面已经完整地描述过JavaScript的两种类型系统。包括:

  - 基础类型系统:由typeof()返回值的六种基础类型

  - 对象类型系统:由new()返回值的、构造器和原型继承组织起来的类型系统JavaScript是弱类型语言,因此类型自动转换是它语言特性的一个重要组成部分。但对于一个指定的变量而言,(在某一时刻,)它总是有确定的数据类型的。“运算”是导致类型转换的方法(但不是根源),因此“运算结果的类型”的确定就非常重要。关于这一部分的内容,推荐大家阅读一份资料:

  http://jibbering.com/faq/faq_notes/type_convert.html类型系统中还有一个特殊的组成部分,就是“直接量”声明。下面的代码简述各种直接量声明的方法,但不再详述具体细节:

// 各种直接量声明(一些错误格式或特例请查看JScript手册)
//---------------------------------------------------------
// 1. Number
var n1 = 11;   // 普通十进制数
var n2 = 013;   // 八进制数
var n3 = 0xB;   // 十六进制数
var n4 = 1.2;   // 浮点值
var n5 = .2;   // 浮点值
var n6 = 1.0e-4; // (或1e-4)浮点值
// 2. String
var s1 = 'test'; // (或"test")字符串
var s2 = "testn";// 带转义符的字符串(转义符规则参见手册)
var s3 = "'test'";// 用""、''以在字符串中使用引号
var s4 = "xD";  // 用转义符来声明不可键入的字符
// 3. Boolean
var b1 = true;
var b2 = false;
// 4. Function
function foo1() {};    // 利用编译器特性直接声明
var foo2 = function() {}; // 声明匿名函数
// 5. Object
// * 请留意声明中对分隔符“,”的使用
var obj1 = null;     // 空对象是可以被直接声明的
var obj2 = {
 value1 : 'value',    // 对象属性
 foo1  : function() {}, // 利用匿名函数来直接声明对象方法
 foo2  : foo2      // 使方法指向已声明过的函数
}
// 6. RegExp
var r1 = /^[O|o]n/;   // 使用一对"/../"表达的即是正则表达式
var r2 = /^./gim;    // (注意,) gim为正则表达式的三个参数
// 7. Array
var arr1 = [1,,,1];   // 直接声明, 包括一些"未定义(undefined)"值
var arr2 = [1,[1,'a']]; // 异质(非单一类型)的数组声明
var arr3 = [[1],[2]];  // 多维数组(其实是从上一个概念衍生下来的
// 8. undefined
var u1 = undefined;   // 可以直接声明, 这里的undefined是Global的属性

  有些时候,我们可以“即声明即使用”一个直接量,下面的代码演示这一特性:

  // 直接量的“即声明即使用”

var obj = function () { 

  // 1. 声明了一个匿名函数

  return {   

  // 2. 函数执行的结果是返回一个直接声明的"对象"

  value: 'test',
  method: function(){}
 }
}();    

  // 3. 使匿名函数执行并返回结果,以完成obj变量的声明在这个例子中,很多处用到了直接量的声明。这其中函数直接声明(并可以立即执行)的特性很有价值,例如在一个.js文件中试图执行一些代码,但不希望这些代码中的变量声明对全局代码导致影响,因此可以在外层包装一个匿名函数并使之执行,例如:

//---------------------------------------------------------
// 匿名函数的执行
// (注:void用于使后面的函数会被执行, 否则解释器会认为仅是声明函数)
//---------------------------------------------------------
void function() {
 if (isIE()) {
  // do something...
 }
}();

  2). 对象系统

  对象系统中一个未被提及的重要内容是delete运算.它用于删除数组元素、对象属性和已声明的变量.

  由于delete运算不能删除用var来声明的变量,也就意味着它只能删除在函数内/外声明的全局变量.--这个说法有点别扭,但事实上的确如此.那么我们可以更深层地透视一个真想:delete运算删除变量的实质,是删除用户在window对象的上下文环境中声明的属性.

  回到前面有关"上下文环境"的讨论,我们注意到(在函数外)声明全局变量的三种形式:

var global_1 = '全局变量1';
global_2 = '全局变量2';
function foo() {
 global_3 = '全局变量3';
}

  全局变量2和3都是"不用var声明的变量",这其实是在window对象的上下文环境中的属性声明.也就是说可以用window.global_2和window.global_3来存取它们.这三种声明window对象的属性的方法,与直接指定"window.global_value = <值>"这种方法的唯一区别,是在"for .. in"运算时,这三种方法声明的属性/方法都会被隐藏.如下例所示:

//---------------------------------------------------------
// 全局变量上下文环境的一些特点:属性名隐藏
//---------------------------------------------------------
var global_1 = '全局变量1';
global_2 = '全局变量2';
void function foo() {
 global_3 = '全局变量3';
}();window.global_4 = '全局变量4';for (var i in window) {
 document.writeln(i, '<br>');
}
document.writeln('<HR>');
document.writeln(window.global_1, '<BR>');
document.writeln(window.global_2, '<BR>');
document.writeln(window.global_3, '<BR>');

  我们注意到在返回的结果中不会出现全局变量1/2/3的属性名.但使用window.xxxx这种方式仍可以存取到它们.

  在window上下文环境中,global_1实质是该上下文中的私有变量,我们在其它代码中能存取到它,只是因为其它(所有的)代码都在该上下文之内.global_2/3则被(隐含地)声明成window的属性,而global_4则显式地声明为window的属性.

  因此我们回到前面的结论:

  - 删除(不用var声明的)变量的实质,是删除window对象的属性.

  此外,我们也得到另外三条推论(最重要的是第一条):

  - delete能删除数组元素,实质上是因为数组下标也是数组对象的隐含属性.

  - 在复杂的系统中,为减少变量名冲突,应尽量避免全局变量(和声明)的使用,或采用  delete运算来清理window对象的属性.

  - window对象是唯一可以让用户声明"隐含的属性"的对象.--注意这只是表面的现  象,因为事实上这只是JavaScript规范带来的一个"附加效果".:)delete清除window对象、系统对象、用户对象等的"用户声明属性",但不能清除如prototype、constructor这样的系统属性.此外,delete也可以清除数组中的元素(但不会因为清除元素而使数组长度发生变化).例如:

  // delete运算的一些示例

  var arr = [1, 2, 3];
  var obj = {v1:1, v2:2};
  global_variant = 3;delete arr[2];
document.writeln('1' in arr, '<BR>'); // 数组下标事实上也是数组对象的隐含属性
document.writeln(arr.length, '<BR>'); // 数组长度不会因delete而改变
delete obj.v2;
document.writeln('v2' in obj, '<BR>');
document.writeln('global_variant' in window, '<BR>');
delete global_variant;
// 以下的代码不能正常执行,这是IE的一个bug
if ('global_variant' in window) {
document.writeln('bug test:', global_variant, '<BR>');
}

  最后这行代码错误的根源,在于IE错误地检测了'global_variant'在window的对象属性中是否仍然存在。因为在同样的位置,“('global_variant' in window)”表达式的返回结果居然为true!——firefox中没有这个bug。delete清除掉属性或数组元素,并不表明脚本引擎会对于该属性/元素执行析构。对象的析构操作是不确定的,关于这一点请查看更前面的内容。