JavaScript面向对象及继承

[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

Contents