[TOC]
面向对象
与其他大多数语言不同,JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。其实全都是对象(Object)。
创建对象
字面量定义
直接通过创建一个对象的方式。
var person = {
name: "nick",
age: 18,
say:function() {
alert(this.name);
}
}
缺点:大量重复代码!
工厂模式
为了解决对象字面量定义存在的大量重复代码,使用常见的工厂模式
function createPerson(name, age) {
var o = new Object();
o.name = name;
o.age = age;
o.say = function () {
alert(this.name);
}
}
var xiaoming = createPerson('xiaoming', 18);
var nick = createPerson('nick', 20);
缺点:无法知道一个对象实例的类型(即对于其他语言来说,不知道是通过哪个类实例化出来的对象)。
构造函数模式
构造函数是一种特殊的函数,具有以下特点:
- 直接将属性和方法赋值给this对象
- 没有return语句
- 按照惯例,函数名首字母大写
注意:如果构造函数作为普通函数被调用(即没有使用new操作符),this指向全局对象(严格模式下将报错),返回值为undefined。
function Person (name, age) {
this.name = name;
this.age = age;
this.say = function () {
alert(this.name);
}
}
//创建实例时需使用new操作符
var xiaoming = new Person('xiaoming', 18);
var nick = new Person('nick', 20
//每个实例都有一个constructor(构造函数)属性,指向 Person()
xiaoming.constructor === Person; //true
nick.constructor === Person; //true
//但是通常会使用instanceof来检测对象类型
xiaoming instanceof Person; //true
xiaoming instanceof Object; //true
缺点:每个实例上,方法都要重新创建一遍,不能共用。(可以将函数都在全局作用域定义然后引用进来,但这有违封装性,而且全局作用域会有大量无关函数)
//每个实例中的方法并不是同一个,每次创建Person实例的时候都会生成一个新的Function实例
xiaoming.say === nick.say; //false
原型模式
每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,可以通过该对象来共享属性及方法。
function Person() {}
Person.prototype.name = 'nick';
Person.prototype.age = 20;
Person.prototype.say = function () {
alert(this.name);
};
var person1 = new Person();
person1.say(); //nick
var person2 = new Person();
person1.say === person2.say; //true
任何情况下,只要创建一个函数,就会有一个prototype(原型)属性指向函数的原型对象,默认情况下,函数的原型对象自动获得一个constructor(构造函数)属性,这是一个指针,指向prototype所在的函数。其他属性和方法则继承自Object。
调用这个构造函数所创建的实例,该实例内部会有一个指针[[prototype]]指向构造函数的原型对象,可以通过__proto__
属性访问。
function Person () {}
//原型对象
Person.prototype;
//原型对象的构造函数属性指向Person函数
Person.prototype.constructor === Person; //true
//实例的__proto__属性指向构造函数的原型对象,与构造函数没有直接关系
var nick = new Person();
nick.__proto__ === Person.prototype; //true
Object.getPrototypeOf(nick) === Person.prototype; //true
nick.constructor.prototype === Person.prototype; //true
//关系检查
Person.prototype.isPrototypeOf(nick); //true
当代码读取某个对象的某个属性时,会进行一次搜索,首先搜索对象实例本身是否存在该属性,如果不存在则根据指针指向的原型对象,在原型对象中搜索。可以访问原型中的属性,但是通过实例不能修改原型中的属性。
function Person() {}
Person.prototype.name = 'default';
Person.prototype.age = 0;
Person.prototype.say = function () {
alert(this.name);
};
//可以访问原型中的属性,但是通过实例不能修改原型中的属性
var nick = new Person();
var xiaoming = new Person();
nick.name = 'nick'; //通过一个新的值屏蔽了原型中的值
nick.name; //nick
xiaoming.name; //default
//如果想重新访问原型中的值
delete nick.name;
nick.name; //default
常用操作:
//判断属性是否存在于本实例中,而不是继承来的
xiaoming.hasOwnProperty('name'); //false
xiaoming.name = 'xiaoming';
xiaoming.hasOwnProperty('name'); //true
//判断指定属性是否存在于实例中,不区分在实例中还是原型中
'name' in xiaoming; //true
//获取所有实例属性
Object.getOwnPropertyNames(Person.prototype);
更简单的原型语法:
function Person() {}
/**
* 通过这种方式定义会直接覆盖之前默认的原型对象,
* 也就是说原型对象的constructor(构造函数)属性不再指向Person,
* 而是默认指向Object构造函数
*/
Person.prototype = {
name : 'default',
age : 0,
say : function () {
alert(this.name);
}
}
var nick = new Person();
//仍然可以通过instanceof判断
nick instanceof Person; //true
nick instanceof Object; //true
/**
* 实例nick中的constructor属性实际上是继承的原型中的constructor属性
* 因为原型对象中的constructor属性不再指向Person
* 所以nick实例中的构造函数属性也不再指向Person
*/
nick.hasOwnProperty('constructor'); //false
nick.constructor === Person; //false
nick.constructor === Object; //true
/**
* 如果constructor属性很重要,可以手动设置
* 但这会导致constructor属性的[[Enumerable]]被设为true
* 变为可枚举属性
*/
Person.prototype = {
constructor : Person, //手动指定
name : 'default',
age : 0,
say : function () {
alert(this.name);
}
}
//可以通过这种方式避免上述情况
Object.defineProperty(Person.prototype, 'constructor', {
enumerable : false,
value : Person
});
/**
* 原型的动态性
*
* 由于对象中查找属性是一次搜索, 并且原型关系通过指针维护
* 所以即使先创建实例对象,再修改原型对象,改变也会立即体现在之前创建的实例上
*/
var friend = new Person();
Person.prototype.sayHi = function () {
console.log('Hi');
};
friend.sayHi(); //Hi
/**
* 但是如果直接重写原型属性,则不会!
* 这样等于新创建了一个原型对象,并没有修改之前那个
* 而且切断了构造函数与之前原型对象的联系
* 由于实例和原型之间是直接联系,所以之前原型的属性和方法,friend仍然可用
*/
Person.prototype = {
sayNo : function() {
console.log('No');
}
};
friend.sayNo(); // error!
friend.sayHi(); //Hi
在对实例修改来自原型的属性时:
- 对于基本值属性,将会在实例中添加一个同名属性,来屏蔽原型中的值
- 对于引用类型值,则有可能会直接修改原型中的值,因为其实际是一个指针
缺点:在创建实例时无法传递初始化参数;另外由于其共享性,原型中的 引用类型值 属性(本质上是一个指针)会导致多个实例共享一个数组的问题(在一个实例上对其进行的修改会体现在其他同原型实例中)。
组合构造函数模式和原型模式(推荐)
为解决上面的问题,使用构造函数+原型模式,构造函数模式用于定义实例属性,原型模式用于定义方法。还支持初始化参数。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
alert(this.name);
}
var nick = new Person('nick', 20);
nick.say(); //nick
动态原型模式(推荐)
把原型的初始化操作写到构造函数中,只在第一次调用时执行。
function Person (name, age) {
this.name = name;
this.age = age;
if (typeof this.say !== 'function') {
Person.prototype.say = function () {
alert(this.name);
}
}
}
动态原型模式下,不能使用对象字面量形式覆盖原型对象,这将切断已创建实例与新原型之间的关系。
寄生构造函数模式
除了使用new操作符,这个模式跟工厂模式一模一样。
function Person(name, age) {
var o = {};
o.name = name;
o.age = age;
o.say = function () {
alert(this.name);
}
return o;
}
var nick = new Person('nick', 20);
一般尽量不用,只在特殊需求下使用,比如要给一个数组增加特殊功能(不能修改Array)
function sArray() {
var arr = [];
arr.push.apply(arr, arguments);
arr.toPipedString = function () {
return this.join('|');
}
return arr;
}
var zimu = new sArray('A', 'B', 'C');
zimu.toPipedString(); // A|B|C
稳妥构造函数模式
不使用new和this。
*构造函数、原型对象和实例的关系
- 构造函数的
prototype
属性指向原型对象 - 实例的
__proto__
属性指向原型对象 - 原型对象的
constructor
属性指向构造函数
继承
原型链
原型链是实现继承的主要方法。通过将原型对象等于另一个类型的实例来实现。
function Person() {
this.name = 'default';
if (typeof this.say !== 'function') {
Person.prototype.say = function () {
alert(this.name);
}
}
}
function Nick() {
this.age = 20;
}
Nick.prototype = new Person();
Nick.prototype.getAge = function () {
alert(this.age);
}
var n = new Nick();
n.say(); //default
//关系判断
n instanceof Nick; //true
n instanceof Person; //true
n instanceof Object; //true
Object.prototype.isPrototypeOf(n); //true
Person.prototype.isPrototypeOf(n); //true
Nick.prototype.isPrototypeOf(n); //true
//原型链:n -> nick -> Person -> Object
通过原型链,实例会继承上层原型的属性和方法,在读取一个属性时,依然是先从本实例开始逐层往上查找。
缺点:1.会有前面提到的多个实例共享一个原型中的引用类型值属性的问题,因为即使在构造函数中定义属性,但生成的实例变成了另一个类的原型对象。 2.在创建子类型实例时,不能向父类的构造函数中传递参数
借用构造函数
可以解决引用类型值的问题,同时支持向父类中传递参数。
function Zuzong (name) {
this.name = 'zuzong';
}
function Person (name) {
//继承父类属性,相当于在子类中执行父类代码,会得到相关属性的副本
Zuzong.call(this, name);
}
缺点:没有解决函数复用的问题,而且无法复用父类原型中的方法,所以很少使用。
组合继承
即将原型链和借用构造函数相结合:通过原型链实现对原型属性及方法的继承,通过借用构造函数来生成自己的属性副本(解决引用类型值的问题)。
function Person(name) {
this.name = name;
this.color = ['red', 'blue'];
}
Person.prototype.say = function () {
alert(this.name);
}
function Man(name, age) {
//继承父类属性,生成自己的属性副本
Person.call(this, name);
this.age = age;
}
//继承父类所有属性和方法
Man.prototype = new Person();
//本类中自定义的方法
Man.prototype.sayAge = function () {
alert(this.age);
}
//实例
var nick = new Man('nick', 28);
nick.color.push('black'); // red blue black
var xiaoming = new Man('xiaoming', 20);
xiaoming.color; // red blue