原型与继承

这次的blog主要是笔记,代码主要来自js高程,对一些概念作了自己的总结和思考(加粗部分)

创建对象

发展

1.起初,通过new Object(),后来则使用对象字面量来创建对象,这种方式在创建重复对象时会导致创建对象代码的冗余。

2.后来产生工厂模式,主要是 将创建对象的代码封装在普通函数内,这解决了代码冗余问题,但创建出来的对象难以区分类型。

3.又产生了构造函数模式,即 用构造函数创建特定类型的对象
这种方式创建出的对象可以区分类别,但是每创建一个对象,会产生新的作用域链,而且所有的属性和方法都将重新建立一遍,没有重用本该共享的属性和方法,以后可能导致共享数据不一致和性能问题。

4.原型模式又产生了,单纯的原型模式主要是 在构造函数的原型对象属性(prototype)中设置实例应要继承且会被共享的属性方法

缺点:

  • 单纯使用原型模式无法通过构造函数初始化属性;
  • 过度共享问题:原型模式带来属性方法的共享,但又导致本不该共享的数据也被共享,对这些数据的更改,会牵一发而动全身地影响到其他实例。

5.后来一个更完善的模式蹦出来了–构造函数与原型模式的结合,它主要是:构造函数模式用于定义实例属性(在构造函数定义不应共享的属性),原型模式用于定义方法和共享的属性

优点:这种模式不但重用了方法,节省了内存,还很好的处理了实例中的非共享属性。

1
2
3
4
5
6
7
8
9
10
11
12
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);
}
}

6.上面的结合模式的代码中:定义构造函数的代码与设置原型的是分开的,于是又产生动态原型模式,它 将设置原型的代码放到构造函数内部,通过判断原型是否已经被设置再决定是否执行设置原型

注意: 使用该模式最好不要重写原型,如果重写原型,初次调用构造函数new出来的对象不是以重写的原型对象为原型的,仍是默认原型(Object对象),之后new的对象才以重写的原型对象为原型。
也就是说:在真正执行构造函数的代码之前,new的实例已经创建并关联着原型。

1
2
3
4
5
6
7
8
9
10
11
12
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);
};
}
}

7.此外:还有寄生构造函数模式稳妥构造函数模式,详情看js高程,个人觉得很少用到,就不说了。
其中的稳妥构造函数觉得是 利用了闭包,不直接在返回的对象上定义变量,若对象不提供方法的话,外界基本无法访问,这样可以实现属性的私有化。

一些概念

原型对象:

  1. 为实例提供可共享的属性和方法,是实例的原型对象;
  2. 构造函数在创建实例时,会将实例与原型对象关联起来;
  3. 构造函数中的prototype属性可访问,创建出来的实例亦可以通过__proto__访问。

默认原型对象: 在定义构造函数时,原型对象默认是一个含有不可枚举的constructor属性的Object对象,而constructor指向构造函数。

重写原型对象: 以特定对象重写原型对象时,会把默认的原型对象覆盖,如果不在这个特定对象中添加constructor属性并指向构造函数的话,当想要访问constructor时,会沿着原型链向上找,结果不是原型对应的构造函数。

构造函数、原型对象和实例的关系: 构造函数以自身为模板,创建一个实例,同时关联起实例与原型对象。

原型链: 以某个类型的实例为原型创建的实例存在一个指向原型的指针__proto__,而原型对象亦存在指向上一层原型的指针,层层连接,构成原型链

继承

原型链实现继承

将SubType构造函数的prototype重写为SuperType类型的实例,从而实现SubType继承SuperType。

缺点:

  • 因为是直接将SuperType的实例替换为SubType的prototype,所以prototype对象里的属性会被所有子实例共享;
  • 如果单纯使用原型链实现继承,要想对父类(即原型对象)的属性赋值,这会导致所有实例都受影响;

借用构造函数

在子类型构造函数内部调用父类型的构造函数,call或apply,使得子类实例以自身实例属性的方式拥有父类的属性和方法。

优点:可以向父类构造函数传递参数,以初始化。
缺点:由于只是简单当做普通函数那样call或apply父构造函数,一方面无法复用父类里的方法,另一方面子类无法获取父类原型中定义的属性和方法

组合继承

上述两种方式的结合:

  1. 子类型构造函数内部调用父类型构造函数,用于实现不共享实例属性的继承;
  2. 将子类构造函数的prototype重写为父类实例,实现要共享的原型属性和方法的继承。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    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.constructor = SubType;
    SubType.prototype.sayAge = function(){
    alert(this.age);
    };

优点:解决了方法无法复用和原型属性过度共享的问题
缺点:要调用两次父类的构造函数,一次是重写子类构造函数的prototype属性时,一次是在子类构造函数中定义非共享的实例属性时,这屏蔽了子类原型中的属性;这时,子类原型中被屏蔽的属性有点多余

寄生组合式继承(组合继承优化版)

关键在于优化 重写子类构造函数的prototype 这一步,利用父类的prototype对象,通过原生式继承的方式生成一个不含任何实例属性的对象,再用该对象重写子类构造函数的prototype,这样便达到少调用一次父类构造函数并剔除了子类prototype的被屏蔽的多余属性。

1
2
3
4
5
6
7
8
9
function SubType(subParams,superParams){
SuperType.call(this,superParams);
this.subParams = subParams;
}
SubType.prototype = Object.create(SuperType.prototype);
Object.defineProperty(SubType.prototype,'constructor',{
value:SubType,
enumerable:false
});

原型式继承

以特定实例为原型对象生成一个子实例
ES5规范提供了Object.create(obj,descriptors)来实现这种继承,其中obj是即将作为原型的对象,descriptors是子实例新增实例属性的属性描述符对象。

特点:这种继承方式不用创建构造函数和定义新的类型,适合小范围的继承,它仍存在原型的属性过度共享的问题

此外,还有寄生式继承,个人感觉也很少用到,就不说了