javascript原型,原型链,继承专讲

251 阅读7分钟

对象,原型,原型链,继承专题

一. 对象,原型

要了解原型,我们先从对象开始入手。

对于对象,ECMA-262是这么定义对象的:无序属性的集合,其属性可以包含基本值,对象或者函数。

那么如何创建一个对象呢?

  1. 字面量方式或者new Object方式 :

  1. // 字面量
    var psrson = {
     name:'smile',
     age:18,
     sayName:function(){
         console.log(this.name)
    }}
    // new Object
    var person = new Object();
    person.name = 'zhou';
    person.age = 18;
    person.sayName = function(){
        console.log(this.name)}

 这两种方式有很大的缺点:如果我要用这种方式创建100个对象,会产生大量的重复代码。

   2. 工厂模式创建对象解决重复代码问题

function createPerson(name,age,sex){
    var obj = new Object();
    obj.name = name;
    obj.age = age;
    obj.sex = sex;

}

优点:解决了代码重复问题

缺点:如果是两个不同的对象,比如Person和Animal都用这样的方法创建。我们则无法识别他们到底属于哪个类型(因为都是用Object创建而来)

  3.构造函数模式

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

}
var person1 = new Person('smile',19,'male');
var person2 = new Person('zhou',19,'female')

在创造Perosn实例的时候,我们用到了new操作符。那么new操作符具体作用是什么呢?

  • 创建一个新对象;
  • 将构造函数的作用域赋给新对象 (将this指向新对象);
  • 执行构造函数中的代码 (为新对象添加属性);
  • 返回新对象

具体过程我们用代码演示:

// 有缺陷,没有考虑到构造函数有返回值且是对象的情况
function objectFactory(){
    var obj = {};
    Constrcutor = [].shift.call(arguments);
    //obj可以访问到构造函数原型中的属性
    obj.__prototype__ = Constrcutor.prototype
    // obj可以访问到构造函数中的属性
    Constrcutor.apply(obj,arguments)    return obj
    
}

但是这样直接使用构造函数创建的依然还存在问题

  1. 即使是每个实例有相同的方法,但是,你创建实例的时候,会将每个方法都重新创建一遍,即person1和person2里面的sayName并不是同一个Function的实例。因为在js中,函数也是对象,你定义了一个函数就相当于 new Function();

那么为了解决这个问题,你可以这样:

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

}

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

这样我们就可以实现所有的实例都可以共享外部的sayName函数。

可是这样,又有一个问题,就是sayName是处于全局作用域当中,如果一个对象的方法过多,会导致全局函数变多,不仅难以维护,而且这样函数只是为了某个对象服务,而不是为了全局服务,所以这种方法的局限性太多。那么有没有一种方法可以实现所有的实例都共享属性和方法呢?

4.原型模式

接下来,就到我们的主角上场了。

在创建函数的时候有几个要点注意:

  1. 每个函数都有prototype(原型)属性
  2. prototype可以让所有的对象实例共享它所包含的对象和属性。

    function Person(){};
    Person.prototype.name = 'smile';
    Person.prototype.sayName = function(){console.log(this.name)};

那么当我们实例化一个对象的时候,又是如何访问到prototype上面的属性的呢?

我们还是分步来看:

  1. 实例化一个对象后,对象内部会包含一个指针,这个指针会指向构造函数的原型对象,我们把这个指针命名为[[prototype]]。也就是说  [[prototype]] ====> Person.prototype.
  2. 当访问实例的某个属性的时候,会发生:
    1. 会访问实例中是否有该属性或方法。实例的属性或方法从哪里来?还记得new操作的时候发生了什么吗?会将new出来的对象上添加构造函数上的属性或者方法。
    2. 如果没有访问到,则会向上搜索[[prototype]]指向的原型对象上的属性或方法。
    3. 如果实例上和[[prototype]]指向的原型对象上都有该方法或属性,则会访问实例上的方法或属性。
  3. 每个原型对象上都有一个constructor属性,它指向关联的构造函数。(重要)
一般绝大多数浏览器的[[prototype]]都叫做__proto__

我们用图来表示__proto__,prototype和构造函数之间的关系。


对于原型对象我们还有以下几点要注意:

  1. 如何知道属性或者方法是在实例上,还是在原型对象上呢?
    1. in操作符:无论该属性在实例或者原型上,都会返回true。
    2. getOwnProperty()方法,只有属性存在于实例上,才返回true
  2. 所以我们得出结论:只要该属性在用in操作符的时候返回true,getOwnProperty()方法的时候返回false,我们就知道这个属性在原型对象上。

    function hasProtypeProperty(object,name){
        return !object.getOwnProperty(name) && (name in object)}

当然,这种模式也有一个缺点:所有的实例在默认情况下都会取得相同的属性值。但是我们既想拥有自己的属性,又想拥有公共属性怎么办呢?


5.组合使用

使用构造函数和原型模式来创建一个对象

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

}
Person.prototype.sayName = function (){console.log('hello')}

二. 原型链,继承

在javascript里面,说是继承,更多的是委托。

因为继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。

那么我们通过什么来在两个对象之间创建关联呢?

答案就是原型链

什么是原型链?如果我们让一个原型对象等于另一个类型的实例。那么这个原型对象将包含一个指向另一个原型对象的指针。这样,依次往下传递,就形成了原型链。

那么我们可以这样实现继承:

function Person(){
    this.property = true
}
Person.prototype.getPersonName = 'person';

function Men(){
    this.myProperty = false
}
//开始继承
Men.prototype = new Person();
Men.prototype.getMyName = 'men';

var men = new Men()
console.log(men.getPersonName) // person

注意:定义子类的方法时,一定要在替换原型之后在写,如果在之前写,会覆盖已经写好了的方法。

那么直接使用原型链来实现继承会有以下两个问题:

  1. Person构造函数的属性会共享到Men的所有实例对象里面。在一个实例上对这个属性修改,会影响到另外的实例。
  2. 在创建子类实例的时候,无法在不影响所以实例的情况下,给超类传递参数

所以,我们开始改造

1.借用构造函数

直接上代码:

function SuperType(color){
     this.color = color
}
function subType(){
    SuperType.call(this,'blue')
}

优势:

  • 每个实例都可以有自己的属性
  • 可以在子类构造函数中向子类传递参数。

缺点:

  • 方法都在构造函数中定义,无法复用
  • 在超类型的原型上定义的方法,对于子类型来说是不可见的。

2.组合继承

代码实现:

function SuperType(name){
    this.name = name;
    this.color = ['blue','green']
}

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

function SubType(name,age){
    //继承属性
    SuperType.call(this,name) //第二次调用    this.age = age

}
//第一次调用
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
var instance = new SubType('smile',18)
 console.log(instance)

这种继承方式既实现了让实例拥有自己的属性,也实现了可以让实例使用共同的方法。

缺点:会两次调用SuperType,第一次调用的时候,会在SubType的原型上生成name和color属性。第二次调用会在SubType的实例上生成name和color属性。

3.改进组合继承

function SuperType(name){
    this.name = name;
    this.color = ['blue','green']
}

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

function SubType(name,age){
    //继承属性
    SuperType.call(this,name) //第二次调用
    this.age = age

}
//第一次调用
SubType.prototype = SuperType.prototype;var instance = new SubType('smile',18)
console.log(instance)

改进后解决的问题:只在实例上生产name和color属性。

缺点:没有办法知道对象是子类还是父类实例化,因为父类和子类都指向同一个原型。

4.寄生组合式继承

function SuperType(name){
    this.name = name;
    this.color = ['blue','green']
}

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

function SubType(name,age){
    //继承属性
    SuperType.call(this,name) //第二次调用
    this.age = age

}
//第一次调用
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubTypevar instance = new SubType('smile',18)
console.log(instance)

这里要强调的是Object.create(obj)

  • 他会创建一个新的对象,新的对象的原型是obj

相当于下面这个函数:

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


参考文章:

冴羽的博客

JavaScript常见的六种继承方式

javascript高级程序设计