资讯专栏INFORMATION COLUMN

JavaScript学习笔记 - 面向对象设计

brianway / 2510人阅读

摘要:与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针内部属性,指向构造函数的原型对象。

本文记录了我在学习前端上的笔记,方便以后的复习和巩固。

ECMAScript中没有类的概念,因此它的对象也与基于类的语言的对象有所不同。
ECMA-262把对象定义为:"无序属性的组合,其属性可以包含基本值,对象或者函数。"对象的每个属性或方法都有一个名字,而每个名字映射到一个值。我们可以把ECMAScript的对象想象成散列表:无非就是一组名值对,其中值可以使数据或函数。
每个对象都是基于一个引用类型创建的,这个引用类型可以使原生类型,也可以是开发人员定义的类型

1. 理解对象

创建自定义对象最简单的方式就是创建一个Object的实例,然后再为它添加属性和方法

var person = new Object();
person.name = "Jason";
person.age = 18;
person.job = "Web";

person.sayName = function() {
    console.log(this.name);
};

对象字面量创建:

var person = {
    name: "Jason",
    age: 18,
    job: "Web",
    sayName = function() {
        console.log(this.name);
    }
}

这两个方法的person对象是一样的,都有相同的属性和方法,这些属性在创建的时都带有一些特征值(characteristic),JavaScript通过这些特征值来定义它们的行为。

1.1 属性类型

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

数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性。

[[Configurable]]: 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。直接在对象上定义的属性,它们的这个特性默认值为true。

[[Enumerable]]: 表示能否通过for-in循环返回属性。直接在对象上定义的属性,它们的这个特性默认值为true。

[[Writable]]: 表示能否修改属性的值。直接在对象上定义的属性,它们的这个特性默认为true。

[[Value]]: 包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候。把新值保存在这个位置。这个特性的默认值为undefined

要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。这个方法接收三个参数: 属性所在的对象、属性的名字和一个描述符对象。其中,描述符(descriptor)对象的属性必须是: configurable、enumerable、writable和value。设置其中的一或多个值,可以修改对应的特征值。

var person = {};
Object.defineProperty(person, "name", {
    writable: false,      //不能修改属性的值....
    configurable: false,  //不能通过delete删除属性.....
    value: "Jason"        //写入属性值
});
console.log(person.name); //Jason
person.name = "Cor";
console.log(person.name); //Jason
delete person.name;
console.log(person.name); //Jason

一旦把属性定义为不可以配置的,就不能再把它变回可配置的了。此时再调用Object.defineProperty()方法修改除writable之外的特性,都会导致错误

var person = {};
Object.defineProperty(person, "name", {
    configurable: false,
    value: "Jason"
});
//抛出错误
Object.defineProperty(person, "name", {
    comfogirable: true,    //这行代码修改了特性导致报错
    value: "Cor"
});

也就是说,可以多次调用Object.defineProperty()方法修改同一属性,但在吧configurable特性设置为false之后就会有限制了。

注意:在调用Object.defineProperty()方法时,如果不指定,configurable、enumerable和writable特性的默认值都是false

访问器属性
访问器属性不包含数据值;它们包含一对儿gettersetter函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4个特性。

[[Configurable]]: 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性默认值为true。

[[Enumerable]]: 表示能否通过for-in循环返回属性。对于直接在对象上定义的属性,它们的这个特性默认值为true。

[[Get]]: 在读取属性时调用的函数。默认值为undefined

[[Set]]: 在写入属性时调用的函数。默认值为undefined

访问器属性不能直接定义,必须使用Object.defineProperty()来定义。

var book = {
    _year: 2004,
    edition: 1
};

Object.defineProperty(book, "year", {
    get: function() {
        return this._year;
    },
    set: function(newValue) {    //接受新值的参数
        if(new Value > 2004) {
            this._year = new Value;
            this.edition += newValue - 2004;
        }
    }
});
book.year = 2005;            //写入访问器,会调用setter并传入新值
console.log(book.edition);  //2

不一定非要同时指定gettersetter。只指定getter意味着属性是不能写,尝试写入属性会被忽略。在严格模式下,尝试写入只指定getter函数的属性会抛出错误。同样只指定setter函数的属性也不能读,否则在非严格模式下会返回undefined,在严格模式下会抛出错误。

1.2 定义多个属性

由于为对象定义多个属性的可能性很大,ECMAScript5又定义了一个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;
            }
        }
    }
});
1.3 读取属性的特性

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

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.value);             //2004
console.log(descriptor.configurable);   //false
console.log(typeof descriptor.get);     //"undefined"
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value);          //"undefined"
console.log(descriptor.enumerable);     //false
console.log(typeof descriptor.get);        //"function"
2.创建对象

虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显得缺点:使用同一个接口创建很多对象,会产生大量的重复的代码。

2.1 工厂模式

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

function createPerson(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 person1 = createPerson("Jason", 18, "WEB");
var person2 = createPerson("Cor", 19, "WEB");

函数createPerson()能够根据接受的参数来构建一个包含所有必要信息的Person对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(就是怎么用知道一个对象的类型)

2.2 构造函数模式

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

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        console.log(this.name);
    }
}
var person1 = new Person("Jason", 18, "WEB");
var person2 = new Person("Cor", 19, "WEB");

构造函数模式和工厂模式存在以下不同之处:

没有显示地创建对象;

直接将属性和方法赋给了this对象;

没有return语句

像上面创建的Person构造函数。构造函数使用都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。

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

创建一个新对象

将构造函数的作用域赋给新对象(因此this就指向了这个新对象)

执行构造函数中的代码(为这个新对象添加属性)

返回新对象。

在前面例子的最后,person1person2分别保存着Person的一个不同的实例。这两个对象都有一个constructor(构造函数)属性,该属性指向Person

console.log(person1.constructor == Person);        //true
console.log(person2.constructor == Person);         //true
console.log(person1 instanceof Object);                //true
console.log(person1 instanceof Person);                //true
console.log(person2 instanceof Object);                //true
console.log(person2 instanceof Person);                //true

创建对的对象既是Object的实例,同时也是Person的实例,上面通过instanceof验证。
创建自定义的构造函数意味着将它的实例标识一种特定类型;而这正是构造函数模式胜过工厂模式的地方。person1person2之所以同时是Object的实例,是因为所有对象均继承自Object

以这种方式定义的构造函数是定义在Global对象(在浏览器就是window对象)中的

2.2.1 把构造函数当函数
//当作构造函数使用
var person = new Person("Jason", 18, "web");
person.sayName();        //"Jason"
//作为普通函数调用
Person("Cor", 19, "web");    //添加到window
window.sayName();        //"cor"
//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 22, "web");
o.sayName();               //"kriten"

当在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器中的window对象),最后使用了call() ( 或者apply() )在某个特殊对象的作用域中调用Person()函数。这里是在对象o的作用域调用的,因此调用后o就拥有了所有属性和方法。

2.2.2 构造函数的问题

构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例。ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。从逻辑角度讲,此时的构造函数也可以这样定义

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
        this.sayName = new Function(console.log(this.name)); //与声明函数在逻辑上是等价的
}

以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制仍然是相同的。因此不同的实例上的同名函数是不相等的

console.log(person1.sayName == person2.sayName);  //false
function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}

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

var person1 = new Person("Jason", 18, "WEB");
var person2 = new Person("Cor", 19, "WEB");

在构造函数内部我们把sayName属性设置成等于全局的sayName函数。由于构造函数的sayName属性包含的是一个指向函数的指针,因此person1和person2对象就共享了在全局作用域中定义的同一个sayName()函数。

2.3 原型模式

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

function Person() {
}

Person.prototype.name = "Jason";
Person.prototype.age = 18;
Person.prototype.job = "Web";
Person.prototype.sayName = function() {
    console.log(this.name);
}

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

console.log(person1.sayName == person2.sayName);     //true

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

2.3.1 理解原型对象

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

创建了自定义构造函数之后,其原型对象默认只会取得constructor属性;至于其他方法,则都是从Object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第五版中管这个指正交[[Prototype]]。虽然在脚本中没有标准的方式访问[[Prototype]],但Firefox、Safari和Chrome在每个对象上都支持一个熟悉 __ proto__;而在其他实现中,这个属性对脚本则是完全不可见的。不过在明确的真正重要一点就是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
前面例子如图:

在此Person.prototype指向了原型对象,而Person.prototype.constructor又指回了Person。原型对象中除了都包含constructor属性之外,还包含后来添加的属性。Person的每个实例person1和person2都包含一个内部属性,该属性仅仅指向了Person.prototype;换句话说它们与构造函数没有直接的关系。此外,虽然这两个实例都不包含属性和方法,但我们却可以调用person1.sayName()。这是通过查找对象属性的过程来实现的。

虽然在所有的实现中都无法访问到[[Prototype]]可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就会返回true

console.log(Person.prototype.isPrototypeOf(person1))  //true
console.log(Person.prototype.isPrototypeOf(person2))  //true

ES5增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值,可以方便地获取一个对象的原型

console.log(Object.getPrototypeOf(person1) == Person.prototype); //true
console.log(Object.getPrototypeOf(person1).name);     //"Jason"

搜索机制:
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象的实例本身开始。如果在实例中找到具有给定名字的属性,则返回该属性的值;如果没有找到。则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到这个属性,则返回该属性的值。简单的说,就是会一层层搜索搜索到则返回,没搜索到则继续下层搜索。

原型最初只包含constructor属性,该属性也是共享的,因此可以通过对象实例访问

虽然可以通过对象实例访问原型的值,但却不能通过对象实例重写原型的值。如果我们为对象实例添加了一个熟悉,并且该属性名和实例原型中的一个属性同名,就会在实例中创建该属性,该属性就会屏蔽原型中的相同属性。因为上面讲过读取对象的属性时,会进行搜索,搜索会先从对象实例先开始搜索,因为对象实例有这个属性,原型就没必要搜索了,就会返回对象实例的的属性值。

function Person() {
}

Person.prototype.name = "Jason";
Person.prototype.age = 29;
Person.prototype.job = "Web";
Person.prototype.sayName = function() {
    console.log(this.name);
}

var person1 = new Person();
var person2 = new Person();
person1.name = "Cor";
person1.sayName();         //"Cor"
person2.sayName();        //"Jason"

如果继续能够重新访问原型中的属性可以用delete操作符

delete person1.name;
person1.sayName();       //"Jason"

使用hasOwnProperty()方法可以检测一个属性是存在实例中,还是存在于原型中。这个方法(不要忘了它是从Object继承来的)只在给定属性存在于对象实例中时,才会返回true

console.log(person1.hasOwnProperty("name"));        //false
person1.name = "Cor";
console.log(person1.hasOwnProperty("name"));        //true;
2.3.2 原型与in操作符

有两种方式使用in操作符:多带带使用何在for-in循环中使用。在多带带使用时,in操作符会在通过对象能访问给定属性时返回true,无论属性存在于实例还是原型中

console.log("name" in person1); true

同时使用hasOwnProperty()方法和in操作符,就可以确定该属性到底是存在于对象中,还是存在于原型中

function hasPrototypeProperty(object, name) {
    return !object.hasOwnProperty(name) && (name in object);
}

要取得对象上所有可枚举的实例属性,可以使用ES5的Object.key()方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。

function Person(){}
Person.prototype.name = "Jason";
Person.prototype.age = 18;
Person.prototype.job = "Web";
Person.prototype.sayName = function() {
    console.log(this.name);
}

var keys = Object.keys(Person.prototype);
console.log(keys);        //"name,age,job,sayName"

var p1 = new Person();
p1.name = "cor";
p1.age = 11;
var p1keys = Object.keys(p1);
console.log(p1keys);    //"name,age"

所有你想得到所有实例属性,无论它是否可以枚举,都可以使用Object.getOwnPropertyNames()方法。

var keys = Object.getOwnPropertyNames(Person.prototype);
console.log(keys);      //"constructor,name,age,job,sayName"
2.3.3 更简单的原型语法

可以用一个包含所有属性和方法的对象字面量来重写整个原型对象

function Person(){}

Person.prototype = {
    name : "Jason",
    age : 29,
    job : "Web",
    sayName : function() {
        console.log(this.name)
    }
};

用对象字面的方法和原来的方法会有区别: constructor属性不再指向Person了。每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而对象字面量的写法,本质上重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。次数尽管instanceof操作符汉能返回正确的结果,但通过constructor已经无法确定对象的类型了。

var friend = new Person();

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

如果constructor的值真的很重要,可以像下面这样特意将它设置回适当的值。

function Person(){}

Person.prototype = {
    constructor : Person,
    name : "Jason",
    age : 29,
    job : "Web",
    sayName : function() {
        console.log(this.name);
    }
};

默认情况下,原生的constructor属性是不可枚举的,因此如果你使用兼容ES5的JavaScript引擎,可以试下Object.defineProperty()

function Person(){}

Person.prototype = {
    name : "Jason",
    age : 29,
    job : "Web",
    sayName : function() {
        console.log(this.name);
    }
};
//重设构造函数,只适用ES5兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false,
    value: Person
});
2.3.4 原型的动态性

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

var friend = new Person();

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

friend.sayHi();        //"hi"

即使person实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。其原因可以归结为实例与原型之间松散连接关系。因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的sayHi属性并返回保存在那的函数。

如果是重写整个原型对象,情况就和上面不一样了。调用函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不指向构造函数。

function Person(){}

var friend = new Person();

Person.prototype = {
    constructor : Person,
    name : "Jason",
    age : 29,
    job : "Web",
    sayName : function() {
        console.log(this.name);
    }
};

friend.sayName();    //error

我们先创建了一个实例,然后重写了其原型对象。下图展示了整个过程

如图,重写原型对象切断了现有原型与任务之前已经存在的对象实例之间的联系;friend实例引用的仍然是最初的原型

2.3.5 原生对象的原型

原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型。都采用这种模式创建的。所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。

console.log(Array.prototype.sort);            //function(){...}        console.log(String.prototype.substring);    //function(){...}

通过原生对象的原型,我们也可以自己定义新方法。

String.prototype.startsWith = function(text) {
    return this.indexOf(text) == 0;
};

var msg = "Hello world!";
console.log(msg.startsWith("Hello")); //true
2.3.6 原型对象的问题

原型的所有属性是被很多实例共享的,这种共享对于函数非常合适。原型的问题就是其共享的本性造成的。

function Person(){}

Person.prototype = {
    constructor : Person,
    name : "Jason",
    age : 29,
    job : "Web",
    friend : ["Cor", "Sam"],
    sayName : function() {
        console.log(this.name);
    }
}

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

person1.friends.push("Court");
console.log(person1.friends);   //"Cor,Sam,Court"
console.log(person2.friends);   //"Cor,Sam,Court"
console.log(person1.friends === person2.friends);    //true

通过原型共享虽然全部实例都可以共享属性和方法。可是实例一般都是要有属于自己的全部属性。

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

创建自定义类型的最常用方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。这样每个实例都会有自己的一份实例属性的副本,但同时共享着对方法的引用,最大限度地节省了内存。这种混成模式还支持向构造函数传递参数。结合了两种模式之长。

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Cor", "Sam"];
}

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

var person1 = new Person("Jason", 18, "Web");
var person2 = new Person("cou", 19, "doctor");

person1.friends.push("Van");
console.log(person1.friends);    //"Cor,Sam,Van"
console.log(person2.friends);    //"Cor,Sam"
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true

构造函数定义属性,原型定义共享的方法和属性。这种构造函数与原型混成的模式,是目前在ECMAScript中使用最广泛,认同度最高的一种创建自定义类型的方法。可以说,这是用来定义引用类型的一种默认模式。

2.5 动态原型模式

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

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

    //方法
    if(typeof this.sayName != "function") {
        Person.prototype.sayName = function() {
            console.log(this.name);
        };
    }
}

var friend = new Person("Jason", 18, "Web");
friend.sayName();

方法那块,只在sayName()方法不存在的情况下,才会将它添加到原型中。这段代码只会在初次调用中构造函数时才会执行。此后,原型已经完成初始化吗,不需要再做什么修改了。不过要记住,这里对原型所做的修改,能够立即在所有实例中得到反映。因此,这种方法确实可以说是非常完美。其中if语句检查的可以是初始化之后应该存在的任何属性和方法——不必用一大堆if语句检查每个属性的每个方法;只需要检查一个即可。对于采用这种模式创建的对象,还可以使用instanceof操作符确定它的类型。

注意:使用动态原型模式时,不能使用对象字面量重写原型。前面已经解释过了,如果在已经创建了实例的情况下重写原型,那么就会切断现有实例与新原型之间的练习。

2.6 寄生构造函数模式

在前面几种模式都不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。

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 friend = new Person("Jason", 19, "Web");
friend.sayName();     //"Jason"

除了使用new操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象的实例。而通过在构造函数的末尾添加一个return语句,可以重写调用构造函数时返回的值。

这个模式可以在特殊的情况下用来为对象创建构造函数。假设我们想创建一个具有额外方法的特殊数组。由于不能直接修改Array构造函数,因此可以使用这个模式。

function SpecialArray() {
    //创建数组
    var values = new Array();
    
    //添加值
    values.push.apply(values, arguments);

    //添加方法
    values.toPipedString = function() {
        return this.join("|");
    };

    //返回数组
    return values;
}

var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString());   //"red|blue|green"

关于寄生构造函数模式,有一点需要说明: 首先,返回的对象与构造函数或者与构造函数的原型之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建对象没有什么不同。不能依赖instanceof操作符来确定对象类型。

2.7 稳妥构造函数模式

所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用this和new)

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

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

    //添加方法
    o.sayName = function() {
        console.log(name)""
    }
    
    //返回对象
    return o;
}

注意,在以这种模式创建的对象中,除了使用sayName()方法之外,没有其他办法访问到name的值。可以像下面使用稳妥的Person构造函数

var friend = Person("Jason", 18, "Web");
friend.sayName();    //"Jason"

这样,变量friend中保存的是一个稳妥对象,除了调用sayName()方法外,没有别的方式可以访问其数据成员。

3.继承

由于函数没有签名,在ECMAScript中无法实现接口继承。ECMAScript只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

3.1 原型链

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

回顾下构造函数、原型和实例的关系:

每个构造函数都有一个原型对象;

原型对象都包含一个指向构造函数的指针;

实例都包含一个指向原型对象的内部指针{{Prototype}}

我们让原型对象等于另一个类型的实例,原型对象将包含一个指向另一个原型的指针,相应地,另一个原型也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上诉关系依然成立,如此层层递进,就构成了实例与原型的链条。

实现原型链的基本模式

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
}

function SubType(){
    this.subproperty = false;
}

//继承了SuperType
//SubType的原型对象等于SubperType的实例,
//这样SubType内部就会有一个指向SuperType的指针从而实现继承
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function(){
    return this.subproperty;
}

var instance = new SubType();
console.log(instance.getSuperValue());  //true

SubType继承了superType,而继承是通过创建SuperType实例,并将该实例赋给SubType.prototype实现的。原来存在于SuperType的实例中的所有属性和方法,现在也存在于SubType.prototype中了。这个例子中的实例、构造函数和原型之间的关系如图:

要注意instance.constructor现在指向的是SuperType,是因为SubType的原型指向了另一个对象SuperType的原型,而这个原型对象的constructor属性指向的是SuperType。

通过实现原型链,本质上扩展了前面说的原型搜索机制。在通过原型链实现继承的情况下,搜索过程就得以沿着原型链继续向上。拿上面的例子来说。调用instance.getSuperValue()会经历三个搜索步骤:

搜索实例;

搜索SubType.prototype;

搜索SuperType.prototype,最后一步才会找到该方法。

在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。

1. 别忘记默认的原型

事实上,前面的例子中展示的原型链还少一环。所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。要记住,所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内布指针,指向Object.prototype。这也正是所有自定义类型都会继承toString()、valueOf()等默认方法的根本原因。所以上面的例子展示的原型链中还应该包含另一个继承层次。

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

2. 确定原型和实例的关系

有两种方式可以确定。

instanceof操作符

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

console.log(instance instanceof Object);    //true    
console.log(instance instanceof SuperType);    //true
console.log(instance instanceof SubType);    //true

由于原型链的关系,可以说instanceObjectSuperTypeSubType中任何一个类型的实例。因此,都返回true

isPrototypeOf()方法

同样,只要是原型链中出现过得原型,都可以说是该原型链所派生的实例的原型。

console.log(Object.prototype.isPrototypeOf(instance));      //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance));   //true
3. 谨慎地定义方法

子类型(上例的SubType)有时候需要重写超类型(上例的SuperType)中的某个方法,或者需要添加超类型中不存在的的某个方法。但不管怎样,给原型添加方法一定要放在替换原型的语句之后。

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;
}

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

当通过SubType的实例调用getSuperValue()时,调用的就是这个重写的方法,这是因为搜索机制搜索首先从实例中搜索然后到子类型的原型再到超类型的原型,然后子类型重写了该方法,搜索机制首先在子类型的原型中找到了该方法就不会继续继续搜索了。但通过SuperType的实例调用getSuperValue()时,还会继续调用原来的那个方法。这里要注意的是,必须在用SuperType的实例替换原型之后,再定义着两个方法。

还有一点需要注意,在通过原型链实现继承时,不能使用对象字面量创建原型方法。这样做会重写原型链。

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
}

function SubType(){
    this.subproperty = false;
}

//继承了SuperType
SubTyoe.prototype = new SuperType();

//使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
    getSubValue : function(){
        return this.subproperty;
    },
    someOtherMethod : function(){
        return false;
    }
}

var instance = new SubType();
console.log(instance.getSuperValue());  //error!

由于现在的原型包含的是一个Object的实例,而非SuperType的实例,因此我们设想中的原型链已经被切断了。SubTypeSuperType之间已经没有关系了。

4. 原型链的问题

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

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

function SubType(){
}

SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);  //"red, blue, green, black"

var instance2 = new SubType();
console.log(instance2.colors);    //"red, blue, green, black"

这个例子中的SuperType构造函数定义了一个colors属性,该属性包含一个数组(引用类型值)。SuperType的每个实例都会有各自包含自己数组的colors属性。当SubType通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType(),所以它也用用了一个colors属性。就跟专门创建了一个SubType.prototype.colors属性一样。结果SubType得所有实例都会共享这一个colors属性。

原型链的第二个问题是:在创建子类型的实例时,不能向超类型的构造函数传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。所以在实际运用中很少会多带带使用原型链。

3.2 借用构造函数

在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此通过使用apply()call()方法也可以在(将来)新创建的对象上执行构造函数。

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

function SubType(){
    //继承了SuperType,同时还传递了参数
    SuperType.call(this, "Jason");

    //实例属性
    this.age = 18;
}

var instance1 = new SubType();
instance1.colors. push("black");
console.log(instance1.colors);         //"red,blue,green,black"
console.log(instance1.name);        //"Jason"
console.log(instance1.age);            //18

var instance2 = new SubType();
console.log(instance2.colors);        //"red,blue,green"

为了确保SuperType构造函数不会重写子类型属性,可以再调用超类型构造函数后,再添加应该在子类型中定义的属性。

借用构造函数的问题

如果仅仅是借用构造函数那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数服用就无从谈起了。而却,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。借用构造函数技术也是很少多带带使用的。

3.3 组合继承

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

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.constructor = SubType;
SubType.prototype.sayAge = function(){
    console.log(this.age);
};

var instance1 = new SubType("Jason", 18);
instance1.colors.push("black");
console.log(instance1.colors);    //"red,blue,green,black"
instance1.sayName();            //"Jason"
instance1.sayAge();                //18

var instance2 = new SubType("Cor", 20);
console.log(instance2.colors)l    //"red,blue,green"
instance2.sayName();            //"Cor"
instance2.sayAge();                //20

这样一来,就可以让两个不同的SubType实例即分别用有自己的属性——包括colors属性,又可有使用相同的方法了。如图:

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JavaScript中最常用的继承模式。而且,instanceofisPrototypeof()也能够用于识别基于组合继承创建的对象。

3.4 原型式继承

基本思想是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。

function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次深浅复制。

var person = {
    name: "Jason",
    friends: ["Cor", "Court", "Sam"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);        //"Cor,Court,Sam,Rob,Barbie"

这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础。如果有这么一个对象的话,可以把它传递给object()函数,然后再根据具体需求对得到的对象加以修改即可。在这个例子中,可以作为另一个对象的基础是person对象,于是我们把它传入到object()函数中,然后该函数就会返回一个新对象。这个新对象将person作为原型,所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。这以为着person.friends不仅属于person所有,而且也会被anotherPerson以及yetAnotherPerson共享。实际上,这就相当于又创建了person对象的两个副本。

ECMAScript5通过新增Object.create()方法规范化了原型式继承。这个方法接收两个参数:一个用于做新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()object()方法的行为相同

var person = {
    name: "Jason",
    friends: ["Cor", "Court", "Sam"]
};

var anotherPerson = Object.create(person);
anotherPerson.friends.push("Rob");


var yetAnotherPerson = Object.create(person, {
    name: {
        value: "Greg"
    }
});
yetAnotherPerson.friends.push("Barbie");

console.log(yetAnotherPerson.name);    //"Greg"
console.log(anotherPerson.name);    //"Jason"
console.log(person.friends);        //"Cor,Court,Sam,Rob,Barbie"

Object.create()方法的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式制定的任何属性会覆盖原型对象上的同名属性。

别忘了,包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样

3.5 寄生式模式

寄生式(parasitic)继承是与原型式继承紧密相关的一种思路。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。

function createAnother(original){
    var clone = object(original);     //通过调用函数创建一个新对象
    clone.sayHi = function(){         //以某种方式来增强这个对象
         alert("Hi");
    };
    return clone;                     //返回这个对象
}

可以像下面这样来使用createAnother()函数:

var person = {
    name: "Jason",
    friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = createAnother(person);
anotherPerson.sayHi();     //"hi"

这个例子中的代码基于person返回了一个新对象——anotherPerson。新对象不仅具有person的所有属性和方法,而且还有自己的sayHi()方法。

3.6 寄生组合式继承

组合继承是JavaScript最常用的继承模式;不过它也有不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型的构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

function SuperType(name){
    this.name = name;
    this.colors = ["red", "bule", "grenn"];
}

SuperType.prototype.sayName = function(){
    console.log(this.name);
}

function SubType(name, age){
    SuperType.call(this, name);          //第二次调用SuperType
 
    this.age = age;
}

SubType.prototype = new SuperType();     //第一次调用SuperType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
    console.log(this.age);
}

有注释的两行代码是调用SuperType构造函数的代码,第一次调用SuperType构造函数时,SubType.prototype会有SuperType的实例属性。第二次调用SuperType的构造函数时SubType会在构造函数中添加了SuperType的实例属性。当创建SubType的实例它的[[Prototype]]和自身上都有相同属性。根据搜索机制自身的属性就会屏蔽SubType原型对象上的属性。等于原型对象上的属性是多余的了。如图:

如图所示,有两组namecolors属性:一组在实例上,一组在SubType原型中。这就是调用两次SuperType构造函数的结果。解决办法是——寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式的基本模式如下:

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype);   //创建对象
    prototype.constructor = subType;               //增强对象
    subType.prototype = prototype                  //指定对象
}

这个实力的inheritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接受两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二部是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。

function object(o){
    function F(){}    //创建个临时构造函数
    F.prototype = o;  //superType.prototype
    return new F();   //返回实例
}

function inheritPrototype(subType, superType){
    /*  创建对象
        传入超类型的原型,通过临时函数进行浅复制,F.prototype的指针就指向superType.prototype,在返回new F()    
    */
    var prototype = object(superType.prototype);   
    prototype.constructor = subType;               //增强对象
    /*  指定对象
        子类型的原型等于F类型的实例,当调用构造函数创建一个新实例后,该实例会包含一个[[prototype]]的指针指向构造函数的原型对象,所以subType.prototype指向了超类型的原型对象这样实现了继承,因为构造函数F没有属性和方法这样就子类型的原型中就不会存在超类型构造函数的属性和方法了。
    */
    subType.prototype = prototype                  //new F();
}

function SuperType(name){
    this.name = name;
    this.colors = ["red", "bule", "grenn"];
}

SuperType.prototype.sayName = function(){
    console.log(this.name);
}

function SubType(name, age){
    SuperType.call(this, name);          
 
    this.age = age;
}

inheritPrototype(SubType, SuperType);A

SubType.prototype.sayAge = function(){
    console.log(this.age);
}

var ins1 = new SubType("Jason", 18);

下图是我自己的理解:

最后,如有错误和疑惑请指出,多谢各位大哥

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82614.html

相关文章

  • 重学前端学习笔记(七)--JavaScript对象面向对象还是基于对象

    摘要:对象有状态对象具有状态,同一对象可能处于不同状态之下。中对象独有的特色对象具有高度的动态性,这是因为赋予了使用者在运行时为对象添改状态和行为的能力。小结由于的对象设计跟目前主流基于类的面向对象差异非常大,导致有不是面向对象这样的说法。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些...

    mayaohua 评论0 收藏0
  • 重学前端学习笔记(七)--JavaScript对象面向对象还是基于对象

    摘要:对象有状态对象具有状态,同一对象可能处于不同状态之下。中对象独有的特色对象具有高度的动态性,这是因为赋予了使用者在运行时为对象添改状态和行为的能力。小结由于的对象设计跟目前主流基于类的面向对象差异非常大,导致有不是面向对象这样的说法。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些...

    yy736044583 评论0 收藏0
  • 重学前端学习笔记(七)--JavaScript对象面向对象还是基于对象

    摘要:对象有状态对象具有状态,同一对象可能处于不同状态之下。中对象独有的特色对象具有高度的动态性,这是因为赋予了使用者在运行时为对象添改状态和行为的能力。小结由于的对象设计跟目前主流基于类的面向对象差异非常大,导致有不是面向对象这样的说法。 笔记说明 重学前端是程劭非(winter)【前手机淘宝前端负责人】在极客时间开的一个专栏,每天10分钟,重构你的前端知识体系,笔者主要整理学习过程的一些...

    xingpingz 评论0 收藏0
  • JavaScript面向对象编程学习笔记---概念定义

    摘要:子类继承自父类的方法可以重新定义即覆写,被调用时会使用子类定义的方法什么是多态青蛙是一个对象,金鱼也是一个对象,青蛙会跳,金鱼会游,定义好对象及其方法后,我们能用青蛙对象调用跳这个方法,也能用金鱼对象调用游这个方法。 1、专用术语 面向对象编程程序设计简称:OOP,在面向对象编程中常用到的概念有:对象、属性、方法、类、封装、聚合、重用与继承、多态。 2、什么是对象? 面向对象编程的重点...

    mikasa 评论0 收藏0
  • JavaScript学习笔记第四天_面向对象编程

    摘要:即另外,注意到构造函数里的属性,都没有经过进行初始化,而是直接使用进行绑定。并且在模式下,构造函数没有使用进行调用,也会导致报错。调用构造函数千万不要忘记写。 1. 基础 JavaScript不区分类和实例的概念,而是通过原型来实现面向对象编程。Java是从高级的抽象上设计的类和实例,而JavaScript的设计理念,听起来就好比Heros里的Peter,可以复制别人的能力。JavaS...

    weapon 评论0 收藏0

发表评论

0条评论

brianway

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<