资讯专栏INFORMATION COLUMN

js继承、构造函数继承、原型链继承、组合继承、组合继承优化、寄生组合继承

孙淑建 / 2040人阅读

摘要:创建子类实例,可以向父类构造函数传参数。修复如下其实方式组合继承优化核心通过这种方式,砍掉父类的实例属性,这样在调用父类的构造函数的时候,就不会初始化两次实例,避免组合继承的缺点。优点只调用一次父类构造函数。

2018.06.03

第一部分:导入 1、构造函数的属性
funcion A(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
    this.arr = [1]; // 实例引用属性 (该属性,强调私用,不共享)
    this.say = function() { // 实例引用属性 (该属性,强调复用,需要共享)
        console.log("hello")
    }
}
注意:数组和方法都属于‘实例引用属性’,但是数组强调私有、不共享的。方法需要复用、共享。

注意:在构造函数中,一般很少有数组形式的引用属性,大部分情况都是:基本属性 + 方法。
2、原型对象的作用
原型对象的用途是为每个实例对象存储共享的方法和属性,它仅仅是一个普通对象而已。并且所有的实例是共享同一个原型对象,因此有别于实例方法或属性,原型对象仅有一份。而实例有很多份,且实例属性和方法是独立的。

在构造函数中:为了属性(实例基本属性)的私有性、以及方法(实例引用属性)的复用、共享。我们提倡:

将属性封装在构造函数中

将方法定义在原型对象上

funcion A(name) {
    this.name = name; // (该属性,强调私有,不共享)
}
A.prototype.say = function() { // 定义在原型对象上的方法 (强调复用,需要共享)
        console.log("hello")
}

// 不推荐的写法:[原因](https://blog.csdn.net/kkkkkxiaofei/article/details/46474303)
A.prototype = {
    say: function() { 
        console.log("hello")
    }
}
第二部分:js 继承---各种方式的优缺点 方式1、原型链继承

核心:将父类实例作为子类原型

优点:方法复用

由于方法定义在父类的原型上,复用了父类构造函数的方法。比如say方法。

缺点:

创建子类实例的时候,不能传参数。

子类实例共享了父类构造函数的引用属性,比如arr属性。

function Parent() {
    this.name = "父亲"; // 实例基本属性 (该属性,强调私有,不共享)
    this.arr = [1]; // (该属性,强调私有)
}
Parent.prototype.say = function() { // -- 将需要复用、共享的方法定义在父类原型上 
    console.log("hello")
}
function Child(like) {
    this.like = like;
}
Child.prototype = new Parent() // 核心

let boy1 = new Child()
let boy2 = new Child()

// 优点:共享了父类构造函数的say方法
console.log(boy1.say(), boy2.say(), boy1.say === boy2.say); // hello , hello , true

// 缺点1:不能传参数
// 缺点2:
console.log(boy1.name, boy2.name, boy1.name===boy2.name); // 父亲,父亲,true

boy1.arr.push(2); // 修改了boy1的arr属性,boy2的arr属性,也会变化,因为两个实例的原型上(Child.prototype)有了父类构造函数的实例属性arr;所以只要修改了boy1.arr,boy2.arr的属性也会变化。  ----  原型上的arr属性是共享的。
console.log(boy2.arr); // [1,2]

注意:修改boy1的name属性,是不会影响到boy2.name。因为name是基本属性,不是引用属性。
方式2、借用构造函数

核心:借用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类。

优点:实例之间独立。

创建子类实例,可以向父类构造函数传参数。

子类实例不共享父类构造函数的引用属性。如arr属性

缺点:

父类的方法不能复用

由于方法在父构造函数中定义,导致方法不能复用(因为每次创建子类实例都要创建一遍方法)。比如say方法。(方法应该要复用、共享)

子类实例,继承不了父类原型上的属性。(因为没有用到原型)

function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
     this.arr = [1]; // (该属性,强调私有)
    this.say = function() { // 实例引用属性 (该属性,强调复用,需要共享)
        console.log("hello")
    }
}
function Child(name,like) {
    Parent.call(this,name);  // 核心
    this.like = like;
}
let boy1 = new Child("小红","apple");
let boy2 = new Child("小明", "orange ");

// 优点1:可传参
console.log(boy1.name, boy2.name); // 小红, 小明

// 优点2:不共享父类构造函数的引用属性
boy1.arr.push(2);
console.log(boy1.arr,boy2.arr);// [1,2] [1]

// 缺点1:方法不能复用
console.log(boy1.say === boy2.say) // false (说明,boy1和boy2 
的say方法是独立,不是共享的)

// 缺点2:不能继承父类原型上的方法
Parent.prototype.walk = function () {   // 在父类的原型对象上定义一个walk方法。
    console.log("我会走路")
}
boy1.walk;  // undefined (说明实例,不能获得父类原型上的方法)
方式3、组合继承

核心:通过调用父类构造函数,继承父类的属性并保留传参的优点;然后通过将父类实例作为子类原型,实现函数复用。

优点:

保留构造函数的优点:创建子类实例,可以向父类构造函数传参数。

保留原型链的优点:父类的实例方法定义在父类的原型对象上,可以实现方法复用。

不共享父类的引用属性。比如arr属性

缺点:

由于调用了2次父类的构造方法,会存在一份多余的父类实例属性,具体原因见文末。

注意:"组合继承"这种方式,要记得修复Child.prototype.constructor指向

第一次Parent.call(this);从父类拷贝一份父类实例属性,作为子类的实例属性,

第二次Child.prototype = new Parent();创建父类实例作为子类原型,此时这个父类实例就又有了一份实例属性,但这份会被第一次拷贝来的实例属性屏蔽掉,所以多余。

为啥是两次?如果还是,不清楚,可以看文末,我会详细讲解!

function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
    this.arr = [1]; // (该属性,强调私有)
}
Parent.prototype.say = function() { // --- 将需要复用、共享的方法定义在父类原型上 
    console.log("hello")
}
function Child(name,like) {
    Parent.call(this,name,like) // 核心   第二次
    this.like = like;
}
Child.prototype = new Parent() // 核心   第一次



let boy1 = new Child("小红","apple")
let boy2 = new Child("小明","orange")

// 优点1:可以传参数
console.log(boy1.name,boy1.like); // 小红,apple

// 优点2:可复用父类原型上的方法
console.log(boy1.say === boy2.say) // true

// 优点3:不共享父类的引用属性,如arr属性
boy1.arr.push(2)
console.log(boy1.arr,boy2.arr); // [1,2] [1] 可以看出没有共享arr属性。

注意:为啥要修复构造函数的指向?
console.log(boy1.constructor); // Parent 你会发现实例的构造函数居然是Parent。
而实际上,我们希望子类实例的构造函数是Child,所以要记得修复构造函数指向。修复如下
Child.prototype.constructor = Child;
其实Child.prototype = new Parent() 

console.log(Child.prototype.__proto__ === Parten.prototype); // true

方式4、组合继承优化1

核心:

通过这种方式,砍掉父类的实例属性,这样在调用父类的构造函数的时候,就不会初始化两次实例,避免组合继承的缺点。

优点:

只调用一次父类构造函数。

保留构造函数的优点:创建子类实例,可以向父类构造函数传参数。

保留原型链的优点:父类的实例方法定义在父类的原型对象上,可以实现方法复用。

缺点:

修正构造函数的指向之后,父类实例的构造函数指向,同时也发生变化(这是我们不希望的)

注意:"组合继承优化1"这种方式,要记得修复Child.prototype.constructor指向

原因是:不能判断子类实例的直接构造函数,到底是子类构造函数还是父类构造函数。
function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
    this.arr = [1]; // (该属性,强调私有)
}
Parent.prototype.say = function() { // --- 将需要复用、共享的方法定义在父类原型上 
    console.log("hello")
}
function Child(name,like) {
    Parent.call(this,name,like) // 核心  
    this.like = like;
}
Child.prototype = Parent.prototype // 核心  子类原型和父类原型,实质上是同一个



let boy1 = new Child("小红","apple")
let boy2 = new Child("小明","orange")
let p1 = new Parent("小爸爸")

// 优点1:可以传参数
console.log(boy1.name,boy1.like); // 小红,apple
// 优点2:
console.log(boy1.say === boy2.say) // true

// 缺点1:当修复子类构造函数的指向后,父类实例的构造函数指向也会跟着变了。
具体原因:因为是通过原型来实现继承的,Child.prototype的上面是没有constructor属性的,就会往上找,这样就找到了Parent.prototype上面的constructor属性;当你修改了子类实例的construtor属性,所有的constructor的指向都会发生变化。

没修复之前:console.log(boy1.constructor); // Parent
修复代码:Child.prototype.constructor = Child
修复之后:console.log(boy1.constructor); // Child
          console.log(p1.constructor);// Child 这里就是存在的问题(我们希望是Parent)
方式5、组合继承优化2 又称 寄生组合继承 --- 完美方式

核心:

优点:完美i

缺点:---

function Parent(name) {
    this.name = name; // 实例基本属性 (该属性,强调私有,不共享)
    this.arr = [1]; // (该属性,强调私有)
}
Parent.prototype.say = function() { // --- 将需要复用、共享的方法定义在父类原型上 
    console.log("hello")
}
function Child(name,like) {
    Parent.call(this,name,like) // 核心  
    this.like = like;
}
Child.prototype = Object.create(Parent.prototype) // 核心  通过创建中间对象,子类原型和父类原型,就会隔离开。不是同一个啦,有效避免了方式4的缺点。


Child.prototype.constructor = Child

let boy1 = new Child("小红","apple")
let boy2 = new Child("小明","orange")
let p1 = new Parent("小爸爸")


注意:这种方法也要修复构造函数的
修复代码:Child.prototype.constructor = Child
修复之后:console.log(boy1.constructor); // Child
          console.log(p1.constructor);// Parent  完美
第三部分:其他 + 相关问题解答 1、Object.create() 或 Object.create(object, [,propertiesObject])
Object.create() 的第二参数,是可选的。
- Object.create() 的内部原理:
// 其中,o 是新创建对象的原型(对象)
function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}
注意:之前,Object.create()没有出现之前,就是采用的这种方式。
参见《js高级程序设计》P170

Object.create() 做了哪几件事情?

创建空对象{}

指定空对象{}的原型为Object.create()的参数。

new 与 Object.create() 的区别?

以下是我的个人见解,(如有不对,还请指正):

new 产生的实例,优先获取构造函数上的属性;构造函数上没有对应的属性,才会去原型上查找;如果构造函数中以及原型中都没有对应的属性,就会报错。

Object.create() 产生的对象,只会在原型上进行查找属性,原型上没有对应的属性,就会报错。

let Base1 = function() {
  this.a = 1
}
let o1 = new Base1()
let o2 = Object.create(Base1.prototype)
console.log(o1.a); // 1
console.log(o2.a); // undefined



let Base2 = function() {}
Base2.prototype.a = "aa"
let o3 = new Base2()
let o4 = Object.create(Base2.prototype)
console.log(o3.a); // aa
console.log(o4.a); // aa



let Base3 = function() {
  this.a = 1
}
Base3.prototype.a = "aa"
let o5 = new Base3()
let o6 = Object.create(Base3.prototype)
console.log(o5.a); // 1
console.log(o6.a); // aa
2、new 的过程
funciton Func(name) {
    this.name = name
}
let p = new Func("小红")

new 的过程,做了啥?做了四件事。

创建一个空对象obj:let obj = new Object()

设置原型链

obj.__proto__ = Func.prototype
就是:将新对象的__proto__ 指向构造函数的prototype

将构造函数Func的this指向obj,并执行构造函数Func

let result = Func.call(obj)
就是:使用call或apply,将构造函数的this绑定到新对象,并执行构造函数

判断构造函数Func的返回值类型

如果是引用类型,就返回这个引用类型的对象。如果是值类型或没有return,则返回空对象obj。
if (typeof(result) === "object"){  
  func=result;  
}  
else{  
   func=obj; // 默认返回
}
注意:js中的构造函数,是不需要有返回值的,所以默认返回的是新创建的空对象obj
3、为啥‘组合继承’这种方式,会执行两次父类构造函数??

第一次:Child.prototype = new Parent()

‘new 的过程’的第三步,其实就是执行了父类构造函数。

第二次:Parent.call(this,name,like)

call的作用是改变函数执行时的上下文。比如:A.call(B)。其实,最终执行的还是A函数,只不过是用B来调用而已。所以,你就懂了Parent.call(this,name,like) ,也就是执行了父类构造函数。
第四部分:参考链接

new操作符具体干了什么呢?

new的过程

你不知道的javascript之Object.create 和new区别

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/108027.html

相关文章

  • js组合模式和寄生组合模式的区别研究

    摘要:组合模式继承结合了构造函数继承时可以为每个属性重新初始化,构造一个副本的优点,以及原型链继承时一次定义处处共享的优点。但令我百思不得其解的是,从上面给出的例子来看,组合继承并没有调用两次超类型构造函数。 最近在阅读《js权威指南》的继承这一章,对于组合模式和寄生组合模式的区别有点混淆,在多次重读以及尝试之后,得到一些心得。 组合模式继承 结合了构造函数继承时可以为每个属性重新初始化,构...

    tolerious 评论0 收藏0
  • js继承的理解

    摘要:创建自定义的构造函数之后,其原型对象只会取得属性,其他方法都是从继承来的。优缺点寄生式继承在主要考虑对象而不是创建自定义类型和构造函数时,是十分有用的。 原文链接:https://kongchenglc.coding.me... 1.原型链   js的继承机制不同于传统的面向对象语言,采用原型链实现继承,基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。理解原型链必须先理...

    BlackFlagBin 评论0 收藏0
  • JS高程笔记 - 继承

    摘要:下面来看一个例子继承属性继承方法在这个例子中构造函数定义了两个属性和。组合继承最大的问题就是无论什么情况下都会调用两次超类型构造函数一次是在创建子类型原型的时候另一次是在子类型构造函数内部。 组合继承 组合继承(combination inheritance),有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链...

    fsmStudy 评论0 收藏0
  • 《javascript高级程序设计》 继承实现方式

    摘要:寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部已某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。 这篇本来应该是作为写JS 面向对象的前奏,只是作为《javascript高级程序设计》继承一章的笔记 原型链 code 实现 function SuperType() { this.colors = [red,blu...

    cppprimer 评论0 收藏0
  • JavaScript面向对象---原型继承

    摘要:因为这造成了继承链的紊乱,因为的实例是由构造函数创建的,现在其属性却指向了为了避免这一现象,就必须在替换对象之后,为新的对象加上属性,使其指向原来的构造函数。这个函数接收两个参数子类型构造函数和超类型构造函数。 最近一直在研究js面向对象,原型链继承是一个难点,下面是我对继承的理解以下文章借鉴自CSDN季诗筱的博客 原型链继承的基本概念: ES中描述了原型链的概念,并将原型链作为实现...

    vspiders 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<