# 1. 以复制方式实现的继承

# 1.1 浅拷贝

基本类型的复制

var parent = {
      lanage: "chinese"
  }
  
  var child = {
      name: "xxx",
      age: 12
  }
  
 function extend(parent, child) {
     var child = child || {};
     for (const propertype in parent) {
         child[propertype] = parent[propertype];
     }
 }
 
 extend(parent, child);
 
 console.log(child);//{ name: 'xxx', age: 12, lanage: 'chinese' }

以上代码中,通过一个 extend () 函数,将父对象 parent 的属性遍历赋给子对象 child,从而实现继承。

但是这种字面量复制的方式存在巨大的缺陷,当父对象有引用类型的属性时,通过这么复制的方式,就像上一节中的 var b = a 一样,只会将 a 对象的引用赋给 b 对象,结果两者是指向同一个对象内存的,继承出来的子对象的属性是同一个,显然是不能满足需求的,(基本类型之间画等号是值的复制,引用类型之间画等号是引用的复制)所以就需要另谋出路。

# 1.2. 深拷贝

利用递归加类型判断复制引用类型

var parent = {
      lanage: "chinese",
      address: {
          home: "gansu",
          office: "xian"
      },
      schools: ["xiaoxue", "zhongxue", "daxue"]
  }
  
 var child = {
     name: "xxx",
     age: 12
 }
 
 function extendDeeply(parent, child) {
     var child = child || {};
     for (const propertype in parent) {
         // 首先判断父对象的当前属性是不是引用类型
         if (typeof parent[propertype] === "object") {
             // 再判断该引用类型是不是数组,是则给子对象初始化为空数组,否则初始化为空对象
             child[propertype] = Array.isArray(parent[propertype]) ? [] : {};
             // 递归调用自己,将父对象的属性复制到子对象
             extendDeeply(parent[propertype], child[propertype]);
         } else {
             // 基本类型直接复制
             child[propertype] = parent[propertype];
         }
     }
 }
 
 extendDeeply(parent, child);
 console.log(child);
 child.schools[0] = "qinghua"; // 改变子对象的属性值
 child.address.home = "tianjin";
 
 console.log(child.schools[0]); //qinghua
 console.log(parent.schools[0]); //xiaoxue
 console.log(child.address.home); //tianjin
 console.log(parent.address.home); //gansu

可见此时子对象的改变并不会影响到父对象,父子彻底决裂。这便是深拷贝实现的继承。

注意:for in 是可以遍历数组的,结果为数组元素下标。

# 2. 以构造函数实现继承

function Parent() {
      this.name = "name";
      this.age = 12;
  }
  
  function Child() {
      Parent.call(this);
      this.language = "chinese";
  }
 
 var child = new Child();
 console.log(child);//{ name: 'name', age: 12, language: 'chinese' }
 child.name = "swk";
 console.log(new Parent().name);//name

如上所示,通过在子类构造函数中调用父类构造函数,将父类属性继承到子类对象上,new 出来的每一个子类对象都是独立的实例,这种方式更符合面向对象的思想。

# 3. 以原型方式实现继承

var person = {name: "pname"};
  
  function myCreate(person) {
      var ins;
      function F() { };
      F.prototype = person;
      ins = new F();
      return ins;
  }
 
 var c = new myCreate(person);
 console.log(c.name); //pname
 console.log(c); //{}

如上所示,将一个构造函数的原型替换为父对象,然后返回该构造函数的实例,如此一来创建的子对象就是 F 的实例,并且其原型为父对象,所以 c.name 可以被查找到,但子对象 c 本身是没有属性的。这种直接重写 prototype 原型对象的弊端在于会断掉原本的原型链,即没有了继承自 Object 的方法,和给原型对象 prototype 上添加属性是不同的。

# 4.JS 原型链

盗一波大神的经典图

img

从该图可以看出两点

# 4.1 从原型 prototype 层面:

  • 不论是 new Foo () 自定义构造函数, new Object () 系统构造函数,还是对象字面量创建出来的对象,只要是对象,其原型最终都指向 Object.prototype 对象,自定义的多绕了一步,就像 Java 里说的, Object.prototype 是一切 JS 对象的根对象。
  • 函数的原型都指向 Function.prototype ,而在 JS 中,函数也是对象的一种,所以符合第一条。

# 4.2 从构造器 constructor 层面

  • 首先对象的构造器肯定是指向他的构造函数,f1, f2 指向 Foo () , o1, o2 指向 Object ()
  • 而构造函数 Foo ()Object () 的 constructor 属性最终指向函数构造器 Function ()