首页 > 代码库 > JS面向对象整理

JS面向对象整理

有次面试的时候,面试官让我谈谈对面向对象的理解,让我一下蒙了,竟然不知道从哪里说起?自己都是在看完视频后,直接用面向对象写东西,也没有好好梳理,导致自己只会简单的用一下,却不会说。于是我就翻了翻《JavaScript高级程序设计》,对其进行整理了一下。

1.什么是对象

在ECMAScript中,对象就是一堆无序属性的集合,这些属性可以是基本值,也可以是别的对象和函数。所以我们也可以吧对象当成一组名值对,一个属性名对应一个值,值可以是数据或方法。

2.创建一个对象

(1)创建一个最简单的对象是通过创建一个Object的实例,再为它添加属性和方法:

var person = new Object();
person.name = "Tom";
person.age = 18;
person.sayName = function(){
alert(this.name);
};

(2)使用Object创建对象的缺点很明显,就是它只能创建一个对象,所以我们开始使用工厂模式来创建对象

            function createPerson(name,age,job){
                var o = new Object();
                o.name = name;
                o.job = job;
                o.age = age;
                o.sayname = function(){
                    alert("你好" + name);
                }
                return o;
            }
            var person1 = createPerson("tom",16,"doctor");
            person1.sayname();    
            var person2 = createPerson("Candy",18,"police");

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

(3)构造函数模式

构造函数模式就可以创建特定类型的对象,我们将上一个函数改成构造函数如下:

            function Person(name,age,job){
                this.name = name;
                this.age = age;
                this.job = job;
                this.sayname = function(){
           alert("My name is" + this.name); 
         }; }var person1 = new Person("tom",16,"doctor"); var person2 = new Person("Candy",18,"police"); alert(person1 instanceof Object); alert(person1 instanceof Person);

上面的函数与工厂模式的主要不同在于它直接将属性复制给this对象,并没有返回值,并且构造函数名都应是首字母大写,以便与其他函数做区别,因为构造函数本身也是函数,只不过可以用来创建对象而已 。

然后是创建Person的实例,这就用到了new操作符,我们就是用new创建了2个实例。在这两种实例中,都包含有一个constructor属性,这个属性指向的就是Person,就是实例的构造函数,例如:

alert(person1.constructor == Person);

这里弹出的就是true,person2同样是这样。而我们最后的instanceof操作符就是为了检测对象类型。上面的两条检测语句弹出的都是true,说明我们创建的实例,既是Person的实例,有属于Object的实例。

但构造函数也有一个缺点,就是相同的属性或方法会创建多遍,这样确实有些不必要。

(4)原型模式

首先,我们要知道每一个函数都有一个prototype的原型属性,这个属性是指向一个对象,这个对象可以包括属性和方法用来给实例共享,也可以说prototype就是通过调用构造函数而创建那个对象实例的原型对象。所以称prototype为原型属性是针对于构造函数的,原型对象是针对创建的实例的

            function Person(){
                
            }
            Person.prototype.name = "Nancy";
            Person.prototype.age = 16;
            Person.prototype.sayName = function(){
                alert(this.name);
            }
            var person1 = new Person();
            var person2 = new Person();

这个原型模式有一个最大的问题,不是数据重复,而是在对于引用类型的属性,例如:

function Person(){
}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends : ["Shelby", "Court"],
sayName : function () {
alert(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true

我们发现我们修改了person1的friends数组,但person2引用的friends也发生改变

(5)构造函数与原型函数相结合使用

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("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true

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

(6)其它的模式

在《JavaScript高级程序设计》众还有其它的设计模式,如动态原型模式,寄生构造函数模式,稳妥构造函数模式等,但我们平时掌握上一种方法就够用了,所以我就偷个懒,就不介绍这几种了。

1.继承

许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。由于函数没有签名,ECMAScript 中无法实现接口继承。 ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的。

(1)原型链

讲到继承就不得不讲一讲原型链了,我们先讲一个对象的原型链:

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

var person1 = new Person("tom",18);

在上面的例子中,我们创建了一个Person构造函数,这个构造函数有一个原型对象prototype,并且这个原型对象有一个属性constructor,这个属性是指向它的构造函数,即Person,在创建的实例中,person1也有一个原型属性_proto_,这个属性指向prototype,即Person.prototype。总的来说就是:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。这就是圆形脸的基本概念。

利用原型链实现继承可以这么写:

            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

利用原型链虽然好用,但缺点也很大,其中一个就是方才讲到的对引用类型属性的改变:

function SuperType(){
    this.colors = ["red", "blue", "green"];
}
function SubType(){
}
//继承了 SuperType
SubType.prototype = new SuperType();
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,black"

原型链的第二个问题是:在创建子类型的实例时,不能向父类型的构造函数中传递参数。

(2)借用构造函数

这种技术的基本思想相当简单,即在子类型构造函数的内部调用父类型构造函数。因此通过使用 apply()和 call()方法也可以在(将来)新创建的对象上执行构造函数。

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"

通过使用 call()方法(或 apply()方法也可以),我们实际上是在(未来将要)新创建的 SubType 实例的环境下调用了 SuperType 构造函数。这样一来,就会在新 SubType 对象上执行 SuperType()函数中定义的所有对象初始化代码。

但是,根据我们的套路,如果只是用构造函数的来实现继承,还是有问题的,那就是方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在父类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。所以我们和创建对象一样,采取两者相结合的方法。

(3)组合继承

            function SuperType(name){
                this.name = name;
                this.colors = ["red","black"];
            }
            SuperType.prototype.sayName = function(){
                alert(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(){
                alert(this.age);
            }
            
            var insstance = new SubType("tom",16);
            insstance.sayName();
            insstance.sayAge();
            
            insstance.colors.push("yellow");
            console.log(insstance.colors);
            
            var insstance2 = new SubType("Nancy",100);
            console.log(insstance2.colors);
            insstance2.sayName();
            insstance2.sayAge();

在这个例子中, SuperType 构造函数定义了两个属性: name 和 colors。 SuperType 的原型定义了一个方法 sayName()。 SubType 构造函数在调用 SuperType 构造函数时传入name 参数,紧接着又定义了它自己的属性 age。然后,将 SuperType 的实例赋值给 SubType 的原型,然后又在该新原型上定义了方法 sayAge()。这样一来,就可以让两个不同的 SubType 实例既分别拥有自己属性——包括 colors 属性,又可以使用相同的方法了。

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。但它也有缺点,就是它在继承的时候会调用两次父类型构造函数 。

(4)这本书还有其他的继承方法,但是对于我这样写一写小项目的来说,上一种就够了,其他的我也只是做一种了解,我就讲一个寄生组合式的继承方式,

我们先写一个利用原型继承的函数

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

我们在 object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例

//寄生组合式继承
function inheritPrototype(subType,superType){
    var prototype = object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}


在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。最后一步,将新创建的对象赋值给子类型的原型。

利用这个寄生组合的模式可以解决上面组合继承的不足,被开发人员认为是引用类型最理想的继承范式 。

JS面向对象整理