1

引言:当理解了对象和函数的基本概念,你可能会发现,在JavaScript中有很多原以为理所当然(或盲目接受)的事情开始变得更有意义了。

1.JavaScript对象

大多数面向对象(简称OO)语言都定义了某种基本的Object对象类型,其它所有的对象都源于这个对象类型。

JavaScript的基本对象也是作为其它对象的基础,但是从基本层面上来看,JavaScript的Object对象与其它大部分的OO语言所定义的基本对象很不同。

对象一旦被创建后,它不保存任何数据并且几乎没有什么语义。但是这些有限的语义的确又给予了它很大的潜力。

新的对象由new操作符和与其相配的Object构造器来产生。创建一个对象非常简单:

var shinyAndNew = new Object();    // 还可以更简单

这个新对象能做什么?似乎什么也没有——没有信息,没有复杂的语义,什么也没有,一点也不吸引人,直到我们开始向其添加称为 属性 的东西。

JavaScript Object的实例(即:对象)就是一组属性集,每个属性都由名称和值构成。属性的名称是字符串,属性值可以是任何JavaScript对象,可以是Number、String、Date、Array、基本的Object,也可以是任何其它的JavaScript对象类型(例如函数)。

这意味着Object实例的主要就是用作容器,包含其它对象的已命名集合。进一步,一个对象属性可以是另一个Object实例,这个实例又包含其它自己的属性集,而属性集也可以包含拥有属性的对象,以此类推。只要对我们塑造的数据模型有意义,就可以嵌套至任何层次。

举个栗子:
假设给一辆车(car)添加一个新的属性以保存车辆的所有者(owner)信息。这个属性是另一个JavaScript对象,它包含了一些属性,如所有者姓名和职业:

var owner = new Object();
owner.name = 'Roger Shieh';
owner.occupation = 'Coder';
car.owner = owner;

为了访问嵌套的属性,我们可以编写如下代码:

var ownerName = car.owner.name;

提示:出于解释的目的而创建的所有中间变量都是可以省略的(比如owner)。于是,我们可以利用这一点来使用更加高效和简洁的方式来声明对象及其属性的代码。

我们使用点操作符(英文的句号字符)来引用对象的属性。但是事实证明,有一个更加通用的操作符来执行属性的引用。

通用的属性引用操作符的格式:

object[propertyNameExpression]

其中propertyNameExpression是JavaScript表达式,其求值的结果作为要引用的属性名称的字符串。

例如,下面的3个引用都是等价的:

car.color    // 第1种
car['color']    // 第2种
car['c'+'o'+'l'+'o'+'r']    // 第3种
var p = 'color'; car[p]    // 第4种

对于其名称并非有效的JavaScript标识符的属性来说,使用通用的引用操作符是引用这种属性的唯一方法,例如:

car["a property name that's rather odd!"]

我们通过new操作符来创建新的实例,并且利用独立的赋值语句来为每个属性赋值从而建立对象,这看起来是一件繁琐的事情。而且,这样做既枯燥乏味,又冗长易错,难以在快速检查代码的时候把握对象的结构

幸运的是,我们可以使用更紧凑和更易于阅读的表示法。参考如下语句:

var car = {
    make:'Ford',
    year:2014,
    purchased:new Date(2014,12,3),
    owner:{
        name:'Roger Shieh',
        occupation:'Coder'
    }
};

这种表示法被称为 JSON(JavaScript Object Notaition,JavaScript对象表示法)。大多数页面开发者对JSON的偏爱有加,不喜欢通过多个赋值语句来建立对象的方式。

至此,我们看到了两种保存JavaScript对象的方式:变量和属性

这两种保存引用的方式使用不同的表示法,如下所示:

var aVariable = 'Before I teamed up with you, I led quite a normal life.';

someObject.aProperty = 'You move that line as you see fit for yourself.';

事实上,这个两个语句在执行相同的操作。

任何在顶层作用域中生成的引用都隐式地创建在window实例中。

比如下面的语句,如果是在顶层中(也就是在函数的作用域之外)生成的,那么它们都是等价的:

var foo = bar;

window.foo = bar;

foo = bar;

不管使用的是哪种表示法,都会创建一个名为foowindow属性(如果foo属性尚未存在),并且将bar赋值给foo。还要注意,因为bar是非限定的,所以将其假定为window的一个属性。

把顶层作用域认为是window作用域,这可能不会让我们陷入概念上的烦恼,因为任何位于顶层的未限定的引用都被假定为window的属性

重要概念总结如下:

  • JavaScript对象是属性的无序集合;
  • 属性由名称和值组成;
  • 可以使用字面值来声明对象;
  • 顶层变量是window的属性。

2.作为一等公民的函数

当我们谈到JavaScript函数是一等对象时,意味着什么?

在许多传统的OO语言中,对象可以包含数据,还可以拥有方法。在这些语言中,数据和方法通常是不同的概念。

JavaScript可不是这样子。

JavaScript中的函数也被认为是对象,与定义在JavaScript中任何其它的对象类型一样,比如StringNumberDate

和其它对象一样,函数也是通过JavaScript构造器来定义的(在这种情况下是Function),可以对函数进行如下操作:

  • 把函数赋值给变量;
  • 将函数指定为一个对象的属性;
  • 把函数作为参数传递;
  • 把函数作为函数结果返回;
  • 使用字面值来创建函数。

因为在JavaScript语言中对待函数的方式与对待其它对象的方式相同,所以我们说函数是一等对象

与对象的其它实例(例如StringDateNumber)一样,只有在把函数赋值给变量、属性或参数的时候,函数才能被引用。

我们经常通过字面值表示法来表示Number实例,例如下面的语句:

213;

完全有效,但同时完全无用。Number实例用处不大,除非将其赋值给属性或对象,或者将其绑定到参数名称上。否则,我们无法引用散落在内存中的实例。

同样的规则也适用于Function对象的实例。

思考下面的代码:

// 这不是创建了名为doSomethingWonderful的函数吗?
function doSomethingWonderful(){
    alert('does something wonderful'); 
}

尽管这种表示法可能看起来很熟悉,而且被广泛用来创建顶层函数,但它与通过var来创建window属性使用的是相同的语法。

function关键字自动创建一个Function实例并将其赋值给使用函数“名称”创建的window属性,如下所示:

doSomethingWonderful = function(){
    alert('does something wonderful');
}

如果看起来觉得奇怪,考虑另一个使用完全相同形式的语句,但这次使用Number的字面值:

aWonderfulNumber = 213;

这个语句不足为奇,它与把函数赋值给顶层变量(window属性)的语句如出一辙。

函数字面值表示法:由关键字function与紧接着的被圆括号所包含的参数列表,以及随后的函数主体所组成。

记住,在HTML页面中创建了顶层变量时,会将变量创建为window实例的属性。因此,下面的语句都是等价的:

function hello(){alert("wow!");}

hello = function(){alert("wow!");}

window.hello = function(){alert("wow!")}

理解这一点很重要:和其它对象类型的实例一样,Function实例可以赋值给变量、属性或参数的值。并且就像其它的那些对象类型,无名称无实体的实例毫无用处,只有将它们赋值给变量、属性或参数,这样才能引用它们

2.1 作为回调的函数。

function hello(){alert('Hey,guy!');}

setTimeout(hello, 5000);

当计时器过期时,hello函数会被调用。因为在代码中setTimeout()方法回调了一个函数,所以该函数被称为回调函数。

大部分高级JavaScript程序员可能会觉得这段代码示例很幼稚,因为没有必要创建hello名称。除非要在页面的其它地方调用此函数,否则没有必要创建window的属性hello来暂时存储Function实例,以便将hello作为回调参数来传递。

所以,编写这个片段更简洁的方式是:

setTimeout(function(){alert('Hey,guy!')},5000);

目前为止,示例中所创建额函数要么是顶层函数(也就是顶层的window属性),要么是在函数调用中被赋值给参数。我们也可以将Function实例赋值给对象的属性,此时事情才真正变得有趣起来。

2.2 this到底是什么

OO语言自动提供了从方法中引用对象当前实例的办法。在Java和C++这样的语言中,this参数指向当前实例。

JavaScript实现中的this和其它OO语言中的this的差异体现在几个重要的方面。

在JavaScript中函数是一等对象,它们不被声明为任何东西的一部分,而this所引用的对象(称为函数上下文)并不是由声明函数的方式决定的,而是由调用函数的方式决定的。

在默认情况下,函数调用的上下文(this)是对象,其属性包含用于调用该函数的引用。

顶层函数是window的属性,因此当将其作为顶层函数来调用时,其函数上下文就是window对象。

尽管这可能是常见的隐式行为,但是JavaScript提供的办法可以显式地控制函数上下文设置为任何想要的内容。通过Function的方法call()apply()来调用函数,就可以将函数上下文设置为任何想要的内容(是的,作为一等对象,函数甚至拥有Function构造器定义的那些方法)。

提示:使用call()方法来调用函数,指定第一个参数作为函数上下文的对象,而其余参数称为被调用函数的参数;apply方法的工作方式与此类似,不同的是,它的第二个参数应该成为被调用函数参数的对象数组。

下面的代码将说明函数上下文的值取决于调用函数的方式

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>What's this</title>
    <script>
        var o1 = {handle:'o1'};
        var o2 = {handle:'o2'};
        var o3 = {handle:'o3'};
        window.handle = 'window';

        function whoAmI(){
            return this.handle;
        }
        o1.identifyMe = whoAmI;

        alert(whoAmI());    // 返回window
        alert(o1.identifyMe());    // 返回o1
        alert(whoAmI.call(o2));    // 返回o2
        alert(whoAmI.apply(o3));   // 返回o3
    </script>
</head>
<body>
</body>
</html>

图片描述
图片描述
图片描述
图片描述

从上面的示例页面表明了:函数上下文是基于每个调用来决定的,可以使用任何对象作为函数上下文来调用单个函数。因此,“函数是对象的方法”这个说法是绝对不正确的

更为准确的表述应该为:

当对象o充当函数f的调用函数上下文时,函数f就充当了对象o的方法。

既然已经理解了函数如何充当对象的方法,下面就来把注意力转移到另一个高级的函数话题——闭包。

2.3 闭包

闭包就是Function实例,它结合了来自环境的(函数执行所必需的)局部变量。

在声明函数时,可以在声明函数时引用函数作用域内任何变量。对于任何有技术背景的开发者来说,这是理所当然的事情。但是使用闭包时,即使在函数声明之后,已经超出函数作用域(也就是关闭了函数声明)的情况下,该函数仍然带有这些变量。

阅读下面的栗子:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Closure Example</title>
    <script src="http://libs.baidu.com/jquery/1.10.2/jquery.min.js"></script>
    <script>
        $(function(){
            var local = 1;
            window.setInterval(function(){
                $('#display').append('<div>At '+new Date()+' laocal= '+local+'</div>');
                local++;
            }, 3000);
        });
    </script>
</head>
<body>
    <div id="display"></div>
</body>
</html>

在运行这个页面时,如果我们不熟悉闭包去查看这段代码,我们会觉得有点问题,可能会猜测:因为回调会在页面加载后3秒触发(在就绪处理器执行完毕很久以后),所以local的值在回调函数的执行期间是未定义的。毕竟,local声明所在的块在就绪处理器执行完毕时超出了作用域,对吧?

然而,当我们运行时发现它能正常运行!

图片描述

闭包允许回调访问环境中的值,即使该环境已经超出了作用域。

当就绪处理器执行完毕,local声明所在的块超出了作用域,但是函数声明所创建的闭包(包括local变量)在函数的生命周期内都保持在作用域中。

注意:在JavaScript中的闭包,其创建方式都是隐式的,而不像其它一些支持闭包的语言那样需要显示的语法。这是一把双刃剑,一方面使得创建闭包很容易,但另一个方面这使得很难在代码中发现闭包。

无意创建的闭包可能会带来意料之外的结果。例如,循环引用可能导致内存泄露。内存泄露的典型示例就是创建后引用闭包中变量的DOM元素,这会阻止那些变量的回收。

闭包的另一重要特征是:函数上下文绝不会被包含为闭包的一部分

阅读下面这段代码:

this.id = `someID`;
$('*').each(function(){
    alert(this id);
});    // 这段代码不会按照我们期望的方式执行

如果需要访问在外部函数中作为函数上下文的对象,可以采用通常的习惯用法:在局部变量中创建this引用的副本,这个副本将会被包含在闭包中

于是,考虑如下改变:

this.id = 'someID';
var outer = this;
$('*').each(function(){
    alert(this id);
});

局部变量outer被赋值为外部函数的函数上下文的引用,从而成为闭包的一部分,可以在回调函数中访问此变量。改变后的代码现在会弹出显示字符串someID,包装集($('*'))中有多少个元素就弹出多少次。

当使用jQuery命令(这些命令利用异步的回调函数)来创建优雅的代码时,我们将会发现闭包是必不可少的,尤其是在Ajax请求和事件处理领域。


参考资料


omgdog
2.4k 声望332 粉丝

科科。