JavaScript深入之从创建对象方式到继承

51 阅读5分钟

初始方式

  • 创建一个Object的实例
var person = new Object();
person.name = 'liushuang';
person.age = 32;
person.job = 'dancer';

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

  • 对象字面量
var person = {
     name : 'liushuang';
     age : 32;
     job : 'dancer';
     
     sayName: function(){
         console.log(this.name)
     }
};

创建对象

工厂模式

Object构造函数和对象字面量创建对象的缺点:使用同一个接口创建很多对象,会产生大量的代码。工厂模式可以解决这个问题。
工厂模式抽象了创建具体对象的过程。ES中无法创建类,开发人员发明了一种函数,用函数来封装以特定接口创建对象的细节。

    function createPerson(name,age,job){
        var obj = new Object();
        
        obj.name = name;
        obj.age = age;
        obj.job = job;
        obj.sayName = function (){
            console.log(this.name);
        }
        
        return obj;
    }
    
    var person1 = createPerson('xzq',20,'singer');
    var person2 = createPerson('ls',32,'dancer');
    

缺点:无法知道一个对象的类型

构造函数模式

    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('xzq',20,'singer');
    var person2 = new person('ls',32,'dancer');

优点:它的实例标识为一种特定的类型。
缺点: 每个方法都要在实例中创建一遍。
上例中的person1person2中都有一个sayName的方法,这两个方法不是同一个Function的实例,但实际上两个函数在做同一件事情。

    function Person(name,age,job){
        this.name = name;
        this.age = age;
        this.job = job;
        
        this.sayName = new Function('console.log(this.name)')
    }
    

可以通过把sayName转移到构造函数外部来解决这个问题。

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

新缺点:

  1. 在全局作用域中定义的函数只能被某个对象调用,这让全局作用域优点名不副实;
  2. 如果对象需要定义很多函数,就得定义很多个全局函数,那么这个自定义的引用类型就没什么封装性可言了。

原型模式

   function Person(){}
   
   Person.prototype.name = 'ls';
   Person.prototype.age = 32;
   Person.prototype.job = 'dancer';
   
   Person.prototype.sayName = function(){
       console.log(this.name);
   }
   
   var person1 = new Person();
   var person2 = new Person();

Person的这些属性和方法由所有实例共享。也就是说,person1和person2访问的都是同一组属性和同一个sayName()函数。
缺点:因为属性被所有实例共享,引用类型的属性被某一个实例修改,其他实例取到的实例也将是被修改后的。

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

这是创建自定义类型最常见的方式。构造函数用于定义实例属性,原型用于定义方法和共享的属性。
优点:每个实例都有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。

  function Person(name,age,job){
       this.name = name;
       this.age = age;
       this.job = job;
  }
  Person.prototype = {
      constructor:Person,
      sayName: function(){
          console.log(this.name)
      }
  }
  
  var person1 = new Person(‘xzq’,20,‘singer’);
  var person2 = new Person(‘ls’,32 ,‘dancer’);   

动态原型模式

独立的构造函数和原型,不便于理解。动态原型模式是解决这一个问题的方案。他把所有信息封装在构造函数中,通过在构造函数中初始化原型(仅在有必要的情况下),又保持了同时使用构造函数的特点和原型的特点。

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(‘xzq’,20,‘singer’);
var person2 = new Person(‘ls’,32 ,‘dancer’); 

寄生构造函数模式

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

缺点:不能依赖instanceof来确定对象类型

稳妥构造函数模式

使用场景:在一些安全的环境中(禁止使用thisnew),或者防止数据被其他程序改动。

function Person(name,age,job){
       var o = new Object();
       //定义私有变量和函数
       
       o.sayName = function(){
           console.log(name);
       }
       return o;
   }

除了sayName()没有其他方式访问其成员变量。 缺点:不能依赖instanceof来确定对象类型

继承

原型链

function Parent(name){
    this.name = name;
}

Parent.prototype.getName = function(){
    return this.name;
}

function Child(){

}
//继承
Child.prototype = new Parent();

var child1 = new Child();

缺点:

  1. 引用类型的属性被所有实例共享
  2. 在创建子类型的实例时,不能向父类中传递参数。

借用构造函数

function Parent(){
    this.names = ['xzq','ls'];
}

function Child(){
    Parent.call(this);
}

var child1 = new Child();
child1.names.push('lh');
console.log(child1.names); //['xzq','ls','lh']

var child2 = new Child();
console.log(child1.names); //['xzq','ls']

缺点:
方法都在构造函数中创建,每创建一次实例都会创建一遍方法。(方法无法复用)

组合继承

function Parent(age){
    this.names = ['xzq','ls'];
    this.age = age;
}

Parent.prototype.getAge = function(){
    console.log(this.age);
}

function Child(age,job){
    Parent.call(this,age); //第二次调用Person()
    this.name = name;
}

Child.prototype.getJob = function(){
    console.log(this.job);
}

Child.prototype = new Person();  //第一次调用Person()
child.prototype.constructor = Child;

var child1 = new Child(26,'singer');
child1.names.push('lh');
console.log(child1.names); // ['xzq','ls','lh']
child1.getAge();//26
child1.getJob();//'singer'

var child2 = new Child(16,'dancer');
console.log(child2.names); // ['xzq','ls']
child2.getAge();//16
child2.getJob();//'dancer'

优点:既通过构造函数保证每个实例有自己的属性,又通过在原型上定义方法实现了方法复用。
缺点:会调用两次父类型构造函数,第一次是在创建子类型原型时;第二次是在子类型构造函数内部。这样子会创建两组属性,一组在子类型原型对象上,一组在实例中。

原型式继承

使用场景:没有必要创建构造函数,只是想让一个对象和另一个对象保持类似的情况。

function createObject(o){
    function Foo(){}
    Foo.prototype = o;
    return new Foo();
}

就是 ES5 Object.create() 的模拟实现,将传入的对象作为创建的对象的原型。
缺点: 引用类型的属性被共享。与原型链继承的缺点一样。

var person = {
    name: 'xzq',
    friends: ['zs','ls']
}

var person1 = createObject(person);
person1.name = 'cici';
person1.friends.push('steve');

var person2 = createObject(person);
person1.name = 'kiki';

console.log(person.name);//'xzq
console.log(person.friends);//['zs', 'ls', 'steve']
console.log(person2.friends);//['zs', 'ls', 'steve']

注意:修改person1.name的值,person2.name的值并未发生改变,并不是因为person1person2有独立的 name 值,而是因为person1.name = 'cici',给person1添加了 name 值,并非修改了原型上的 name 值。

寄生式继承

使用场景:主要考虑对象,而不是自定义类型和构造函数的情况。

function creatAnother(original){
    var clone = Object(original);
    clone.sayHi = function(){
        console.log('hi');
    }
    
    return clone;
}

var person = {
    name: 'xzq',
    friends: ['zs','ls']
} 

var person1 = creatAnother(person);
person1.sayHi(); //"hi"

缺点:每次创建对象都会创建一遍函数,不能做到函数复用,与构造函数模式类似。

寄生组合式继承

使用场景:解决组合式继承会调用两次父类型构造函数的问题。
思路:使用寄生式继承来继承父类型的原型,并将结果指定给子类型的原型。

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

示例:

function Parent(name){
    this.name = name;
    this.frineds = ['zs','ls'];
}

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

function Child(name,age){
    Parent.call(this,name);
    this.age = age;
}

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

inheritPrototype(Child,Parent);

var child1 = new Child('kiki',29);
child1.sayName(); // 'kiki'

参考《JavaScript高级程序设计》
总结:有时间再来完善