首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java

对象(object)是组成JavaScript的基本单元,事实上,JavaScript中的一切都是对象,而且充分发挥了这一点。不过JavaScript还包含了构成一门丰富坚实的面向对象语言的大量特性,这使它无论是在功能还是在风格上都显得极为独特。

本章将从介绍JavaScript语言中最重要的几个部分开始:引用(reference)、作用域(scope)、闭包(closure)以及上下文(context),而这些内容正是其他JavaScript图书很少提及的。有了这些重要的基础知识之后,我们就可以开始探索面向对象的JavaScript重要的特性了,包括对象的如何行为、如何创建新的对象和设置不同权限的方法(method)。严格地说,这恐怕是全书最重要的一章了,因为它会彻底改变你对JavaScript这门语言的看法。

2.1 语言特性

和许多其他语言不同,JavaScript之所以成为JavaScript,它的大量语言特性起到了至关重要的作用。我个人觉得这样的特性搭配得恰到好处,构成了这样一门比想象中还要强大的语言。

2.1.1 引用

引用(reference)的概念是JavaScript的基础之一,“引用”是一个指向对象实际位置的指针。这是一个极为强大的特性,但有一个前提:实际的对象肯定不会是引用。字符串永远是字符串,数组永远是数组。不过多个变量却能够指向同一对象。JavaScript基于的就是这样一个引用系统。这门语言通过维护一系列对其他对象的引用,提供了极大的灵活性。

此外,对象可以包含一系列属性(property),这些属性也都不过是到其他对象(比如字符串、数字、数组等等)的引用。如果多个变量指向的是同一个对象,那该对象的类型一改变,所有这些变量也会跟着相应改变。在代码清单2-1中可以看到这样的一个例子:两个变量都指向同一个对象,而对该对象内容的修改对这两个变量都会产生影响。

代码清单2-1 多个变量引用同一个对象

    // 将 obj 置为空对象

    var obj = new Object();

   

    // objRef 现在是另一个对象的引用

    var objRef = obj;

   

    // 修改原对象的一个属性

    obj.oneProperty = true;

   

    // 我们现在看到,这个改变在两个变量中都反映了出来

    // (因为它们引用的是同一个对象)

    alert( obj.oneProperty === objRef.oneProperty );

前文提到,自修改(self-modifying)对象在JavaScript中是很少见的。我们可以来看一个最常出现这种情况的实例:用push()方法来给数组(array)对象添加新的元素。因为Array对象实质上是把这些值作为属性保存,所以结果和代码清单2-1的情形差不多,即多个变量的内容可以同时被修改(见代码清单2-2)。

代码清单2-2 自修改对象的例子

    // 创建一个数组

    var items = new Array( "one", "two", "three" );

   

    // 创建数组的一个引用

    var itemsRef = items;

   

    // 将一个元素添加到原数组中

    items.push( "four" );

   

    // 这两个数组的长度应该是一致的,

    // 因为它们指向同一个数组对象

    alert( items.length == itemsRef.length );

必须记住的是,引用指向的只能是具体的对象,而不是另一个引用。不像Perl语言中允许多层引用。JavaScript里的结果是沿着引用链一直上溯到原来那个对象。代码清单2-3是这种情况的一个例子:实际对象已经改变了,但原来指向它的引用仍然保持指向旧的对象。

代码清单2-3 修改对象的引用,同时保持完整性

    // 将 items 置为字符串的数组

    var items = new Array( "one", "two", "three" );

   

    // 将 itemsRef 置为 items 的引用

    var itemsRef = items;

   

    // 将 items 置为一个新对象

    items = new Array( "new", "array" );

   

    // items 和 itemsRef 现在指向不同的对象了。

    // items 指向的是 new Array( "new","array" )

    // itemsRef 指向的是 new Array( "one","two","three" )

    alert( items != itemsRef );

最后,让我们来看一个特殊的例子,看似是自修改对象,其结果却产生了一个新的非引用对象。在执行字符串连接操作时,结果总会是一个新的字符串对象,而非原字符串的修改版本(见代码清单2-4)。

代码清单2-4 修改对象而生成新对象

    // 将 item 置为一个新的字符串对象

    var item = "test";

   

    // itemRef 现在指向同一个字符串对象

    var itemRef = item;

   

    // 将一些新的文本接在这个字符串后面

    // 注意: 这会创建一个新对象,而非修改原对象

    item += "ing";

   

    // item 和 itemRef 的值不相等了,因为新的字符串对象已被创建

    alert( item != itemRef );

如果你刚接触这些,可能会被难以捉摸的引用概念搞晕。不过理解引用究竟是如何运作的,对于写出优秀、整洁的JavaScript代码至关重要。下面几个小节里我们将了解的一些特性,虽然不那么新颖或者激动人心,但对于写出好的代码也是很重要的。

2.1.2 函数重载和类型检查

其他面向对象语言如Java的一个常见特性是,能够根据传入的不同数量或类型的参数,通过“重载(overload)”函数来发挥不同的功用。尽管这个特性在JavaScript中并没有被直接支持,也有很多办法能够实现。

函数重载(function overloading)必须依赖两件事情:判断传入参数数量的能力和判断传入参数类型的能力。我们先来看看参数的数量。

JavaScript的每个函数都带有一个仅在这个函数范围内作用的变量(contextual variable)称为参数(argument),它是一个包含所有传给函数的参数的伪数组(pseudo-array),所以它并非真正意义的数组(也就是说你不能修改它,也不能用push()来添加新元素),但可以访问其中的元素,它也具有.length属性。代码清单2-5是它的两个例子。

代码清单2-5 JavaScript 中函数重载的两个例子

    // 发送一条消息的简单函数

    function sendMessage( msg, obj ) {

        // 如果消息和对象(的参数)都被提供

        if ( arguments.length == 2 )

            // 给对象发送消息

            obj.handleMsg( msg );

   

        // 否则,假定只提供了一条消息

        else

            // 那么仅显示默认的错误信息

            alert( msg );

    }

   

    // 仅用一个参数调用这个函数 - 用 alert 来显示此消息

    sendMessage( "Hello, World!" );

   

    // 又或者我们可以将一个我们自己写好的对象传入

    // 负责用另一套办法显示信息

    sendMessage( "How are you?", {

        handleMsg: function( msg ) {

            alert( "This is a custom message: " + msg );

        }

    });

   

    // 一个接受任意数量参数并将其转换为数组的函数

    function makeArray() {

        // 临时使用的数组

        var arr = [];

   

        // 遍历传入的每个参数

        for ( var i = 0; i < arguments.length; i++ ) {

            arr.push( arguments[i] );

        }

   

        // 返回结果数组

        return arr;

    }

此外,虽然显得有点怪,但是还有一个用来判断传入参数类型的方法。如果没有提供参数,它的类型必为undefined,我们要利用这一特性来作判断。代码清单2-6展示了这样一个简单函数,它用于显示错误信息,如果没有提供信息的内容,就显示默认的一条。

代码清单2-6 显示错误信息和默认信息

    function displayerror( msg ) {

        // 检查并确认 msg 是否 undefined

        if ( typeof msg == 'undefined' ) {

            // 如果是,则置 msg 为默认信息

            msg = "An error occurred.";

        }

   

        // 显示该消息

        alert( msg );

    }

typeof语句的应用为我们引入了下一个话题:类型检查。既然JavaScript(现在而言)是一个动态类型(dynamically typed)的语言,类型检查必然是个非常有用而且重要的话题。有许多方法可以检查变量的类型,但我们这里只讨论两种特别有用的方法。

第一种方法是使用显而易见的typeof操作符。这个工具提供了一个字符串名称(string name),用于表达变量内容的类型。当变量不是object或者array类型时,这应该算是最完美的解决方法了。但是对于自定义的对象,比如user就不能用这个方法进行类型检查,因为它只会返回object,很难跟其他的object区分开来。这一方法的一个例子如代码清单2-7所示。

代码清单2-7 使用typeof 来判断对象类型的一个例子

    // 检查我们的数字是否实际上是字符串

    if ( typeof num == "string" )

        // 若是,则根据这个字符串解析出整数来

        num = parseInt( num );

   

    // 检查我们的数组是否实际上是字符串

    if ( typeof arr == "string" )

        // 若是,则根据逗号切分出数组来

        arr = arr.split(",")

第二种检查对象类型的方法,需要引用所有JavaScript对象都带有的一个的属性,称为构造函数(constructor)。这一属性引用的是原本用来构造该对象的那个函数。这种方法的一个例子如代码清单2-8所示。

代码清单2-8 使用构造函数属性来判断对象的类型

    // 检查我们的数字实际上是否为字符串

    if ( num.constructor == String )

        // 如果是,则根据这个字符串解析出整数来

        num = parseInt( num );

   

    // 检查我们的字符串实际上是否位数组

    if ( str.constructor == Array )

        // 如果是,则根据数组用逗号归并出字符串来

        str = str.join(',');

表2-1展示了用上述的两种方法对不同类型对象进行类型检查的结果。表格的第一栏是尝试判断类型的对象。第二栏是执行typeof变量的结果(其中变量是第一栏的值)。这一栏的所有结果都是字符串。第三栏展示的是执行对第一栏的这些变量运行变量.构造函数的结果,这一栏的所有结果都是对象。

表2-1 变量的类型检查

变  量

typeof变量

变量.构造函数

{ an: "object" }

object  

Object

[ "an", "array" ]

object   

Array

function() {}  

function   

Function 

"a string"   

string    

String

55            

number   

Number

true         

boolean   

Boolean

new User()   

object 

User

现在你可以用表2-1提供的信息来构造一个通用的类型检查函数。很明显,把变量的构造函数作为对象类型引用恐怕是最不容易犯错的合法类型检查了。严格的类型检查可以帮助你判断,传入函数的参数是否有正确的数量和正确的类型,我们可以在代码清单2-9看到一个这样的例子。

代码清单2-9 一个函数,可以用来严格维护传入函数的所有参数

    // 用一个变量类型列表严格检查一个参数列表

    function strict( types, args ) {

   

        // 保证类型的数量和参数的数量相匹配

        if ( types.length != args.length ) {

            // 否则抛出一个有用的异常

            throw "Invalid number of arguments. Expected " + types.length +

                ", received " + args.length + " instead.";

        }

   

        // 遍历所有的参数,检查它们的类型

        for ( var i = 0; i < args.length; i++ ) {

            if ( args[i].constructor != types[i] ) {

                throw "Invalid argument type. Expected " + types[i].name +

                    ", received " + args[i].constructor.name + " instead.";

            }

        }

    }

   

    // 一个简单的函数,打印用户列表

    function userList( prefix, num, users ) {

        // 保证 prefix 是字符串,num 是数字,users 是数组

        strict( [ String, Number, Array ], arguments );

   

        // 遍历 'num' 个用户

        for ( var i = 0; i < num; i++ ) {

            // 显示每个用户的信息

            print( prefix + ": " + users[i] );

        }

    }

事实上,变量类型的检查和检验参数数组的长度都是简单的概念,但可以用来提供一些可使用的复杂方法,能给开发者和使用者带来更好的体验。下面我们将看看JavaScript中的作用域(scope)的概念,以及如何更好去控制它。

2.1.3 作用域

作用域(scope)是JavaScript中一项让人感到棘手的特性。所有的面向对象编程语言都有某种形式的作用域,不过和把这个概念放在什么上下文中有关。在JavaScript里,作用域是由函数划分的,而不是由块(block)划分(比如while,if和for语句中间)的。这样导致的结果是某些代码不好理解(如果你曾经使用过用块划分域的语言)。代码清单2-10展示了一个例子,是根据函数划分作用域而来的代码。

代码清单2-10 展示JavaScript的变量作用域的例子

    // 设置全局变量 foo,并置为 "test"

    var foo = "test";

   

    // 在 if 块中

    if ( true ) {

        // 将 foo 置为 'new test'

        // 注意:现在还在全局作用域中!

        var foo = "new test";

    }

   

    // 如我们所见,现在 foo 等于 'new test' 了

    alert( foo == "new test" );

   

    // 创建一个会修改变量 foo 的新函数

    function test() {

        var foo = "old test";

    }

   

    // 然而在调用时,foo 只在函数作用域内起作用

    test();

   

    // 这里确认了 foo 还是等于 'new test'

    alert( foo == "new test" );

可以看到,在代码清单2-10中,变量都在全局作用域里。基于浏览器的JavaScript的一个有趣的特性是,所有属于全局作用域的变量其实都是window对象的属性(property)。尽管某些早期版本的Opera和Safari并非如此,但还是可以大致认为浏览器都遵循此规则。代码清单2-11展示了这种全局作用域的出现。

代码清单2-11 JavaScript 中的全局作用域和 window 对象

    // 一个全局作用域下的变量,存储了字符串 'test'

    var test = 'test';

   

    // 你可以发现我们的全局变量和 window 对象的 test 属性是一致的

    alert( window.test == test );

最后,让我们看看当变量缺乏声明时会是什么情况。在代码清单2-12中,对变量foo的赋值是在函数test()的作用域中进行的,然而整个例子里并没有哪个作用域实际声明了这个变量。如果变量没有显式定义,它就是全局定义的,虽然它可能只在这个函数作用域的范围内使用。

代码清单2-12 隐式全局作用域的变量声明

    // 一个设置了 foo 值的函数

    function test() {

        foo = "test";

    }

   

    // 调用此函数以设置 foo 的值

    test();

   

    // 我们发现 foo 现在是在全局作用域下

    alert( window.foo == "test" );

虽然JavaScript中的作用域规则不如块级作用域语言那么严格,但它还是非常强大和功能完备的。尤其是在和下一节讨论的闭包概念一起使用时,JavaScript就能表现出脚本语言的强大本色。

2.1.4 闭包

闭包(closure)意味着内层的函数可以引用存在于包围它的函数内的变量,即使外层函数的执行已经终止。这个特性非常强大和复杂。强烈推荐你访问本节结尾所提及的站点,那里有关于闭包的许多精彩资料。

让我们先来看看闭包的两个简单例子,如代码清单2-13所示。

代码清单2-13 闭包如何使代码更清晰的两个例子

    // 找出 ID 为 'main' 的元素

    var obj = document.getElementById("main");

   

    // 修改它的 border 样式

    obj.style.border = "1px solid red";

   

    // 初始化一个在一秒后执行的回调函数 (callback)

    setTimeout(function(){

        // 它将隐藏此对象

        obj.style.display = 'none';

    }, 1000);

   

    // 一个用于延时显示警告信息的通用函数

    function delayedAlert( msg, time ) {

        // 初始化一个封装的回调函数

        setTimeout(function(){

            // 它将使用包含本函数的外围函数传入的 msg 变量

            alert( msg );

        }, time );

    }

   

    // 用两个参数调用 delayedAlert 函数

    delayedAlert( "Welcome!", 2000 );

第一个对setTimeout的函数调用是新JavaScript开发者常犯错误之处,在新手的程序里容易看到这样的代码:

    setTimeOut("otherFunction()", 1000);

   

    // 或

    setTimeout("otherFunction(" + num + "," + num2 + ")", 1000);

完全可以运用闭包的概念来避免这种糟糕的代码,第一个例子很简单,不过是个在注册后1000毫秒后发生的setTimeout回调函数,执行的时候还引用了obj变量(这肯定是全局变量,因为它连ID都有,ID为main)。第二个函数delayedAlert展示了解决setTimeout混乱的一个办法,并指明可以在函数作用域内使用闭包。

可以发现,在代码里使用这样的简单闭包功能后,原本乱成一锅粥的代码变得清晰多了。

让我们看看用闭包能实现什么有趣的额外作用。在一些函数式程序设计语言里,有一种称为Curry化(currying)的技术。本质上,Curry化是一种通过把多个参数填充到函数体中,实现将函数转换为一个新的经过简化的(使之接受的参数更少)函数的技术。代码清单2-14是Curry化的一个简单的例子,它通过向另外一个函数预填参数而创建了一个新函数。

代码清单2-14 用闭包实现的函数 Curry 化

    // 数字求和函数的函数生成器

    function addGenerator( num ) {

        // 返回一个简单的函数,求两个数字的和,其中第一个数字来自生成器

        return function( toAdd ) {

            return num + toAdd

        };

    }

   

    // addFive 现在包含一个接受单一参数的函数,这个函数能求得 5 加上该参数的和

    var addFive = addGenerator( 5 );

   

    // 这里我们可以看到,在传入参数为 4 时,addFive 函数的结果是 9

    alert( addFive( 4 ) == 9 );

闭包还能解决另一个常见的JavaScript编写问题。JavaScript开发新手经常会留下大量多余的全局变量。而这通常被认为是一个坏习惯,因为这些多余的变量可能会悄悄地影响其他的库,导致怪异问题的出现。通过自执行的匿名函数你可以把所有原本属于全局的变量都隐藏起来,如代码清单2-15所示。

代码清单2-15 使用匿名函数来隐藏全局作用域变量的例子

    // 创建一个新的匿名函数,作为包装

    (function(){

        // 变量原本应该是全局的

        var msg = "Thanks for visiting!";

        

        // 将一个新函数绑定到全局对象

        window.onunload = function(){

            // 这个函数使用了“隐藏”的 msg 变量

            alert( msg );

        };

       

        // 关闭匿名函数并执行之

    })();

最后,让我们看看使用闭包会遇到的一个问题。你应该记得,闭包允许你引用父函数中的变量,但提供的值并非该变量创建时的值,而是在父函数范围内的最终值。你会看到,这样带来的最常见的问题是在for循环中,有一个变量作为循环计数(比如i),在这个循环里创建了新的函数,利用闭包来引用循环的计数器。问题是,在这个新的闭包函数被调用时,它引用的计数器值是其最后一次的赋值(比如数组的最后一个位置),而不是你期望的那个值。代码清单2-16展示了一个使用匿名函数来激发出作用域的例子,用实例证明了上面所期望的闭包效果是可以办到的。

代码清单2-16 使用匿名函数来激发出创建多个使用闭包的函数所需的作用域

    // 一个 ID 为 main 的元素

    var obj = document.getElementById("main");

   

    // 用于绑定的一个数组

    var items = [ "click", "keypress" ];

   

    // 遍历数组的每个成员

    for ( var i = 0; i < items.length; i++ ) {

        // 使用一个自执行的匿名函数来激发出作用域

        (function(){

            // 记住在这个作用域内的值

            var item = items[i];

            // 将一个函数绑定到该元素

            obj[ "on" + item ] = function() {

                // item 引用本 for 循环上下文所属作用域中的一个父变量

                alert( "Thanks for your " + item );

            };

        })();

    }

闭包的概念不容易掌握,我花了大量的时间和精力才完全理解闭包的强大功能。幸运的是,现在已经有一篇精彩的文章解释JavaScript的闭包是如何工作的:Jim Jey的JavaScript Closures一文,可以在http://jibbering.com/faq/faq_notes/closures.html中找到。

最后我们将看看上下文对象(context)的概念,这是构筑JavaScript面向对象功能的基础。

2.1.5 上下文对象

在JavaScript中,你的代码总是有一个上下文对象(代码处在该对象内)。这是面向对象语言的常见特点,但其他语言没有JavaScript发挥得那么极致。

上下文对象是通过this变量体现的,这个变量永远指向当前代码所处的对象中。回忆一下,全局对象其实是window对象的属性。这意味着即使是在全局上下文中,this变量也能指向一个对象。上下文对象可以称为一个强大的工具,在面向对象代码中也是一个必备的工具。代码清单2-17展示了上下文对象的一些简单例子。

代码清单2-17 在上下文对象内使用函数并将其上下文对象切换为另一个变量

    var obj = {

        yes: function(){

            // this == obj

            this.val = true;

        },

        no: function(){

            this.val = false;

        }

    };

   

    // 我们发现 'obj' 对象没有 val 属性

    alert( obj.val == null );

   

    // 执行了 yes 函数后,将 val 属性与 'obj' 对象关联起来

    obj.yes();

    alert( obj.val == true );

   

    // 不过现在把 window.no 指向 obj.no 并执行之

    window.no = obj.no;

    window.no();

   

    // 结果是 obj 对象的 val 不变(因为 no 的上下文已经改变为 window 对象了)

    alert( obj.val == true );

   

    // 而 window 的 val 属性被更新了

    alert( window.val == false );

你可能注意到了,在代码清单2-17中,我们把obj.no变量的上下文对象切换为window变量时,代码变得不好理解了。幸运的是,JavaScript提供了一套方法来让这一过程变得更好理解和实现。代码清单2-18展示了call和apply两个方法,可以用于实现这一功能。

代码清单2-18 修改函数上下文对象的例子

    // 一个设置上下文对象颜色样式的简单函数

    function changeColor( color ) {

        this.style.color = color;

    }

   

    // 在 window 对象中调用此函数会失败,因为 window 对象没有 style 属性

    changeColor( "white" );

   

    // 找出 ID 为 main 的文档

    var main = document.getElementById("main");

   

    // 使用 call 方法将它的颜色置为黑色。call 方法将上下文对象设置为第一个参数,

    // 并将其他参数作为原函数的参数

    changeColor.call( main, "black" );

   

    // 设置 body 元素颜色的函数

    function setBodyColor() {

        // apply 方法将上下文对象设置为第一个参数指定的 body 元素,第二个参数是

        // 传给函数的所有参数的数组

        changeColor.apply( document.body, arguments );

    }

   

    // 将 body 的背景色置为黑色

    setBodyColor( "black" );

虽然现在上下文对象的用途还不那么明显,但在下一节的面向对象的JavaScript后就会显现出来。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论