计算机 / 读书笔记 · 2022年8月27日 0

Professional JavaScript for Web Developers, 3rd Edition, Part 4

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

ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或函数”。

1.理解对象

创建对象时可以通过new Object()来创建,也可以通过字面量的方式来创建:

var person = {
  name: "Nicolas",
  age: 29,
  job: "Software Engineer",

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

1.1 属性类型

属性的属性类似C++里的注解,放在双重方括号里,描述了属性的行为特征。

数据属性

数据属性包含数据值的位置,在这个位置可以读取和写入值。数据属性具有下列特性:

  • [[Configurable]] 表示能否通过delete删除属性从而从新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认为true。
  • [[Enumerable]]表示能否通过for-in循环返回属性,默认为true。
  • [[Writable]]表示能否修改属性的值。默认为true。
  • [[Value]]包含属性的值。默认为undefined。

要修改属性默认的特性,需要使用Object.defineProperty()方法。

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

把configurable设置为false后,不能再将其设置为true。

在使用Object.defineProperty()方法创建新的属性时,configurable,enumerable,writable的默认值是false。

访问器属性

访问器属性包含一对get/set函数,相当于重载了对象中某个属性的读写函数,但是这个也不是完全是重载,因为看下面的示例代码,一个是_year,一个是year,两个名字不一样,相当于定义了一个不用加括号调用的函数吧。

访问器属性必须使用Object.defineProperty来定义。

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

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

book.year = 2005;
alert(book.edition);  // 2

可以不同时指定get/set函数,但是只指定其中一个get/set的话,意味着就不能实现对该对象的写/读操作。

老旧版本浏览器可以使用__defineGetter__()和__defineSetter__()方法定义对象属性的set/get方法。

1.2定义多个属性

通过Object.defineProperties()方法可以一次性定义多个属性的属性。

var book = {};

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

  edition: {
    writable: true,
    value: 1
  },

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

1.3读取属性的特性

通过Object.getOwnPropertyDescriptor()方法可以获取属性的属性。

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
alert(descriptor.value);  // 2004
alert(descriptor.configurable);  // false

2.创建对象

1.工厂模式

2.构造函数模式

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

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

构造函数必须搭配new操作符使用,且可以看到构造函数使用了this指针,且没有return语句。

这样一个构造函数就相当于定义了一个类。

通过这种方式创建的对象的constructor值为使用new操作符时调用的构造函数,并且可以通过instanceof操作符检验对象是否是这个类的实例。

构造函数可以直接当做普通函数调用,但此时的效果就是为Global对象添加/设置了属性。

使用new操作符和不使用new的区别使用了new操作符,新创建对象的this指针指向新创建对象自己,否则新创建对象的this指针指向Global对象。

上面的实例代码中为对象添加的sayName函数实际上对于每个创建的对象都是有一个独立的实例的。如果要让同一个类的所有对象都共享同一个类方法的实例,需要在构造函数外部定义这个类方法,然后让构造函数里的方法名指向这个函数。但是这样做呢又会导致要在构造函数外面定义很多的全局函数,所以这个时候原型模式出场了。

3.原型模式

每个函数都包含有一个名为prototype的(原型)属性,这个属性指向一个可以包含共享属性和方法的对象。prototype原型对象的constructor属性默认为包含这个prototype原型对象的函数。

a.理解原型对象

通过构造函数创建一个实例后,该实例内部将包含一个指向原型对象的指针__proto__(即[[Prototype]])。当我们用a.attribute的语法访问对象a的属性/方法attribute时,解释器会先查找对象a自己是否具有这个属性/方法,如果没有再查找原型对象是否具有这个属性/方法。

当为实例添加和原型对象中的同名属性/方法时,实例自己的属性/方法会屏蔽原型对象中的同名属性/方法,只有使用delete操作符删除实例属性才可以再次访问原型对象中的属性/方法。

isPrototypeOf()方法可以用来判定一个对象是否为另一个对象的原型对象。

ECMAScript 5增加了Object.getPrototypeOf()方法来获取原型对象。

hasOwnProperty方法可以检测一个属性是否存在于实例中还是原型对象中,只有存在于对象实例中时,才会返回true。

b.in操作符

in操作符可以单独使用和放在for循环里,for-in循环返回的是所有能够通过对象访问的、可枚举的属性,既包括实例中的属性,也包括原型对象中的属性。for-in循环中,实例属性会屏蔽原型对象中的属性。

也可以使用Object.keys()Object.getOwnPropertyNames()方法获取属性名字的集合,区别在于:

Object.keys()返回的是对象自己的属性,不包括原型对象的属性,也不包括不能枚举的属性;

Object.getOwnPropertyNames()方法返回的是对象自己的属性,不包括原型对象的属性,包括不能枚举的属性。

3.更简单的原型语法

原型对象可以通过字面量的方式创建,但是这时候需要手动设置其constructor属性,并且此时constructor的[[Enumerable]]特性被设置为true。

4.原型的动态性

在原型中查找值是一个动态过程,因此对原型对象所做的任何修改都能够立即从实例上反映出来。

var friend = new Person();

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

friend.sayHi();

friend对象虽然在sayHi函数之前创建,但是查找sayHi函数时按照现在friend实例中查找,再在Person原型对象中查找的顺序,而在Person原型对象中查找时相当于按指针查找。

可以随时修改原型对象中的属性和方法,并且能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,就相当于改变了原型对象指针指向的对象,在这之前创建的实例中的原型对象指针就失效了。

function Person() {}

var friend = new Person();

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

friend.sayName(); // error, friend实例中的原型对象已经失效了

5.原生对象的原型

可以修改原生引用类型的原型对象中的方法和属性,但是不推荐这么做。

6.原型对象的问题

原型对象的问题在于原型对象中的属性是所有实例共享的,一个实例修改了原型对象的属性,另一个实例也会立即感受到这个属性的改变。

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

实例的私有属性卸载构造函数里,公用的方法写在原型对象里,以最大限度地节省空间。

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

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

var Person1 = new Person("Nicholas", 29, "Software Engineer");
var Person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");
alert(person1.friends);
alert(person2.friends);
alert(person1.friends === person2.friends); // false
alert(person1.sayName === person2.sayName); // true

5.动态原型模式

动态原型模式:把所有的逻辑都封装在构造函数里,并且原型对象也是在构造函数中动态初始化的。

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

  if (typeof this.sayName != "function") {
    Person.prototype.sayName = function() {
      alert(this.name);
    }
  }
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

6.寄生构造函数模式

寄生(parasitic)构造函数模式:在构造函数内部创建并返回对象。

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

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();

构造函数默认返回新对象实例,但是这里通过return语句返回了内部创建的对象。这个内部创建的对象o与Person类的构造函数和原型对象没有任何关系,与在构造函数外部创建的对象没有区别。

使用寄生构造函数创建的对象不能用instanceof操作符来确定对象类型,所以一般不要使用这种模式。

7.稳妥构造函数模式

稳妥对象(durable objects):没有公共属性也没有this的对象,只能通过其方法访问其内部属性。

稳妥构造函数的要求:1.新创建对象的实例方法不引用this,2.不使用new操作符调用构造函数。

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

var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName();

3.继承

1.原型链

通过原型链实现继承的方式:让子类的原型对象指向父类的一个实例。

对一个子类对象搜索属性和方法时,首先在该子类对象中搜索,然后在子类的原型对象中搜索,由于子类的原型对象指向了一个父类的对象,会现在父类的对象中搜索,最后才在父类的原型对象中搜索。

所有引用类型都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针指向Object.prototype。

可以通过instanceof操作符来确定原型和实例之间的关系。其次可以通过isPrototypeOf()方法判断原型和实例之间的关系。

不能使用对象字面量创建原型方法,这样会重写原型链。

原型链的问题:

1.子类原型对象指向父类实例时,所有子类对象都会共享这个父类实例中的属性;

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

2.借用构造函数(constructor stealing)

借用构造函数也叫伪造对象或者经典继承。

借用构造函数实际上是在子类的构造函数中通过call/apply方法直接执行了”父类”的构造函数,因此这种继承实际上是一种伪继承。

借用构造函数可以实现为每个子类实例提供自己独有的”继承”属性。

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

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

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

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

3.组合继承(combination inheritance)

又名伪经典继承,是一种将原型链和借用构造函数组合的方法。是JavaScript中最常用的继承模式。

缺点在于会存在两组名字相同的属性,一组在子类对象中,一组在子类的原型对象指向的父类对象中。解决方法见后面的寄生组合式继承。

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

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

        SubType.prototype = new SuperType();
        
        SubType.prototype.sayAge = function(){
            alert(this.age);
        };
        
        var instance1 = new SubType("Nicholas", 29);
        instance1.colors.push("black");
        alert(instance1.colors);  //"red,blue,green,black"
        instance1.sayName();      //"Nicholas";
        instance1.sayAge();       //29
        
       
        var instance2 = new SubType("Greg", 27);
        alert(instance2.colors);  //"red,blue,green"
        instance2.sayName();      //"Greg";
        instance2.sayAge();       //27

4.原型式继承(Prototypal Inheritance)

原型式继承与原型链类似,区别在于不需要兴师动众地创建构造函数,而是通过对一个已有的对象做修改创作出新的类似对象。所以原型式继承创造的新对象仍然是共享”父类”对象的属性。

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

ECMAScript5通过新增Object.create()方法规范化了原型式继承:

        var person = {
            name: "Nicholas",
            friends: ["Shelby", "Court", "Van"]
        };
        
        var anotherPerson = Object.create(person);
        anotherPerson.name = "Greg";
        anotherPerson.friends.push("Rob");
        
        var yetAnotherPerson = Object.create(person);
        yetAnotherPerson.name = "Linda";
        yetAnotherPerson.friends.push("Barbie");
        
        alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie"

5.寄生式继承(parasitic inheritance)

寄生式继承类似于工厂模式,但又并不是完全的工厂模式,寄生式继承会对构造函数生成的对象作修改,并且通过这种方式为新对象添加的函数不能做到函数复用,会降低程序的效率。

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

6.寄生组合式继承

为了解决组合继承需要创建额外创建父类原型实例和每个属性在父类实例和子类实例中均存在一份的问题,可以使用寄生组合式继承:

            
        function object(o){
            function F(){}
            F.prototype = o;
            return new F();
        }
    
        function inheritPrototype(subType, superType){
            var prototype = object(superType.prototype);   //create object
            prototype.constructor = subType;               //augment object
            subType.prototype = prototype;                 //assign object
        }
                                
        function SuperType(name){
            this.name = name;
            this.colors = ["red", "blue", "green"];
        }
        
        SuperType.prototype.sayName = function(){
            alert(this.name);
        };

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

        inheritPrototype(SubType, SuperType);
        
        SubType.prototype.sayAge = function(){
            alert(this.age);
        };
        
        var instance1 = new SubType("Nicholas", 29);
        instance1.colors.push("black");
        alert(instance1.colors);  //"red,blue,green,black"
        instance1.sayName();      //"Nicholas";
        instance1.sayAge();       //29
        
       
        var instance2 = new SubType("Greg", 27);
        alert(instance2.colors);  //"red,blue,green"
        instance2.sayName();      //"Greg";
        instance2.sayAge();       //27

简单地说就是先给父类原型对象创建一个拷贝,然后把这个拷贝的构造函数改成子类的构造函数,并并且把这个拷贝赋给子类的原型对象。