面向对象的程序设计

面向对象(Object-Oriented,OO)的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象,比如java和c++。但是ECMAScript中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。

定义

ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”也就是说,对象是一组没有特定顺序的名对值。对象的每个属性或方法都有一个名字,而每个名字都隐射到一个值(数据或者函数)。
每个对象都是基于一个引用类型创建的。
创建自定义对象的最简单方式就是创建一个Object的实例,然后再为它 添加属性和方法:

var person = new Object();
person.name = "Eviler";
person.age = 22;
person.sayName = function(){
    alert(this.name);
}
知识兔对象字面量模式创建:

var person = {
    name: "Eviler",
    age: 22,
    sayName: function(){
        alert(this.name);
    }
}
知识兔h2 id="属性类型">属性类型

ECMA-262第五版在定义只有内部才用的特征(attribute)时,描述了属性(property)的各种特征。这些特征是为了实现javascript引擎用的,因此在JavaScript中不能直接访问它们。ESCAScript有两种属性:数据属性和访问器属性

数据属性

configurable:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。对于直接在对象上定义的属性,默认值为true。
enumerable:表示能否通过for-in循环返回属性。对于直接在对象上定义的属性,默认值为true。
writable:表示能够修改属性的值。对于直接在对象上定义的属性,默认值为true。
value:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认值为undefined。
要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。这个方法接受三个参数:属性所在的对象、属性的名字和一个描述符的对象。其中描述符对象的属性必须是:configurable、enumerable、wriable和value中的一个或者多个。例如:

var person = {};
Object.defineProperty(person, "name", {
    writable: false,
    value: "Eviler"
}),
console.log(person.name); //Eviler
person.name = "Hello";
console.log(person.name); //Eviler
知识兔son.name 的赋值语句在非严格模式下会被忽略,但在严格模式下,赋值操作将会导致抛出错误:TypeError: Cannot assign to read only property ‘name’ of object ‘#<Object>‘。

同样,对于下面的语句:

var person = {};
Object.defineProperty(person, "name", {
    configurable: false,
    value: "Eviler"
});
console.log(person.name); //Eviler
delete person.name; //非严格:忽略; 严格:TypeError: Cannot delete property 'name' of #<Object>
Object.defineProperty(person, "name", {
    configurable: false,
    value: "Eviler2"
}); //不管是非严格还是严格模式都会报错:TypeError: Cannot redefine property: name
知识兔b>特别注意:在调用Object.defineProperty()方法时,如果不指定,configurable、enumerable和writable特征的默认值都是false。

"use strict"//严格模式下
var person = {};
Object.defineProperty(person, "name", {
    value: "Eviler"
});
console.log(person.name); //Eviler
delete person.name; //TypeError: Cannot delete property 'name' of #<Object>
person.name = "Hello"; //TypeError: Cannot assign to read only property 'name' of object '#<Object>'
for (var key in person) {
    console.log(key + ": " + person[key]);
} //只会打印可枚举的熟悉
知识兔h3 id="访问器属性">访问器属性

访问器属性不包含数据值;它们包含一对getter和setter函数(都不是必需的)。在读取访问器属性时,会调用getter函数,返回有效的值;在写入属性访问器时,会调用setter函数并传入新值,这个函数负责决定图个处理数据。访问器属性有如下4个特征:
configurable:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。对于直接在对象上定义的属性,默认值为true。
enumerable:表示能否通过for-in循环返回属性。对于直接在对象上定义的属性,默认值为true。
get:在读取属性时调用的函数。默认值为undefined。
set:在写入属性时调用的函数。默认值为undefined。
访问器属性不能直接定义,必须使用Object.defineProperty()方法

"use strict"
var book = {
    _year: 2018,
    edition: 1
}
Object.defineProperty(book, "year", {
    get: function () {
        console.log("getter");
        return this._year;
    },
    set: function (newValue) {
        console.log("setter");
        if (newValue > 2018) {
            this._year = newValue;
            this.edition += newValue - 2018;
        }
    }
})
console.log(book.edition);
console.log(book.year);
book.year = 2019;
console.log(book.edition);
console.log(book.year);
// 打印结果:
1
"getter"
2018
"setter"
2
"getter"
2019
知识兔b>javascript中不存在私有属性的概念,所有属性都是公开的。为了表示私有变量,我们用_year表示,表示它只能通过对象方法访问
不一样非要指定getter和setter,只指定getter意味着属性不能写,尝试写入属性会被忽略,严格模式下,会抛出错误。类似地,没有指定setter函数的属性也不能读,否则在非严格模式写会返回undefined,而在严格模式下会抛出错误。
浏览器支持:IE 9+(IE8知识部分实现)、Firefox 4+、Safari 5+、Opera 12+和Chrome。在这个方法之前,要创建访问器属性,一般都使用两个非标准的方法:defineGetter()和defineSetter()。熟悉Vue.js的人应该知道,Vue不支持IE8及以下版本,因为Vue使用了IE8无法模拟的ECMAScript 5特性,其中就包括Object.defineProperty()这个函数,其实vue中数据的双向绑定就是使用这个特性实现的。

定义多个属性

使用ECMAScript 5的Object.defineProperties()函数,这个函数接受两个对象参数,第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应,如:

"use strict"
var book = {};
Object.defineProperties(book, {
    _year: {
        writable: true,
        value: 2018
    },
    edition: {
        writable: true,
        value: 1
    },
    year: {
        get: function () {
            return this._year;
        },
        set: function (newValue) {
            if (newValue > 2018) {
                this._year = newValue;
                this.edition += newValue - 2018;
            }
        }
    }
})
console.log(book.year);
book.year = 2019;
console.log(book.year);
浏览器支持:IE 9+、Firefox 4+、Safari 5+、Opera 12+和Chrome。
知识兔h2 id="读取属性的特征">读取属性的特征

使用ECMAScript 5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接受两个参数:属性所在的对象和要读物其描述符的属性名称,返回值是一个对象,如果是访问器属性,这个对象的属性有configurable、enumerable、get和set;如果是数据属性,这个对象的属性有configurable、enumerable、writable和value,例如:

"use strict"
var book = {};
Object.defineProperties(book, {
    _year: {
        writable: true,
        value: 2018
    },
    edition: {
        writable: true,
        value: 1
    },
    year: {
        get: function () {
            return this._year;
        },
        set: function (newValue) {
            if (newValue > 2018) {
                this._year = newValue;
                this.edition += newValue - 2018;
            }
        }
    }
})
console.log(Object.getOwnPropertyDescriptor(book, "edition"));
console.log(Object.getOwnPropertyDescriptor(book, "year"));
/* 打印结果:

{ value: 1,
writable: true,
enumerable: false,
configurable: false }
{ get: [Function: get],
set: [Function: set],
enumerable: false,
configurable: false }

*/
知识兔h1 id="创建对象">创建对象

虽然Object构造函数或对象字面量都可以用来创建单个对象,但是这两种方式有一个明显的缺点:使用同一个接口创建创建很多对象,会产生大量的重复代码。为了解决这个问题,人们开始使用工程模式的一种变体。

工程模式

这种模式抽象了创建具体对象的过程,由于ECMAScript无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节,例如:

function createPerson(name, age) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayName = function () {
        console.log(this.name);
    }
    return o;
}
var p1 = createPerson("Eviler", 22);
var p2 = createPerson("Hello", 18);
p1.sayName(); //Eviler
p2.sayName(); //Hello
知识兔createPerson()能够根据接受的参数来和构建一个包含所有信息的Person对象。可以无数地调用这个函数,而每次它都返回一个包含两个属性一个方法的对象。工程模式虽然解决了创建多个相似对象的文尼提,但却没有解决对象识别的问题(即怎样知道一个对象的类型,如下,对上面的p1使用instanceof运算符检测结果)。

console.log(p1 instanceof createPerson);//false
console.log(p1 instanceof Object);//true
知识兔h2 id="构造函数模式">构造函数模式

使用构造函数模式对前面的例子重写

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function () {
        console.log(this.name);
    }
}
var p1 = new Person("Eviler", 22);
var p2 = new Person("Hello", 18);
p1.sayName(); //Eviler
p2.sayName(); //Hello
知识兔b>要创建Person的新实例,必须使用new操作符。以这种方式调用构造函数实际会经历以下4个步骤:

  1. 创建一个新对象;
  2. 将构造函数的作用域赋值给新对象(因此this就指向了这个新对象);
  3. 执行构造函数中的代码(为这个新对象添加属性);
  4. 返回新对象。

p1和p2分别保存着Person的一个不同的实例,这两个对象都有一个constructor(构造函数)属性,该属性指向Person,如下所示:

console.log(p1.constructor == Person); //true
console.log(p1.constructor == Person); //true
知识兔对象的constructor属性最初是用来表示对象类型的。但是,instanceof操作符来检测对象类型更可靠一些,对上面的p1使用instanceof运算符检测结果:

console.log(p1 instanceof createPerson);//true
console.log(p1 instanceof Object);//true//所有对象均继承自Object,所以结果为true
知识兔h3 id="把构造函数当作函数">把构造函数当作函数

构造函数与其他函数的唯一区别,就在于调用它们的方式不同。任何函数,只要通过new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。例如,对于前面的Person的构造函数:

// 当构造函数使用
var p1 = new Person("Eviler", 22);
p1.sayName(); //Eviler
// 作为普通函数使用(this会被指向global对象,在浏览器中即window对象; 在nodejs中即global对象)
Person("Global", 15);
console.log(window.name);//Global
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "OtherObject", 20);
o.sayName(); //OtherObject
知识兔h3 id="构造函数的问题">构造函数的问题

构造函数模式对然好用,但是存在一个主要问题,就是每个方法都要在每个实例上重写创建一遍。在前面的例子中,p1和p2都有一个名为sayName()的方法,但是那两个方法不是同一个Function的实例(ECMAScript中的函数时对象),因此每定义一个函数,也就实例化了一个对象,对内存造成了一定的浪费。况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,大可像下面这样,通过把函数定义转移到构造函数外面来解决这个问题:

var sayName = function () {
    console.log(this.name);
}
function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayName = sayName;
}
var p1 = new Person("Eviler", 22);
p1.sayName(); //Eviler
知识兔虽然这样解决了重复定义函数的问题,但是又出现了新的问题:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域优点名副其实。而更仍然无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。好在,这写问题可以通过使用原型模式来解决。

原型模式

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype就是以通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法,如下:

function Person() {}
console.log(Person.prototype); // Person(){}

Person.prototype.name = "Eviler";
Person.prototype.age = 22;
Person.prototype.sayName = function () {
    console.log(this.name);
}
console.log(Person.prototype); //Person { name: 'Eviler', age: 22, sayName: [Function] }
var p1 = new Person();
var p2 = new Person();
p1.sayName(); //Eviler
p2.sayName(); //Eviler
console.log(p1.sayName == p2.sayName); //true
知识兔我们将sayName()方法和所有属性直接添加到了Person的prototype属性中,构造函数变成了空函数。即使如此,也仍然可以通过构造函数来创建对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说p1和p2访问的都是同一组属性和同一个sayName()函数。我们先来看看ECMAScript原型对象的性质

理解原型对象

无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象,默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。例如:

function Person() {}
Person.prototype.name = "Nicholas"
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
知识兔对象Person、Person.prototype、person1和person2的关系如下图所示:

对象关系图

可以看出,Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。原型对象中除了包含constructor属性之外,还包括后来添加的其他属性。Person的每个实例—-person1和person2都包含一个内部属性,该属性仅仅指向了Person.prototype; 换句话说,它们与构造函数没有直接关系。虽然这两个实例都不包含属性和方法,但我们却可以调用person1.sayName()。这是通过查找对象属性的过程来实现的。
在所有实现中都无法直接访问[[Prototype]]属性,但可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系:
console.log(Person.prototype.isPrototypeOf(person1)); //true
console.log(Person.prototype.isPrototypeOf(person2)); //true
知识兔还可以通过ECMAScript5新增的Object.getPrototypeOf()方法取得实例的原型:

console.log(Object.getPrototypeOf(person1));
/*打印结果
Person {
    name: 'Nicholas',
    age: 29,
    job: 'Software Engineer',
    sayName: [Function]
}
*/
console.log(Object.getPrototypeOf(person2) === Person.prototype); //true
知识兔b>查找对象属性的过程: 搜索首先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。
事实上,person1调用的sayName()函数是第二次搜索的结果:第一次在实例本身中没有找到,第二次在原型中找到,看如下示例:

function Person() {}
Person.prototype.name = "Nicholas"
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.name = "Eviler";
person1.sayName(); //Eviler
Object.getPrototypeOf(person1).sayName(); //Nicholas
delete person1.name;
person1.sayName(); //Nicholas
delete Object.getPrototypeOf(person1).name;
person1.sayName(); //undefined
知识兔使用hasOwnProperty()方法可以检测一个属性是否存在于实例中,还是存在于原型中。这个方法只在给定属性存在于对象实例中时,才会返回true,如下所示:

function Person() {}
Person.prototype.name = "Nicholas"
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
person1.name = "Eviler";
console.log(person1.hasOwnProperty("name")); //true
console.log(person2.hasOwnProperty("name")); //fasle
知识兔ECMAScript 5的Object.getOwnPropertyDescriptor()方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescriptor()方法,如:

Object.getOwnPropertyDescriptor(Object.getPrototypeOf(person1), "name")
知识兔h3 id="原型与-in-操作符">原型与 in 操作符

有两种方法使用 in 操作符:单独使用和在for-in循环中使用。
在单独使用时,in 操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中,如下所示:

function Person() {}
Person.prototype.name = "Eviler";
var person1 = new Person();
var person2 = new Person();
person1.age = 22;
console.log("age" in person1); //true
console.log("name" in person2); //true
console.log("job" in person2); //false
知识兔所以我们可以用 in 操作符和 Object.hasOwnProperty()来确定一个属性是否为原型属性:

console.log(("name" in person1) && !person1.hasOwnProperty("name"));
知识兔在使用for-in循环时,返回的是所有能够通过对象访问、可枚举(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会在for-in循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的(IE8及更早版本中例外)。
要取得对象上所有课枚举的实例属性,可以使用ECMAScript 5的Object.keys()方法,这个方法接受一个对象作为参数,返回一个包含所有课枚举属性的字符串数组。例如:

function Person() {}
Person.prototype.name = "Nicholas"
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
    console.log(this.name);
}
var person1 = new Person();
var person2 = new Person();
for (var key in person1) {
    console.log(key);
}
/* 打印结果
name
age
job
sayName
*/
console.log(Object.keys(person1));
/* 打印结果
[]
*/
知识兔还可以使用Object.getOwnPropertyNames()方法获取所有实例属性,不论它是否可枚举,如下所示:

console.log(Object.getOwnPropertyNames(Object.getPrototypeOf(person1)));
// [ 'constructor', 'name', 'age', 'job', 'sayName' ]
知识兔h3 id="更简单的原型语法">更简单的原型语法
function Person() {}
Person.prototype = {
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function () {
        console.log(this.name);
    }
}
知识兔这样创建的效果就和前面那样创建的效果一样了,但是有一点不一样: constructor属性不再指向Person了,前面说过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而我们在这里使用的语法,本质是是完全重写了默认的prototype对象,因此constructor属性属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。此时,使用 instanceof 操作符能返回正确的结果,但是不能通过constructor 来确定对象的类型。如果constructor真的很重要,可以特意将它设置回适当的值(但是这样会使constructor的[[enumerable]]特征被设置为true):

function Person() {}
Person.prototype = {
    constructor: Person,
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function () {
        console.log(this.name);
    }
}
知识兔h3 id="原型的动态性">原型的动态性

由于原型中查找值得过程是一次搜索,因此我们对原型对象上所做得任何修改都能够立即从实例上反应出来—即使先创建了实例后修改原型也照样如此,如下所示:

function Person() {}
Person.prototype = {
    name: "Eviler",
    sayName: function () {
        console.log(this.name);
    }
}
var person = new Person();
person.sayName();
Person.prototype.sayHi = function () {
    console.log("hi");
}
person.sayHi(); //hi (调用成功,即使先创建了实例)
知识兔其原因可以归根结底为实例与原型直接得松散连接关系(实例与原型之间得连接只不过是一个指针,而非一个副本)。
但是如果是重写整个原型对象,那情况就不一样了,因为重写原型对象会切断构造函数与最初原型之间的联系。

原生对象的原型

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object、Array、String,等待)都在其构造函数的原型上定义了方法。例如,在Array.prototype中可以找到sort()方法,而在String.prototype中可以找到substring()方法。
这样,通过原生对象的原型,不仅可以取得所有默认方法的引用,而且可以定义新方法,还可以修改原生对象的方法(不建议)。

原生对象的问题

  1. 忽略了为构造函数传递初始化参数这一环境,结果所有实例在默认情况下都将取得相同的属性值
  2. 共享的本性导致的问题,这是最大的问题。原型中所有属性是被很多实例共享的,这种共享对于函数非常合适。对于那些包含基本值得属性倒也说的过去,毕竟通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值得属性来说,问题就比较突出了,看下面的示例:

    function Person() {}
    Person.prototype = {
        name: "Eviler",
        age: 22,
        skills: ["English", "javascript"],
        sayName: function () {
            console.log(this.name);
        }
    }
    var person1 = new Person();
    var person2 = new Person();
    console.log(person1.skills); //[ 'English', 'javascript' ]
    person2.skills.push("python");
    console.log(person1.skills); //[ 'English', 'javascript', 'python' ]
    知识兔在这个例子中,person1和person2共享了属性原型对象的skills属性,所以,person2改变它的值,person1的skills也就变了。但是,实例一般都是要有属于自己的全部属性的。所以我们需要一种方法来解决这个问题。这就出现了组合使用构造函数和原型模式。

组合使用构造函数和原型模式

这是创建自定义类型最常见的方法。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果每个实例都会有自己的一份实例属性但又同时共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持像构造函数传递参数;可谓是集两种模式之长。下面的代码重写了前面的例子:

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
}
Person.prototype = {
    sayName: function () {
        console.log(this.name);
    }
}
知识兔h2 id="动态原型模式">动态原型模式

这个模式是受到其他OO语言封装性的影响而产生的一种模式,其实它也是组合使用构造函数和原型模式,只不过它看起来更舒服一些,因为它将所有信息都封装在了构造函数中,同时又保存了组合使用构造函数和原型模式的优点。它可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型,如下示例(重写上面的示例):
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
if (typeof !Person.sayName == “function”) {
Person.prototype.sayName = function () {
console.log(this.name);
}
}
}
使用动他原型模式时,不能使用对象字面量重写原型,如果在以及创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的联系。

寄生构造函数模式

通常,在前述的几种模式都不适用的情况下,可以使用寄生构造函数模式。如下所示:

function Person(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function () {
        console.log(this.name);
    }
    return o;
}
var p = new Person("Eviler", 22, "Software Engineer");
p.sayName(); //Eviler
知识兔这个模式其实跟工程模式其实是一模一样的,都无法检测对象类型。非要说它们的区别,那么就是函数名了如果是工程模式,我们会把函数名定义为createPerson,而使用寄生构造函数模式,直接将函数名定义为构造函数的样子Person,并且使用了new 操作符来创建实例。

稳妥构造函数模式

稳妥对象:没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境会进制使用this和new),或者在防止数据被其他应用程序(如Mashuo程序)改的时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:意识新创建对象的实例方法不引用this;二是不适应new 操作符调用构造函数。重写前面的示例:

function Person(name, age, job) {
    var o = new Object();
    o.sayName = function () {
        console.log(name);
    }
    return o;
}
var person = new Person("Eviler", 22, "Software Engineer");
person.sayName(); //Eviler
知识兔在这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问name的值。这样person中保存的是一个稳妥对象。

继承

许多OO语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。在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();
console.log(instance.getSuperValue()); //true
console.log(instance.getSubValue()); //false
知识兔这样,原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。这个例子中的实例以及构造函数和原型之间的关系如图所示:

构造函数、原型和实例关系图

注意:instance.constructor 现在指向的是SuperType,这是因为原来SubType.prototype中的constructor被重写了。实际上,不是SubType的原型的constructor属性被重写了,而是SubType的原型指向了另一个对象—-SuperType的原型,而是这个原型对象的constructor属性指向的是SuperType。

勿忘记默认的原型

前面的原型链还少一环。我们知道,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针指向Object.prototype。这正数所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。所以原型链中还应该包括另一个继承层次,如图:

确定原型和实例的关系

可以通过两种方式来确定原型和实例之间的关系。

  1. 第一种是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。

    console.log(instance instanceof SubType); //true
    console.log(instance instanceof SuperType); //true
    console.log(instance instanceof Object); //true
    知识兔li>
  2. 第二种方法是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPrototypeOf()方法也会返回true。

    console.log(SubType.prototype.isPrototypeOf(instance)); //true
    console.log(SuperType.prototype.isPrototypeOf(instance)); //true
    console.log(Object.prototype.isPrototypeOf(instance)); //true
    知识兔h3 id="谨慎地定义方法">谨慎地定义方法

    子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。并且不能使用对象字面量创建方法,因为这样做回重写原型链。

原型链的问题

最主要的问题就是来自包含引用类型的原型。包含引用类型值得原型属性会被所有实例共享,这正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。但是通过原型来实现继承时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就顺利成章地变成了现在的原型属性了。如下代码:

function SuperType() {
    this.colors = ["read", "blue", "green"];
}
function SubType() {}
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push("back");
console.log(instance1.colors); //['read','blue','green','back']
var instance2 = new SubType();
console.log(instance2.colors); //['read','blue','green','back']
知识兔这样,所有实例都会共享某些属性(引用类型),显然这样的继承不是我们想要得到的结果。
原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该是没有办法在不影响所有实例的情况下,给超类型的构造函数传递参数。
所以,实践中很少会单独使用原型链。

借用构造函数

借用构造函数,有时候也叫做伪造对象或经典继承。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超型构造函数,s函数只不过是在特定环境中指向代码的对象,因此通过使用apply()和call()方法也可以在(将来)新创建的对象上指向构造函数,如下所示:

function SuperType() {
    this.colors = ["read", "blue", "green"];
}
function SubType() {
    // 继承了SuperType
    SuperType.call(this);//或者使用apply()函数
}
var instance1 = new SubType();
instance1.colors.push("back");
console.log(instance1.colors); //['read','blue','green','back']
var instance2 = new SubType();
知识兔这样,在SubType的构造函数中“借调”了超类型的构造函数,这样就会在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例就都会具有自己的colors属性的副本了。
借用构造函数可以在子类型构造函数中向超类型构造函数传递参数

function SuperType(name) {
    this.name = name;
}
function SubType() {
    // 继承了SuperType,同时还传递了参数
    SuperType.call(this, "Eviler");
    //实例属性
    this.age = 22;
}
var instance = new SubType();
console.log(instance.name); //Eviler
console.log(instance.age); //22
知识兔借用构造函数的问题:如果仅仅借用构造函数,那么无法避免构造函数模式存在的问题—-方法都在构造函数中定义,因此函数复用就无从谈起。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。所以,借助构造函数的技术也是很少单独使用的。

组合继承

组合继承有时候也叫伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而返回二者之长的一种继承模式。其背后的思路是使用原型链实现对原型方法的继承,而通过借用构造函数来实现对实例属性的继承。如下所示:

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}
function SubType(name, age) {
    //继承属性
    SuperType.call(this, name);
    //自己新加的属性
    this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function () {
    console.log(this.age);
}
var instance1 = new SubType("Eviler", 22);
instance1.colors.push("black");
console.log(instance1.colors); //["read", "blue", "green", "black"];
instance1.sayName(); //Eviler
instance1.sayAge(); //22
var instance2 = new SubType("Greg", 25);
console.log(instance2.colors); //["read", "blue", "green"];
instance2.sayName(); //Greg
instance2.sayAge(); //25
知识兔这样就解决了原型链模式继承的所有实例共享超类型实例的引用属性的问题和借用构造函数模式的函数复用问题。而且,instanceof和isPrototypeOf()特能够用于识别基于组合继承创建的对象。

原文:大专栏  面向对象的程序设计


计算机