JavaScript面向对象

面向对象的程序设计

我们可以把 ECMAScript 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建一个 Object 的实例
var person = new Object();
person.name = "Nicholas";
person.age = 26;
person.job = "Software Engineer";

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

// 对象字面量语法
var person = {
name: "Nicholas",
age: 29,
job: "Software Engineer",

sayName: function(){
alert(this.name);
}
}

属性类型 数据属性和访问器属性

  1. 数据属性:包含一个数据值的位置,在这个位置可以读取和写入值。
    (1)[[Configurable]]: 表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,他们的这个特性默认值是 true
    (2)[[Enumerable]]: 表示能否通过 for-in 循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true
    (3)[[Writable]]: 表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为 true
    (4)[[Value]]: 包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined
    要修改属性默认的特性,必须使用 Object.defineProperty() 方法,这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象(属性必须是: configurable、enumerable、writable、value)。一旦把 writable 属性定义为不可配置,再调用 Object.defineProperty() 方法修改除 writable 之外的特性,都会导致错误
  2. 访问器属性
    不包含数据值;包含一对儿 getter 和 setter 函数(不是必须)。再读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。
    (1)[[Configurable]]: 表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。对于直接在对象上定义的属性,他们的这个特性默认值是 true
    (2)[[Enumerable]]: 表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,它们的这个特性默认值为 true
    (3)[[Get]]: 在读取属性时调用的函数。默认值为 undefined。
    (4)[[Set]]: 在写入属性时调用的函数。默认值为 undefined。
    访问器属性不能直接定义,必须使用 Object.defineProperty() 来定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    var book = {
    _year: 2004,
    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;
    alert(book.edition); // 2 响应式更新数据,设置一个属性的值会导致其他属性发生变化

    var book = {};
    Object.defineProperties(book, {
    _year: {
    value: 2004
    },
    edition: {
    writable:true, // 默认为false,设置了这个值才能响应式改变
    value: 1
    },
    year: {
    get: function () {
    return this._year;
    },
    set: function (newValue) {
    if (newValue > 2004) {
    this._year = newValue;
    this.edition += newValue-2004;
    }
    }
    }
    })
    book.year = 2005;
    alert(book.edition);
  3. 读取属性的特性
    Object.getOwnPropertyDescriptor() 方法,可以取得给定属性的描述符。该方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有 configurable、enumerable、get 和 set;如果是数据属性,这个对象属性有 configurable、enumerable、writable 和 value

创建对象

  1. 工厂模式: 抽象了创建具体对象的过程
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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("张三", 26, "软件工程师");
    var person2 = createPerson("李四", 24, "运维工程师");

解决了创建多个相似对象的问题,没有解决对象识别的问题

  1. 构造函数模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function(){
    alert(this.name);
    };
    }

    var person1 = new Person("张三", 26, "软件工程师");
    var person2 = new Person("李四", 24, "运维工程师");

任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。
使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。

  1. 原型模式
    (1)我们创建的每个函数都有一个 prototype (原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法(原型对象),在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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();
    person1.sayName(); // "Nicholas"

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

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

    (2) 使用 hasOwnProperty() 方法可以检测一个属性时存在实例中,还是存在于原型中,这个方法只在给定属性存在于对象实例中时,才返回 true
    (3) 有两种方式使用 in 操作符:单独使用和在 for-in 循环中使用。单独使用时,in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。

    1
    2
    3
    4
    // 确定该属性存在于对象中(false),还是存在于原型中(true)
    function hasPrototypeProperty(object,name){
    return !object.hasOwnProperty(name) && (name in object);
    }

    (4) 使用 for-in 循环,返回的是所有能够通过对象访问的、可枚举的 (enumerated) 属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。
    (5) 取得对象上所有可枚举的实例属性,可以使用Object.key() 方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组
    (6) 取得所有实例属性,无论它是否可枚举,都可以使用 Object.getOwnPropertyNames() 方法
    (7) 更简单的原型语法:对象字面量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Person(){

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

    (8) 重写整个原型对象,等于切断了构造函数与最初原型之间的联系。实例中的指针仅指向原型,而不指向构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function Person(){   
    }

    var friend = new Person();

    Person.prototype.sayHi = functon(){
    console.log("hi");
    };

    friend.sayHi(); // "hi"

    // 重写原型对象
    Person.prototype = {
    constructor: Person,
    name: "Nicholas",
    age: 29,
    job: "SoftWare Engineer",
    sayName: function(){
    console.log(this.name);
    }
    }

    friend.sayName(); // 运行报错

    (8) 所有原生引用类型(Object、Array、String,等等)都在其构造函数的原型上定义了方法。

    1
    2
    console.log(typeof Array.prototype.sort); // function
    console.log(typeof String.prototype.substring); // function

    (9) 原型模式省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都取得相同的属性值。这种共享对于函数非常合适,对于那些包含基本值的属性倒也说得过去(通过在实例上添加一个同名属性可以覆盖原型中的对应属性),然而,对于包含引用类型值的属性就有问题了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    function Person(){
    }

    Person.prototype = {
    constructor: Person,
    name: "Nicholas",
    age: 29,
    job: "SoftWare Engineer",
    friends: ["Shelby", "Court"],
    sayName: function(){
    console.log(this.name);
    }
    }

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

    person1.friends.push("Van");

    console.log(person1.friends); // "Shelby,Court,Van"
    console.log(person2.friends); // "Shelby,Court,Van"
    console.log(person1.friends === person2.friends); // true


    4. 组合使用构造函数模式和原型模式
    (1) 构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数
    function Person(name,age,job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friends = ["Shelby", "court"]
    }

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

    var person1 = new Person("Nicholas", 29, "SoftWare Engineer");
    var person2 = new Person("Greg", 27, "Doctor");

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

这种构造函数与原型混成的模式,是目前在 ECMAScript 中使用最广泛。认同度最高的一种创建自定义类型的方法

  1. 动态原型模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    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 person1 = new Person("Nicholas", 29, "SoftWare Engineer");
    person1.sayName(); // "Nicholas"

这里只在 sayName() 方法不存在的情况下,才会将它添加到原型中。不能使用对象字面量重写原型。

  1. 寄生构造函数模式
    这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Person(name,age,job){
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function(){
    alert(this.name);
    };
    return o;
    }

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 操作符来确定对象类型,不推荐使用这种模式

  1. 稳妥构造函数模式
    稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。最适合在一些安全的环境中(禁止使用 this 和 new),或者在防止数据被其他应用程序改动时使用。稳妥构造函数遵循与寄生构造函数内饰的模式,但有两点不同:一是新创建对象的实例方法不引用 this;二是不使用 new 操作符条用构造函数。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function Person(name,age,job){

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

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

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

    // 返回对象
    return o; // 除了使用 sayName() 方法外,没有其他方法访问 name 的值
    }

继承(接口继承和实现继承)

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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

    (2)所有引用类型默认都继承了 Object,因此默认原型都会包括一个内部指针,指向 Object.prototype。一句话, Subtype 继承了 SuperType,而 SuperType 继承了 Object。
    (3)instanceof 操作符 和 isPrototypeOf() 方法都可以用来确定原型和实例之间的关系。
    (4)子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后,并且不能使用对象字面量创建原型方法
    (5) 原型链的问题:包含引用类型值的原型;在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

  2. 借用构造函数(伪造对象或经典继承)
    (1) 通过使用 apply() 和 call() 方法可以在新创建的对象上执行构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function SuperType() {
    this.colors = ["red", "blue", "green"];
    }

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

    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'

    (2) 传递参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    function SuperType(name) {
    this.name = name;
    }

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

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

    var instance = new SubType();
    console.log(instance.name); // Nicholas
    console.log(instance.age); // 29
    (3) 仅仅是借用构造函数,方法都在构造函数中定义,因此函数复用就无从谈起了,这个技术也是很少单独使用的。
    3. 组合继承(伪经典继承)
    (1) 将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。背后的思路是使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。
    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); // 第二次调用 SuperType()
    this.age = age
    }

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

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

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

组合继承避免了原型链和借用构造函数得缺陷,融合了它们的优点,是JavaScript 中最常用得继承模式。

  1. 原型式继承
    这种方法没有使用严格意义上的构造函数。借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
    1
    2
    3
    4
    5
    function object(o){
    function F(){}
    F.prototype = o;
    return new F();
    }

从本质上来说,object()对传入其中的对象执行了一次浅复制,ECMAScript 5通过新增 object.create() 方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create() 与 object()方法的行为相同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
}

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

var yeotherPerson = Object.create(person);
yeotherPerson.name = "Linda";
yeotherPerson.friends.push("barbie");

console.log(person.friends) //'Shelby', 'Court', 'Van', 'Rob', 'barbie'

var weotherPerson = Object.create(person,{
name:{
value: "Greg" // 覆盖原型对象上的同名属性
}
});
console.log(weotherPerson.name) // Greg

  1. 寄生式继承
    创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象
    1
    2
    3
    4
    5
    6
    7
    function createAnother(original){
    var clone = object(original); // 通过调用函数创建一个新对象
    clone.sayHi = function(){ // 以某种方式来增强这个对象
    console.log("Hi");
    }
    return clone; // 返回这个对象
    }

使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。

  1. 寄生组合式继承
    组合继承是 JavaScript 最常用的继承模式;不过,组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    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
    }

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

    inheritPrototype(SubType,SuperType)

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

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

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