在ECMAScript中,引用类型是一种数据结构,用于将数据和功能组织在一起。它在其他语言中通常被称作类,但这种称呼在ECMAScript中并不妥当,因为虽然ECMAScript从技术上来说是一门面向对象的语言,但它不具备传统的面向对象语言所支持的类和接口等基本结构。引用类型有时候也被称作对象定义,因为它描述的是一类对象所具有的属性和方法。
如前所述,对象是某个特定引用类型的实例。一个对象是使用new操作符后接构造函数来创建的。构造函数本身是一个函数,只不过该函数是出于创建新对象的目的而定义的。下面是一个创建对象的例子:
var person = new Object();
上面的代码创建了Object引用类型的一个实例,然后把该实例保存在了person变量中。使用的构造函数是Object(),它只为对象定义了默认的属性和方法。ECMAScript内置了许多引用类型(例如Object,Array等),供开发人员直接使用。
1. Object类型
Object类型是ECMAScript中使用最多的类型,虽然其不具备太多的功能,但对于在应用程序中存储和传输数据来说,确实是一种非常理想的选择。创建Object类型的实例有两种方式:
- new操作符后跟Object构造函数
- 使用对象字面量表示法
下面是一个使用new操作符创建Object实例的例子:
var person = new Object();
person.name = "Ivan";
person.age = 22;
下面是一个使用对象字面量表示法创建Object实例的例子:
var person = {
name: "Ivan",
age: 22
}
我们可以看到,使用字面量表示法创建对象,简化了创建包含大量属性的对象的过程。另外,使用对象字面量语法时,如果留空其花括号,则等价于new Object()
,下面是一个例子:
var person = {};
person.name = "Ivan";
person.age = 22;
在通过字面量定义对象时,实际上不会调用Object构造函数。
一般来说,访问对象属性时使用的都是点表示法,这也是很多面向对象语言通用的语法。不过,在JavaScript中,也可以通过方括号([])表示法来访问对象属性。在使用方括号表示法时,应该将要访问的属性以字符串的形式放在方括号中,如下面的例子所示:
alert(person["name"]); //等价于alert(person.name)
从功能上看,这两种访问属性的方式没有任何区别。但方括号表示法的优点在于可以通过变量来访问属性,例如:
var propertyName = "name";
alert(person[propertyName]);
另外,如果属性名中包含特殊字符,也只能使用方括号表示法:
alert(person["first name"]); //属性名包含空格
通常,除非必须使用变量访问属性,否则我们建议使用点表示法。
2. Function类型
在ECMAScript中,函数的本质实际上是对象。每个函数都是Function类型的实例,并且与其他引用类型一样拥有属性和方法。由于函数是对象,因此函数名实际上是指向函数对象的指针。函数通常使用函数声明语法定义,例如:
function sum(num1, num2){
return num1 + num2;
}
另一种定义函数的方式是使用函数表达式:
var sum = function(num1, num2){
return num1 + num2;
};
由于函数名仅仅是指向函数对象的指针,因此它和其他包含对象指针的变量没有什么不同。换句话来说,一个函数可能会有多个名字。下面是一个例子:
function sum(num1, num2){
return num1 + num2;
}
alert(sum(10, 10)); //20
var sum2 = sum;
alert(sum2(10, 10)); //20
sum = null;
alert(sum2(10, 10)); //20
2.1 没有重载
将函数名想象成指针,也有助于我们理解ECMAScript中为什么没有函数重载的概念。下面是一个例子:
function increase(num){
return num + 1;
}
function increase(num){
return num + 2;
}
alert(increase(100)); //102
上面的例子声明了两个同名函数,结果是后面的函数定义覆盖了前面一个。因为,上面的代码实际上与下面的代码没有区别:
var increase = function (num){
return num + 1;
}
increase = function (num){
return num + 2;
}
alert(increase(100)); //102
2.2 函数声明与函数表达式
如前文所述,函数定义可以通过两种方式:函数声明和函数表达式。实际上,解析器在向执行环境中加载数据时,对函数声明和函数表达式并非一视同仁。解析器会率先读取函数声明,并使其在执行任何代码前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会被真正解释执行。请看下面的例子:
alert(sum(10, 10)); //20
function sum(num1, num2){
return num1 + num2;
}
上面的代码完全可以正常运行。因为在代码开始执行之前,解析器就通过一个名为函数声明提升(function declaration hoisting)的过程,读取并将函数声明添加到执行环境中。然而,将上面的函数声明改为等价的函数表达式,就会在执行期间产生"unexpected identifier"的错误。
除了什么时候可以通过变量访问函数这一点区别外,函数声明与函数表达式的语法其实是等价的。
2.3 作为值的函数
由于ECMAScript中的函数名本身就是变量,所以函数也可以作为值来使用。也就是说,不仅可以像传递参数一样将一个函数传递给另一个函数,也可以将一个函数作为另一个函数的结果返回。请看下面的例子:
function callSomeFunction(someFunction, someArgument){
return someFunction(someArgument);
}
function plus10(num){
return num + 10;
}
var result1 = callSomeFunction(plus10, 10);
alert(result1); //20
function getGreeeting(name){
return "Hello" + name;
}
var result2 = callSomeFunction(getGreeting, "Ivan");
alert(result2); //"Hello, Ivan"
另外,也可以从一个函数中返回另一个函数,而且这是一个极为有用的技术。下面是一个根据传递进去的对象属性对一个对象数组进行排序的例子:
function createComparisionFunction(propertyName){
return funtion(obj1, obj2){
var v1 = obj1[propertyName];
var v2 = obj2[propertyName];
if (v1 < v2){
return -1;
} else if (v1 > v2){
return 1;
}else{
return 0;
}
};
}
var persons = [{name: "Ivan", age: 19}, {name: "Roy", age: 22}];
persons.sort(createComparisionFunction("name"));
alert(persons[0].name); //"Ivan"
persons.sort(createComparisionFunction("age"));
alert(persons[0].age); //19
2.4 函数内部属性
在函数内部,有两个特殊的对象,分别为arguments和this。argumnets在前文中已经提及,它是一个类数组对象,保存中传入函数中的所有参数。而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可能在代码执行过程中引用不同的对象。当在全局作用域中调用sayColor()函数时,this引用全局对象window,因此对this.color求值就会转换为对window.color求值,因此输出"red"。而当把这个函数赋值给对象o并调用o.sayColor()方法时,this引用的对象是o,因此this.color求值就会转换为对o.color求值,因此输出"blue"。
2.5 函数属性和方法
如前文所说,ECMAScript中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:length和prototype。其中,length表示函数希望接收的命名参数个数。而prototype属性则存在于所有的引用类型中,保存了该引用类型的所有实例方法。因此,在自定义引用类型和实现继承时,prototype属性是极其重要的。
另外,每个函数都包含了两个非继承而来的方法:apply()和call()。这两个方法的用途都是使函数运行在特定的作用域中,实际上是设置函数体内this对象的值。首先apply()方法接收两个参数:第一个参数是在其中运行函数的作用域,第二个参数是一个参数数组(或者arguments对象)。下面是一个例子:
function sum (num1, num2){
return num1 + num2;
}
fucntion callSum1(num1, num2){
return sum.apply(this, arguments);
}
function callSum2(num1, num2){
return sum.apply(this, [num1, num2]);
}
callSum1(10, 10); //20
callSum2(10, 10); //20
在上面这个例子中,callSum1()在执行 sum()函数时传入了 this 作为 this 值(因为是在全局作用域中调用的,所以传入的就是 window 对象)和 arguments 对象。而 callSum2 同样也调用了 sum()函数,但它传入的则是 this 和一个参数数组。这两个函数都会正常执行并返回正确的结果。
call()方法与apply()方法的作用完全相同,唯一的区别在于call()方法接收的是命名参数列表而不是参数数组。call()方法和apply()方法真正强大的地方在于它们能够扩充函数赖以运行的作用域。请看下面的例子:
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
上面的例子在全局环境中定义了一个函数:sayColor()。直接在全局环境中调用时,this指向此函数运行的全局环境,因此对this.color的求值会转换为对window.color的求值。而sayColor.call(this)和sayColor.call(window)则是显示的设置此函数运行时的执行环境,此处设置为在全局环境中运行sayColor()函数,因此也输出"red"。而最后使用sayColor.call(o)调用则将执行环境切换到对象o,此时函数体内的 this 对象指向了 o,因此结果显示的是"blue"。
使用 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()函数。object- SayColor()函数的 this 值等于 o,因此即使是在全局作用域中调用这个函数,也会看到"blue"。
网友评论