JavaScript高级程序设计——面向对象

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

创建自定义对象的两种方法:

// 创建一个Object实例
var person = new Object();
person.name = 'andy';
person.age = 29;

person.sayName = function () {
    alert(this.name)
}

// 对象字面量
var person = {
    name: 'andy',
    age: 29,

    sayName: function () {
        console.log(this.name)
    }
}

1. 工厂模式

这种模式抽象了创建具体对象的过程。考虑到在ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节。

function createPerson(name, age) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.sayName = function () {
        alert(o.name);
    }
    return o;
}
var person1 = createPerson('andy', 21);

函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的person对象。可以无数次地调用这个函数,而每次它会返回一个包含两个属性一个方法的对象。工程模式虽然解决了创建多个类似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。随着JavaScript的发展,又一个新模式出现了。

2. 构造函数模式

ECMAScript中的构造函数可以用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。

function Person (name, age) {
    this.name = name;
    this.age = age;
    this.sayName = function () {
        alert(this.name);
    }
}
var per = new Person('andy', 21);
per.sayName();    // andy

在该构造函数中,有以下几点不同:

  • 没有显式地创建对象;
  • 直接将属性和方法赋给了this对象;
  • 没有return语句。

要创建Person对象的新实例,必须使用new操作符。以这种方式调用构造函数实际上会经历以下4个步骤:

  • 创建一个新对象 ;
  • 将构造函数的作用域赋给新对象;
  • 执行构造函数中的代码;
  • 返回新对象。

我们可以用instanceof操作符来进行对象检测:

console.log(per.constructor === Person);    // true
console.log(per instanceof Object);    // true
console.log(per instanceof Person);    // true
console.log(per instanceof String);    // false

构造函数模式虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。每定义一个函数,也就实例化了一个对象。以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的。

console.log(per1.sayName === per2.sayName);    // false

创建两个完全同样任务的Function实例的确没有必要;况且有this对象在,根本不用再执行代码前就把函数绑定到特定对象上面。所以这就促使我们可以使用原型模式来自定义对象。

3. 原型模式

a. 原型语法

我们创建的每一个函数都有一个prototype属性,这个属性是一个指针,指向一个对象。而这个对象的用途是包含由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中:

function Person() {
}
Person.prototype.name = 'andy';
Person.prototype.age = 13;
Person.prototype.sayName = function () {
    console.log(this.name);
}


var per1 = new Person();
var per2 = new Person();
per1.sayName();    // andy
per2.sayName();    // andy

per1.name = 'qiqi';
per1.sayName();    // qiqi

在此,我们将sayName()方法的所有属性直接添加到了Person的prototype属性中,构造函数编程了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。也就是说per1和per2访问的都是同一组属性和同一个sayName()函数。

无论什么时候,只要创建了一个函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性包含一个指向prototype属性所在函数的指针。以前边例子来说,Person.prototype.constructoy指向Person。通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。

几种操作符:

  • isPrototypeOf() 确定对象之间是否存在关系
console.log(Person.prototype.isPrototypeOf(person1));    // true
  • getPrototypeof() 返回prototype的值
console.log(Object.getPrototypeOf(person1) === Person.prototype);    // true
console.log(Object.getPrototypeOf(person1.name));    // andy
  • hasOwnProperty() 用来检测一个属性是存在于实例中,还是存在于原型中。
console.log(per1.hasOwnProperty('name'));    // false

per1.name = 'qiqi';
console.log(per1.hasOwnProperty('name'));    // true
  • in 只要通过对象能够访问到属性就返回true,不管是在实例中还是在原型中
console.log('name' in per1);    // true
console.log('name' in Person);    // true
b. 更简单的原型语法

在前面的例子中,每添加一个属性和方法就要敲一遍prototype,为了减少不必要的输入可以采用以下的写法:

function Person() {
}
Person.prototype = {
    constructor: Person,    // 不添加的话constructor会变化,此处为了使该指针指向原型
    name: 'andy',
    age: 21,
    sayName: function() {
        console.log(this.name);
    }
}

原型模式并不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都取得相同的属性值。虽然这会在某种程度上带来一些不方便,但这不是原型的最大问题。原型模式的最大问题是由其共享的本性导致的。

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

结合以上各种优点缺点,在自定义对象的时候可以采用组合使用的方式!

function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype = {
    constructor: Person,
    sayName: function() {
        console.log(this..name);
    }
}

4. 继承

继承是面向对象语言中一个最为津津乐道的概念,许多面向对象语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。因为函数没有签名,所以ECMAScript中无法实现接口继承,只支持实现继承,而且其实实现继承主要是依靠原型链来实现。

a. 原型链
function SuperType() {
    this.property = true;
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
}
function SubType() {
    this.subproperty = false;
}
SubType.prototype = new SuperType();    // 原型链继承
SubType.prototype.getSubValue = function() {
    return this.subproperty;
}
var ins = new SubType();
console.log(ins.getSubValue());

以上代码定义了两个类型:Supertype和SubType。每个类型分别有一个属性和一个方法。他们的主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋给SubType.prototype实现的。实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType中了 。确立了继承关系之后,给SubType.prototype添加了一个方法。

b. 借用构造函数

在解决原型链中包含引用类型值所带来的问题中,开发人员开始使用一种叫做借用构造函数的技术。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数。别忘了,函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在新创建的对象上执行构造函数:

function SuperType(name) {
    this.name = name;  
    this.colors = ['red', 'blue', 'black'];
}
function SubType() {
    // 继承了SuperType,还传递了参数
    SuperType.call(this, 'andy');
}
c. 组合继承

组合继承,有时候也叫伪经典继承,指的是将原型链和借用构造函数的技术组合到一起,从而发挥二者之长的一种继承方式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数的复用,又能保证每个实例都有它自己的属性:

function SuperType(name) {
    this.name = name;
    this.colors = ['red',  'blue', 'black'];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
}
function SubType() {
    // 继承属性
    SuperType.call(this, name);
    this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayName = function () {
    console.log(this.name);
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,392评论 5 470
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,258评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,417评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,992评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,930评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,199评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,652评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,327评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,463评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,382评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,432评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,118评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,704评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,787评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,999评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,476评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,057评论 2 341

推荐阅读更多精彩内容