JavaScript面向对象精要(txt+pdf+epub+mobi电子书下载)


发布时间:2020-06-08 14:46:35

点击下载

作者:[美]尼古拉斯(Nicholas C.Zakas)

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

JavaScript面向对象精要

JavaScript面向对象精要试读:

版权信息书名:JavaScript面向对象精要作者:[美]尼古拉斯(Nicholas C.Zakas)排版:昷一出版社:人民邮电出版社出版时间:2014-04-01ISBN:9787115383846本书由人民邮电出版社授权北京当当科文电子商务有限公司制作与发行。— · 版权所有 侵权必究 · —第1章原始类型和引用类型

大多数开发者在使用Java或C#等基于类的语言的过程中学会了面向对象编程。由于JavaScript没有对类的正式支持,这些开发者在学习JavaScript时往往会迷失方向。JavaScript不需要在开头就定义好各种类,你可以在写代码的过程中根据需要创建数据结构。由于JavaScript缺少类,也就缺少用于对类进行分组的包。在Java中,包和类的名字不仅定义了对象的类型,也在工程中列出文件和目录的层次结构,JavaScript编程就好像从一块空白石板开始:你可以在上面组织任何你想要的东西。些开发者选择模仿其他语言的结构,也有一些人则利用JavaScript的灵活性来创建一些全新的东西。对没有掌握JavaScript的人来说,这种选择的自由令人崩溃,然而一旦你熟悉了它,你会发现JavaScript是一种无比灵活的语言,可以很轻松地适应你的编程习惯。

为了便于开发者从传统的面向对象语言过渡,JavaScript把对象作为语言的中心。几乎所有JavaScript的数据要么是一个对象要么从对象中获取。其实就连函数在JavaScript中也被视为对象,这使得它们成为JavaScript的一等公民。

使用和理解对象是理解整个JavaScript的关键。你可以在任何时候创建对象,在任何时候给对象添加、删除属性。JavaScript对象是如此灵活,可以创造出其他语言不具有的独特而有趣的模式。

本章致力于鉴别和使用两种JavaScript基本数据类型:原始类型和引用类型。虽然两者都通过对象进行访问,但是理解它们行为之间的区别是非常重要的。1.1 什么是类型

JavaScript虽然没有类的概念,但依然存在两种类型:原始类型和引用类型。原始类型保存为简单数据值。引用类型则保存为对象,其本质是指向内存位置的引用。

为了让开发者能够把原始类型和引用类型按相同方式处理,JavaScript花费了很大努力来保证语言的一致性。

其他编程语言用栈储存原始类型,用堆储存引用类型,JavaScript则完全不同:它使用一个变量对象追踪变量的生存期。原始值被直接保存在变量对象内,而引用值则作为一个指针保存在变量对象内,该指针指向实际对象在内存中的存储位置。虽然看上去原始值和引用值一样,但是它们还是有区别的,本章稍后会介绍。

当然,原始类型和引用类型还有其他区别。1.2 原始类型

原始类型代表照原样保存的一些简单数据,如true和25。JavaScript共有5种原始类型,如下。

Boolean  布尔,值为true或false

Number  数字,值为任何整型或浮点数值

String   字符串,值为由单引号或双引号括出的单个字符或连续字符(JavaScript不区分字符类型)

Null    空类型,该原始类型仅有一个值:null

Undefined 未定义,该原始类型仅有一个值:undefined(undefined会被赋给一个还没有初始化的变量)

前3种类型(boolean,number和string)表现的行为类似,而后2种(null和undefined)则有一点区别,本章后面将会讨论。所有原始类型的值都有字面形式。字面形式是不被保存在变量中的值,如硬编码的姓名或价格。下面是每种类型使用字面形式的例子。// stringsvar name = "Nicholas";var selection = "a";// numbersvar count = 25;var cost = 1.51;// booleanvar found = true;// nullvar object = null;// undefinedvar flag = undefined;var ref;  // assigned undefined automatically

JavaScript和许多其他语言一样,原始类型的变量直接保存原始值(而不是一个指向对象的指针)。当你将原始值赋给一个变量时,该值将被复制到变量中。也就是说,如果你使一个变量等于另一个时,每个变量有它自己的一份数据拷贝。例如,var color1 = "red";var color2 = color1;

这里,color1被赋值为“red”。变量color2被赋予color1的值,这样变量color2中就保存了“red”。虽然color1和color2具有同样的值,但是两者毫无关联,改变color1的值不会影响color2,反之亦然。这是因为存在两个不同的储存地址,每个变量拥有一个。图1-1展示了这段代码的变量对象。图1-1 变量对象

因为每个含有原始值的变量使用自己的存储空间,一个变量的改变不会影响到其他变量。例如,var color1 = "red";var color2 = color1;console.log(color1);  // "red"console.log(color2);  // "red"color1 = "blue";console.log(color1);  // "blue"console.log(color2);  // "red"

在这段代码中,color1被改为“blue”,而color2还保有原来的值“red”。1.2.1 鉴别原始类型

鉴别原始类型的最佳方法是使用typeof操作符。它可以被用在任何变量上,并返回一个说明数据类型的字符串。Typeof操作符可用于字符串、数字、布尔和未定义类型。下面是typeof对不同原始类型的输出。console.log(typeof "Nicholas");  // "string"console.log(typeof 10);       // "number"console.log(typeof 5.1);    // "number"console.log(typeof true);    // "boolean"console.log(typeof undefined); // "undefined"

正如我们所期望的,对于字符串,typeof将返回“string”,对于数字将返回“number”(无论整型还是浮点数),对于布尔类型将返回“Boolean”,对于未定义类型将则返回“undefined”。

至于空类型则有一些棘手。

下面那行代码的运行结果困扰了很多开发者。console.log(typeof null);   // "object"

当你运行typeof null时,结果是“object”。但这是为什么呢?(其实这已经被设计和维护JavaScript的委员会TC39认定是一个错误。在逻辑上,你可以认为null是一个空的对象指针,所以结果为“object”,但这还是很令人困惑。)

判断一个值是否为空类型的最佳方法是直接和null比较,如下。console.log(value === null);   // true or false非强制转换比较注意这段代码使用了三等号操作符(===)而不是双等

号。原因是三等号在进行比较时不会将变量强制转换为另一

种类型。为了理解这点,请看下面的例子。

console.log("5" == 5);       // true

console.log("5" === 5);       // false

console.log(undefined == null);   // true

console.log(undefined === null);   // false当你使用双等号进行比较时,双等号操作符会在比较之

前把字符串转换成数字,因此认为字符串“5”和数字5相

等。三等号操作符认为这两个值的类型不同,因此不相等。

同样原因,当你比较undefined和null时,双等号认为它们相

等而三等号认为不相等。当你试图鉴别null时,使用三等号

才能让你正确鉴别出类型。1.2.2 原始方法

虽然字符串、数字和布尔是原始类型,但是它们也拥有方法(null和undefined没有方法)。特别是字符串有很多方法,可以帮助你更好地使用它们。例如,var name = "Nicholas";var lowercaseName = name.toLowerCase();   // convert to lowercasevar firstLetter = name.charAt(0);      // get first charactervar middleOfName = name.substring(2, 5);  // get characters 2-4var count = 10;var fixedCount = count.toFixed(2);   // convert to "10.00"var hexCount = count.toString(16);   // convert to "a"var flag = true;var stringFlag = flag.toString();      // convert to "true"注意:

尽管原始类型拥有方法,但它们不是对象。JavaScript使它们看上去像对象一样,以此来提供语言上的一致性体验,你会在本章后面看到这点。1.3 引用类型

引用类型是指JavaScript中的对象,同时也是你在该语言中能找到的最接近类的东西。引用值是引用类型的实例,也是对象的同义词(本章后面将用对象指代引用值)。对象是属性的无序列表。属性包含键(始终是字符串)和值。如果一个属性的值是函数,它就被称为方法。JavaScript中函数其实是引用值,除了函数可以运行以外,一个包含数组的属性和一个包含函数的属性没有什么区别。

当然,在使用对象前,你必须先创建它们。1.3.1 创建对象

有时候,把JavaScript对象想象成图1-2中的哈希表可以帮助你更好地理解对象结构。图1-2 对象结构

JavaScript有好几种方法可以创建对象,或者说实例化对象。第一种是使用new操作符和构造函数。构造函数就是通过new操作符来创建对象的函数——任何函数都可以是构造函数。根据命名规范,JavaScript中的构造函数用首字母大写来跟非构造函数进行区分。例如下列代码实例化一个通用对象,并将它的引用保存在object中。var object = new Object();

因为引用类型不在变量中直接保存对象,所以本例中的object变量实际上并不包含对象的实例,而是一个指向内存中实际对象所在位置的指针(或者说引用)。这是对象和原始值之间的一个基本差别,原始值是直接保存在变量中的。

当你将一个对象赋值给变量时,实际是赋值给这个变量一个指针。这意味着,将一个变量赋值给另一个变量时,两个变量各获得了一份指针的拷贝,指向内存中的同一个对象。例如,var object1 = new Object();var object2 = object1;

这段代码先用new创建了一个对象并将其引用保存在object1中。然后将object1的值赋值给object2。两个变量都指向第一行被创建的那个对象实例,如图1-3所示。图1-3 两个变量指向同一个对象1.3.2 对象引用解除

JavaScript语言有垃圾收集的功能,因此当你使用引用类型时无需担心内存分配。但最好在不使用对象时将其引用解除,让垃圾收集器对那块内存进行释放。解除引用的最佳手段是将对象变量置为null。var object1 = new Object();// do somethingobject1 = null;  // dereference

这里,对象object1被创建然后使用,最后设置为null。当内存中的对象不再被引用后,垃圾收集器会把那块内存挪作它用(在那些使用几百万对象的巨型程序里,对象引用解除尤其重要)。1.3.3 添加删除属性

在JavaScript中,对象另一个有趣的方面是你可以随时添加和删除其属性。例如,var object1 = new Object();var object2 = object1;object1.myCustomProperty = "Awesome!";console.log(object2.myCustomProperty);  // "Awesome!"

这里,object1上增加了myCustomProperty属性,值为“Awesome!”。该属性也可以被object2访问,因为object1和object2指向同一个对象。注意:

本例演示了JavaScript的一个独特的方面:可以随时修改对象,即使并没有在开始时定义它们。同时,你将会在本书后续内容中看到还存在很多方法阻止此类修改。

除了通用对象引用类型以外,JavaScript还有其他一些内建类型任你使用。1.4 内建类型实例化

你已经见过如何用new Object()创建和使用通用对象。Object类型只是JavaScript提供的少量内建引用类型之一。其他内建类型各有它们的特殊用途,可在任何时候被实例化。

这些内建类型如下。

Array   数组类型,以数字为索引的一组值的有序列表

Date   日期和时间类型

Error   运行期错误类型(还有一些更特别的错误的子类型)

Function 函数类型

Object  通用对象类型

RegExp  正则表达式类型

可以用new来实例化每一个内建引用类型,如下。var items = new Array();var now = new Date();var error = new Error("Something bad happened.");var func = new Function("console.log('Hi');");var object = new Object();var re = new RegExp("\\d+");1.4.1 字面形式

内建引用类型有字面形式。字面形式允许你在不需要使用new操作符和构造函数显式创建对象的情况下生成引用值(你曾在本章前面见过原始类型的字面形式,包括字符串、数字、布尔、空类型和未定义)。1.4.2 对象和数组字面形式

要用对象字面形式创建对象,可以在大括号内定义一个新对象及其属性。属性的组成包括一个标识符或字符串、一个冒号以及一个值。多个属性之间用逗号分隔。例如,var book = { name: "The Principles of Object-Oriented JavaScript", year: 2014};

属性名字也可以用字符串表示,特别是当你希望名字中包含空格或其他特殊字符时。var book = { "name": "The Principles of Object-Oriented JavaScript", "year": 2014};

本例等价于前例,仅在语法上有所区别。下例是另一种等价写法。var book = new Object();book.name = "The Principles of Object-Oriented JavaScript";book.year = 2014;

上述3例的结果是一致的:一个具有两个属性的对象。写法完全取决于你。注意:

虽然使用字面形式并没有调用new Object(),但是JavaScript引擎背后做的工作和new Object()一样,除了没有调用构造函数。其他引用类型的字面形式也是如此。

定义数组的字面形式是在中括号内用逗号区分的任意数量的值。例如,var colors = [ "red", "blue", "green" ];console.log(colors[0]);  // "red"

这段代码等价于:var colors = new Array("red", "blue", "green")console.log(colors[0]);  // "red"1.4.3 函数字面形式

基本上都要用字面形式来定义函数。考虑到在可维护性、易读性和调试上的巨大挑战,通常不会有人使用函数的构造函数,因此很少看到用字符串表示的代码而不是实际的代码。

使用字面形式创建函数更方便也更不容易出错,如下例。function reflect(value) { return value;}// is the same asvar reflect = new Function("value", "return value;");

这段代码定义了reflect()函数,它的作用是将任何传给它的参数返回。即使是这样一个简单的例子,使用字面形式都比构造函数的形式方便和易读。另外,用构造函数创建的函数没什么好的调试方法:JavaScript调试器认不出这些函数,它们在程序里就好像黑盒一样。1.4.4 正则表达式字面形式

JavaScript允许用字面形式而不是使用RegExp构造函数定义正则表达式。它们看上去类似Perl中的正则表达式:模式被包含在两个“/”之间,第二个“/”后是由单字符表示的额外选项。例如,var numbers = /\d+/g;// is the same asvar numbers = new RegExp("\\d+", "g");

使用字面形式比较方便的一个原因是你不需要担心字符串中的转义字符。如果使用RegExp构造函数,传入模式的参数是一个字符串,你需要对任何反斜杠进行转义(这就是为什么字面形式使用“\d”而构造函数使用“\\”d的原因)。在JavaScript中,除非需要通过一个或多个字符串动态构造正则表达式,否则都建议使用字面形式而不是构造函数。

总之,除了函数,对内建类型没什么正确或错误的实例化方法。很多开发者喜欢字面形式,另一些则喜欢用构造函数。你可以选择能令你觉得更舒服的那种。1.5 访问属性

属性是对象中保存的名字和值的配对。点号是JavaScript中访问属性的最通用做法(就跟许多面向对象语言一样),不过也可以用中括号访问JavaScript对象的属性。

例如,下面的代码使用点号。var array = [];array.push(12345);

也可以如下例用中括号,方法的名字现在由中括号中的字符串表示。var array = [];array["push"](12345);

在需要动态决定访问哪个属性时,这个语法特别有用。例如下例的中括号允许你用变量而不是字符串字面形式来指定访问的属性。var array = [];var method = "push";array[method](12345);

在这段代码中,变量method的值是“push”,因此在array上调用了push()方法。这种能力极其有用,你会在本书中随处看到这种用法。记住一点:除了语法不同,在性能或其他方面点号和中括号都大致相同,唯一区别在于中括号允许你在属性名字上使用特殊字符。开发者通常认为点号更易读,所以你更多地看到点号而不是中括号。1.6 鉴别引用类型

函数是最容易鉴别的引用类型,因为对函数使用typeof操作符时,返回值是“function”。function reflect(value) { return value;}console.log(typeof reflect);  // "function"

对其他引用类型的鉴别则较为棘手,因为对于所有非函数的引用类型,typeof返回“object”。在处理很多不同类型的时候这帮不上什么忙。为了更方便地鉴别引用类型,可以使用JavaScript的instanceof操作符。

instanceof操作符以一个对象和一个构造函数为参数。如果对象是构造函数所指定的类型的一个实例,instanceof返回true;否则返回false,如下例。var items = [];var object = {};function reflect(value) { return value;}console.log(items instanceof Array);    // trueconsole.log(object instanceof Object);   // trueconsole.log(reflect instanceof Function); // true

本例用instanceof和构造函数测试了几个值,它们真正的类型都被正确鉴别出来(即使该构造函数并没有被用于创建该变量)。

instanceof操作符可鉴别继承类型。这意味着所有对象都是Object的实例,因为所有引用类型都继承自Object。

作为示范,下列代码用instanceof检查了之前那3种引用。var items = [];var object = {};function reflect(value) { return value;}console.log(items instanceof Array);    // trueconsole.log(items instanceof Object);    // trueconsole.log(object instanceof Object);   // trueconsole.log(object instanceof Array);    // falseconsole.log(reflect instanceof Function); // trueconsole.log(reflect instanceof Object);   // true

每种引用类型的对象都被正确鉴别为Object的实例。1.7 鉴别数组

虽然instanceof可以鉴别数组,但是有一个例外会影响网页开发者:JavaScript的值可以在同一个网页的不同框架之间传来传去。当你试图鉴别一个引用值的类型时,这就有可能成为一个问题,因为每一个页面拥有它自己的全局上下文——Object、Array以及其他内建类型的版本。结果,当你把一个数组从一个框架传到另一个框架时,instanceof就无法识别它,因为那个数组是来自不同框架的Array的实例。

为了解决这个问题,ECMAScript 5引入了Array.isArray()来明确鉴别一个值是否为Array的实例,无论该值来自哪里,该方法对来自任何上下文的数组都返回true。如果你的环境兼容ECMAScript 5,Array.isArray()是鉴别数组的最佳方法。var items = [];console.log(Array.isArray(items));  // true

大多数环境都在浏览器和Node.js中支持Array.isArray()方法。IE8或更早的版本不支持该方法。1.8 原始封装类型

JavaScript中一个最让人困惑的部分可能就是原始封装类型的概念。原始封装类型共有3种(String、Number和Boolean)。这些特殊引用类型的存在使得原始类型用起来和对象一样方便。(如果你不得不用独特的语法或切换为基于过程的编程方式来获取一个子字符串,那就太让人困惑啦)。

当读取字符串、数字或布尔值时,原始封装类型将被自动创建。例如,下列代码第一行,一个原始字符串的值被赋给name。第二行代码把name当成一个对象,使用点号调用了charAt方法。var name = "Nicholas";var firstChar = name.charAt(0);console.log(firstChar);   // "N"

这是在背后发生的事情如下。// what the JavaScript engine doesvar name = "Nicholas";var temp = new String(name);var firstChar = temp.charAt(0);temp = null;console.log(firstChar);   // "N"

由于第二行把字符串当成对象使用,JavaScript引擎创建了一个字符串的实体让charAt(0)可以工作。字符串对象的存在仅用于该语句并在随后被销毁(一种称为自动打包的过程)。为了测试这一点,试着给字符串添加一个属性看看它是不是对象。var name = "Nicholas";name.last = "Zakas";console.log(name.last);   // undefined

这段代码试图给字符串name添加last属性。代码运行时没有错误,但是属性却消失了。到底发生了什么?你可以在任何时候给一个真的对象添加属性,属性会保留至你手动删除它们。原始封装类型的属性会消失是因为被添加属性的对象立刻就被销毁了。

下面是在JavaScript引擎中实际发生的事情。// what the JavaScript engine doesvar name = "Nicholas";var temp = new String(name);temp.last = "Zakas";temp = null;        // temporary object destroyedvar temp = new String(name);console.log(temp.last);   // undefinedtemp = null;

实际上是在一个立刻就被销毁的临时对象上而不是字符串上添加了新的属性。之后当你试图访问该属性时,另一个不同的临时对象被创建,而新属性并不存在。虽然原始封装类型会被自动创建,在这些值上进行instanceof检查对应类型的返回值却都是false。var name = "Nicholas";var count = 10;var found = false;console.log(name instanceof String);   // falseconsole.log(count instanceof Number);   // falseconsole.log(found instanceof Boolean);  // false

这是因为临时对象仅在值被读取时创建。instanceof操作符并没有真的读取任何东西,也就没有临时对象的创建,于是它告诉我们这些值并不属于原始封装类型。

你也可以手动创建原始封装类型,但有某些副作用。var name = new String("Nicholas");var count = new Number(10);var found = new Boolean(false);console.log(typeof name);    // "object"console.log(typeof count);   // "object"console.log(typeof found);   // "object"

如你所见,手动创建原始封装类型实际会创建出一个object,这意味着typeof无法鉴别出你实际保存的数据的类型。

另外,使用String、Number和Boolean对象和使用原始值有一定区别。例如,下列代码使用了Boolean对象,对象的值是false,但console.log(“Found”)依然会被执行。这是因为一个对象在条件判断语句中总被认为是true,无论该对象的值是不是等于false。var found = new Boolean(false);if (found) { console.log("Found");   // this executes}

手工创建的原始封装类型在其他地方也很容易让人误解,在大多数情况下都只会导致错误。所以,除非有特殊情况,你应该避免这么做。1.9 总结

JavaScript中虽然没有类,但是有类型。每个变量或数据都有一个对应的原始类型或引用类型。5种原始类型(字符串、数字、布尔、空类型以及未定义)的值会被直接保存在变量对象中。除了空类型,都可以用typeof来鉴别。空类型必须直接跟null进行比较才能鉴别。

引用类型是JavaScript中最接近类的东西,而对象则是引用类型的实例。可以用new操作符或字面形式创建新对象。通常可以用点号访问属性和方法,也可以用中括号。函数在JavaScript中也是对象,可以用typeof鉴别它们。至于其他引用类型,你应该用instanceof和一个构造函数来鉴别。

为了让原始类型看上去更像引用类型,JavaScript提供了3种原始封装类型:String、Number和Boolean。JavaScript会在背后创建这些对象使得你能够像使用普通对象那样使用原始值,但这些临时对象在使用它们的语句结束时就立刻被销毁。虽然你也可以自己创建原始封装类型的实例,但是它们太容易令人误解,所以最好别这么干。第2章函数

我们已经在第1章讨论过,在JavaScript中,函数其实就是对象。使函数不同于其他对象的决定性特点是函数存在一个被称为[[Call]]的内部属性。内部属性无法通过代码访问而是定义了代码执行时的行为。ECMAScript为JavaScript的对象定义了多种内部属性,这些内部属性都用双重中括号来标注。

[[Call]]属性是函数独有的,表明该对象可以被执行。由于仅函数拥有该属性,ECMAScript定义typeof操作符对任何具有[[Call]]属性的对象返回“function”。这在过去曾经导致一些问题,因为某些浏览器曾经在正则表达式中包含[[Call]]属性,导致后者被错误鉴别为函数。现在,所有的浏览器行为都一致,typeof不会再将正则表达式鉴别为函数了。

本章讨论在JavaScript中定义和执行函数的各种方法。由于函数是对象,它们的行为和其他语言中函数的行为不同,理解函数的行为是理解JavaScript的核心。2.1 声明还是表达式

函数具有两种字面形式。第一种是函数声明,以function关键字开头,后面跟着函数的名字。函数的内容放在大括号内,例如,下面就是函数声明。function add(num1, num2) {   return num1 + num2;}

第二种形式是函数表达式,function关键字后面不需要加上函数的名字。这种函数被称为匿名函数,因为函数对象本身没有名字。取而代之的函数表达式通常会被一个变量或属性引用,下面就是函数表达。var add = function(num1, num2) {   return num1 + num2;};

这段代码实际上将一个函数作为值赋值给变量add。除了没有函数名并在最后多了一个分号以外,函数表达式几乎和函数声明完全一样。函数表达式的赋值通常在最后有一个分号,就如同其他对象的赋值一样。

虽然这两种形式颇为相似,但是它们有一个非常重要的区别。函数声明会被提升至上下文(要么是该函数被声明时所在的函数的范围,要么是全局范围)的顶部。这意味着你可以先使用函数后声明它们。例如,var result = add(5, 5);function add(num1, num2) {   return num1 + num2;}

这段代码看上去似乎会造成错误,但实际上可以工作。那是因为JavaScript引擎将函数声明提升至顶部来执行,就好像它被写成如下形式。// how the JavaScript engine interprets the codefunction add(num1, num2) {return num1 + num2;}var result = add(5, 5);

JavaScript能对函数声明进行提升,这是因为引擎提前知道了函数的名字。而函数表达式仅能通过变量引用,因此无法提升。所以下面这段代码会导致错误。// eror!var result = add(5, 5);var add = function(num1, num2) {return num1 + num2;};

只要你始终在使用函数之前定义它们,你就可以随意使用函数声明或表达式。2.2 函数就是值

函数是JavaScript的一大重点,你可以像使用对象一样使用函数。也可以将它们赋给变量,在对象中添加它们,将它们当成参数传递给别的函数,或从别的函数中返回。基本上只要是可以使用其他引用值的地方,你就可以使用函数。这使得JavaScript的函数威力无穷。考虑下面的例子。function sayHi() {  console.log("Hi!");}sayHi();    // outputs "Hi!"var sayHi2 = sayHi;sayHi2();    // outputs "Hi!"

这段代码首先有一个函数声明sayHi。然后有一个变量sayHi2被创建并被赋予sayHi的值。sayHi和sayHi2现在指向同一个函数,两者都可以被执行,并具有相同结果。为了更好地理解这点,让我们来看一下用Function构造函数重写的具有相同功能的代码。var sayHi = new Function("console.log(\"Hi!\");");sayHi();    // outputs "Hi!"var sayHi2 = sayHi;sayHi2();    // outputs "Hi!"

Function构造函数更加清楚地表明sayHi能够像其他对象一样被传来传去。只要你记住函数就是对象,很多行为就变得容易理解了。

例如,你可以将函数当成参数传递给其他的函数。JavaScript数组的sort()方法接受一个比较函数作为可选参数。每当数组中两个值需要进行比较时都会调用比较函数。如果第一个值小于第二个,比较函数返回一个负数。如果第一个值大于第二个,比较函数返回一个正数。如果两个值相等,函数返回0。

在默认情况下,sort()将数组中每个对象转换成字符串然后进行比较。这意味着,你无法在不指定比较函数的情况下为数字的数组精确排序。例如,var numbers = [ 1, 5, 8, 4, 7, 10, 2, 6 ];numbers.sort(function(first, second) {  return first - second;});console.log(numbers);  // "[1, 2, 4, 5, 6, 7, 8, 10]"numbers.sort();console.log(numbers);  // "[1, 10, 2, 4, 5, 6, 7, 8]"

在本例,被传递给sort()的比较函数其实是一个函数表达式。请注意它没有名字,仅作为引用被传递给另一个函数(这使得它成为匿名函数)。比较函数对两个值进行相减以返回正确的结果。

作为对比,第二次sort()不使用比较函数。结果和预期的不一样,1后面跟着的是10。这是因为默认的比较函数将所有值都转换成字符串进行比较。2.3 参数

JavaScript函数的另一个独特之处在于你可以给函数传递任意数量的参数却不造成错误。那是因为函数参数实际上被保存在一个被称为arguments的类似数组的对象中。如同一个普通的JavaScript数组,arguments可以自由增长来包含任意个数的值,这些值可通过数字索引来引用。arguments的length属性会告诉你目前有多少个值。

arguments对象自动存在于函数中。也就是说,函数的命名参数不过是为了方便,并不真的限制了该函数可接受参数的个数。注意:

arguments对象不是一个数组的实例,其拥有的方法与数组不同,Array.isArray(arguments)永远返回false。

另一方面,JavaScript也没有忽视那些命名参数。函数期望的参数个数保存在函数的length属性中。还记得吗?函数就是对象,所以它可以有属性。Length属性表明了该函数的期望参数个数。了解函数的期望参数个数在JavaScript中是非常重要的,因为给它传递过多或过少的参数都不会抛出错误。

下面是一个简单的使用arguments和函数的期望参数个数的例子。注意实际传入的参数的数量不影响函数的期望参数个数。function reflect(value) {  return value;}console.log(reflect("Hi!"));    // "Hi!"console.log(reflect("Hi!", 25));  // "Hi!"console.log(reflect.length);   // 1reflect = function() {  return arguments[0];};console.log(reflect("Hi!"));    // "Hi!"console.log(reflect("Hi!", 25));  // "Hi!"console.log(reflect.length);   // 0

本例先定义了一个具有单一命名参数的reflect()函数,但是当有两个参数传递给它时没有任何错误发生。由于只有一个命名参数,length属性为1。代码随后重新定义reflect()为无命名参数的函数,它返回传入的第一个参数arguments[0]。这个新版本的函数和前一个版本的输出一模一样,但它的length为0。

因为使用了命名参数,reflect()的第一个实现容易理解(和在别的语言里一样)。使用arguments对象的版本有点让人莫名其妙,因为没有命名参数,你不得不浏览整个函数体来确定是否使用了参数。这就是为什么许多开发者尽可能避免使用arguments的原因。

不过,在某些情况下使用arguments比命名参数更有效。例如,假设你想创建一个函数接受任意数量的参数并返回它们的和。因为你不知道会有多少个参数,所以你无法使用命名参数。在这种情况下,使用arguments是最好的选择。function sum() {  var result = 0,    i = 0,    len = arguments.length;  while (i

sum()函数接受任意数量的参数并在while循环中遍历它们的值来求和。这就和对一个数组中的数字求和一样。由于result初始值为0,该函数就算没有参数也能正常工作。2.4 重载

大多数面向对象语言支持函数重载,它能让一个函数具有多个签名。函数签名由函数的名字、参数的个数及其类型组成。因此,一个函数可以有一个接受一个字符串参数的签名和另一个接受两个数字参数的签名。JavaScript语言根据实际传入的参数决定调用函数的哪个版本。

之前已经提过,JavaScript函数可以接受任意数量的参数且参数类型完全没有限制。这说明JavaScript函数其实根本没有签名,因此也不存在重载。看看当你试图声明两个同名函数会发生什么。function sayMessage(message) {  console.log(message);}function sayMessage() {  console.log("Default message");}sayMessage("Hello!");      // outputs "Default message"

如果这是其他的语言,sayMessage(“Hello!”)就会输出“Hello!”。然而在JavaScript里,当你试图定义多个同名的函数时,只有最后定义的有效,之前的函数声明被完全删除,只使用最后那个。下面,让我们用对象来帮助理解。var sayMessage = new Function("message", "console.log(message);");sayMessage = new Function("console.log(\"Default message\");");sayMessage("Hello!");     // outputs "Default message"

这样看代码,前一个函数为什么不工作就一目了然了。对sayMessage连续赋了两次函数对象,第一个自然就丢失了。

JavaScript函数没有签名这个事实不意味着你不能模仿函数重载。你可以用arguments对象获取传入的参数个数并决定怎么处理。例如,function sayMessage(message) {  if (arguments.length === 0) {    message = "Default message";}  console.log(message);}sayMessage("Hello!");   // outputs "Hello!"

本例中,sayMessage()函数的行为视传入参数的个数而定。如果没有传入参数(arguments.length === 0),那么就使用默认的信息。否则使用第一个传入的参数为信息。和其他语言中的重载相比,这里有更多的人为介入,但是结果是相同的。如果你还想检查不同的数据类型,你可以用typeof和instanceof。注意:

在实际使用中,检查命名参数是否为未定义比依靠arguments.length更常见。2.5 对象方法

第1章中介绍了可以在任何时候给对象添加或删除属性。如果属性的值是函数,则该属性被称为方法。你可以像添加属性那样给对象添加方法。例如,在下面代码中,变量person被赋予了一个对象的字面形式,包含属性name和方法sayName。var person = {  name: "Nicholas",  sayName: function() {    console.log(person.name);  }};person.sayName();     // outputs "Nicholas"

注意定义数据属性和方法的语法完全相同——标识符后面跟着冒号和值。只不过sayName的值正好是一个函数。定义好以后你立刻就能在对象上调用方法person.sayName()。2.5.1 this对象

你可能已经注意到前面例子中一些奇怪之处。sayName()方法直接引用了person.name,在方法和对象间建立了紧耦合。有太多理由证明这是有问题的。首先,如果你改变变量名,你也必须要改变方法中引用的名字。其次,这种紧耦合使得同一个方法很难被不同对象使用。幸好JavaScript对此有一个解决办法。

JavaScript所有的函数作用域内都有一个this对象代表调用该函数的对象。在全局作用域中,this代表全局对象(浏览器里的window)。当一个函数作为对象的方法被调用时,默认this的值等于那个对象。所以你应该在方法内引用this而不是直接引用一个对象。前例代码可以改写如下。var person = {  name: "Nicholas",  sayName: function() {    console.log(this.name);  }};person.sayName(); // outputs "Nicholas"

这段代码和前面的版本输出相同,但是这一次,sayName()引用this而不是person。这意味着你可以轻易改变变量名,甚至是将该函数用在不同对象上。function sayNameForAll() {  console.log(this.name);}var person1 = {  name: "Nicholas",  sayName: sayNameForAll};var person2 = {  name: "Greg",  sayName: sayNameForAll};var name = "Michael";person1.sayName();   // outputs "Nicholas"person2.sayName();   // outputs "Greg"sayNameForAll();    // outputs "Michael"

本例先定义函数sayNameForAll,然后以字面形式创建两个对象以sayNameForAll函数作为sayName方法。函数就是引用值,所以你可以把它们作为属性值赋给任意个对象。当person1调用sayName()时,输出“Nicholas”;person2则输出“Greg”。那是因为this在函数调用时才被设置,所以this.name是正确的。

本例最后部分定义了全局变量name。全局变量被认为是全局对象的属性,所以当直接调用sayNameForAll时输出“Michael”。2.5.2 改变this

在JavaScript中,使用和操作函数中this的能力是良好地面向对象编程的关键。函数会在各种不同上下文中被使用,它们必须到哪都能正常工作。一般this会被自动设置,但是你可以改变它的值来完成不同的目标。有3种函数方法允许你改变this的值。(记住函数是对象,而对象可以有方法,所以函数也有。)1.call()方法

第一个用于操作this的函数方法是call(),它以指定的this值和参数来执行函数。call()的第一个参数指定了函数执行时this的值,其后的所有参数都是需要被传入函数的参数。假设你更新sayNameForAll让它接受一个参数,代码如下。function sayNameForAll(label) {  console.log(label + ":" + this.name);}var person1 = {  name: "Nicholas"};var person2 = {  name: "Greg"};var name = "Michael";sayNameForAll.call(this, "global");     // outputs "global:Michael"sayNameForAll.call(person1, "person1");   // outputs "person1:Nicholas"sayNameForAll.call(person2, "person2");   // outputs "person2:Greg"

在本例中,sayNameForAll接受一个label参数用于输出。然后该函数被调用3次。注意调用函数时在函数名后没有小括号,因为它被作为对象访问而不是被执行的代码。第一次调用使用全局this并传入参数“global”来输出“global:Michael”。之后两次调用分别使用person1和person2。由于使用了call()方法,你不需要将函数加入每个对象——你显式指定了this的值而不是让JavaScript引擎自动指定。2.apply()方法

apply()是你可以用来操作this的第二个函数方法。apply()的工作方式和call()完全一样,但它只接受两个参数:this的值和一个数组或者类似数组的对象,内含需要被传入函数的参数(也就是说你可以把arguments对象作为apply()的第二个参数)。你不需要像使用call()那样一个个指定参数,而是可以轻松传递整个数组给apply()。除此之外,call()和apply()表现得完全一样。下例演示了apply()的用法。function sayNameForAll(label) {  console.log(label + ":" + this.name);}var person1 = {  name: "Nicholas"};var person2 = {  name: "Greg"};var name = "Michael";

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载