JavaScript是一种描述性语言,是一种基于对象和事件驱动的,具有安全性能的脚本语言。
JavaScript语言是通过一种叫做“原型”的方式来实现面向对象编程的。
一、对象
(1)内置对象
String、Date、Array、Boolean、Math、RegExp…这里不做赘述。
(2)自定义对象
方式一:基于Object对象的方式创建对象
<script>
var user = new Object();
user.id = 1;
user.name = "张三";
user.showName = function() {
alert(this.name);
}
user.showName();//调用方法测试
</script>
方式二:使用字面量赋值方式创建对象(Json格式)
<script>
var user = {
id:1,
name:"张三",
showName:function(){
alert(this.name);
}
};
user.showName();//调用方法测试
</script>
二、构造函数
构造函数可用来创建特定的类型的对象,像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中,此外,我们也可以自定义构造函数。
原理:所谓的“构造函数”就是一个普通函数,但是内部使用了this变量,对构造函数使用new操作符,就能生成实例,并且this变量会绑定在实例对象上,从而定义自定义对象类型的属性和方法。
创建自定义构造方法:
<script>
function User(id, name) {
this.id = id;
this.name = name;
this.showName = function() {
alert(this.name);
}
}
// 测试1
var user1 = new User(1,"张三");
user1.showName(); // 张三
// 测试2
var user2 = new User(2,"李四");
user2.showName(); // 李四
</script>
由于上述构造函数,在使用new创建每一个对象时,showName()方法都要在每个实例上重新创建一遍,这个完全没有必要,故此,我们可以将showName()的定义转移到构造函数的外部来解决此问题。
<script>
function User(id, name) {
this.id = id;
this.name = name;
this.showName = showName;
}
function showName() {
alert(this.name);
}
// 测试1
var user1 = new User(1,"张三");
user1.showName(); // 张三
// 测试2
var user2 = new User(2,"李四");
user2.showName(); // 李四
</script>
这样的确解决了三个函数做同一件事的问题,可是新问题又来了,在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。如果对象需要定义很多方法,那么就要定义很多全局方法,于是这个自定义的引用类型就丝毫没有封装性可言了。对于上述问题,我们可以采用下面的原型对象予以解决。
三、原型对象
在JavaScript中创建的每一个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。按照字面理解,prototype就是通过调用构造函数而创建的那个对象实例的原型对象,使用原型对象的好处就是可以让所有对象实例共享它所有的属性和方法。
<script>
function User() {
}
User.prototype.id = 1;
User.prototype.name = "张三";
User.prototype.showName = function() {
alert(this.name);
}
// 测试1
var user1 = new User();
user1.showName();
// 测试2
var user2 = new User();
user2.showName();
// 测试
alert(user1.shoeName == user2.shoeName);//true
</script>
分析: 在默认情况下,所有原型对象都会自动获得一个construct(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针,而通过这个构造函数还可以继续为原型对象添加其他属性和方法。
创建自定义的构造函数之后,其原型对象默认会取得construct属性,其他方法则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(很多实现中,这个指针为_proto_
)指向构造函数的原型对象。
由上可知,虽然可以通过对象实例访问保存在原型中的共享属性值,但却不能通过对象实例重写原型中的值。如果在实例中添加一个属性,该属性与实例原型中的一个属性同名,那就会在实例中创建该属性,该属性将会屏蔽(不去访问)原型中的那个属性。
访问原则(类似全局与局部变量):先在本实例中查找指定属性,如果没有该属性,则会去原型中查找该属性;如果本实例中有该属性,则直接返回该属性值,不再去原型中查找,如下例。
<script>
function User() {
}
User.prototype.id = 1;
User.prototype.name = "张三";
User.prototype.showName = function() {
alert(this.name);
}
// 测试
var user1 = new User();
var user2 = new User();
user1.name = "周星驰";
alert(user1.name); // 周星驰
alert(user2.name); // 张三
</script>
四、继承
ECMAScript是一种脚本语言标准,JavaScript语言就是遵循该标准的一种体现。许多OO语言都支持两种继承方式:接口继承、实现继承。而ECMAScript只支持实现继承,而且ECMAScript中的继承主要是依靠 原型链 来实现的。
(1)原型链
在JavaScript中,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针(constructor),而实例对象都包含一个指向原型对象的内部指针(_proto_
)。如果将一个类型的实例赋值给原型对象,此时的原型对象将包含一个指向另一个原型的指针(_proto_
),另一个原型也包含着一个指向又一个构造函数的指针(constructor)……,如此层层递进,就构成了实例与原型的链条,即 原型链。
<script>
function Humans() {
this.foot = 2;
}
Humans.prototype.getFoot = function() {
return this.foot;
}
function Man() {
this.head = 1;
}
Man.prototype = new Humans(); // 继承了Humans
Man.prototype.getHead = function() {
return this.head;
}
//测试
var man1 = new Man();
alert(man1.getFoot());//2
alert(man1 instanceof Object);//true
alert(man1 instanceof Humans);//true
alert(man1 instanceof Man);//true
</script>
分析: 以上代码定义了两个类型,分别为Humans和Man,每个类型分别有一个属性和一个方法,他们的主要区别是Man继承了Humans,而继承是通过创建Humans的实例,并将这个实例赋值给Man.prototype实现的。实际上就是重写原型对象,赋值于一个新类型的实例,也就是说,原来存在于Humans的实例中的所有属性和方法,现在也存在于Man.prototype中了,在确立了继承关系之后,又给Man.prototype添加了一个方法,这样就在继承了Humans的属性和方法的基础上又添加了一个新方法。
所有的引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype,这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。
原型链虽然强大,可以用它来实现继承,但是也存在两个问题。
第一个问题:创建子类型实例时,不能向父类型的构造函数传递参数,本质是没有办法在不影响所有对象实例的情况下,给父类型的构造函数传递参数。
第二个问题:包含引用类型值的原型属性会被所有实例共享。在通过原型来实现继承时,原型实际上会变成另一个类型的实例,那么原来的实例属性也就变成现在的原型属性了(被共享了)。如下例:
<script>
function Humans() {
this.clothing = ["连衣裙","夹克","背心"];
}
function Man() {
}
Man.prototype = new Humans(); // 继承了Humans
var man1 = new Man();
var man2 = new Man();
// 测试
man1.clothing.push("大衣");
alert(man1.clothing);// 连衣裙,夹克,背心,大衣
alert(man2.clothing);// 连衣裙,夹克,背心,大衣
</script>
基于以上两个原因,实际开发中很少会单独使用原型链。为了解决原型中包含引用类型值所带来的问题时,可以使用一种叫做借用 构造函数 的技术。
(2)借用构造函数(constructor stealing)
基本思想:在子类型构造函数的内部调用父类型构造函数,即在子类型构造函数的内部通过apply()
和call()
方法调用父类型的构造函数,也可以在将来新创建的对象上执行构造函数。
apply([thisObj[,argArray]]); //最多两个参数,第一个参数是函数运用的作用域,第二个是参数数组。
call([thisObj[,arg1[,arg2[, [argN]]]]]); //可以多个参数,第二个参数起即为传递给函数的参数列表
例:
<script>
function Humans() {
this.clothing = ["连衣裙","夹克","背心"];
}
function Man() {
Humans.call(this);// 使用借用构造函数继承了Humans
}
var man1 = new Man();
var man2 = new Man();
// 测试
man1.clothing.push("大衣");
alert(man1.clothing);// 连衣裙,夹克,背心,大衣
alert(man2.clothing);// 连衣裙,夹克,背心
</script>
构造函数还有一个优点,即向父类型构造函数传递参数。
<script>
function Humans(foot) {
this.foot = foot;
}
function Man() {
Humans.call(this, 2);// 使用借用构造函数继承了Humans,并传参
this.age = 23;
}
var man1 = new Man();
var man2 = new Man();
// 测试
alert(man1.foot);// 2
alert(man2.age);// 23
</script>
如果仅仅使用借用构造函数技术,也将无法避免构造函数模式存在的问题,那就是方法都在构造函数中定义,因此函数复用就无从谈起了,而且在父类型的原型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式。基于这些问题,组合继承 很好地解决了这些。
(3)组合继承(combination inheritance)
也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。
实现思路:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来说实现对实例属性的继承。这样既实现了函数复用,又保证了每个实例都有它自己的属性。
<script>
function Humans(foot) {
this.foot = foot;
this.clothing = ["连衣裙","大衣"];
}
Humans.prototype.showFoot = function() {
alert(this.foot);
}
function Man(foot, age) {
Humans.call(this, foot);// 继承属性
this.age = age;
}
Man.prototype = new Humans();// 继承方法
Man.prototype.showAge = function() {
alert(this.age);
}
// 测试1
var man1 = new Man(2, 23);
man1.clothing.push("东北大棉袄");
alert(man1.clothing);// 连衣裙,大衣,东北大棉袄
man1.showFoot(); // 2
man1.showAge(); // 23
// 测试2
var man2 = new Man(1, 80);
alert(man2.clothing);// 连衣裙,大衣
man2.showFoot(); // 1
man2.showAge(); // 80
</script>
总结:组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最为常用的继承模式。