JavaScript 高级程序设计 笔记二

第5章 引用类型

引用类型的值(对象)是引用类型的一个示例。在ECMAScript 中,引用类型是一种数据结构,用于将数据和功能组织在一起。

对象是某个特定引用类型的实例。新对象是使用new操作符后跟一个构造函数来创建的。构造函数本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的。

Object 类型

创建Object 实例的方式有两种。第一种是使用new 操作符后跟Object 构造函数。

var person = new Object();
person.name = "Nicholas";
person.age = 29;

另一种方式是使用对象字面量表示法。对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程。

var person = {
  name : "Nicholas",
  age : 29
}

在这个例子中,左边的花括号{表示对象字面量的开始,因为它出现在了表达式上下(expression context)文中。ECMAScript 中的表达式上下文指的是该上下文期待一个值(表达式)。赋值操作符表示后面是一个值,所以左花括号在这里表示一个表达式的开始。同样的花括号,如果出现在一个语句上下文(statement context)中,例如跟在if 语句条件的后面,则表示一个语句块的开始。

在使用对象字面量语法时,属性名也可以使用字符串。如果留空其花括号,则可以定义只包含默认属性和方法的对象。

对象字面量也是向函数传递大量可选参数的首选方式。

访问对象属性时使用的是点表示法。不过,在JavaScript 也可以使用方括号表示法来访问对象的属性。在使用方括号语法时,应该将要访问的属性以字符串的形式放在方括号中。方括号语法的主要优点是可以通过变量来访问属性。如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或保留字,也可以使用方括号表示法。通常,除非必须使用变量来访问属性,否则建议使用点表示法。

Array 类型

ECMAScript 中的数组与其他多数语言中的数组有着相当大的区别。ECMAScript 数组的每一项可以保存任何类型的数据。且ECMAScript 数组的大小是可以动态调整的。

创建数组的基本方式有两种。第一种是使用Array 构造函数,如:

var colors = new Array()

第二种方式是使用数组字面量表示法。数组字面量由一对包含数组项的方括号表示,多个数组项之间以逗号隔开。如:

var colors = ["red","blue","green"];

数组的length 属性很有特点——它不是只读的。因此,通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项。如果将其length 属性设置为大于数组项数的值,则新增的每一项都会取得undefined 值。

检测数组

对于一个网页,或者一个全局作用域而言,使用instanceof 操作符即可:

if (value instanceof Array){
  // 对数组执行某些操作
}

ECMAScript 5 新增了Array.isArray()方法。这个方法的目的是最终确定某个值到底是不是数组,而不管它是在哪个全局执行环境中创建的。用法如下:

if (Array.isArray(value)){
  // 对数组执行某些操作
}

转换方法

调用数组的toString()方法会返回由数组中每个值得字符串形式拼接而成的一个以逗号分隔的字符串。而调用valueOf()返回的还是数组。

toLocaleString()方法经常也会返回与toString()valueOf()方法相同的值,但也不总是如此。

以上三种方法,在默认情况下都会以逗号分隔的字符串的形式返回数组项。而如果使用join()方法,则可以使用不同的分隔符来构建这个字符串。join()方法只接收一个参数,即用作分隔符的字符串,然后返回包含所有数组项的字符串。

var colors = ["red","green","blue"];
alert(colors.join(",")) //red,green,blue
alert(colors.join("||"))  //red||green||blue

如果不给join()方法传入任何值,或者给它传入undefined,则使用逗号作为分隔符。

栈方法

数组可以表现的就像栈一样,后者是一种可以限制插入和删除项的数据结构。栈是一种 LIFO(Last-In-First-Out,后进先出)的数据结构,也就是最新添加的项最早被移除。而栈中项的插入(叫做推入)和移除(叫做弹出),只发生在一个位置——栈的顶部。ECMAScript 为数组专门提供了push()pop()方法,以便实现类似栈的行为。

push()方法可以接受任意数量的参数,把它们逐个添加到数组末尾,并返回修改后数组的长度。而pop()方法则从数组末尾移除最后一项,减少数组的length值,然后返回移除的项。

队列方法

队列数据结构的访问规则是FIFO(First-In-First-Out,先进先出)。队列在列表的末端添加项,从列表的前端移除项。shift()方法可以移除数组中的第一个项并返回该项,同时数组长度减1。结合使用shift()push()方法,可以像使用队列一样使用数组。

unshift()方法能在数组前端添加任意个项并返回新数组的长度。同时使用unshift()pop()方法,可以从相反的方向来模拟队列,即在数组的前端添加项,从数组末端移除项。

重排序方法

reverse()方法会反转数组项的顺序。

在默认情况下,sort()方法按升序排列数组项——即最小的值位于最前面,最大的值排在最后面。为了实现排序,sort()方法会调用每个数组项的toString()转型方法,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值,sort()方法比较的也是字符串。

sort()方法可以接收一个比较函数作为参数,以便指定哪个值位于哪个值前面。比较函数接受两个参数,如果第一个参数应该位于第二个之前则返回一个负数,如果两个参数相等则返回0,如果第一个参数应该位于第二个之后则返回一个正数。

操作方法

concat()方法可以基于当前数组中的所有项创建一个新数组。具体来说,这个方法会先创建当前数组的一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。在没有给concat()方法传递参数的情况下,它只是复制当前数组并返回副本。

slice()能够基于当前数组中的一或多个项创建一个新数组。slice()方法可以接受一或两个参数,即要返回项的起始和结束为止。在只有一个参数的情况下,该方法返回从该参数指定为止开始到当前数组末尾的所有项。 slice()方法不会影响原始数组。

如果slice()方法的参数中有一个负数,则用数组长度加上该数来确定相应的位置。如果结束位置小于起始位置,则返回空数组。

splice()的主要用途是向数组的中部插入项,方法:

  • 删除:可以删除任意数量的项,只需指定2个参数:要删除的第一项的位置和要删除的项数。例如,splice(0,2)会删除数组中的前两项。
  • 插入:可以向指定位置插入任意数量的项,只需提供3个参数:起始位置、0(要删除的项数)和要插入的项。如果要插入多个项,可以再传入第四、第五,以至任意多个项。
  • 替换:可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定3个参数:起始位置、要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。

splice()方法始终都会返回一个数组,该数组包含从原始数组中删除的项。

位置方法

  • indexOf()
  • lastIndexOf()

接收两个参数:要查找的项和表示查找起点位置的索引(可选)。

返回要查找的项在数组中的位置,或者在没找到的情况下返回-1。查找时必须严格相等(===)。

迭代方法

ECMAScript 5 为数组定义了5个迭代方法。每个方法都接收两个参数:要在每一项上运行的函数和运行该函数的作用域对象——影响this 的值(可选)。传入这些方法中的函数会接收三个参数:数组项的值、该项在数组中的位置和数组对象本身。

  • every():对数组中的每一项运行给定函数,如果该函数对每一项都返回true,则返回true
  • filter():对数组中的每一项运行给定函数,返回该函数会返回true 的项组成的数组
  • forEach():对数组中的每一项运行给定函数。这个方法没有返回值
  • map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组
  • some():对数组中的每一项运行给定函数,只要有任一项返回true,则返回true

filter() 示例:

var numbers = [1,2,3,4,5,4,3,2,1];
var filterResult = number.filter(function(item,index,array){
  return (item>2);
});
alert(filterResult);  //[3,4,5,4,3]

map() 示例:

var numbers = [1,2,3,4,5,4,3,2,1];
var mapResult = numbers.map(function(item,index,array){
  return item * 2;
});
alert mapResult;  //[2,4,6,8,10,8,6,4,2]

归并方法

  • reduce()
  • reduceRight()

这两个方法都会迭代数组的所有项,然后构建一个最终返回的值。

这两个方法都接收两个参数:一个在每一项上调用的函数和作为归并基础的初始值(可选)。传给的函数接受4个参数:前一个值、当前值、项的索引和数组对象。这个函数返回的任何值都会作为第一个参数自动传给下一项。

使用reduce() 方法可以执行求数组中所有值之和的操作,例如:

var values = [1,2,3,4,5];
var sum = values.reduce(function(prev,cur,index,array){
  return prev + cur;
});
alert (sum);  //15

Date 类型

Date 类型使用自UTC 1970年1月1日零时开始经过的毫秒数来保存日期。

要创建一个日期对象,使用new 操作符和Date 构造函数即可:

var now = new Date();

在调用Date 构造函数而不传递参数的情况下,新创建的对象自动获得当前日期和时间。

Date.parse()方法接收一个表示日期的字符串参数,然后尝试根据这个字符串返回相应日期的毫秒数。

Date.UTC()方法同样也返回表示日期的毫秒数,参数分别是年份、基于0的月份、月中的那一天、小时数、分钟、秒以及毫秒数。在这些参数中,只有前两个参数是必需的。

ECMAScript 5 添加了Date.now()方法,返回表示调用这个方法时的日期和时间的毫秒数。这个方法简化了使用Date 对象分析代码的工作。例如:

var start = Date.now();
doSomething();
var stop = Date.now();
var result = stop - start;

RegExp 类型

字面量形式:

var expression = / pattern / flags ;

flags:

  • g:表示全局模式global
  • i:表示不区分大小写模式ignoreCase
  • m:表示多行模式multiline

RegExp 构造函数:

var pattern = new RegExp("pattern","flags");

模式中使用的所有元字符都必须转义。正则表达式中的元字符包括:

( [ { \ ^ $ | ) * + . ] }

RegExp 实例属性

  • global:布尔值,表示是否设置了g 标志
  • ignoreCase:布尔值,表示是否设置了i 标志
  • lastIndex:整数,表示开始搜索下一个匹配项的字符位置,从0算起
  • multiline:布尔值,表示是否设置了m 标志
  • source:正则表达式的字符串表示,按照字面量形式而非传入构造函数中的字符串模式返回

RegExp 实例方法

主要方法是exec(),该方法是专门为捕获组而设计的。exec()接受一个参数,即要应用模式的字符串,然后返回包含第一个匹配项信息的数组;或者在没有匹配项的情况下返回null。返回的数组虽然是Array 的实例,但包含两个额外的属性:index 和input。其中,index 表示匹配项在字符串中的位置,而input 表示应用正则表达式的字符串。在数组中,第一项是与整个模式匹配的字符串,其他项是与模式中的捕获组匹配的字符串(如果模式中没有捕获组,则该数组只包含一项)。

对于exec() 方法而言,即使在模式中设置了全局标志g ,它每次也只会返回一个匹配项。在不设置全局标志的情况下,在同一个字符串上多次调用exec() 将始终返回第一个匹配项的信息。而在设置全局标志的情况下,每次调用exec() 则都会在字符串中继续查找新匹配项

方法test()接受一个字符串参数。在模式与该参数匹配的情况下返回true;否则,返回false。此方法经常用在if 语句中。

RegExp 构造函数属性

Function 类型

由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。

使用函数声明语法定义:

function sum(num1,num2){
  return num1 + num2;
}

使用函数表达式定义:

var sum = function(num1,num2){
  return num1 + num2;
};

解析器在执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行任何代码之前可用;至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。

因为ECMAScript 中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样把一个函数传递给另一个函数,而且可以将一个函数作为另一个函数的结果返回。

函数内部属性

  • arguments
  • this

虽然arguments 的主要用途是保存函数参数,但这个对象还有一个名叫callee的属性,该属性时一个指针,指向拥有这个arguments 对象的函数。

阶乘函数示例,使用arguments.callee ,可消除紧密耦合的现象:

function factorial(num){
  if (num<=1){
    return 1;
  } else {
    return num*arguments.callee(num-1);
  }
}

this 引用的是函数执行的环境对象。例如在网页的全局作用域中调用函数时,this 对象引用的就是window。

ECMAScript 5 也规范化了另一个函数对象的属性:caller。这个属性中保存着调用当前函数的函数的引用,如果是在全局作用域中调用当前函数,它的值为null。

函数属性和方法

ECMAScript 中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:length 和 prototype。

length 属性表示函数希望接收的命名参数的个数。

每个函数都包含两个非继承而来的方法:apply() 和call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内this 对象的值。

apply() 方法接收两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是Array 的实例,也可以是arguments 对象。

call() 方法接受的第一个参数this 值和上面相同,变化的是其余参数都直接传递给函数。换句话说,传递给函数的参数必须逐个列举出来。

apply() 和call() 真正强大的地方是能够扩充函数赖以运行的作用域,示例:

window.color = "red";
var o = {color:"blue"};
function sayColor(){
  alert(this.color);
}
sayColor(); //red
sayColor.call(this);  //red
sayColor.call(window);  //red
sayColor.call(o); //blue

基本包装类型

引用类型与基本包装类型的主要区别就是对象的生存期。使用new 操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。而自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。

Number 类型

toFixed() 方法会按照指定的小数位返回数值的字符串表示,例如:

var num = 10;
alert(num.toFixed(2));  //"10.00"

toExponential() 方法返回以指数表示法表示的数值的字符串形式。

var num = 10;
alert(num.toExponentia(1)); //"1.0e+1"

String 类型

String 类型的每个实例都有一个length 属性,表示字符串中包含多个字符。

两个用于访问字符串中特定字符的方法是:charAt() 和 charCodeAt()。

var stringValue = "hello world";
alert(stringValue.charAt(1)); //"e"
alert(stringValue.charCodeAt(1)); //输出"101"
alert(stringValue[1]);  //"e"

concat() 用于将一或多个字符串拼接起来,返回拼接得到的新字符串。实践中更多使用加号操作符(+)

var a = "hello";
var b = a.concat(" world");
var c = a.concat(" world","!");
alert(b); //"hello world"
alert(a); //"hello"
alert(c); //"hello world!"

基于子字符串创建新字符串的方法:

  • slice()
  • substr()
  • substring()

字符串位置方法:

  • indexOf()
  • lastIndexOf()

从一个字符串中搜索给定的子字符串,然后返回子字符串的位置,如果没有找到该子字符串,则返回-1。

trim() 方法:创建一个字符串的副本,删除前置及后缀的所有空格,然后返回结果。

大小写转换方法:

  • toLowerCase()
  • toUpperCase()

字符串中匹配模式的方法:

  • match()
  • search()

替换方法replace():

var text = "cat, bat, sat, fat";
var result = text.replace("at","ond");
alert(result);  //"cond, bat, sat, fat"
result = text.replace(/at/g,"ond");
alert(result);  //"cond, bond, sond, fond"

split() 可以基于指定的分隔符将一个字符串分割成多个子字符串,并将结果放在一个数组中。

fromCharCode() 接收一或多个字符编码,然后将它们转换成一个字符串。从本质上来看,这个方法与实例方法charCodeAt() 执行的是相反的操作。

单体内置对象

在所有代码执行之前,作用域中就已经存在两个内置对象:Global 和Math。在大多数ECMAScript 实现中都不能直接访问Global 对象;不过,Web 浏览器实现了承担该角色的window 对象。全局变量和函数都是Global 对象的属性。Math 对象提供了很多属性和方法,用于辅助完成复杂的数学计算任务。

Global 对象

URI编码方法:

  • encodeURI()
  • encodeURIComponent()
  • decodeURI()
  • decodeURIComponent()

eval() 方法就像是一个完整的ECMAScript 解析器,它只接受一个参数,即要执行的ECMAScript(或JavaScript)字符串。

当解析器发现代码中调用eval() 方法时,它会将传入的参数当做实际的ECMAScript 语句来解析,然后把执行结果插入到原位置。通过eval() 执行的代码被认为是包含该次调用的执行环境的一部分,因此被执行的代码具有与该执行环境相同的作用域链。这意味着通过eval() 执行的代码可以引用在包含环境定义的变量。

使用eval() 时必须谨慎,特别是在它执行用户输入数据的情况下。否则,可能会有恶意用户输入威胁你的站点或应用程序安全的代码(代码注入)。

Math 对象

Math 对象包含的属性大都是数学计算中可能会用到的一些特殊值。

min() 和 max() 方法用于确定一组数值中的最小值和最大值。这两个方法都可以接收任意多个数值参数。

如需要找到数组中的最大或最小值,可以使用apply() 方法

var values = [1,2,3,4,5];
var max = Math.max.apply(Math,values);

舍入方法:

  • Math.ceil() 执行向上舍入
  • Math.floor() 执行向下舍入
  • Math.round() 执行标准舍入(四舍五入)

Math.random() 方法返回大于等于0小于1的一个随机数。套用下面的公式,就可以利用Math.random() 从某个整数范围内随机选择一个值:

值 = Math.floor(Math.random() * 可能值的总数 + 第一个可能的值)

其他方法:

第6章 面向对象的程序设计

ECMAScript 中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。

可以把ECMAScript 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。

每个对象都是基于一个引用类型创建的。

理解对象

属性类型

ECMAScript 有两种属性:数据属性和访问器属性。

数据属性包含一个数据值的位置。在这个位置可以读取和写入。数据属性有4个描述其行为的特性。

  • [[Configurable]]:表示能否通过delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
  • [[Enumerable]]:表示能否通过for-in 循环返回属性。
  • [[Writable]]:表示能否修改属性的值。
  • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined。

要修改属性默认的特性,必须使用ECMAScript 5 的Object.defineProperty() 方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中描述符对象的属性必须是:configurable、enumerable、writable 和value。设置其中的一或多个值,可以修改对应的特性值。

访问器属性不包含数据值。有如下4个特性:

  • [[Configurable]]
  • [[Enuerable]]
  • [[Get]]:在读取属性时调用的函数。默认值为undefined。
  • [[Set]]:在写入属性时调用的函数。默认值为undefined。

访问器属性不能直接定义,必须使用Object.defineProperty() 来定义。

访问器属性的常见使用方式:设置一个属性的值会导致其他属性发生变化。

定义多个属性

由于为对象定义多个属性的可能性很大,ECMAScript 5 又定义了一个Object.defineProperties() 方法。利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。

读取属性的特性

使用ECMAScript 5 的Object.getOwnPropertyDescriptor() 方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurable、enumerable、get和set;如果是数据属性,这个对象的属性有configurable、enumerable、writable和value。

创建对象

用Object 构造函数或对象字面量创建单个对象的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。

工厂模式

工厂模式抽象了创建具体对象的过程。考虑到在ECMAScript 中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,例如:

function createPerson(name, age, job){
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function(){
    alert(this.name);
  };
  return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");

函数createPerson() 能够根据接受的参数来构建一个包含所有必要信息的Person 对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性和一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎么知道一个对象的类型)。

构造函数模式

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function(){
    alert(this.name);
  };
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

Person() 中的代码与createPerson() 有存下下面不同:

  • 没有显式地创建对象
  • 直接将属性和方法赋给了this 对象
  • 没有return 语句

函数名Person 使用的是大写字母P。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。主要是为了区别于ECMAScript 中的其他函数,因为构造函数本身也是函数,只不过可以用来创建对象而已。

要创建Person 的新实例,必须使用new 操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(因此this 就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象

此例的两个对象都有constructor (构造函数)属性,该属性指向Person。

alert(person1.constructor == Person); //true

对象的constructor 属性最初是用来标识对象类型的。还可用instanceof 来检测对象类型。

alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true

可看出通过上例创建的所有对象既是Object 的实例,也是Person 的实例。

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定的类型;而这正是构造函数模式胜过工厂模式的地方。

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new 操作符来调用,那它跟普通函数也不会有什么两样。

构造函数的缺点:每个方法都要在每个实例上重新创建一遍。

原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。示例:

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
  alert(this.name);
};
var person1 = new Person();
person1.sayName();  //"Nicholas"
var person2 = new Person();
person2.sayName();  //"Nicholas"
alert(person1.sayName == person2.sayName);  //true

实例中的[[Prototype]] 这个连接存在于实例与函数的原型对象之间,而不是存在于实例与构造函数之间。实例中的指针仅指向原型,而不指向构造函数。

alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Object.getPrototypeOf(person1) == Person.prototype);  //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具体给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。

当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。使用delete 操作符可以完全删除实例属性,从而让我们能够重新访问原型中的属性。

使用hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在于原型中。这个方法只在给定属性存在于对象实例中时,才会返回ture。

alert(person1.hasOwnProperty("name"));  //false
person1.name = "Greg";
alert(person1.name);  //"Greg"——来自实例
alert(person1.hasOwnProperty("name")) //true

delete person1.name;
alert(person1.name);  //"Nicholas"——来自原型
alert(person1.hasOwnProperty("name"));  //false

in 操作符会在通过对象能够访问给定属性时返回ture,无论该属性存在于实例中还是原型中。

同时使用hasOwnProperty() 方法和in 操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中。

要取得对象上所有可枚举的实例属性,可以使用ECMAScript 5 的Object.keys() 方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

如果想得到所有实例属性,无论是否可枚举,可以使用Object.getOwnPropertyNames() 方法。

为了减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。但需注意constructor

由于在原型中查找值的过程是一次搜索的,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。

原型模式缺点:

  • 所有实例在默认情况下都将取得相同的属性值
  • 若原型中包含引用类型值的属性,会出现所有实例共享的问题

组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长。示例:

function Person(name, age, job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ["Shelby", "Court"];
}
Person.prototype = {
  constructor : Person,
  sayName : function(){
    alert(this.name);
  }
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,court,Van"
alert(person2.friends); //"Shelby,court"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

这种构造函数与原型混成的模式,是目前在ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式

动态原型模式

function Person(name, age, job){
  //属性
  this.name = name;
  this.age = age;
  this.job = job;
  //方法
  if (typeof this.sayName != "function"){
    Person.prototype.sayName = function(){
      alert(this.name);
    };
  }
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

继承

许多OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实现的方法。由于函数没有签名,在ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

原型链

构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

原型链的基本概念为:让原型对象等于另一个类型的实例。此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。

实现原型链有一种基本模式,代码大致如下:

function SuperType(){
  this.property = true;
}
SuperType.prototype.getSuperValue = function(){
  return this.property;
};
function SubType(){
  this.subproperty = false;
}
// 通过重写原型对象,继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
  return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue());  //true

实现继承的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于SuperType 的实例中的所有属性和方法,现在也存在于SubType.prototype 中了。新原型不仅具有作为一个SuperType 的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了SuperType 的原型。

最终的结果是这样的:instance 指向了SubType 的原型,SubType 的原型又指向了SuperType 的原型。

需注意instance.constructor 现在指向的是SuperType。

确定原型和实例的关系:

  • instanceof
  • isPrototypeOf()

给原型添加方法的代码一定要放在替换原型的语句之后。

在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做会重写原型链。

原型链的问题:

  • 包含引用类型值时
  • 创建子类型的实例时,不能向超类型的构造函数中传递参数

借用构造函数

在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此通过使用apply() 和call() 方法也可以在(将来)新创建的对象上执行构造函数,示例:

function SuperType(){
  this.colors = ["red","blue","green"];
}
function SubType(){
  //继承了SuperType
  SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);  //"red,blue,green,black"
var instance2 = new SubType();
alert(instance2.colors);  //"red,blue,green"

相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。

缺点:方法都需在构造函数中定义,不能复用函数。

组合继承

使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript 中最常用的继承模式。

寄生式继承

function createAnother(original){
  var clone = Object(original); //通过调用函数创建一个新对象
  clone.sayHi = function(){ //以某种方式来增强这个对象
    alert("hi");
  };
  return clone; //返回这个对象
}

寄生组合式继承

组合继承无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数构造函数内部。

寄生组合式继承:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。

function inheritPrototype(subType, superType){
  var prototype = Object(superType.prototype);  //创建对象
  prototype.constructor = subType;  //弥补下一步重写原型而失去的默认的constructor
  subType.prototype = prototype;  //指定对象
}

function SuperType(name){
  this.name = name;
  this.colors = ["red","blue","green"];
}
SuperType.prototype.sayName = function(){
  alert(this.name);
};
function SubType(name,age){
  SuperType.call(this,name);
  this.age = age;
}
inheriPrototype(SubType,SuperType);
SubType.prototype.sayAge = function(){
  alert(this.age);
}

这个例子的高效率体现在它只调用了一次SuperType 构造函数,并且因此避免了在SubType.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和isPrototypeOf() 。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式

第7章 函数表达式

函数声明:

function functionName(arg0,arg1,arg2){
  //函数体
}

函数表达式最常见的一种形式:

var functionName = function(arg0,arg1,arg2){
  //函数体
}

创建一个函数并将它赋值给变量,这种情况下创建的函数叫做匿名函数,又称拉姆达函数

在把函数当成值来使用的情况下,都可以使用匿名函数。

递归

递归函数是在一个函数通过名字调用自身的情况下构成的。

var factorial = (function f(num){
  if (num <= 1){
    return 1;
  } else {
    return num * f(num-1)
  }
})

以上代码创建了一个名为f() 的命名函数表达式,然后将它赋值给变量factorial 。即便把函数赋值给另一个变量,函数的名字f 仍然有效,所以递归调用照样能正确完成。

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式就是在一个函数内部创建另一个函数。

闭包只能取得包含函数中任何变量的最后一个值。闭包所保存的是整个变量对象,而不是某个特殊的变量。

模仿块级作用域

JavaScript 没有块级作用域的概念。在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。

用作块级作用域(私有作用域)的匿名函数的语法如下:

(function(){
  //这里是块级作用域
})();

以上代码定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。而紧随其后的另一对圆括号会立即调用这个函数。

无论在什么地方,只要临时需要一些变量,就可以使用私有作用域。

私有变量

任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。

用于访问私有变量的公有方法:在函数内部创建一个闭包,闭包可以通过自己的作用域链访问函数内部的变量。

有权访问私有变量和私有函数的公有方法称为特权方法

推荐阅读更多精彩内容