发布时间

JavaScript原型,原型链,继承

作者

Chad

原型

  • 原型的作用: 1、通过原型来添加方法,解决数据共享,节省内存空间 2、作用于继承,模拟面向对象,来做到继承,多态

  • 构造函数

function Person(name, age) {
  this.name = name
  this.age = age
}
  • 通过原型添加方法:
Person.prototype.eat = function() {
  console.log("吃饭")
}
  • 实例化对象:
var per1 = new Person("小明", "18")
var per2 = new Person("小红", "20")
  • 接下来就是我们所要探讨的了,per1 和 per2 的 eat 的这个方法是不是同一个方法呢? 我们来验证一下:
console.log(per1.eat == per2.eat) // true

结果是 true

  • 我们来查看一下他们的结构 console.dir(per1); console.dir(per2);
  • 结论:我们可以看到在 Person 结构目录下它们都有 age 和 name 属性,同时也有 __proto__ 这个属性。在 __proto__ 下面可以看到 eat 方法,这样就实现了简单的数据共享。至于后面的 constructor 和 __proto__: Object,这涉及到原型链和继承的内容,请看下文我们继续深入。

我们虽然做到了同一个构造函数内共享方法且节省了内存空间,但是这并不是我们的真正目的

下面我们再来一个构造函数

function Student(name, age, sex) {
  this.name = name
  this.age = age
  this.sex = sex
}

为原型添加方法

Student.prototype.study = function() {
  console.log("学习")
}

实例化 Student 对象

var stu = new Student("小红", 17, "女")

那么问题来了,如何让 stu 可以访问 per 的方法呢?答案是可以的 -- 我们可以通过改变 Student 的原型指向来做到。

Student.prototype=new Person("小明","18")
输出一下,用stu去访问Person的方法
stu.eat;//吃饭

通过改变原型的指向,我们可以去访问别的构造函数的方法。但这样做冒出来一个问题:改变原型指向后,Student 没办法访问自己原本的方法 study 了。

console.log(stu.study) //undefined
stu.study() // 报错

为什么会出现这个问题呢?让我们想一下 -- 我们是先给 Student 添加了原型方法,然后才改变的整个原型指向。Student 的原型本来指向的是包含 study 方法的对象,但之后被指向了 new Person,所以自然 Student 的原型方法 study 被架空了。

如果我们换个顺序来写:先改变 Student 的原型指向,再添加方法,这样 study 就不会被架空了。

我把代码重新集中一下(方便阅读)

//人的构造函数
function Person(age) {
  this.age = 10
}
//人的原型对象方法
Person.prototype.eat = function() {
  console.log("人")
}
//学生的构造函数
function Student() {}
Student.prototype = new Person(10)
Student.prototype.sayHi = function() {
  console.log("学生")
}

输出:

stu.eat() //人
stu.sayHi() //学生
查看结构:console.dir(stu); 这里写图片描述
  • 于是推出以下结论
当调用某种方法或查找某种属性时,首先会在自身查找。如果自身没有该属性或方法,则会沿着 __proto__ 链向上查找,也就是去其构造函数的 prototype 中查找。 附一张原型截个图(图片来自于网络) 这里写图片描述

继承

其实上面那种方式就是继承中的其中一种 -- 原型继承了。为什么要到现在才说呢?就是为了结合继承让对原型的理解更加深入。下面我将继续阐述几种继承方式,并指出它们的优缺点和相应案例。

构造继承 核心:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Cat(name) {
  Animal.call(this)
  this.name = name || "Tom"
}

// Test Code
var cat = new Cat()
console.log(cat.name)
console.log(cat.sleep())
console.log(cat instanceof Animal) // false
console.log(cat instanceof Cat) // true

特点:

  1. 解决了 1 中,子类实例共享父类引用属性的问题
  2. 创建子类实例时,可以向父类传递参数
  3. 可以实现多继承(call 多个父类对象)

缺点:

  1. 实例并不是父类的实例,只是子类的实例
  2. 只能继承父类的实例属性和方法,不能继承原型属性/方法
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

实例继承 核心:为父类实例添加新特性,作为子类实例返回

function Cat(name) {
  var instance = new Animal()
  instance.name = name || "Tom"
  return instance
}

// Test Code
var cat = new Cat()
console.log(cat.name)
console.log(cat.sleep())
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // false

特点:

  1. 不限制调用方式,不管是 new 子类()还是子类(),返回的对象具有相同的效果

缺点:

  1. 实例是父类的实例,不是子类的实例

  2. 不支持多继承

拷贝继承

var obj1 = {
  name: 10,
  age: "女",
  sleep: function() {
    console.log("睡觉")
  },
}
//改变了地址指向
var obj2 = obj1
console.log(obj2.name, obj2.age)
obj2.sleep()
//这个拷贝只是改变了栈的指向
//而下面方法,是把堆里面的属性和方法复制一份重新开了一个空间

function Cat(name) {
  var animal = new Animal()
  for (var p in animal) {
    Cat.prototype[p] = animal[p]
  }
  Cat.prototype.name = name || "Tom"
}

// Test Code
var cat = new Cat()
console.log(cat.name)
console.log(cat.sleep())
console.log(cat instanceof Animal) // false
console.log(cat instanceof Cat) // tru

特点:

  1. 支持多继承

缺点:

  1. 效率较低,内存占用高(因为要拷贝父类的属性)
  2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用 for in 访问到)

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

//原型实现继承
//借用构造函数实现继承
//组合继承:原型继承+借用构造函数继承
function Person(name, age, sex) {
  this.name = name
  this.age = age
  this.sex = sex
}
Person.prototype.sayHi = function() {
  console.log("人的打招呼")
}
function Student(name, age, sex, score) {
  //借用构造函数:属性值重复的问题
  Person.call(this, name, age, sex)
  this.score = score
}
//改变原型指向----继承
Student.prototype = new Person() //不传值
Student.prototype.eat = function() {
  console.log("学生的吃原型")
}
var stu1 = new Student("小明", "18", "男", "100")
console.log(stu1.name, stu1.age, stu1.sex, stu1.score) //小明 18 男 100
stu1.sayHi() //人的打招呼
stu1.eat() //学生的吃原型

特点:

  1. 弥补了方式 2 的缺陷,可以继承实例属性/方法,也可以继承原型属性/方法
  2. 既是子类的实例,也是父类的实例
  3. 不存在引用属性共享问题
  4. 可传参
  5. 函数可复用

缺点:

  1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)

寄生组合继承 核心:通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点

function Cat(name) {
  Animal.call(this)
  this.name = name || "Tom"
}
;(function() {
  // 创建一个没有实例方法的类
  var Super = function() {}
  Super.prototype = Animal.prototype
  //将实例作为子类的原型
  Cat.prototype = new Super()
})()

// Test Code
var cat = new Cat()
console.log(cat.name)
console.log(cat.sleep())
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) //true

特点:

  1. 堪称完美

缺点:

  1. 实现较为复杂


    附一点题外话:

  • 面向对象编程思想:根据需求分析对象,找出对象的特征和行为,通过代码来实现需求。想要实现需求就要创建对象,想要创建对象就应该先有构造函数,然后通过构造函数来创建对象,通过对象调用属性和方法来实现相应的功能。

  • 首先,JS 不是一门面向对象的语言,它是一门基于对象的语言。那为什么学 JS 还要学面向对象呢?因为面向对象的思想符合人的思维习惯,编程起来更方便,后期维护也更容易。

  • 面向对象的编程语言中有类(class)的概念(也是一种特殊的数据类型),但 JS 中没有类。不过 JS 可以模拟面向对象的思想来编程 -- 通过构造函数来模拟类的概念。

  • 所有引用类型(函数,数组,对象)都拥有proto属性(隐式原型)

  • 所有函数拥有 prototype 属性(显式原型)(仅限函数)

  • 原型对象:拥有 prototype 属性的对象,在定义函数时就被创建

这里写图片描述

Support

赞赏

如果这些内容对你有所帮助,欢迎赞赏支持。