《JavaScript高级程序设计》第六章【面向对象的程序设计】 包括对象、创建对象、继承

一、理解对象
二、创建对象
     1. 工厂模式
     2. 构造函数模式
     3. 原型模式
     4. 组合使用构造函数模式和原型模式【使用最广泛】
     5. 动态原型模式
   6. 寄生构造函数模式
     7. 稳妥构造函数模式
三、继承
     1. 原型链
     2. 借用构造函数
     3. 组合继承【最常用】
     4. 原型式继承
     5. 寄生式继承
     6. 寄生组合式继承

 

一、理解对象


 

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

 

二、创建对象


 

1. 工厂模式

  使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这种方法后来被构造函数模式所取代。

 

2. 构造函数模式

  可以创建自定义引用类型,可以像创建内置对象实例一样使用new操作符。但是它的每个成员都无法得到复用,包括函数。

  但是这样说好像也不准确——如果是通过一个指针指向构造函数外部的函数的话,应该算是复用?

 1         function Person(name,age){
 2             this.name = name;
 3             this.age = age;
 4             this.sayName = sayName;  //一个指向函数的指针,所以所有的实例共享同一函数
 5             this.sayAge = function(){
 6                 console.log(this.age);
 7             }
 8         }
 9         function sayName(){
10             console.log(this.name);
11         }
12         var person1 = new Person('Jack');
13         var person2 = new Person('Amy');
14         person1.sayName();
15         person2.sayName();
16         console.log(person1.sayName === person2.sayName);   //true
17         console.log(person1.sayAge == person2.sayAge);      //false

  但是这种方法:1)sayName函数在全局作用域中定义,但实际只被某个对象调用,名不副实

         2)没有封装性

 

3. 原型模式

  使用构造函数的prototype属性来指定那些应该共享的属性和方法。

 

4. 组合使用构造函数模式和原型模式【使用最广泛】

  构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的熟悉。且这种模式还支持构造函数传递参数

  

 1         function Person(name){
 2             this.name = name;
 3         }
 4         Person.prototype = {
 5             constructor : Person,       //因为这里通过对象字面量重写了整个原型对象,但constructor会指向Object。所以要特意设置
 6             sayName : function(){
 7                 console.log(this.name); 
 8             }
 9         }
10         var person1 = new Person('Nick');
11         var person2 = new Person('Amy');
12         console.log(person1.sayName == person2.sayName);        //true

 

5. 动态原型模式

1         function Person(name){
2             this.name = name;
3             if(typeof this.sayName != "function"){          //这段代码只会在在初次调用构造函数时才会执行
4                 Person.prototype.sayName = function(){
5                     console.log(this.name);
6                 }
7             }
8         }
9         var person = new Person('Jack');

  可以使用instanceof操作符来确定实例类型  

  注意:使用动态函数模型时,不能使用对象字面量重写原型。

     在已经创建了实例的情况下重写原型,就会切断现有实例和新原型之间的关系。

     举个栗子:

 1         function Person(name){
 2             this.name = name;
 3             if(typeof this.sayName != "function"){          //这段代码只会在在初次调用构造函数时才会执行
 4                 Person.prototype.sayName = function(){
 5                     console.log(this.name);
 6                 }
 7             }
 8         }
 9         var person = new Person('Jack');
10         Person.prototype = {
11             sayHi : function(){
12                 console.log("hi");
13             }
14         }
15         person.sayName();       //Jack
16         //person.sayHi();         //Uncaught TypeError: person.sayHi is not a function
17 
18         var person2 = new Person('Amy');
19         person2.sayHi();        //hi
20         person2.sayName();      //Amy

  打印出person和person2:

       

 

  这里我的理解是:调用构造函数会为实例增加一个指向最初原型的[[prototype]]指针,而把原型修改为另一个对象,则会切断构造函数与最初原型之间的关系。因为实例中的指针只会指向原型,而不指向构造函数,此时修改的是构造函数的原型,而之前创建的实例仍然指向之前的原型对象。故其仍然用于sayName()函数。然而,为什么后来创建的实例person2也会拥有sayName()函数呢?不是之前的构建已经切断了吗?这里我猜测原因是因为在创建实例person2时,if逻辑检测到Person中没有sayName()函数,于是又增加了这样一个函数。为了验证猜想,我在if逻辑里打印一句话,这样只要进入循环就会打印出来这句话,果然打印了两次,猜想得证。

 

6. 寄生构造函数模式

  基本思想:创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。

  常用于为对象创建构造函数。

  一个栗子:创建一个具有额外方法的特殊数组

  

 1         function SpecialArray(){
 2             //创建数组
 3             var values = new Array();
 4 
 5             //添加值
 6             values.push.apply(values, arguments);       //这里比较疑惑的是为什么要用apply,既然values是一个数组,那么直接调用push不可以吗?
 7             //values.push(arguments);                 //[object Arguments]
 8 
 9             //添加方法
10             values.toPipedString = function(){
11                 return this.join("|");
12             };
13 
14             //返回数组
15             return values;
16         }
17         var color = new SpecialArray("red", "blue", "green");
18         console.log(color.toPipedString());     //red|blue|green

  上述代码中遇到了一个问题:如第6.7行所示,为什么要用apply而不能直接使values.push(arguments)呢?

  ——换成直接使用push后输出是[object Arguments]。然后查了下发现arguments果然是一个对象,那为什么apply中可以直接用?猜想应当是apply内部实现对arguments进行了解析。

  arguments并不是一个数组,而是一个伪数组,具有length属性. 这里也可以直接用[].push来代替.

 

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

  所以,可以用其他,不要用这个方法。

 

7. 稳妥构造函数模式

  稳妥对象,指的是没有公共属性,而且其方法也不引用this的对象。稳妥对象适用在一些安全的环境中(禁止this和now),或在防止数据被其他应用程序改动时使用。

        function Person(name){
            var o = new Object();
            
            //在这里定义私有变量和函数
            
            o.sayName = function(){
                console.log(name);
            };

            return o;
        }
        var person = Person("nick");
        person.sayName();   //nick

  这种方法与上一个方法的区别在于:新创建对象的实例方法不引用this,不使用new操作符调用构造函数。

  这种方法除了调用sayName()函数外,没有别的办法可以访问到其数据成员,所以具有安全性。

  instanceof操作符对这种方法也无效

 

三、继承


 

  两种继承方式:接口继承和实现继承。ECMAScript只支持实现继承,且其实现继承主要是依靠原型链来实现的

1. 原型链

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

  构造函数,原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,如果让原型对象等于另一个对象的实例,则原型对象将包含一个指向另一个原型的指针,另一个原型中也包含一个指向另一个构造函数的指针。(这都是什么鬼!)

  实现原型链模式:

 

        function Parent(){
            this.name = "parent";
        }
        Parent.prototype.getName = function(){
            return this.name;
        }
        function Child(){
            this.subName = 'child';
        }
        //继承了Parent
        Child.prototype = new Parent();
        Child.prototype.getSubName = function(){
            return this.subName;
        };
        var child = new Child();
        console.log(child.getName());   //parent
        console.log(child.getSubName());    //child

        //两种确定原型和实例的关系的方法
        //1)instanceof
        console.log(child instanceof Object);       //true
        console.log(child instanceof Parent);       //true
        console.log(child instanceof Child);        //true

        //isPrototypeOf()
        console.log(Object.prototype.isPrototypeOf(child));     //true
        console.log(Parent.prototype.isPrototypeOf(child));     //true
        console.log(Child.prototype.isPrototypeOf(child));      //true

  子类型有时候需要重写超类中的某个方法,或者需要添加超类中不存在的某个方法,给原型添加方法的代码一定要放到替换原型的语句之后。否则,子类的方法会被覆盖。因为原型指针指向了父类的原型。

 1         function Parent(name){
 2             this.name = name;
 3         }
 4         Parent.prototype.sayName = function(){
 5             console.log("Parent: my name is " + this.name);
 6         }
 7         function Child(name,age){
 8             this.name = name;
 9         }
10         Child.prototype.sayName = function(){
11             console.log("Child: my name is "+ this.name);
12         };
13         Child.prototype = new Parent();
14         var child = new Child('Amy');
15 
16         child.sayName();        //Parent: my name is Amy

  原型链继承的问题:原先的实例属性也会变成原型属性。且不能向构造函数传递参数

2. 借用构造函数

  apply()和call()

3. 组合继承【最常用】

  使用原型链实现对原型属性和方法的继承,而通过构造函数实现对实例属性的继承。

  instanceof和isPrototype()也能够识别基于组合继承创建的对象

 1         function Parent(name){
 2             this.name = name;
 3         }
 4         Parent.prototype.sayName = function(){
 5             console.log(this.name);
 6         }
 7         function Child(name,age){
 8             Parent.call(this,name);
 9             this.age = age;
10         }
11         Child.prototype = new Parent();
12         Child.prototype.sayAge = function(){
13             console.log(this.age);
14         }
15         var child = new Child('Jack',27);
16         child.sayName();    //Jack
17         child.sayAge();     //27

 

4. 原型式继承

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

  

1         function object(o){
2             function F(){}      //创建一个临时的构造函数
3             F.prototype = o;    //将传入的对象作为这个构造函数的原型
4             return new F();     //返回这个临时类型的一个实例
5         }

  这种方法要求必须有一个对象可以作为另一个对象的基础。

  ECMAScript5中新增了一个函数实现了原型式继承:Object.create()。

  Object.create()函数接收两个参数:一个用于新对象原型,一个为新对象定义额外属性的对象。

  这种方法也可能会使引用类型值的属性共享

 1         var Person = {
 2             name: "Nick",
 3             friends: [1,2,3,4]
 4         };
 5         var anotherPerson = Object.create(Person);
 6         anotherPerson.name = "Joe";
 7         anotherPerson.friends.push(5);
 8 
 9         var yetAnotherPerson = Object.create(Person);
10         yetAnotherPerson.name = "Amy";
11         yetAnotherPerson.friends.push(6);
12 
13         console.log(Person.friends);    //[1,2,3,4,5,6]
14 
15         var person2 = Object.create(Person, {
16             name: {
17                 value: "Lee"
18             }
19         });
20         console.log(Person.name);   //Nick
21         console.log(person2.name);  //Lee
22         console.log(person2);       //name:Nick在其原型之中。原型链会先找实例属性

 

5. 寄生式继承

  创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,然后返回对象

 1         function createAnothor(o){
 2             var clone = Object.create(o);
 3             clone.sayHi = function(){
 4                 console.log("hi");
 5             };
 6             return clone;
 7         }
 8         var person = {
 9             name : "Nick",
10         }
11         var anotherPerson = createAnothor(person);
12         anotherPerson.sayHi();

  这种模式用于主要考虑对象不是自定义类型和构造函数的情况下。

  这种方法来为对象添加函数,也不能做到函数复用。

 

6. 寄生组合式继承

  之前提到的组合函数的不足在于:无论什么情况下,都会调用两次超类型构造函数。一次是在创建子类原型的时候,一次是在子类构造函数内部。

  所谓寄生式组合继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

  思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

 1         function inheritPrototype(Child, Parent){
 2             var prototype = Object.create(Parent.prototype);    //创建对象
 3             prototype.constructor = Child;                      //增强对象
 4             Child.prototype = prototype;                        //指定对象
 5         }
 6         function Parent(name){
 7             this.name = name;
 8         }
 9         Parent.prototype.sayName = function(){
10             console.log(this.name);
11         };
12         function Child(name,age){
13             Parent.call(this,name);
14             this.age = age;
15         }
16         inheritPrototype(Child,Parent);
17         Child.prototype.sayAge = function(){
18             console.log(this.age);
19         }
20         var child = new Child('Jack',29);
21         child.sayName();
22         child.sayAge();
23         console.log(child); //这时,child的__proto__中就不会有从父类继承来的name和age属性了

用寄生组合式继承打印出来的child:

 

很干净。而如果用组合继承的话:

可以看到其原型中有一个多余的name属性。

posted on 2016-08-15 22:28  杠子  阅读(547)  评论(0编辑  收藏  举报

导航