美文网首页
★ 原型、原型链、继承

★ 原型、原型链、继承

作者: 行走的蛋白质 | 来源:发表于2019-12-18 00:25 被阅读0次
  • 如何理解原型,如何理解原型链?
  • new 一个对象发生了什么?
  • new 的实现

  • 每一个函数 ( 类 ) 都有一个 prototype ( 原型 ) 属性,属性值是一个对象:这个对象中存储了当前类供实例调取使用的公有属性和方法
  • 在 “浏览器默认” 给原型开辟的堆内存中有一个属性 constructor:存储的是当前类本身
  • 每一个对象 ( 实例 ) 都有一个 __proto__ ( 原型链 ) 属性,这个属性指向当前实例所属类的原型 ( 不确定的类都指向 Object.prototype )

  • 原型链是一种基于 __proto__ 向上查找的机制。当我们操作实例的某个属性或者方法的时候,首先找自己空间中私有的属性和方法
    • 找到了则结束查找
    • 没有找到,则基于 __proto__ 找自己所属类的 prototype,如果找到就用这个公有的。如果没有找到则基于原型上的 proto 继续向上查找,一直找到 Object.prototype 原型为止,如果没有说明操作的属性或者方法不存在

  • JS 中每个对象都有 __ proto __ 属性,这个属性指向了原型。
  • 对于一个 object 来说可以通过 __ proto __ 属性找到一个原型对象。
  • 原型的 constructor 属性指向构造函数,构造函数又通过 prototype 指回原型,但是并不是所有函数都具有这个属性,Function.prototype.bind() 就没有这个属性。
  • 为什么 obj 可以访问到 valueOf 函数,就是因为 obj 通过原型链找到了 valueOf 函数。

构造函数、原型与实例之间的关系
  • 每创建一个函数,该函数就会自动带有一个 prototype 属性。这个属性是一个指针,指向了一个对象,我们称之为原型对象。
  • 原型对象上默认有一个 constructor 属性,该属性也是一个指针指向与其相关联的构造函数。
  • 通过调用构造函数产生的实例都有一个内部属性 __ proto __,指向了原型对象,所以实例能够访问原型对象上的所有属性和方法。

所以三者的关系是:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针 __ proto __。也就是说实例可以通过内部指针访问到原型对象,原型对象通过 constructor 指针找到构造函数。

如下示例:

function Dog(name) {
    this.name = name
    this.type = 'Dog'
}
Dog.prototype.speak = function() {
    console.log('wangwangwang')
}
var doggie = new Dog('taidi')
doggie.speak() // wangwangwang

console.log(Dog.prototype.constructor === Dog) //true
console.log(doggie.__proto__.constructor === Dog) // true
doggie.prototype // undefined 只有函数对象才有 prototype 属性

如下图所示:

原型-构造函数-实例.png
原型链

前面我们说到所有的实例都有一个内部指针 __ proto __ 指向它的原型对象,并且可以访问原型对象上的所有属性和方法。doggie 实例指向了 Dog 的原型对象,可以访问 Dog 原型对象上的所有属性和方法;如果 Dog 原型对象变成了某一个类的实例 some1,这个实例又会指向一个新的原型对象 Some1,那么此时 doggie 就能访问 some1 的实例属性和 Some1 原型对象上的所有属性和方法。同理新的原型对象 Some1 恰巧又是另外一个对象的实例 some2,这个实例 some2 又会指向新的原型对象 Some2,那么此时 doggie 就能访问 some2 的实例属性和 Some2 原型对象上的所有属性和方法。

  • 通过原型链实现继承的方法代码如下:
// 定义一个 Animal 函数作为 Dog 的父类
function Animal() {
    this.superType = 'Animal'   
}

Animal.prototype.superSpeak = function() {
    console.log(this.superType)
}

function Dog(name) {
    this.name = name
    this.type = 'Dog'
}

// 改变 Dog 的 prototype 指针指向 Animal 的实例
Dog.prototype = new Animal()
// 等同于下面注释部分
/*
    var animal = new Animal()
    Dog.prototype = animal
*/

Dog.prototype.speak = function () {
    console.log(this.type)
}

var doggie = new Dog('taidi')
doggie.superSpeak() // Animal

console.log(doggie.__proto__.constructor === Animal) // true

以上代码,首先定义了一个 Animal 构造函数,通过new Animal()得到实例,会包含一个实例属性 superType 和一个原型属性 superSpeak。另外又定义了一个Dog构造函数。然后情况发生变化,代码中加粗那一行,将Dog的原型对象覆盖成了 animal 实例。当 doggie 去访问superSpeak属性时,js会先在doggie的实例属性中查找,发现找不到,然后,js就会去doggie 的原型对象上去找,doggie的原型对象已经被我们改成了一个animal实例,那就是去animal实例上去找。先找animal的实例属性,发现还是没有 superSpeack, 最后去 animal 的原型对象上去找,诶,这才找到。

流程如下图所示:

原型链.png

这就说明我们可以通过原型链的方式,实现 Dog 继承 Animal 的所有属性和方法。

总结:当重写了 Dog.prototype 指向的原型对象后,实例的内部指针也发生了改变,指向了新的原型对象,然后就能实现类与类之间的继承了。
MDN详解-继承与原型链请戳这里

查找性能
  • 在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。
  • 遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty 方法。hasOwnProperty 是 JavaScript 中唯一一个处理属性并且不会遍历原型链的方法。
new 一个对象发生了什么?
  • 创建一个新对象
  • 将构造函数的 this 指向新对象,执行构造函数中的代码给对象添加属性
  • 设置新对象的 proto 属性指向构造函数的 prototype 原型对象
  • 返回该对象 ( 如果构造函数没有 return 也会默认返回 this )
function _new(fun, ...args) {
    const newObj = Object.create(fun.prototype)
    const result = fun.apply(newObj, args)
    return typeof result == 'object' ? result : newObj
}
继承

父类代码如下:

// 定义一个动物类
function Animal(name = 'Animal') {
    // 属性
    this.name = name
    // 实例方法
    this.sleep = function () {
        console.log(`${this.name} is sleeping`)
    }
}
// 原型方法
Animal.prototype.eat = function(food) {
    console.log(`${this.name} is eating ${food}`)
}
    1. 原型继承 --- 将父类的实例做为子类的原型
      child.prototype = new parent()
      child.prototype.constructor = child
function Cat() {

}
Cat.prototype = new Animal()
Cat.prototype.name = 'cat'

var cat = new Cat()
console.log(cat.name) // cat
console.log(cat.eat('fish')) // cat is eating fish
console.log(cat.sleep()) // cat is sleeping
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // true

特点:

  1. 非常纯粹的继承关系,实例是子类的实例,也是父类的实例
  2. 父类新增原型方法/原型属性,子类都能访问到
  3. 简单,易于实现

缺点:

  1. 要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
  2. 无法实现多继承
  3. 来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)(详细请看附录代码: 示例1)
  4. 创建子类实例时,无法向父类构造函数传参
    1. 构造继承 --- 使用父类的构造函数来增强子类的实例,即复制父类的属性给子类 ( 没用到原型 )
function Cat(name = 'Tom') {
    Animal.call(this)
}

var cat = new Cat()
console.log(cat.name) // Tom
// console.log(cat.eat('fish')) // 报错找不到 eat 方法
console.log(cat.sleep()) // Tom is sleeping
console.log(cat instanceof Animal) // false
console.log(cat instanceof Cat) // true

特点:

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

缺点:

  1. 实例并不是父类的实例,只是子类的实例
  2. 只能继承父类的实例属性和方法,不能继承原型属性/方法
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
    1. 实例继承 --- 为父类实例添加新特性,做为子类的实例返回
function Cat(name = 'Tom') {
    var animal = new Animal()
    animal.name = name
    return animal
}

var cat = new Cat()
console.log(cat.name) // Tom
console.log(cat.eat('fish')) // Tom is eating fish
console.log(cat.sleep()) // Tom is sleeping
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // false

特点:

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

缺点:

  1. 实例是父类的实例,不是子类的实例
  2. 不支持多继承
    1. 拷贝继承
function Cat(name = 'Tom') {
    var animal = new Animal()
    for(var o in animal) {
        Cat.prototype[o] = animal[o]
    }
    Cat.prototype.name = name
}

var cat = new Cat()
console.log(cat.name) // Tom
console.log(cat.eat('fish')) // Tom is eating fish
console.log(cat.sleep()) // Tom is sleeping
console.log(cat instanceof Animal) // false
console.log(cat instanceof Cat) // true

特点:

  1. 支持多继承

缺点:

  1. 效率较低,内存占用高(因为要拷贝父类的属性)
  2. 无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
    1. 组合继承 --- 通过调用父类的构造继承父类的属性并保留传参的优点,然后通过将父类的实例做为子类原型,实现函数的复用
function Cat(name = 'Tom') {
    Animal.call(this)
    this.name = name
}
Cat.prototype = new Animal()

var cat = new Cat()
console.log(cat.name) // Tom
console.log(cat.eat('fish')) // Tom is eating fish
console.log(cat.sleep()) // Tom is sleeping
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // true

特点:

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

缺点:

  1. 调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了)
    1. 寄生组合继承 --- 通过寄生方式砍掉父类的实例属性,这样在调用两次父类构造的时候就不会初始化两次实例方法和属性,避免组合继承的缺点
function Cat(name = 'Tom') {
    Animal.call(this)
    this.name = name
}
(function() {
    // 创建一个没有实例方法的类
    var Trans = function () {}
    Trans.prototype = new Animal()
    //将实例作为子类的原型
    Cat.prototype = new Trans()
})()

var cat = new Cat()
console.log(cat.name) // Tom
console.log(cat.eat('fish')) // Tom is eating fish
console.log(cat.sleep()) // Tom is sleeping
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // true

特点:

  1. 堪称完美

缺点:

  1. 实现较为复杂

原型题

function fun() {
    this.a = 0
    this.b = function() {
        console.log(this.a)
    }
}
fun.prototype = {
    b: function() {
        this.a = 20
        console.log(this.a)
    },
    c: function() {
        this.a = 30
        console.log(this.a)
    }
}
var my_fun = new fun()
my_fun.b() // 0
my_fun.c() // 30
function Fn() {
    var n = 10
    this.m = 20
    this.aa = function() {
        console.log(this.m)
    }
}
Fn.prototype.bb = function() {
    console.log(this.n)
}
var f1 = new Fn
Fn.prototype = {
    aa: function() {
        console.log(this.m + 10)
    }
}
var f2 = new Fn
console.log(f1.constructor) // Fn
console.log(f2.constructor) // Object
f1.bb() // undefined
f1.aa() // 20
f2.bb // TypeError
f2.aa() // 20
f2.__proto__.aa() // NaN

相关文章

  • JavaScript 原型、原型链与原型继承

    原型,原型链与原型继承 用自己的方式理解原型,原型链和原型继承 javascript——原型与原型链 JavaSc...

  • 继承

    原型链直接继承 原型链直接继承prototype 原型链继承_prototype属性 继承_构造函数绑定

  • js中的实现继承的几种方式

    大纲:原型链借用构造函数组合继承原型式继承寄生式继承寄生组合式继承 1、原型链: 什么是原型链? 原型链的基本思想...

  • Javascript 面向对象的程序设计(原型链与继承)

    继承 原型链 讲原型的时候提到过继承,设计原型的初衷就是为了继承,原型链是实现继承的主要方法。那什么是原型链,还记...

  • js基础之实现继承的几种方式

    js 实现继承的方式有: 原型链继承; 构造函数继承; 组合继承(原型链继承 + 构造函数继承)(最常用);(原型...

  • es5的部分继承以及es6的class

    一、JavaScript常用的原型继承方式 原型链继承 2,构造函数继承(对象冒充继承) 3,组合继承(原型链继承...

  • 构造函数原型的继承方式分析

    1.通过原型链继承 综上我们可以总结出 通过原型链来实现继承的原理通过原型链来实现继承的原理原型链继承方案中,父类...

  • js_继承及原型链等(四)

    js_继承及原型链等(三) 1. 继承 依赖于原型链来完成的继承 发生在对象与对象之间 原型链,如下: ==原型链...

  • JavaScript继承方式详解

    JavaScript实现继承的方式主要有两种: 原型链继承和借助构造函数继承 一、原型链继承 原型链继承的主要思想...

  • js常见的继承方式

    1.原型链继承 基于原型链查找的特点,我们将父类的实例作为子类的原型,这种继承方式便是原型链继承。 Child.p...

网友评论

      本文标题:★ 原型、原型链、继承

      本文链接:https://www.haomeiwen.com/subject/oxhynctx.html