第5章 引用类型
引用类型是一种数据结构,用于将数据和功能组织在一起,有时候也被称为对象定义。它也常被称为类,但这种称呼并不妥当。
对象在 JavaScript 中被称为引用类型的值,而且有一些内置的引用类型可以用来创建特定的对象:Object类型、Array 类型、Date 类型、RegExp 类型、Function 类型、基本包装类型。
5.1 object类型
创建 Object 实例的方式有两种。第一种是使用 new 操作符后跟 Object 构造函数,另一种方式是使用对象字面量表示法。
var person = {
name : "Nicholas",
age : 29
};
上面就是字面量法,左边的花括号({)表示对象字面量的开始。在 age 属性的值 29 的后面不能添加逗号,因为 age 是这个对象的最后一个属性。
ECMAScript 中的表达式上下文指的是能够返回一个值(表达式)。
访问对象属性时使用的都是点表示法,不过,在 JavaScript 也可以使用方括号表示法来访问对象的属性。(2种方法)如果属性名中包含会导致语法错误的字符,或者属性名使用的是关键字或保留字,可使用方括号表示法。
alert(person["name"]); //"Nicholas"
alert(person.name); //"Nicholas"
5.5 function 类型
每个函数都是function类型的实例。函数是对象,函数名是指针。
定义函数的方式(3种):
function sum (num1, num2) {
return num1 + num2;
} //方法1
var sum = function(num1, num2){
return num1 + num2;
}; //方法2
方法3 使用 Function 构造函数:var sum = new Function("num1", "num2", "return num1 + num2"); // 不推荐
解析器会率先读取函数声明,并使其在执行任何代码之前可用(可以访问);至于函数表达式,则必须等到解析器执行到它所在的代码行,才会真正被解释执行。所以函数声明==>变量提升
待续5.5.3 函数作为值传递??
5.5.4
在函数内部,有两个特殊的对象:arguments 和 this
window.color = "red";
var o = { color: "blue" };
function sayColor(){
alert(this.color);
}
sayColor(); //"red"
o.sayColor = sayColor;
o.sayColor(); //"blue"
当在全局作用域中调用sayColor()时,this 引用的是全局对象 window;换句话说,对 this.color 求值会转换成对window.color 求值,于是结果就返回了"red"。而当把这个函数赋给对象 o 并调用 o.sayColor()时,this 引用的是对象 o,因此对 this.color 求值会转换成对 o.color 求值,结果就返回了"blue"。
ECMAScript 中的函数是对象,因此函数也有属性和方法。每个函数都包含两个属性:length 和 prototype。其中,length 属性表示函数希望接收的命名参数的个数
每个函数都包含两个非继承而来的方法:apply()和 call()。这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内 this 对象的值。它们真正强大的地方是能够扩充函数赖以运行的作用域
function sum(num1, num2){
return num1 + num2;
}
function callSum1(num1, num2){
return sum.apply(this, arguments); // 传入 arguments 对象
}
function callSum2(num1, num2){
return sum.apply(this, [num1, num2]); // 传入数组
}
function callSum3(num1, num2){
return sum.call(this, num1, num2); //使用call()方法时,传递给函数的参数必须逐个列举出来
}
alert(callSum3(10,10)); //20
alert(callSum1(10,10)); //20
alert(callSum2(10,10)); //20
当运行 sayColor.call(o)时,函数的执行环境就不一样了,因为此时函数体内的 this 对象指向了 o,于是结果显示的是"blue"。
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
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()函数。objectSayColor()函数的 this 值等于 o,因此即使是在全局作用域中调用这个函数,也会看到"blue"。
5.6 基本包装类型
为了便于操作基本类型值,ECMAScript 还提供了 3 个特殊的引用类型:Boolean、Number 和String。每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装类型的对象,从而让我们能够调用一些方法来操作这些数据。
自动创建的基本包装类型的对象,则只存在于一行代码的执行瞬间,然后立即被销毁。这意味着我们不能在运行时为基本类型值添加属性和方法。来看下面的例子:
var s1 = "some text";
s1.color = "red";
alert(s1.color); //undefined
因为第二行创建的 String 对象在执行第三行代码时已经被销毁了。第三行代码又创建自己的 String 对象,而该对象没有 color 属性
布尔表达式中的所有对象都会被转换为 true
第6章 面向对象的程序设计
创建自定义对象的最简单方式就是创建一个 Object 的实例,然后再为它添加属性和方法,如下所示。
var person = new Object(); //创建了一个名为 person 的对象,并为它添加了三个属性(name、age 和 job)和一个方法(sayName())
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function(){
alert(this.name);
};
前面的例子用对象字面量语法可以写成这样:
var person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",
sayName: function(){
alert(this.name);
}
};
6.2 创建对象
虽然 Object 构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
6.2.1 工厂模式
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 对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。
6.2.2 构造函数模式
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 使用的是大写字母 P。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。要创建 Person 的新实例,必须使用 new 操作符。
我们在这个例子中创建的所有对象既是 Object 的实例(是因为所有对象均继承自 Object),同时也是 Person的实例。
在前面的例子中,person1 和 person2 都有一个名为 sayName()的方法,但那两个方法不是同一个 Function 的实例。以这种方式创建函数,会导致不同的作用域链和标识符解析:alert(person1.sayName == person2.sayName); //false
6.2.3 原型模式
我们创建的每个函数都有一个 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 ,因为访问同一个sayName()函数
我们将 sayName()方法和所有属性直接添加到了 Person 的 prototype 属性中。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1 和 person2 访问的都是同一组属性和同一个 sayName()函数。
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype属性,这个属性指向函数的原型对象。创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,则都是从 Object 继承而来的。每创建一个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。也就是说,在我们调用 person1.sayName()的时候,会先后执行两次搜索。首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。”然后,它继续搜索,再问:“person1 的原型有 sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函数。
使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。
更简单的原型语法,用一个包含所有属性和方法的对象字面量来重写整个原型对象:
function Person(){
}
Person.prototype = {
constructor : Person, //不加上这句的话,constructor 属性不再指向 Person 了,指向Object。加上后,指向Person
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
原型的动态属性:我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。请看下面的例子。
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); //"hi"(没有问题!)
以上代码先创建了 Person 的一个实例,并将其保存在 person 中。然后,下一条语句在 Person. prototype 中添加了一个方法 sayHi()。即使 person 实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。也是先在实例中搜索sayHi,再到原型中搜索。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHi 属性并返回保存在那里的函数。尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。
6.2.4 组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
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,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方法 sayName()则是在原型中定义的。
待续。。。
6.3 继承
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。每个类型分别有一个属性和一个方法。它们的主要区别是 SubType 继承了SuperType,而继承是通过创建 SuperType 的实例,并将该实例赋给SubType.prototype 实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。在确立了继承关系之后,我们给 SubType.prototype 添加了一个方法,这样就在继承了 SuperType 的属性和方法的基础上又添加了一个新方法。
事实上,前面例子中展示的原型链还少一环。我们知道,所有引用类型默认都继承了 Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype。一句话,SubType 继承了 SuperType,而 SuperType 继承了 Object。
待续。。。
第7章 函数表达式
定义函数的方式有两种:一种是函数声明,另一种就是函数表达式
当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用 arguments 和其他命名参数的值来初始化函数的活动对象(activation object)。但在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。
活动对象肯定是第0位