原型链
JavaScript将原型链作为实现继承的主要方法,其基本思想是利用原型让每一个引用类型继承另一个引用类型的属性和方法。
以构造函数、原型和实例的关系为例:每个构造函数都有一个原型对象,原型对象都有一个指向构造函数的指针(constructor),而实例都包含一个指向原型对象的内部指针(__proto__)。
类继承
// 类继承
// 声明父类
function SuperType(){ this.superValue = true }
// 在父类的原型对象上添加方法 SuperType.prototype.getSuperValue = function(){ return this.superValue }
// 声明子类 function SubType (){ this.subValue = false }
// 把父类的实例对象赋值给子类的原型对象 SubType.prototype = new SuperType()
// 在子类的原型对象上添加方法 SubType.prototype.getSubValue = function(){ return this.subValue } var instance = new SubType(); console.log(instance.subValue) // false console.log(instance.superValue) // true console.log(instance.getSubValue()) // false console.log(instance.getSuperValue()) // true console.log("-------------------instance------------------------") // 新创建对象的原型__proto__指向父类的原型对象 console.log(instance.__proto__ === SubType.prototype) // true console.log(instance) console.log(instance.__proto__) console.log('--------------------SubType--------------------------') // SubType.prototype是SuperType的实例 console.log(SubType.prototype.__proto__ === SuperType.prototype) console.log(SubType.prototype) console.log(SubType.prototype.constructor) console.log("---------------------SuperType------------------------") console.log(SuperType.prototype) console.log(SuperType.prototype.constructor)
以上代码定义了两个类型:SuperType和SubType,每个类型都有一个方法和一个属性。它们的主要区别是SubType继承了SuperType,而继承是通过创建SuperType的实例,并将该实例赋值给SubType的原型实现的(类继承:将第一个类的实例赋值给第二个类的原型)。
类的原型对象的作用就是为类的原型添加共有方法,但类不能直接访问这些属性和方法,必须通过原型的prototype来访问,而在实例化一个父类的时候,新创建的对象复制了父类的构造函数内的属性和方法并将原型__proto__指向了父类的原型对象,这样就拥有了父类的原型对象上的属性与方法,并且这个新创建的对象可以直接访问到父类原型对象上的属性和方法。如果将这个新创建的对象赋值给子类的原型,那么子类的原型就可以访问到父类的原型属性和方法。
类继承的本质是重写了子类的原型对象,代之的是父类的原型对象与构造函数属性,所以必须要在继承之后对才能对子类添加原型方法与属性。
另外,可以通过instanceof来检查对象是否是某个类的实例,或者说某个对象是否继承了某个类。这样就可以判断对象与类之间的继承关系了。instanceof是通过判断对象的prototype链来确定这个对象是否是某个类的实例,而不关心对象与类的自身结构。
console.log(instance instanceof SubType) // true console.log(instance instanceof SuperType) // true console.log(SubType.prototype instanceof SuperType) // true console.log(SubType instanceof SuperType) // false console.log(SuperType instanceof Object) // true console.log(SuperType.prototype instanceof Object) // true
instanceof是判断前面的对象是否是后面类(对象)的实例,并不代表两者的继承关系,其次在实现继承的时候,将SuperType的实例赋值给SubType的原型对象prototype,所以说SubType.prototype继承了SuperType。而所有的实例和类都继承与Object。
类继承的缺点:一、由于子类通过其原型prototype对父类实例化进行继承,如果父类的属性中有引用类型就会被子类的所有实例共用,因此一个子类的实例更改了子类原型从父类构造函数中继承的引用类型的共有属性就会直接影响其他的子类。
function SuperType(){ this.superValue = true this.books = ['javascript', 'html', 'css'] } function SubType (){ this.subValue = false } SubType.prototype = new SuperType() var i1 = new SubType(); var i2 = new SubType(); console.log(i1.books) // Array(3) ["javascript", "html", "css"] console.log(i2.books) // Array(3) ["javascript", "html", "css"] i1.books.push('vue'); console.log(i1.books) // Array(4) ["javascript", "html", "css", "vue"] console.log(i2.books) // Array(4) ["javascript", "html", "css", "vue"]
二、由于子类实现的继承是靠其原型prototype对父类实例化实现的,因此在创建父类的时候无法向父类传递参数,因而也无法在实例化父类的时候对父类构造函数内的属性进行初始化。
构造函数继承
// 构造函数继承
function SuperType(id){ this.id = id this.books = ['javascript', 'html', 'css'] } function SubType (id){ // this指向SubType SuperType.call(this, id) } var instance = new SubType(2); var instance1 = new SubType(10); console.log(instance.id) // 2 console.log(instance.books) // Array(3) ["javascript", "html", "css"] console.log(instance1.id) // 10 console.log(instance1.books) // Array(3) ["javascript", "html", "css"] instance.books.push('vue') console.log(instance.books) // Array(4) ["javascript", "html", "css", "vue"] console.log(instance1.books) // Array(3) ["javascript", "html", "css"]
由于call和apply都可以改变函数this指针的指向,因此在子类中对SuperType调用这个方法就是将其执行环境改变(this指向),由于父类中的是给this绑定的属性,因此子类也就继承了父类的共有属性。由于这种类型的继承没有涉及原型prototype,所以父类的原型方法不会被子类继承,如果想要被子类继承就要将方法放到构造函数中,如此一来每个实例都会单独拥有一份,这就违背了代码复用的原则。为了综合两者的优点也就有了组合继承。
组合继承
function SuperType(id, name){ this.id = id this.name = name this.books = ['javascript', 'html', 'css'] } SuperType.prototype.getId = function(){ return this.id } function SubType (id, name, num){ // this指向SubType SuperType.call(this, id, name) this.num = num } SubType.prototype = new SuperType(); SubType.prototype.getNum = function(){ return this.num } var instance = new SubType(2, 'll', 88); console.log(instance.id) // 2 console.log(instance.name) // ll console.log(instance.num) // 88 console.log(instance.getId()) // 2 console.log(instance.getNum()) // 88
原型继承
道格拉斯.克罗克福德在2006年写的一篇名为《JavaScript中的原型继承》的文章中,介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数。他的观点是,借助原型prototype可以根据已有的对象创建一个新的对象,同时不必要创建新的自定义对象类型。
和类式继承一样,父类对象中的引用类型的属性都是被其子类所共用。
寄生式继承
寄生式继承是与原型式继承紧密相关的一种思路,并且同样是由克罗克福德推而广之的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后在像真的是它做了所有工作一样返回对象。
function object(o){ function F(){} F.prototype = o return new F() } function createObj(o){ var clone = object(o) clone.getName = function(){ console.log(this.name) } return clone } var person = { name: 'zhangsan', friends: ['lisi', 'wanger', 'mazi'] } var a = createObj(person) a.getName() // zhangsan
寄生组合继承
组合继承是JavaScript最常用的继承模式,但是它也有自己的不足之处。组合继承最大的问题就是在无论什么情况下,都会调用两次超类型构造函数:一次是在子类型构造函数内部,一次是在创建子类型原型的时候。
所谓的寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,仅需要超类型的一个副本。本质上,就是使用寄生式继承来继承超类型的原型,然后再将其结果指定给子类型的原型。
function object(o){ // 声明一个过度函数 function F(){} // 过度函数的原型继承父对象 F.prototype = o // 返回过度函数的一个实例,该实例的原型继承了父对象 return new F() } // 定义一个函数传递两个参数:子类和父类 function inheritPrototype(SubType, SuperType){ // 复制一份父类的原型副本保存在变量中 var prototype = object(SuperType.prototype); // 修正因为重写子类原型导致子类的constructor属性被修改 prototype.constructor = SubType // 设置子类原型 SubType.prototype = prototype } // 定义父类 function SuperType(name){ this.name = name this.color = ['red', 'pink'] } // 添加父类原型方法 SuperType.prototype.getName = function(){ console.log(this.name) } // 定义子类 function SubType(name,age){ // 构造函数继承 SuperType.call(this, name) // 子类新增属性 this.age = age } // 寄生式继承父类原型 inheritPrototype(SubType, SuperType) // 子类新增原型方法 SubType.prototype.getAge = function(){ console.log(this.age) }
其中最大的改变就是对子类原型的处理,被赋予父类原型的一个引用,这是一个对象,因此子类在想添加原型方法就必须通过prototype对象,就必须通过点语法或者Object.assing()方法添加方法,否则直接赋予对象就会覆盖掉从父类原型继承的对象。
参考资料:JavaScript设计模式、JavaScript高级程序设计