第5章:引用类型(下)

5.5 Function类型

说起来ECMAScript中最有意思,我想莫过于函数了– 而有意思的根源实际上是对象。每个函数都是Function类型的实例,而且都与其他引用类型一样具有属性和方法。由于函数是对象,因此函数名实际上也是一个执行函数对象的指针,不会与某个函数绑定。函数通常是使用函数声明语法定义的,如下面的例子所示:

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

这与下面使用函数表达式定义函数的方式几乎相差无几。

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

以上代码定义了变量sum并将其初始化为一个函数。有读者可能注意到了,function关键字后面没有函数名。这是因为在使用函数表达式定义函数的时候,没有必要使用函数名–通过变量sum即可以引用函数。另外,还要注意函数末尾有一个分号,就像声明其他变量一样。

最后一种定义函数的方法是使用Function构造函数。Function构造函数可以接受任意数量的参数,但是最后一个参数始终都被看成函数体,而前面的参数则枚举了新函数的参数。来看下面的例子:

var sum = new Function("num1","num2","return num1+num2");

从技术的角度上看,这是一个函数表达式。但是,我们不推荐使用这种方法定义函数,因为这种语法导致两次代码(第一次解析的是常规ECMAScript代码,第二次是解析传入的构造函数中的字符串),从而影响性能。不过,这种语法对于理解”函数是对象,函数名是指针“的概念是非常直观的。

由于函数名仅仅是指向函数的指针,因此函数名与包含对象指针的其他变量没有什么不同。换句话说,一个函数可能有多个名字,如下面例子所示:

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

var anotherSum = sum;
alert(anotherSum(10,10));   // 20

sum = null;
alert(anotherSum(10.10));   // 20

以上代码首先定义了一个名为sum()的函数,用于求两个值的和。然后,又声明了变量anotherSum,并将其设置为与sum相等。注意,使用不带括号的函数名是访问函数指针。而非调用函数。此时,anotherSum和sum都将指向同一个函数,因此anotherSum()也可以调用并返回结果。即使将sum设置为null,让它与函数”断绝关系”,但是仍然可以正常调用anotherSum()。

5.5.1 没有重载

将函数名想象为指针,也有助于理解为什么ECMAScript中没有函数重载的概念。以下是曾在第3章使用过的例子。

function addSomeNumber(num){
    return num + 100;
}
function addSomeNumber(num){
    return num + 200;
}
var result = addSomeNumber(100);    // 300

显然,这个例子中声明了两个同名函数,而结果则是后面的函数覆盖了前面的函数。以上代码实际上与下面的代码没有什么区别。

var addSomeNumber = function(num){
    return num + 100;
};
addSomeNumber = function(num){
    return num + 200;
};
var result = addSomeNumber(100);    // 300

通过观察重写之后的代码,很容易看清楚到底是怎么一回事–在创建第二个函数时候,实际上覆盖了引用第一个函数的变量addSomeNumber。

5.5.2 函数声明与函数表达式

本节到目前为止,我们一直没有对函数声明和函数表达式加以区别。而实际上,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行代码之前可用(可以访问);值与函数表达式,则必须等到解释器执行到它所在的代码行,才会被正真执行。请看下面的例子:

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

以上代码完全可以正常执行。因为在开始执行代码之前,解析器就已经通过函数声明提升(function declaration hoisting)的过程,读取并将函数声明添加到执行环境当中。所以,即使声明函数的代码在调用它的后面,JavaScript引擎在第一遍会声明函数并将它们放到源代码的顶部。所以,即使声明函数的代码在调用它的代码后面,JavaScript引擎也能把函数声明提升到顶部。如果像下面的例子所示,把上面的函数声明改为等价的函数表达式,就会在执行期间导致错误。

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

以上代码之所以会在运行期间发生错误,原因在于函数位于一个初始化语句当中,而不是一个函数声明。换句话说,在执行到函数所在的语句之前,变量sum中不会保存对函数的引用;而且,由于第一行代码就会导致”unexpected identifier”错误。实际上也不会执行到下一行。

除了什么时候可以通过变量访问函数这一点区别之外,函数声明和函数表达式之前的语法其实是等价的。

也可以同时使用函数声明和函数表达式,例如var sum = function sum(){}。不过,这种语法在Safari中是错误的。

5.5.2 作为值的函数

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

function callSomeFunction(somenFunction,someArgument){
    return someFunction(someArgument);
}

这个函数接受两个参数。第一个参数应该是一个函数,第二个函数应该是要传递给该函数的一个值。然后,就像下面的例子一样传递函数了。

function add10(num){
    return num + 10;
}
var result1 = callSomeFunction(add10,10);
function getGreeting(name){
    return "Hello, " + name;
}
var result2 = callSomeFunction(getGreeting,"Nicholas");
alert(result2);     // "Hello,Nicholas"

这里的callSomeFunction()函数是通用的,即无论第一个参数中传递进来的是什么函数,他都会返回第一个参数后面的结果。还记得吧,要访问函数的指针不执行函数的话,必须去掉函数名后面的那对括号。因此上面的例子中传递给callSomeFunction()的是add10和getGreeting,而不是执行它们后的结果。

当然,可以从一个函数中返回另外一个函数,而且这也是极为有用的一种方式。例如,假设有一个对象数组,我们想要根据某个对象属性对数组进行排序。而传递给数组sort()方法比较函数要接受两个参数,即要比较的值。可是我们需要一种方式指明按照那种属性来排序。要解决这个问题,可以定义一个函数,它接受一个属性名,然后根据这个属性名来创建一个比较函数。下面这个函数的定义。

function createComparsionFunction(propertyName){
    return function(object1,object2){
        var value1 = object1[propertyName];
        var value2 = Object2[propertyName];
        if(value1 < value2){
            return -1;
        }else if(value1 > value2){
            return 1;
        }else{
            return 0;
        }
    }
}

这个函数看起来有些复杂,但是实际上无非就是在一个函数中嵌套了另一个函数,而且内部函数前面加了一个return操作符。取得想要的属性之后,定义比较函数就非常简单了。上面这个函数可以像在下面的例子中这样使用。

var data = [{name:"Zachary",age:28},{name:"Nicholas",age:29}];
data.sort(createComparisonFunction("name"));
alert(data[0].name);    // Nicholas
data.sort(createComparisonFunction("age"));
alert(data[0].name);    // Zachary

这里我们创建了一个包含两个对象的数组data。其中,每个对象都包含一个name属性和一个age属性。在默认情况下,sort()方法会调用每个对象的toString()方法已确定它们的次序;但是得到的结果往往不符合人们的思维习惯。因此,我们调用createComparisonFunction(“name”)方法创建可一个比较函数,以便按照每个对象的name属性进行排序。而结果排在前面的第一项是name为”Nicholas”,age是29的对象。然后,我们又使用了createComparisionFunction(“age”)返回的比较函数,这次是按照对象的age属性进行排序。得到的结果是name值为”Zachary”,age值为28的对象排在第一位置。

5.5.4 函数内部属性

在函数内部,有两个特殊的对象:arguments和this。其中,arguments是一个类数组,包含着传入函数中的 所有参数。虽然,arguments的主要用途是保存函数参数,但是这个对象还有一个名叫callee属性,该属性是一个指针,指向拥有这个arguments对象的函数。请看下面这个非常经典的阶乘函数。

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

定义阶乘函数一般要用到递归算法;如上面的代码所示,在函数有名字,而且名字以后不会有变化的情况下,这样的定义没有问题。但问题是这个函数的执行与函数名factorial仅仅耦合在一起。为了消除这种紧密耦合现象,可以像下面这样使用arguments.callee。

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

这个重写后的factorial()函数体内,没有引用函数名factorial。这样,无论引用函数时使用时什么名字,都可以保证正常完成递归调用。例如:

var trueFactorial  = factorial;
factorial = function(){
    return 0;
};
alert(trueFactorial(5));// 120
alert(fatorial(5)); // 0

在此,变量trueFactorial获得了factorial的值,实际上是在另外一个位置保存了一个函数指针。然后我们又将一个简单的返回0的函数赋值给factorial变量。如果像原来的factorial()那样不使用arguments.callee,调用trueFactorial()就会返回0。可是,在解除了函数体内的代码与函数名耦合状态之后,trueFactorial()仍然能够正常地计算阶乘;至于factorial(),它现只是一个返回0的函数。

函数内部另外一个特殊对象是this,其行为与Java和c#中的this大致类似。换句话说,this引用的是函数以执行的环境对象–或者也可以说是this值(挡在网页的全局对象作用域中调用函数时,this对象引用的就是window)。来看下面的例子:

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

上面这个函数sayColor()是在全局作用域中定义的,它引用了this对象。由于在调用函数之前,this的值并不确定,因此this可能会在代码的执行过程中引用不同的对象。当在全局作用域中调用sayColor()时,this引用的是全局对象window;换句话说,对this.color求值会转换成对象window.color求值,于是返回结果为”red”。而当这个函数赋给对象o并调用o.sayColor()时,this引用的是对象o,因此对this.color求值会转换成o.color求值,结果返回了”blue”。

ES5也规范了另一个函数对象的属性:caller。除了Opera的早期版本不支持,其他浏览器都支持这个ES3并没有定义的属性。这个属性中保存着调用当前函数的函数的引用,如果在全局作用域中调用当前函数,它的值为null。例如:

function outer(){
    inner();
}
function inner(){
    alert(inner.caller);
}
outer;

以上代码会导致告警框中outer()函数的源代码。因为outer()调用了inner(),所以inner.caller就指向outer()。为了实现更松散的耦合,也可以通过arguments.callee.caller来访问相同的信息。

function outer(){
    inner();
}
function inner(){
    alert(arguments.callee.caller);
}
outer();

IE、Firefox、Chrome和Safari的所有版本以及Opera 9.6都支持caller属性。

当函数在严格模式下运行时,访问arguments.callee会导致错误。ECMAScript5还定义了arguments.caller属性,但是在严格模式下访问它会导致错误,而在非严格模式下这个属性始终是undefined。定义这个属性是为了分清arguments.caller和函数的caller属性。以上变化都是为了加这门语言的安全性,这样第三方代码就不能再相同的环境里面窥视其他代码了。

严格模式还有一个限制:不能为函数的caller属性赋值,否则会导致错误。

5.5.5 函数属性和方法

前面曾经提到过,ECMAScript中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:length和prototype。其中,length属性表示函数希望接收的命名参数的个数,如下面的例子所示:

function sayName(name){
    alert(name);
}
function sum(num1,num2){
    return num1 + num2;
}
function sayHi(){
    alert("hi");
}
alert(sayName.length);  // 1
alert(sum.length);  // 2
alert(sayHi.length);    // 0

以上代码定义了3个函数,但每个函数接收的命名参数个数不同。首先,sayName()函数定义了一个参数,因此其length属性的值为1。类似地,sum()函数定义了两个参数,结果其length属性中保存的值为2,而sayHi()没有命令参数,所以其length值为0。

在ECMAScript核心所定义的全部属性中,最耐人寻味的就要数prototype属性了。对于ECMAScript中引用类型而言,prototype是保存它们所有实例方法的正真所在。换句话说,诸如toString()和valueOf()等方法实际上都保存在prototype名下,只不过是通过各自对象的实例访问罢了。在创建自定义引用类型以及实现继承时,prototype属性的作用是极为重要的。在ECMAScript5中,prototype属性是不可枚举的,因此使用for-in无法发现。

每个函数都都包含两个非继承而来的方法:apply和call()。这个两个方法的用途都是特定的作用域中调用函数,实际上等于设置函数体内this对象的值。首先,apply方法接受两个参数:一个是在其中运行函数的作用域,另一个是参数数组。其中,第二个参数可以是可以Array的实例,也可以是arguments对象。例如:

function sum(num1,num2){
    return num1 + num2;
}
function callSum1(num1,num2){
    return sum.apply(this,arguments); 
}
function callSum2(num1,num2){
    return sum.apply(this,[num1,num2]);
}
alert(callSum1(10,10)); // 20
alert(callSum2(10,10)); // 20

在上面的例子中,callSum1()在执行sum()函数时传入this作为this值(因为在全局作用域中调用的,所以传入的就是window对象)和arguments对象。而callSum2同样也调用了sum()函数,但是它传入的则是this和一个参数数组,这两个函数都会正常执行并返回正确结果。

在严格模式下,位指定环境对象而调用函数,则this值不会转型为window。除非明确把函数添加到某个对象或者调用apply()或者call(),否则this值将是undefined。

function sum(num1,num2){
    return num1 + num2;
}
function callSum(num1,num2){
    return sum.call(this,num1,num2);
}
alert(callSum(10,10));  // 20

在使用call()的情况下,callSum()必须明确地传入每一个参数。结果与使用apply()没有什么不同。至于是否使用apply()还是call(),完全取决于那种给函数传递参数的方式最方便。如果你打算直接传入arguments对象,或者包含函数中先接收的也是一个数组,那么使用apply()肯定更加的方便;否则,选择call()可能更合适。

事实上,传递参数并非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

这个例子和前面说明this对象的示例基础上修改而成的。这一次,sayColor()也是作为全局函数定义的,而且当全局作用域中调用它时,他确定会显示”red”–因为this.color的求值会转换成window.color的求值。而sayColor.call(this)和sayColor(window),则是两种显式地在全局作用域中调用函数的方式,结果当然湖显示”red”。但是当运行sayColor(o)时候,函数的执行环境就不一样了,因此此时函数体内的this对象不需要与方法有任何耦合关系。

使用call()或者apply()来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系。在前面的例子的第一个版本中,我们是先将sayColor()函数放到了对象o中,然后在通过o来调用它的;而在这里重写例子中,就不需要先前那个多余的步骤了。

ECMAScript 5还定义了一个方法bind()。这个方法会创建一个函数实例,其this值会被绑定到传给bind()函数的值。例如:

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

这里sayColor()调用bind()并传入对象o,创建了objectSayColor()函数,夜壶看到”blue”。这种技巧的优点请参看22章。

支持bind()方法的浏览器有IE9+、FireFox4+、Safari5.1+、Opera 12+和chrome。

每个函数都继承的toLocaleString()和toString()方法始终都会返回函数的代码,返回代码的格式因浏览器而异,有的范湖的代码与源代码的函数代码一样,而有的则返回函数代码中的内部表示,即解析器删除了注释对某些代码做了改动后的代码。由于存在这种差异,我们无法想象根据这两个方法返回的结果来实现任何重要的功能。不过,这些信息在调用代码的时候倒是很有用。另一个继承的valueOf()方法同样也只返回函数代码。

5.6 基本包装类型

为了便于操作基本类型,ES3还提供了3中特殊的引用类型:Boolean、Number和String。这些类型与本章介绍的其他引用类型相似,但是同时也具有各自的基本类型相应的特殊行为。实际上,每当读取一个基本类型值的时候,后台救护创建一个对应的基本包装类型的对象,从而让我们能够调用这些方法来操作这些数据。来看下面的例子:

var s1 = "some text";
var s2 = s1.substring(2);

这个例子中变量s1包含一个字符串,字符串当然是基本类型值。而下一行调用了s1的substring()方法,并将返回的结果保存在了s2中。我们知道基本类型不是对象,因而从逻辑上讲它们不应该有方法。其实,为了我们事先这种直观的操作,后台已经自动完成了一系列的处理。当第二行代码访问s1时候,访问过程处于一种读取模式,也就是要从内存中读取整个字符串的值。而在读取模式中访问字符串时候,后台都会自动完成下列处理。

  1. 创建String类型的一个实例;
  2. 在实例上调用指定的方法;
  3. 销毁整个实例。

    可以将以上三个步骤想象成是执行下列ECMAScript代码:

var s1 = new String("some text");
var s2 = s1.substring(2);
s1 = null;

经过此番处理,基本的字符串就变得和对象一样了。而且,上面这个三个步骤也分别使用Boolean和Number类型对应的布尔值和数字值。

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

var s1 = "some text";
s2.color = "red";
alert(s1.color);    //undefined

在此,第二行代码视图为s1添加一个color属性。但是,当第三行代码在此访问s1的时候,其color属性不见了。问题的原因就是就是第二行创建的String对象在执行第三行代码时候已经被销毁了。第三行代码又创建了自己的String对象,而该对象没有color属性。

当然,可以显式地调用Boolean、Number和String来创建基本包装类型的对象。不过,应该在绝对必要的情况下再这样做,因为这种做法很容易让人分不清楚自己是在处理基本类型还是引用类型的值。对基本包装类型的实例调用typeof会返回”object”,而所有基本包装类型的对象都会被装换为布尔值true。

Object构造函数也会像工厂方法一样,根据传入的值的类型返回相应基本包装类型的实例。例如:

var obj = new Object("some text");
alert(obj instanceof String);   // true

把字符串传给Object构造函数,就会创建String的实例;而传入数值参数会得到Number的实例,传入布尔值参数就会得到Boolean的实例。

需要注意的是,使用new 调用基本包装类型的构造函数,与直接调用同名的类型转型函数是不一样的。
例如:

var value = "25";
var number = Number(value); // 转型函数
alert(typeof number);       // "number"

var obj = new Number(value);    // 构造函数
alert(typeof obj);      // "object"

在这个例子中,变量number中保存的是基本类型的值25,而变量obj中保存的是Number的实例。需要了解有关转型函数的更多信息。请参考第3章。

尽管我们不建议显式地创建基本包装类型的对象,但是它们操作类型的值的能力还是相当重要的。而每个基本包装类型都提供了操作相应值的便捷方法。

5.6.1 Boolean类型

Boolean类型是与布尔值对应的引用类型。要创建Boolean对象,可以像下面这样调用Boolean构造函数并传入true或false值。

var booleanObject = new Boolean(true);

Boolean类型的实例重写了valueOf()方法,返回基本类型true或false;重写了toString()方法,返回字符串”true”或者”false”。可是,Boolean对象在ECMAScript中的用处不大,因为它经常造成人们误解。其中常见的问题就是在布尔表达式中使用Boolean对象,例如:

var falseObject = new Boolean(false);
var result = falseObject && true;
alert(result);  // true

var falseValue = false;
result = falseValue && true;
alert(result);  // false

在这个例子中,我们使用了false值创建了一个Boolean对象,将这个对象与基本类型值true构成逻辑与表达式。在布尔运算中,false && true等于false。可是,示例中的这段代码是对falseObject而不是它的布尔值(false)进行求值。前面讨论过,布尔表达式中的所有对象都会被转换成true,因此falseObject对象在布尔表达式中代表true。结果,true && true当然就等于true。

基本类型与引用类型的布尔值还有两个区别。首先,typeof操作符对基本类型返回”boolean”,而对引用类型返回”object”。其次,Boolean对象是Boolean类型的实例,所以使用instanceof操作符测试Boolean对象返回true,而测试基本类型的布尔值则返回false。例如:

alert(typeof falseObject);      // object
alert(typeof falseVale);        // boolean
alert(falseObject indeanceof Boolean);  // true
alert(falseValue instanceof Boolean);   // false

理解基本类型的布尔值与Boolean对象之间的区别非常重要–当然,我们的建议不永远不要使用Boolean对象。

5.6.2 Number类型

Number与数字值对应的引用类型。要创建的Number对象,可以调用Number构造函数时其中传递相应的参数。下面是一个例子:

var numberObject = new Number(10);

与Boolean类型一样,Number类型也重写了valueOf()方法、toLocaleString()和toString()方法。重写后的valueOf()方法返回对象表示的基本类型的数值。另外两个方法则返回字符串形式数值。我们在第3章还介绍过,可以为toString()方法传递一个表示基数的参数,告诉它返回几进制数值的字符串形式,如下面例子所示:

var num = 10;
alert(num.toString());  // "10"
alert(num.toString(2)); // "1010"
alert(num.toString(8)); // "12"
alert(num.toString(10));    // "10"
alert(num.toString(16));    // "a"

除了继承的方法之外,Number类型提供了一些用于将数值格式转换为字符串的方法。其中toFixed()方法会按照执行的小数位返回数值的字符串表示,例如:

var num = "10.005";
alert(num.toFixed(2)); // "10.01"

这里给toFixed()方法传入了数值2,意思是显示几位小数。于是,这个方法返回了”10.00”,即以0填补了必要的小数位。如果数值本身包含的小数位比指定的还多,那么接近指定的最大小数位的值就会舍入,如下面的例子所示:

var num = 10.005;
alert(num.toFixed(2));  // "10.01"

能够自动舍入的特性,使得toFixed()方法和适合处理币缺值。但是需要注意的是,不同浏览器给这个方法设定的舍入规则可能会有所不同。

toFixed()方法可以表示带有0到20个小数位的数值。但是这只是标准实现的规范,有些浏览器也可能支持更多的位数。

另外一种用于格式化数值的方法是toExponential(),该方法返回以指数表示法(也称为e表示法)表示的数值的字符串形式。与toFixed()一样,toExponential()也接受一个参数,而且该参数同样也是指定输出结果中的小数位数。看下面的例子:

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

以上代码输出”1.0e+1”;不过,这么小的数值一般不必使用e表示法。如果你想得到表示某个数值的最合适的格式,就应该使用toPrecision()方法。

对于一个数值来说,toPrecision()方法可能会返回固定大小(fixed)格式,也可能返回指数(exponential)格式;具体规则是看哪种格式最适合。这个方法接受一个参数,即表示数值所有数字的位数(不包括指数部分)。请看下面的例子。

var num = 99;
alert(num.toPrecision(1));  // "1e+2"
alert(num.toPrecision(2));  // "99"
alert(num.toPrecision(3));  // "99.0"

以上代码首先完成的任务是以一位数来表示99,结果是”1e+2”,即100。因为一位数无法准确地表示99,当然还是“99”。最后,在想以三位数表示99时,toPrecision()方法返回了”99.0”。实际上,toPrecision()方法返回了”99.0”。实际上,toPrecision()会根据要处理的数据决定到底是调用toFixed()还是调用toExponential()。而这三个方法都可以通过上或者想下舍入,做到最准确的形式带有正确小数位的值。

toPrecision()方法可以表现到1到21位小数。某些浏览器支持的范围更大,但这是典型实现的范围。

与Boolean对象类似,Number对象也以后台方式提供了重要功能。但是与此同时,我们仍然不建议直接实例化Number类型,而原因与显式创建Boolean对象一样。具体来说,就是使用typeof和instanceof操作符测试基本类型数值与引用类型数值,得到的结果完全不同,如下面的例子所示:

var numberObject = new Number(10);
var numberValue = 10;
alert(typeof numberObject); // "object"
alert(typeof numberValue);  // "number"
alert(numberObject instanceof Number);  // true
alert(numberValue instanceof Number);   // false

使用typeof操作符测试基本类型数值的时候,始终会返回”number”,而在测试Number对象时,则会返回”Object”。类似地,Number对象时Number类型的实例,而基本实例的数值则不是。

5.6.3 String类型

String是字符换的对象包装类型,可以像下面这样使用String构造函数来创建。

var stringObject = new String("hello,world");

String对象的方法也可以在所有基本的字符串中访问到。其中,继承valueOf()、LocaleString()和toString()方法,都返回对象所表示的基本字符串。

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

var stringValue = "hello,world";
alert(stringValue.length);  // "1"

这个例子输出了字符串”hello world”中字符数量,即”11”。应该注意的是,即使字符串中包含双字节字符(不占用一个字节的ASCII字符),每个字符仍然算一个字符。

String类型提供了很多方法,用于辅助完成对ECMAScript中字符串的解析和操作。

1.字符方法
两个用于访问字符串中特定字符的方法是:charAr()和charCodeAt()。这两个方法都接受一个参数,即基于0的字符位置。其中,charAt()方法以单字符串的形式返回给定位置的那个字符(ECMAScript中没有字符类型)。例如:

var stringValue = "hello world";
alert(stringValue.charAt(1));   // "e"

字符串”hello world”位置1的字符是“e”,因此调用charAt(1)就返回了”e”。如果你想得到的不是字符而是字符编码,那么就像下面这样使用charCodeAt()了。

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

这个例子输出的是”101”,也就是小写字母”e”的字符编码。

ECMAscript5还定义了另外一个访问个别字符的方法。在支持此方法的浏览器中,可以使用方括号加数字索引来访问字符串中特定的字符,如下面的例子所示:

var stringValue = "hello world";
alert(stringValue[1]);  // "e"

2.字符串操作方法
下面介绍与操作字符串有关的结果方法。第一个是concat(),用于将一个或者多个字符串拼接起来。返回拼接后得到的新字符串。来看一个例子:

var stringValue = "hello ";
var result = stringValue.concat("world");
alert(result);          // "hello world"
alert(stringValue);     // "hello"

在这个例子中,通过stringValue调用concat()方法返回的结果是”hello world”—但是stringValue的值保持不变。实际上,concat()方法可以接受任意多个参数,也就是说它可以通过它拼接任意多个字符串。再看一个例子:

var stringValue = "hello ";
var result = stringValue.concat("world","!");
alert(result);      // "hello world!"
alert(stringValue); // "hello"

这个例子将”world”和”!”拼接到了”hello”的末尾。虽然concat()是专门用来拼接字符串的方法,但是实践中使用更多的是操作符(+)。而且,使用加号操作符大多数情况下都比使用concat()方法要简单易行。

ECMAScript还提供了三个基于字符串创建新字符串的方法:slice()、substr()、和substring()。这三个方法都会返回被操作字符串的一个子字符串,而且都接受两个参数。第一个参数是指定字符串的开始位置,第二个字符串表示子字符串在哪里结束。具体来说,slice()和substring()的第二个参数指定的是字符串最后一个字符串后面的位置。而substr()的第二个参数指定的则是返回字符串的个数。如果没有给这些方法传第二个参数,则将字符串的长度作为结束位置。与concat()方法一样,slice()、substr()和substring()也不会修改字符串本身的值–它们只是返回一个基本类型的字符串值,对原始字符串没有任何影响。请看下面的例子:

var stringValue = "hello world";
alert(stringValue.slice(3));    // "lo world"
alert(stringValue.subString(3));    // "lo world"
alert(stringValue.substr(3));   // "lo world"
alert(stringValue.slice(3,7));  // "lo w"
alert(stringValue.substring(3,7));  // "lo w"
alert(stringValue.substr(3,7));     // "lo worl"

这个例子以相同方式调用slice()、substr()和substring()得到的结果,而且多数情况下是相同的。在指定一个参数3的情况下,这三个方法都返回”lo world”,因为”hello”中第二个”1”处于位置3。而在指定两个参数3和7的情况下,slice()和substring()返回”lo w”,但是substr()返回”lo worl”,因为它的第二个参数指定的是要返回的字符个数。

在传递给这些方法的参数是负数的情况下,它们的行为就不尽相同。其中,slice()方法会将传入的负数与字符串的长度相加,substr()方法将负的第一个参数加上字符串的长度,而该负的第二个参数转换为0。最后substring()方法会把所有负数参数都转换为0。下面来看例子。

var stringValue = "hello world";
alert(stringValue.slice(-3));   // "rld"
alert(stringValue.substring(-3));   // "hello,world"
alert(stringValue.substr(-3));  // "rld"
alert(stringValue.slice(3,-4)); // "lo w"
alert(stringValue.substring(3,-4)); // "hel"
alert(stringValue.substr(3,-4));    // ""(空字符)

这个例子清晰地展示了上述三个方法的不同行为。在给slice()和substr()传递一个负值参数时,它们的行为相同。这是因为-3会被转换为8(字符串长度加参数11+(-3)=8),实际上相当于调用slice(8)和substr(8)。但是substring()方法返回了全部字符串,因为它将-3转换为了0。

当第二个参数是负数的时候,这三个方法的行为各不相同。slice()方法会会把第二个参数转换为7,这相当于调用了slice(3,7),因此返回”lo w”。substring()方法会把第二个参数转换为0,使调用变成了substring(3,0),而由于这个方法将较小的数作为开始问题,将较大的数作为结束位置,因此最终相当于调用了substring(0,3)。substr()也会将第二个参数转换为0,这也意味着返回包含零个字符的字符串,也就是空字符串。

3.字符串位置方法

有两个可以从字符串中查找字符串的方法:indeOf()和lastIndexOf()。这两个方法都是从一个字符串中搜索给定子字符串,然后返回字符串的位置。这两个方法的区别在于:indexOf()方法从字符串的开头向后搜索字符串,而lastIndexOf()方法是从字符串末尾向前搜索字符串。还是来看一个例子吧。

var stringValue = "hello world";
alert(stringValue.indexOf("o"));    // 4
alert(stringValue.lastIndexOf("o"));    // 7    

字符串”o”第一次出现的位置是”4”,即”hello”中的”o”;最后一次出现的位置是7,即”world”中的”o”。如果”o”在这个字符串中仅仅出现一次,那么indexOf()和lastIndexOf()会返回相同的位置值。

这两个方法都可以接受第二个参数,表示从字符串中那个位置开始搜索。换句话说,indexOf()会从该参数指定位置向后搜索,忽略该位置之前的所有字符;而lastIndexOf()从指定位置向前搜索,忽略该位置之后的所有字符,看下面的例子:

var stringValue = "hello world";
alert(stringValue.indexOf("o",6));  // 7
alert(stringValue.lastIndexOf("o",6));  // 4

在将第二个参数6传递给这两个方法之后,得到了与前面例子相反的结果。这一次,由于indexOf()是从位置6开始向后搜索,结果在位置7找到了”o”,因此它返回7。而lastIndexOf()是存位置6开始向后搜索,结果在位置7找到了”o”。因此它返回了4。在使用第二个参数的情况下,可以铜鼓循环调用indexOf()或者lastIndexOf()来找到所有匹配的子字符串,如下面的例子所示:

var stringValue = "Lorem ipsum dolor sit amet,consectetur asippisicing eli";
var positions = new Array();
var pos = stringValue.indexOf("e");
while(pos > -1){
    positions.push(pos);
    pos = stringValue.indexOf("e",pos + 1);
}
alert(posotions); // “”3,24,32,35,52“”

这个例子通过不断增加indexOf()方法开始查找位置,遍历一个长字符串。在循环之外,首先找到了”e”在字符串的初始位置;而进入循环后,则每次给indexOf()传递上一次位置加1。这样,就确保了每次新搜索从上一次找到的子串的后面开始。每次搜索返回的位置依次被保存在数组positions中,以便将来使用。

4.trim()方法

ECMAScript 5位所有字符串定义了trim()方法。这个方法创建一个字符串的副本,删除前置以及后缀的所有空格,然后返回结果。例如:

var stringValue = " hello world ";
var trimmedStringValue = stringValue.trim();
alert(stringValue);     // " hello world "
alert(trimmedStringValue);  // "hello world"

由于trim()返回的是字符串的副本,所以原始字符串中的前置以及后缀空格会保持不变。

5.字符串大小写转换方法

接下来我们要介绍的是一组大小写转换的有关方法。ECMAScript中设计字符串大小写转换的方法有4个:toLowerCase()、toLocaleLowerCase()、toUpperCase()和toLocaleUpperCase()。其中,toLowerCase()和toUpperCase()是两个经典的方法,借鉴自java.lang.String中的同名方法。而toLocaleLowerCase()和toLocaleUpperCase()方法则是针对特定地区的实现。对于有些地区而言,针对地区的方法与其通用方法得到结果相同,但是少数语言会为Uncidoe大小写转换应用特殊的规则,这时候就必须针对地区的方法来保证实现正确的转换。以下是几个例子:

var stringValue = "hello world";
alert(stringValue.toLocaleUpperCase()); // "HELLO WORLD"
alert(stringValue.toUpperCase());   // "HELLO WORLD"
alert(stringValue.toLocaleLowerCase);   // "hello world"
alert(stringValue.toLowerCase());   // "hello world"

以上代码调用toLocaleUpperCase()和toUpperCase()都返回了”HELLO WORLD”,就像调用toLocaleLowerCase()和toLowerCase()都返回”hello world”一样。一般来说,在不知道自己的代码在那种语言环境下运行的情况下,还是针对地区的方法更加稳妥一些。

6 字符串的模式匹配方法

String()类型定义了几个用于字符串匹配模式的方法。第一个方法就是match(),在字符串上调用这个方法,本质上与调用RegExp的exec()方法相同。match()方法只接受一个参数,要么是一个正则表达式,要么是一个RegExp对象。来看下面的例子:

var text = "cat,bat,sat,fat";
var pattern = /.at/;
// 与pattern.exec(text)相同
var macthes = text.match(pattern);
alert(matches.index);       // 0
alert(matches[0]);      // "cat"
alert(pattern.lastIndex);   // 0

本例中的match()方法返回了一个数组,如果调用RegExp对象的exec()方法并传递本例中的字符串作为参数,那么会得到与此相同的数组:数组的第一项与整个模式匹配的字符串,之后的每一项保存着与此正则表达式中的捕获组匹配的字符串。

另一个用于查找模式的方法是search()。这个方法的唯一参数与match()方法的参数相同:由字符串或者RegExp对象指定的一个正则表达式。search()方法返回字符串中的第一个匹配项的索引;如果没有找到匹配项,则返回-1。而且search()方法是从字符串开头向后查找。来来看下面的例子。

var text = "cat,bat,sat,fat";
var pos = text.search(/at/);
alert(pos); // 1

这个例子中的search()方法返回1,即”at”在字符串中第一次出现的位置。

为了简化字符串的操作,ECMAScript提供了replace()方法。这个方法接受两个参数:第一个参数可以是RegExp对象或者一个字符串(这个字符串不会被转换成正则表达式),第二个参数可以是一个字符串或者一个函数。如果第一个参数是字符串,那么只会替换第一个字符串。想要替换所有字符串,唯一的办法是提供一个正则表达式,而且要指定全局(g)标志,如下所示:

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"

这个例子中,首先传入replace()方法的是字符串”at”和替换用的字符串”ond”。替换的结果是把”cat”变成了”cond”,但字符串中的其他字符串并没有收到影响。最后,通过将第一个参数修改为带有全局编制的正则表达式,就将全部”at”替换为”ond”。

如若第二个参数是字符串,那么还可以使用一些特殊的字符序列,将正则表达式操作得到的值插入到结果字符串中。下表提供了这些特殊的字符序列。

字符序列替换文本
$$$
$&匹配整个模式的子字符串。与RegExp.lastMatch的值相同
$’匹配的子字符串之前的子字符串。与RegExp.leftContext的值相同
$`匹配的子字符串之前的子字符串。与RegExp.rightContext的值相同
$n匹配第n个捕获组的子字符串,其中n等于0-9。例如$1是匹配的第一个捕获组的字符串…以此类推。如果正则表达式中没有定义捕获组,则使用空字符串
$nn匹配第nn个捕获组的子字符串,其中n等于0-99。例如$01是匹配的第一个捕获组的字符串…以此类推。如果正则表达式中没有定义捕获组,则使用空字符串

通过这些特殊的字符序列,可以使用最近一次匹配的内容,如下面的例子所示:

var text = "cat,bat,sat,fat";
result = text.replace(/(.cat)/g,"word($1)");
alert(result);  // world(cat),world(bat),world(sat),world(fat)

在此,每个以”at”结尾的单词都被替换了,替换结果是world后跟着一对圆括号,而圆括号中是被字符序列$1所替换的单词。

replace()方法的第二个参数也可以是一个函数,在只有一个匹配项的情况下,回向这个函数传递3个参数:模式的匹配项、模式匹配项在字符串中的位置和原始字符串。在正则表达式总定义多个捕获组的情况下,传递给函数的参数依次是模式的匹配项、第一个捕获组的匹配项、第二个捕获组的匹配项……,但是最后两个参数仍然分别是模式的匹配项在字符串中的位置和原始字符串。这个函数应该返回一个字符串,表示应该被替换的匹配项使用函数作为replace()方法的第二个参数可以实现更加精细地替换操作,请看下面的例子:

function htmlEscape(text){
    return text.replace(/[<>"&]/g,function(match,pos,originalText){
        switch(match){
            case "<" :
                return "&lt";
            case ">" :
                return "&gt";
            case "&" :
                return "&amp";
            case "\"":
                return "&quot";
        }   
    });
}

这里我们为插入HTML代码定义了函数htmlEscape(),这个函数能够转义4个字符:小于号、大于号和号以及双引号。实现这种转义的最简单的方式,就是使用正则表达式查找这几个字符,然后定义一个能够针对每个匹配的字符返回特定的HTML实体的函数。

最后一个与模式匹配的有关方法是split(),这方法可以基于指定的分隔符将一个字符串分割成多个子字符串,并将结果放到一个数组中。分隔符可以是字符串,也可以是一个RegExp对象。split()方法可以接收可选的第二个参数,用于指定数组的大小,以确保返回的数组不会超过既定的大小。请看下面的例子:

var colorText = "red,blue,green,yellow";
var color1 = colorText.split(",");  // ["red","blue","green","yellow"]
var color2 = colorText.split(",",2);    // ["red","green"]
var color3 = colorText.split(/[^\,]+/); // ["",",",",",",",""]

在这个例子中,colorText是逗号分隔的颜色名的字符串。基于该字符串调用split(“,”)或得到以包含其中的颜色名的数组,用于分割字符串的分隔符是逗号。为了将数组截短,让它只包含两项,可以为split()方法传递第二个参数2。最后在通过使用正则表达式,还可以取得包含逗号字符串的数组。需要注意的是,在最后一次调用split()返回的数组中,第一项和最后一项是两个空字符串。之所以会这样,是因为通过正则表达式指定的分割符出现在了字符串的开头和末尾。

对split()正则表达式中的支持因浏览器而异。尽管简单的模式没有什么差别。但对于未发现匹配项以及带有捕获组的正则表达式,匹配的行为就不大相同了。

7 localeCompare()方法

与操作字符串有关的最后一个方法是localeCompare(),这个方法比较两个字符串,并返回下列值中的一个:

  • 如果字符串在字母表中应该排在字符串参数之前,则返回一个负数。
  • 如果字符串等于字符串参数,则返回0。
  • 如果字符串在字母表中应该应该排在字符串之后,则返回一个正数。

    下面是一个例子:

var stringValue = "yellow";
alert(stringValue.localeCompare("brick"));  // 1
alert(stringValue.localeCompare("yellow")); // 0
alert(stringValue.localeCompare("zoo"));    // -1

这个例子比较了字符串”yellow”和另外几个值:”brick”、”yellow”和”zoo”。因为在”brick”在字母表中排在”yellow”之前,所以localeCompare()返回了1;而”yellow”等于”yellow”,所以,localeCompare返回了0;最后,”zoo”在字母表中排在了”yellow”后面,所以,localeCompare()返回了-1。在强调一次,因为localeCompare()返回的数值取决于实现,所以最好是下面例子所示的这个方法。

function determinOrder(value){
    var result = stringValue.localeCompare(value);
    if(result < 0){
        alert("The string 'yellow' comes before the string '" + value + "'.");
    }else if(result > 0){
        alert("The string 'yellow' after the string '" + value + "'.");
    }else{
        alert("The string 'yellow' is equal to the string '" + value + "'.");
    }
}
determinOrder("brick");
determinOrder("yellow");
determinOrder("zoo");

使用这种结构,就可以确保自己的代码在任何实现中都可以正确的运行了。

localCompare()方法比较与众不同的地方,就是实现所支持的地区决定了方法的行为。比如,美国以英语作为ECMAScript实现的标准语言,因此,localeCompare()就是区分大小写的,于是大写字母在字母表中排在小写字母前头就成了一项决定性的比较规则。不过,在其他地区就不是这种情况了。

8.fromCharCode()方法

另外,String()构造函数本身还有一个静态方法:fromCharCode()。这个方法的任务就是接收一个或者多个字符编码,然后将它们转换成一个字符串。从本质上看,这个方法与实例方法charCodeAt()执行的是相反的操作。来看一个例子:

alert(String.fromCharCode(104,101,108,108,111));    // "hello"

在这里,我们给fromCharCode()传递的是字符串”hello”中每个字符串的字符编码。

9 HTML方法

早期的Web浏览器供应商觉察到使用JavaScript动态格式化HTML的需求。于是,这些供应商就扩展了标准,实现了一些专门用于简化常见HTML格式化任务的方法。下表列出了这些HTML方法。不过,需要读者注意的是,尽量不要使用这些方法,因为它们创建的标记通常无法表达语义。

这里写图片描述

5.7 单体内置对象

ECMAScript-262对内置对象的定义是:”由ECMAScript实现提供的、不依赖与宿主环境的对象,这些对象在ECMAScript程序执行之前就已经存在了”。意思就是说,开发人员不必显式地实例化内置对象,因为它们已经实例化了。前面已经介绍了大多数内置对象。例如Object、Array和String。ECMAScript-262还定义了两个单体内置对象:Global和Math。

5.7.1 Global对象

Global(全局)对象可以说是ECMAScript中最为特别的一个对象了,因为不管你从什么角度来看,这个对象是不存在的。ECMAScript中的Global对象在某种意义上是作为一个终极的”兜底儿对象”来定义的。换句话说,不属于任何其他对象的属性和方法,最终都是它的属性和方法。事实上,没有全局变量或者全局函数,所有在全局作用域中定义的属性和函数,都是Global对象的属性。本节前面介绍过的那些函数,诸如isNaN()、isFinite()、ParseInt()以及parseFloat(),实际上全都是Global对象的方法。除此之外,Global对象还包含其他方法。

1.URL编码方法
Global对象的encodeURI和encodeURIComponent()方法可以对URI进行编码。以便发送给浏览器。有效的URI中不能包含某些字符,例如空格。而这两个URI编码方法可以对URI进行编码,它们用特殊的UTF-8编码替换所有无效的字符,从而让浏览器能够接收和理解。

其中,encodeURI()主要用于整个URI(例如,http://www.wrox.com/illegal value.html),而encodeURICOMponent()主要是对URI中的某一段进行编码。它们的主要区别在于,encodeURI不会对本身属于URI的特殊字符进行编码,例如冒号、正斜杠、问号和井号;而encodeURIComponent()则会对它发现的任何非标准字符进行编码。来看下面的例子:

这里写图片描述

使用encodeURI()编码后的结果是除了空格之外的其他字符都原封不动,只有空格被替换成了&2D。而encodeURIComponent()丰富则会使用对应的编码替换所有非字母数字字符。这也是对整个URI使用encodeURI(),而只能对附加在现有URI后面的字符串使用encodeURIComponent()的原因所在。

一般情况下,我们使用encodeURICompenent()方法的时候要比使用encodeURI()更多,因为在实践中更常见的是对查询字符参数而不是对基础URI进行编码。

与encodeURI()和encodeURIComponent()方法对应的是两个方法分别是decodeURI()和decodeURIComponent()。其中decodeURI()只能对encodeURI()替换的字符进行编码。例如,它可将%2D替换成一个空格,但是不会对%23做任何处理,因为%23表示井号(#),而井号不是使用encodeURI()替换的。同样的,decodeURIComponent()能够解码使用encodeURIComponent()编码的所有字符,它可以解码任何特殊字符的编码。来看下面的例子:

这里写图片描述

这里,变量uri包含着一个由encodeURIComponent()编码的字符串。在第一次调用decodeURI()输出的结果中,只有%2D被替换成了空格。而在第二次调用decodeURIComponent()输出的结果中,所有特殊字符的编码都被替换成了原来的字符,得到一个未经转义的字符串。

2 eval()方法

现在我们介绍最后一个–大概也是整个ECMAScript语言中最强大的一个方法:eval()。eval()方法就像是一个完整的ECMAScript解析器,它只接受一个参数,即要执行的ECMAScript(或者JavaScript)字符串,来看下面的例子:

eval("alert('hi')");

运行代码的作用等价于下面这行代码:

alert("hi");

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

var msg = "hello world";
eval("alert('msg')");

可见,变量msg是在eval()调用的环境之外定义的,但其中调用alert()仍然能够显示”hello world”。这是因为上面第二行代码最终被替换成一行正真的代码。同样地,我们可以在eval()调用中定义一个函数,然后再调用外部代码引用这个函数:

eval("function sayHi(){ alert('hi') }");
sayHi();

显然,函数sayHi()是在eval()内部定义的。但对于eval()的调用最终会被替换成定义函数的实际代码,因此可以在下一行调用sayHi()。对于变量也是一样的:

eval("var msg = 'hello world';");
alert(msg); // "hello world"

在eval()中创建的任何变量或者函数都不会被提升,因为在解析代码的时候,它们被包含在一个字符串中;它们只在eval()执行的时候创建。

严格模式下,在外部访问不到eval()中创建的任何变量或者函数,因此前面的两个例子都会导致错误。同样,在严格模式下,为eval赋值也会导致错误。

"use starick";
eval = "hi";    // causes error

能够解释代码字符串的能力非常强大,但是也非常危险。因此在使用eval()时候须极为谨慎,特别是在用它执行用户输入数据的情况下。否则,可能会有恶意用户输入威胁你的站点或者应用程序安全的代码。

3 Global对象的属性

Global对象还包含一些属性,其中一部分属性已经在本书前面介绍过了。例如,特殊的值undefined、NaN以及Infinity都是Global对象的属性。此外,所有原生引用类型的构造函数,像Object和Function,也都是Global对象的属性。下表列出了Global对象的所有属性。

属性说明属性说明
undefined特殊值undefinedDate构造函数Date
NaN特殊值NaNRegExp构造函数RegExp
Infinity特殊值InfinityError构造函数Error
Object构造函数ObjectEvalError构造函数EvalError
Array构造函数ArrayRangError构造函数RangError
Function构造函数FunctionReferenceError构造函数ReferenceError
Boolean构造函数BooleanSyntaxError构造函数SyntaxError
String构造函数StringTypeError构造函数TypeError
Number构造函数NumberRUIError构造函数URIError

ECMAScript5 明确禁止给undefined、NaN和Infinity赋值,这样做即使在非严格模式下也会导致错误。

4 window对象

ECMAScript虽然没有指定如何直接访问Global对象,但是Web浏览器都是将这个全局对象作为window对象的一部分加以实现的。因此,在全局作用域中声明的所有变量和函数,就都成为了window对象的属性。来看下面的例子。

var color = "red";
function sayColor(){
    alert(window.color);
}
window.sayColor();

这里定义了一个名为color的全局变量和一个名为sayColor()的全局函数。在sayColor()内部,我们通过window.color来访问color变量,以说明全局变量是window对象的属性。然后,又使用window.sayColor()来直接访问window对象调用这个函数,结果显示在警告框中。

JavaScript中的window对象除了扮演ECMAScript规定的Global对象的角色外,还承担了很多别的任务。

另一种取得Global对象的方法是使用下面的代码:

var global = function(){
    return this;
}();

以上代码创建了一个立即调用函数表达式,返回this的值。如前面所述,在没有给函数指定this的情况下(无论是通过将函数添加为对象的方法,还是通过调用call()或者apply()),this的只等于Global对象。而像这样通过简单地返回this来获取Global对象,在任何执行环境都是可行的。

5.7.2 Math对象

ECMAScript还为保存数学公式和信息提供了一个公共位置,即Math对象。与我们在JavaScript直接编写的计算功能相比,Math对象提供的计算功能执行起来要快的多。Math对象还提供了辅助完成这些计算的属性和方法。

1 Math对象的属性

这里写图片描述

虽然这些值的含义和用途超出了本书的范围,但是你确实可以随时使用它们。

2 min()和max()方法
Math对象还包括可许多的方法,用于辅助完成简单和复杂的数据计算。
其中,min()和max()方法用于确定一组数值中的最小值和最大值。这两个方法都可以接受任意多的数值参数,如下面的例子所示:

var max = Math.max(3,53,32,16);
alert(max); // 54
var min = Math.min(3,54,32,16);
alert(min); // 3

对于3、54、32和16,Math.max()返回54,而Math.min()返回3。这两个方法经常用于避免多余的循环和if语句中确定一组数的最大值。

要找到数组中的最大值和最小值,可以像下面这样使用apply方法。

var values = [1,2,3,4,5,6,7,8];
var max = Math.max.call(Math,values);

这种技巧的关键是把Math对象作为apply()的第一个参数,从而正确地设置this值,然后可以将任何数组作为第二个参数。
3 舍入方法
下面来介绍将小数舍入为整数的几个方法:Math.ceil()、Math.floor()和Math.round()这三个方法分别遵循下列舍入规则:

  • Math.ceil()执行向上舍入,即总是将数值向上舍入为最接近的整数;
  • Math.floor()执行向下舍入,即它总是将数值向下舍入为最接近的整数;
  • Math.round()执行标准舍入,即它总是将数值四舍五入为最接近的整数。

    4 random()方法
    Math.random()方法返回大于等于0小于1的一个随机数。对应某些站点来说,这个方法非常实用,因为可以利用它来随机显示一些名人名言和新闻事件。套用下面的公式,就可以利用Math.random()从某个整数返回内随机选择一个值。

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

公式中用到了Math.floor()方法,这是因为Math.random()总返回一个小数值。而用这个小数值乘以一个整数,然后再加上一个整数,最终结果仍然是一个小数。举例来说,如果你想选择一个1到10之间的数值,可以像下面这样编写代码:

var num = Math.floor(Math.random() * 10 + 1);

总共有10个可能的值(1到10),而第一个可能的值是1。而如果想要选择一个介于2和10之间的值,就应该将上面的代码改成这样:

var num = Math.floor(Math.random() * 9 + 2);

从2数到10要数的9个数,因此可能只的总数就是9,而第一个可能的值就是2。在大多数情况下,其实都可以通过一个函数来计算可能只的总数和第一个可能的值。例如:

function selectFrom(lowerValue,upperValue){
    var choices = upperValue - lowerValue;
    return Math.floor(Math.random() * choices + lowerValue);
}
var num = selectFrom(2,10);

函数selectFrom()接收两个参数:应该返回的最小值和最大值。而用最大值减去最小值加1得到了可能的总数,然后它又把这些数值套用到了公式中。这样,通过调用selectFrom(2,10)就可以得到一个介于2和10之间的数值了。利用这个函数,可以方便地从随机数组中随机取出一项,例如:

var colors = ["red","green","blue","yellow","black","purple","brown"];
var color = colors[selectFrom(0,colors.length-1)];
alert(color);   // 可能数组中包含的任何一个字符串

在这个例子中,传递给selectFrom()的第二个参数是数组的长度减1,也就是数组中的最后一项位置。

5 其他方法

Math对象中还包含其他一些与完成各种简单或者复杂计算有关的方法,但是详细讨论其中每一个方法的细节以及适用情形超出了本书的范围。下面就给出了一个表格,其中列出了这些没有介绍到的Math对象的方法。

这里写图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值