第6章 面向对象的程序设计

面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。

在JS中,每个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员定义的类型。

6.1 理解对象

6.1.1 属性类型

ECMAScript中有两种属性:数据属性和访问器属性。

  1. 数据属性

数据属性有4个描述其行为的特性:

[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,
或者能否把属性修改为访问器属性。默认为true。
[[Enumerable]]:表示能否通过for-in循环返回属性。默认为true。
[[Writable]]:表示能否修改属性的值。默认为true。
[[Value]]:包含这个属性的数据值。读取属性值时,从这个位置读;写入属性值的时候,
把新值保存在这个位置。默认为undefined。

比如:

var person = {
    name: 'Nick'
}

对于上面这个对象来说,其[[Value]]被设置为"Nick",[[Configurable]]、[[Enumerable]]和[[Writable]]则都为其默认值true。

要修改属性默认的特性,必须使用ECMAScript5的 Object.defineProperty()方法,这个方法接收三个参数:属性所在的对象,属性的名字和一个描述符对象。其中,描述符对象的属性必须是:Configurable、Enumerable、Writable和Value。

var person = {};
Object.defineProperty(person, 'name', {
    writable: false,
    value: 'Nick'
})

alert(person.name); //"Nick"
person.name = 'Greg';
alert(person.name);//"Nick"
  1. 访问器属性
    访问器属性不包含数据值,包含getter和setter函数。getter函数负责返回有效的值;setter函数负责决定如何处理数据。

访问器属性有如下4个特性:

[[Configurable]]:表示能否通过 delete 删除属性从而重新定义属性,
能否修改属性的特性,或者能否把属性修改为访问器属性。默认为true。
[[Enumerable]]:表示能否通过for-in循环返回属性。默认为true。
[[Get]]:在读取属性时调用的函数。默认值为undefined。
[[Set]]:在写入属性时调用的函数。默认值为undefined。
var book = {
    _year: 2014,
    edition: 1
}

Object.defineProperty(book, 'year', {
    get: function(){
        return this._year;
    },
    set: function(newValue){
        if( newValue > 2004 ){
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
})

book.year = 2005;
console.log(book) ==> {edition: 2, _year: 2005}

6.1.2 定义多个属性

ECMAScript 5定义了一个 Object.defineProperties()方法通过描述符一次定义多个属性。
用法如下:

var book = {};

Object.defineProperties(book, {
    _year: {
        value: 2004
    },

    edition: {
        value: 1
    },

    year: {
        get: function(){
            return this._year;
        },

        set: function(newValue){
             if( newValue > 2004 ){
                 this._year = newValue;
                 this.edition += newValue - 2004;
             }
        }
    }
})

6.1.3 读取属性的特性

使用 ECMAScript 5的 Object.getOwnPropertyDescriptor() 方法可以取得给定属性的描述符。接收两个参数:属性所在的对象和要读取其描述符的属性名称。

var book = {};

Object.defineProperties(book, {
    _year: {
        value: 2004
    },
    edition: {
        value: 1
    },
    year: {
        get: function(){
            return this._year;
        },
        set: function(newValue){
             if( newValue > 2004 ){
                 this._year = newValue;
                 this.edition += newValue - 2004;
             }
        }
    }
})
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor)

6.2 创建对象

6.2.1 工厂模式

function createPerson(name, age, job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
        alert(this.name);
    };
    return o;
}

var person1 = createPerson('Nick', 29, 'Doctor');

工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。

6.2.2 构造函数模式

ECMAScript中的构造函数可用来创建特定类型的对象。

例如,可以使用构造函数模式将前面的例子重写如下:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        alert(this.name);
    }
}

var person1 = new Person('Nick', 29, 'Doctor');
var person2 = new Person('Jack', 30, 'Teacher');

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

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

创建自定义的构造函数意味着将来可以将它的实例标识为一种特定类型;而这正是构造函数模式胜过工厂模式的地方。

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

例如,前面例子中定义的 Person() 函数可以通过下列任何一种方式来调用。

//当做构造函数使用
var person = new Person('Nick', 29, 'Doctor');
person.sayName();

//作为普通函数调用
Person('Greg', 27, 'Doctor'); //添加到 window
window.sayName(); ==> 'Greg';

//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, 'Kristen', 25, 'Nurse');
o.sayName(); ==> 'Kristen';
  1. 构造函数的问题
    使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面例子中,person1和person2都有一个名为sayName()的方法。

6.2.3 原型模式

使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。

function Person(){
}

Person.prototype.name = "Nick";
Person.prototype.age = 29;
Person.prototype.job = 'Software Enginner';
Person.prototype.sayName = function() {
    alert(this.name);
}

var person1 = new Person();
person1.sayName(); //"Nick"

var person2 = new Person();
person2.sayName(); //"Nick"

person1.sayName == person2.sayName ==> true;

以上与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1和person2访问的都是同一组属性和同一个 sayName() 函数。

要理解原型模式的工作原理,必须先理解 ECMAScript 中原型对象的性质。

  1. 理解原型对象。

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

如何去检测 person1是否继承了 Person原型中方法?

可以使用 isPrototypeOf()

Person.prototype.isPrototypeOf(person1)

ECMAScript5 增加了一个新的方法,叫Object.getPrototypeOf()
用法:

Object.getPrototypeOf(person1);

一段代码执行某个对象中属性时,先在该对象中查找,如果没有找到,就会去原型链中查找。

虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。

function Person(){

}

Person.prototype.name = "wang";
Person.prototype.age = 27;

var person1 = new Person();
var person2 = new Person();

person1.name="jack";

person1.name ==> 'jack'; //来自实例
person2.name ==> 'wang'; //来自原型

使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是原型中。
存在于实例中,返回true 存在于原型中,返回false。

  1. 原型与 in 操作符

有两种情况会使用in操作符
(1) 在 for-in循环中使用
(2) 单独使用,在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。

function Person(){

}

Person.prototype.name = "wang";
Person.prototype.age = 27;

var person1 = new Person();

var person2 = new Person();

"name" in person1 ==> true

要取得对象上所有可枚举的实例属性,可以使用 Object.keys()

function Person(){

}

Person.prototype.name = "wang";
Person.prototype.age = 27;
Person.prototype.job = "teacher";
Person.prototype.sayName = function(){
    alert(this.name);
}

var keys = Object.keys(Person.prototype)  => ['name', 'age', 'job', 'sayName']

var p1 = new Person();
p1.name="Rob";
p1.age = 31;
var p1keys = Object.keys(p1) ==> ['name', 'age']

如果想得到所有实例属性,无论它是否可以枚举,都可以使用

Object.getOwnPropertyNames();

var keys = Object.getOwnPropertyNames(Person.prototype);

==> ['constructor', 'name', 'age', 'job', 'sayName']
  1. 更简单的原型语法

可以使用对象字面量方法来重写整个原型对象

function Person(){
    
}

Person.prototype = {
    name: 'jack',
    age: 29,
    job: 'teacher',
    sayName: function(){
        alert(this.name)
    }
}

上面操作相当于重写了默认的 prototype 对象,因此其 constructor 指向了 Object 构造函数,不再指向 Person 函数。

可以通过下面方法设置 constructor 让其指向 Person 函数。

Person.prototype = {
    constructor:‘Person’,
    name: 'jack',
    age: 29,
    job: 'teacher',
    sayName: function(){
        alert(this.name)
    }
}

不过,又会带来新的问题,会导致它的 [Enumerable]被设置为 true。所以得使用 Object.defineProperty() 设置:

Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
})

不过,defineProperty目前对于IE,只支持 IE9+,所以还是避免使用这种简化的原型语法吧。

  1. 原型的动态性。
var friend = new Person();

Person.prototype.sayHi = function() {
    alert('hi');
}

friend.sayHi();

虽然 friend 实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。

原因是:实例和原型之间是一个松散连接关系,它们之间的连接不过是一个指针,而非一个副本。

  1. 原生对象的原型

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。例如,在 Array.prototype 中可以找到 sort() 方法,而在 String.prototype 中可以找到 substring() 方法。

  1. 原型对象的问题

  2. 省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。

  3. 原型中的属性被所有实例共享,这一点,对于包含引用类型的属性来说,问题就比较突出了。

Person.prototype = {
    constructor: "Person",
    name: 'jack',
    age: 29,
    job: 'teacher',
    friends: ['Shelby', 'Court'],
    sayName: function(){
        alert(this.name)
    }
}

var person1 = new Person();
var person2 = new Person();

person1.friends.push('Van');

person1.friends ==> ["Shelby", "Court", "Van"]
person2.friends ==> ["Shelby", "Court", "Van"]

可以看到,为person1实例增加了 "Van", 会导致所有实例都共享了"Van"。

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

创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大程度的节省了内存。另外,这种混成模式还支持向构造函数传递参数,可谓是集两种模式之长。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ['Shelby', 'Court'];
}

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

var person1 = new Person('Nick', 29, 'Software Enginner');
var person2 = new Person('Greg', 27, 'Doctor');

person1.friends.push('Van');

person1.friends ==> ["Shelby", "Court", "Van"]
person2.friends ==> ["Shelby", "Court"]

person1.sayName === person2.sayName ==> true

6.2.5 动态原型模式

把所有信息都封装在了构造函数里,通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点,换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    if( typeof this.name != 'function' ){
        Person.prototype.sayName = function(){
            alert(this.name);
        }
    }
}

var friend = new Person('nick', 29, 'soft');
friend.sayName()

使用 动态原型模式 模式时,不能使用对象字面量重写原型。

6.2.6 寄生构造函数模式

这种模式存在诸多问题,不建议使用。

6.2.7 稳妥构造函数模式

function Person(){
    
    //创建要返回的对对象
    var o = new Object();

    //可以在这里定义私有变量和函数

    //添加方法
    o.sayName = function() {
        alert(name);
    }

    //返回对象
    return o;
}

var friend = Person('Nick', 29, 'teacher');
friend.sayName();

存在问题:除了调用 sayName() 方法外,没有别的方式可以访问其数据成员。

不建议使用。

6.3 继承

6.3.1 原型链

其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

 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();
 alert(instance.getSuperValue())  ==> true

上面继承,是通过将 SuperType 的实例赋给 SubType.prototype 来实现的。实现的本质是重写原型对象,代之一个新类型的实例。
换句话说,原先存在于 SuperType 的实例中的属性和方法,现在也存在于 SubType.prototype 中了。

需要注意的是, instance.constructor 现在指向的是 SuperType,这是因为原来 SubType.prototype 中的constructor被重写了的缘故。

通过实现原型链,本质上扩展了本章前面介绍的原型搜索机制,拿上面例子来说,调用 instance.getSuperValue() 会经历三个搜索步骤:

(1) 搜索实例
(2) 搜索SubType.prototype 
(3) 搜索SuperType.prototype
  1. 别忘记默认的原型

大家要记住,所有函数的默认原型都是 Object 的实例,因此默认原型都会包含一个内部指针,指向 Object.prototype 。这也正是所有自定义类型都会继承
toString()、valueOf() 等默认方法的根本原因。

就像上面那个,SubType 继承了 SuperType, 而 SuperType 继承了 Object。当调用 instance.toString()时,实际上调用的是保存在 Object.prototype 中的那个方法。

  1. 确定原型和实例的关系
    可以通过两种方法来确定原型和实例之间的关系
(1) instanceof

instance instanceof Object ==> true
instance instanceof SuperType ==> true
instance instanceof SubType ==> true

(2) isPrototypeof()

Object.prototype.isPrototypeof(instance) ==> true
SuperType.prototype.isPrototypeof(instance) ==> true
SubType.prototype.isPrototypeof(instance) ==> true
  1. 谨慎的定义方法
 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;
 }

 //重写超类型中的方法
 SubType.prototype.getSuperValue = function(){
    return false;
 }

 var instance = new SubType();
 alert(instance.getSuperValue())   ==> false

 var instances = new SuperType();
 alert(instances.getSuperValue())  ==> true
  1. 原型链的问题
 (1)
 function SuperType() {
    this.colors = ['red', 'blue', 'green'];
 }

 function SubType(){

 }

 SubType.prototype = new SuperType();

 var instance1 = new SuperType();
 instance1.colors.push('black');

 instance1.colors ==> ["red", "blue", "green", "black"]

 var instance2 = new SuperType();
 instance2.colors ==> ["red", "blue", "green", "black"]

(2) 在创建子类型的实例时,不能向超类型的构造函数中传递参数。

由于以上的问题,实际中很少单独使用原型链

6.3.2 借用构造函数

 function SuperType() {
    this.colors = ['red', 'blue', 'green'];
 }

 function SubType(){
    //继承了 SuperType
    SuperType.call(this);
 }

 SubType.prototype = new SuperType();

 var instance1 = new SuperType();
 instance1.colors.push('black');

 console.log(instance1.colors)  ==> ["red", "blue", "green", "black"]

 var instance2 = new SuperType();
 console.log(instance2.colors)  ==> ["red", "blue", "green"]
  1. 传递参数
  function SuperType(name) {
    this.name = name;
 }

 function SubType(){
    //继承了 SuperType,同时还传递了参数
    SuperType.call(this, 'Nich');
    //实例属性
    this.age = 29;
 }

var instance = new SubType();

instance.name ==> 'Nich';
instance.age ==> 29
  1. 借用构造函数的问题
    方法都在构造函数内部定义,因此函数复用就无从谈起了。
    考虑到这些问题,借用构造函数的技术是很少单独使用的。

6.3.3 组合继承

指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长
其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。

6.3.4 原型式继承

ECMAScript 5 新增 Object.create() 方法

6.3.5 寄生式继承

6.3.6 寄生组合式继承

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

推荐阅读更多精彩内容