JavaScript 面向对象与原型
ECMAScript 有两种开发模式:1.函数式(过程化),2.面向对象(OOP)
。面向对象的语言有一个标志,那就是类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。但是,ECMAScript 没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。
创建对象
创建一个对象,然后给这个对象新建属性和方法。
var person = new Object(); // 创建一个Object对象
person.name = 'strive_or_die'; // 创建一个name属性并赋值
person.age = 23; // 创建一个age属性并赋值
person.run = function () { //创建一个run()方法并返回值
return this.name + ',' + this.age;
};
alert(person.run()); //输出属性和方法的值
上面创建了一个对象,并且创建属性和方法,在 run()方法里的 this,就是代表 person对象本身。这种是 JavaScript 创建对象最基本的方法,但有个缺点,想创建一个类似的对象,就会产生大量的代码。为了解决多个类似对象声明的问题,我们可以使用一种叫做工厂模式的方法,这种方法就是为了解决实例化对象产生大量重复的问题。
function createObject(name, age) { //集中实例化的函数
var obj = new Object();
obj.name = name;
obj.age = age;
obj.run = function () {
return this.name + ',' + this.age;
};
return obj;
}
var personA = createObject('strive_or_die', 22); //第一个实例
var personB = createObject('Jack', 23); //第二个实例
alert(personA.run());
alert(personB.run()); //保持独立
工厂模式解决了重复实例化的问题,但还有一个问题,那就是识别问题,因为根本无法搞清楚他们到底是哪个对象的实例。
//基于上面的personA和personB
alert(typeof personA); //Object
alert(personA instanceof Object); //true
ECMAScript 中可以采用构造函数(构造方法)可用来创建特定的对象。类型于 Object 对象。
function Person(name, age) { // 构造函数模式
this.name = name;
this.age = age;
this.run = function () {
return this.name + ',' + this.age;
};
}
var personA = new Person('strive_or_die', 22); //new Person() 即可
var personB = new Person('Jack', 23);
alert(personA.run());
alert(personB instanceof Person); //很清晰的识别他从属于Person
使用构造函数的方法,即解决了重复实例化的问题,又解决了对象识别的问题,但问题是,这里并没有 new Object(),为什么可以实例化 Person(),这个是哪里来的呢?使用了构造函数的方法,和使用工厂模式的方法他们不同之处如下:
-
1.构造函数方法没有显示的创建对象(new Object());
-
2.直接将属性和方法赋值给 this 对象;
-
3.没有 renturn 语句。
造函数的方法有一些规范:
-
1.函数名和实例化构造名相同且大写,(注意:非强制,但这么写有助于区分构造函数和普通函数);
-
2.通过构造函数创建对象,必须使用 new 运算符。
既然通过构造函数可以创建对象,那么这个对象是哪里来的,new Object()在什么地方执行了?
执行的过程如下:
-
1.当使用了构造函数,并且 new 构造函数(),那么就后台执行了 new Object();
-
2.将构造函数的作用域给新对象,(即 new Object()创建出的对象),而函数体内的 this 就代表 new Object()出来的对象。
-
3.执行构造函数内的代码;
-
4.返回新对象(后台直接返回)。
关于 this 的使用,this 其实就是代表当前作用域对象的引用。如果在全局范围 this 就代表 window 对象,如果在构造函数体内,就代表当前的构造函数所声明的对象。
var num = 2;
alert(this.num); // 全局,代表window
构造函数和普通函数的唯一区别,就是他们调用的方式不同。只不过,构造函数也是函数,必须用 new 运算符来调用,否则就是普通函数。
var person = new Person('strive_or_die', 22); // 构造模式调用
alert(person.run());
Person('strive_or_die', 23); // 普通模式调用,无效
var o = new Object();
Person.call(o, 'Jack', 24) // 函数,对象冒充调用
探讨构造函数内部的方法(或函数)的问题,首先看下两个实例化后的属性或方法是否相等。
var personA= new Person('strive_or_die', 23); // 传递一致
var personB = new Person('strive_or_die', 23); // 同上
alert(personA.name == personB.name); //true,属性的值相等
alert(personA.run == personB.run); //false,方法其实也是一种引用地址
alert(personA.run() == personB.run()); //true,方法的值相等,因为传参一致
可以把构造函数里的方法(或函数)用 new Function()方法来代替,得到一样的效果,更加证明,他们最终判断的是引用地址,唯一性。
function Person(name, age) { //new Function()唯一性
this.name = name;
this.age = age;
this.run = new Function("return this.name + ',' + this.age");
}
我们可以通过构造函数外面绑定同一个函数的方法来保证引用地址的一致性,但这种做法没什么必要,只是加深学习了解:
function Person(name, age) {
this.name = name;
this.age = age;
this.run = run;
}
function run() { //通过外面调用,保证引用地址一致
return this.name + ',' + this.age;
}
虽然使用了全局的函数 run()来解决了保证引用地址一致的问题,但这种方式又带来了一个新的问题,全局中的 this 在对象调用的时候是 Person本身,而当作普通函数调用的时候,this 又代表 window。
原型
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个对象,它的用途是包含可以由特定类型的所有实例共享的属性和方法。逻辑上可以这么理解:prototype 通过调用构造函数而创建的那个对象的原型对象。使用原型的好处可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。
function Person() {// 声明一个构造函数
}
Person.prototype.name = 'strive_or_die'; // 在原型里添加属性
Person.prototype.age = 23;
Person.prototype.run = function () { //在原型里添加方法
return this.name + ',' + this.age;
};
比较一下原型内的方法地址是否一致:
var personA = new Person();
var personB = new Person();
alert(personA.run == personB.run); //true,方法的引用地址保持一致
为了更进一步了解构造函数的声明方式和原型模式的声明方式,我们通过图示来了解一下:
构造函数方式
原型模式方式
在原型模式声明中,多了两个属性,这两个属性都是创建对象时自动生成的。 proto 属性是实例指向原型对象的一个指针,它的作用就是指向构造函数的原型属性 constructor。通过这两个属性,就可以访问到原型里的属性和方法了。IE 浏览器在脚本访问 proto 会不能识别,火狐和谷歌浏览器及其他某些浏览器均能识别。虽然可以输出,但无法获取内部信息。
alert(personA.proto); //[object Object]
判断一个对象是否指向了该构造函数的原型对象,可以使用 isPrototypeOf()方法来测试。
alert(Person.prototype.isPrototypeOf(personA)); //只要实例化对象,即都会指向原型
原型模式的执行流程:
-
1.先查找构造函数实例里的属性或方法,如果有,立刻返回;
-
2.如果构造函数实例里没有,则去它的原型对象里找,如果有,就返回;
虽然我们可以通过对象实例访问保存在原型中的值,但却不能访问通过对象实例重写原型中的值。
var personA = new Person();
alert(personA.name); //strive_or_die,原型里的值
personA.name = 'Jack';
alert(personA.name); //Jack,就近原则,参考上面的原型的执行流程
var personB = new Person();
alert(personB.name); //strive_or_die,原型里的值,没有被personA修改
如果想要personA也能在后面继续访问到原型里的值,可以把构造函数里的属性删除即可,具体如下:
delete personA.name; // 删除属性,当然此属性要是可以删除的。
alert(personA.name);
如何判断属性是在构造函数的实例里,还是在原型里?可以使用 hasOwnProperty()函数来验证:
alert(personA.hasOwnProperty('name')); //实例里有返回true,否则返回 false
构造函数实例属性和原型属性示意图
in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。
alert('name' in personA); //true,存在实例中或原型中
我们可以通过 **hasOwnProperty()**方法检测属性是否存在实例中,也可以通过 in 来判断实例或原型中是否存在属性。那么结合这两种方法,可以判断原型中是否存在属性。
function isProperty(object, property) { //判断原型中是否存在属性
return !object.hasOwnProperty(property) && (property in object);
}
var personA = new Person();
alert(isProperty(personA, 'name')) //true,如果原型有
为了让属性和方法更好的体现封装的效果,并且减少不必要的输入,原型的创建可以使用字面量的方式:
function Person() {};
Person.prototype = { //使用字面量的方式
name : 'strive_or_die',
age : 100,
run : function () {
return this.name + ',' + this.age;
}
};
使用构造函数创建原型对象和使用字面量创建对象在使用上基本相同,但还是有一些区别,字面量创建的方式使用 constructor 属性不会指向实例,而会指向 Object,构造函数创建的方式则相反。
var poerson = new Person();
alert(person instanceof Person);
alert(person instanceof Object);
alert(person .constructor == Person); //字面量方式,返回 false,否则,true
alert(person .constructor == Object); //字面量方式,返回 true,否则,false
如果想让字面量方式的 constructor 指向实例对象,那么可以这么做:
Person.prototype = {
constructor : Person, // 直接强制指向即可
};
注意:字面量方式为什么 constructor 会指向 Object?因为 Person.prototype={};这种写法其实就是创建了一个新对象。而每创建一个函数,就会同时创建它 prototype,这个对象也会自动获取 constructor 属性。所以,新对象的 constructor 重写了 Person原来的 constructor,因此会指向新对象,那个新对象没有指定构造函数,那么就默认为 Object。
原型的声明是有先后顺序的,所以,重写的原型会切断之前的原型。
function Person() {};
Person.prototype = {
age = 200
};
Person.prototype = { // 原型被重写了
constructor : Person,
name : 'strive_or_die',
age : 100,
run : function () {
return this.name + ',' +this.age;
}
};
var person = new Person(); // 在这里声明
alert(person.run()); //person只是最初声明的原型,调用run会报错。
原型对象不仅仅可以在自定义对象的情况下使用,而 ECMAScript 内置的引用类型都可以使用这种方式,并且内置的引用类型本身也使用了原型。
alert(Array.prototype.sort); //sort就是Array类型的原型方法
alert(String.prototype.substring); //substring 就是String类型的原型方法
String.prototype.addstring = function () { //给String类型添加一个方法
return this + ',被添加了!'; //this代表调用的字符串
};
alert('strive_or_die'.addstring()); //使用这个方法
注意:尽管给原生的内置引用类型添加方法使用起来特别方便,但我们不推荐使用这种方法。因为它可能会导致命名冲突,不利于代码维护。
原型模式创建对象也有自己的缺点,它省略了构造函数传参初始化这一过程,带来的缺点就是初始化的值都是一致的。而原型最大的缺点就是它最大的优点,那就是共享。原型中所有属性是被很多实例共享的,共享对于函数非常合适,对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题:
function Person() {};
Person.prototype = {
constructor : Person,
name : 'strive_or_die',
age : 23,
hobbies : ['篮球', '足球'], //添加了一个数组属性
run : function () {
return this.name + ',' + this.age + ',' + this.hobbies;
}
};
var personA = new Person();
personA.hobbies.push('跑步'); // 在实例中添加'跑步'
alert(personA.run());
var personB = new Person();
alert(personB.run()); // 共享带来的麻烦,也有'跑步'了,其实是personA中添加的,但是在peronB也能访问到,这就问题大了。
数据共享的缘故,导致很多开发者放弃使用原型,因为每次实例化出的数据需要保留自己的特性,而不能共享。
为了解决构造传参和共享问题,可以组合构造函数+原型模式:
function Person(name, age) { // 不共享的使用构造函数
this.name = name;
this.age = age;
this.hobbies : ['篮球', '足球'], //添加了一个数组属性
};
Person.prototype = { // 共享的使用原型模式
constructor : Person,
run : function () {
return this.name + ',' + this.age + ',' + this.hobbies;
}
};
这种混合模式很好的解决了传参和引用共享的大难题。是创建对象比较好的方法。
原型模式,不管你是否调用了原型中的共享方法,它都会初始化原型中的方法,并且在声明一个对象时,构造函数+原型部分让人感觉又很怪异,最好就是把构造函数和原型封装到一起。为了解决这个问题,我们可以使用动态原型模式。
function Person(name ,age) { // 将所有信息封装到函数体内
this.name = name;
this.age = age;
if (typeof this.run != 'function') { //仅在第一次调用的初始化
Person.prototype.run = function () {
return this.name + ',' + this.age + ',' + this.hobbies;
};
}
}
var person = new Person('strive_or_die', 23);
alert(person.run());
当第一次调用构造函数时,run()方法发现不存在,然后初始化原型。当第二次调用,就不会初始化,并且第二次创建新对象,原型也不会再初始化了。这样及得到了封装,又实现了原型方法共享,并且属性都保持独立。
注意:使用动态原型模式,要注意一点,不可以再使用字面量的方式重写原型,因为会切断实例和新原型之间的联系。
以上讲解了各种方式对象创建的方法,如果这几种方式都不能满足需求,可以使用一开始那种模式:寄生构造函数。
function Person(name, age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.run = function () {
return this.name + ',' + this.age;
};
return obj;
}
寄生构造函数,其实就是工厂模式+构造函数模式。这种模式比较通用,但不能确定对象关系,所以,在可以使用之前所说的模式时,不建议使用此模式。
在什么情况下使用寄生构造函数比较合适呢?假设要创建一个具有额外方法的引用类型。由于之前说明不建议直接 String.prototype.addstring
,可以通过寄生构造的方式添加。
function myString(string) {
var str = new String(string);
str.addstring = function () {
return this + ',被添加了!';
};
return str;
}
var test = new myString('strive_or_die'); //比直接在引用原型添加要繁琐好
alert(test.addstring())
在一些安全的环境中,比如禁止使用 this 和 new,这里的 this 是构造函数里不使用 this,这里的 new 是在外部实例化构造函数时不使用 new。这种创建方式叫做稳妥构造函数,稳妥构造函数和寄生类似。
function Person(name , age) {
var obj = new Object();
obj.run = function () {
return this.name + ',' + this.age; //直接打印参数即可
};
return obj;
}
var person = Person('strive_or_die', 100); // 直接调用函数,没使用new关键字
alert(person.run());
继承
继承是面向对象中一个比较核心的概念。其他正统面向对象语言都会用两种方式实现继承:一个是接口实现,一个是继承。而 ECMAScript 只支持继承,不支持接口实现,而实现继承的方式依靠原型链完成。
function SuperA() { //SuperA 构造
this.name = 'strive_or_die';
}
function SubB() { //SubB构造
this.age = 23;
}
SubB.prototype = new SuperA(); //SubB继承了SuperA,通过原型,形成链条
var subB = new SubB();
alert(subB.age);
alert(subB.name); // 得到被继承的属性
原型链继承流程图
以上原型链继承还缺少一环,那就是 Obejct,所有的构造函数都继承自 Obejct。而继承 Object 是自动完成的,并不需要程序员手动继承。经过继承后的实例,他们的从属关系会怎样呢?
alert(subB instanceof Object); //true
alert(subB instanceof SuperA); //true
在 JavaScript 里,被继承的函数称为超类型(父类,基类也行,其他语言叫法),继承的函数称为子类型(子类,派生类)。继承也有之前问题,比如字面量重写原型会中断关系,使用引用类型的原型,并且子类型还无法给超类型传递参数。
为了解决引用共享和超类型无法传参的问题,我们采用一种叫**借用构造函数&&的技术,或者成为对象冒充(伪造对象、经典继承)的技术来解决这两种问题。
function SuperA(age) {
this.name = ['strive_or_die', 'Jack', 'Hello']
this.age = age;
}
function SubB(age) {
SuperA.call(this, age); // 对象冒充,给超类型传参
}
var subB = new SubB(24);
alert(subB.age); //24,通过对象冒充,传给超类的参数。
alert(subB.name);
subB.name.push('AAA'); //添加的新数据,只给subB
alert(desk.name);
借用构造函数虽然解决了刚才两种问题,但没有原型,复用则无从谈起。所以,我们需要原型链+借用构造函数的模式,这种模式成为组合继承。
function SuperA(age) {
this.name = ['strive_or_die', 'Jack', 'Hello']
this.age = age;
}
SuperA.prototype.run = function () {
return this.name + this.age;
};
//1
function SubB(age) {
SuperA.call(this, age); //对象冒充
}
//2
SubB.prototype = new SuperA(); //原型链继承
var subB = new SubB(23);
alert(subB.run());
继承就只说了这么几种,另外还有寄生式继承和寄生组合式继承,在这里就不多讲了。