JS之原型链与继承

275 阅读6分钟

对象.png

  • 1. 构造函数

1.1 什么是构造函数

首先构造函数也是一个普通函数,创建方式和普通函数一样,但构造函数习惯首字母大写。而构造函数与普通函数的区别在于调用方式不同,构造函数需要用new关键字来调用。

1.2 为什么要用构造函数

抽取对象公用的属性和方法封装成一个类(也就是一个模板),我们可以通过对类进行实例化,这些实例化对象就可以具有相似的属性和方法,从而减少冗余代码,实现 代码复用

function Star(name, skill) {
    this.name = name;
    this.skill = skill;
}

var star1 = new Star('刘德华', '唱歌')
var star2 = new Star('杨紫', '表演')

console.log(star1)  // Star {name: "刘德华", skill: "唱歌"}
console.log(star2)  //Star {name: "杨紫", skill: "表演"}

1.3 构造函数的执行过程

构造函数是用new关键字进行调用的,所以构造函数的执行过程也就是new的执行过程,主要有以下4步:

1. 创建一个空对象;
2. 设置原型链:将这个对象的__proto__指向构造函数的prototype;
3. 将新创建的对象作为this的上下文;
4. 如果这个函数有返回值, 则返回; 否则默认返回新对象。
  • 2. 原型

只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype属性,指向原型对象。原型对象也有一个constructor属性,指回与之关联的构造函数。每次调用构造函数创建一个实例,这个实例就会有一个__proto__的属性,指向原型对象。

三者关系如图:

图片.png

  • 判断
  1. instanceof 检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上,返回布尔值
function Star(name, skill) {
    this.name = name;
    this.skill = skill;
}

var star1 = new Star('刘德华', '唱歌')
console.log(star1 instanceof Star)  //true
  1. isPrototypeOf( ) 原型对象的方法,检查实例中是否有__proto__指向obj.prototype,MDN文档解释:测试一个对象是否存在于另一个对象的原型链上
function Star(name, skill) {
    this.name = name;
    this.skill = skill;
}

var star1 = new Star('刘德华', '唱歌')
console.log(Star.prototype.isPrototypeOf(star1))  //true
  • 获取 Object.getPrototypeOf(object) 获取指定对象的原型
  • 3. 原型链

通过对象访问属性或方法时,会先搜索实例本身;若没有,则通过__proto__指向的原型对象中查找属性或方法,如果还没有找到就会继续沿__proto__寻找,直至Object的原型对象,这样一层一层向上查找就会形成一个链式结构,成为原型链。

原型链的终点:Object的原型对象也有__proto__,指向null,即Object.prototype.__proto__ === null

注:给对象的实例增加一个同名属性,会遮蔽原型对象的同名属性,虽然不会修改它,但是会屏蔽对它的访问

  • 判断
  1. hasOwnPropertypeOf( ) 确定某个属性在实例还是在原型对象上。存在它的实例上时返回true
  2. in 无论改属性在实例还是原型上都返回true
  • 枚举
  1. for…in 通过对象访问且可被枚举的属性都会返回,包括实例属性和原型属性,枚举顺序不确定
  2. Object.keys() 返回对象实例上所有可枚举的属性名称的字符串,枚举顺序不确定
  3. Object.getOwnPropertyNames() 列出所有实例属性,无论是否可被枚举(包括constructor),枚举顺序确定
  • 4.继承

4.1 原型链继承

创建父亲的实例,并将该实例赋值给子.prototype

function Father(name) {
    this.name = name
    this.color = ['red','black','pink']
}
Father.prototype.getName = () => {
    console.log('我是父亲的方法')
}

function Son(score) {
    this.score = score
}

Son.prototype = new Father('张三')  //核心代码

var son1 = new Son(34)
son1.getName()
console.log(son1.name, son1.color)

输出结果:

图片.png

  • 缺点: (1)多个实例对引用类型的操作会被篡改
var son2 = new Son(99)
son1.color.push('gary')
console.log(son2.name)
console.log(son1.color)
console.log(son2.color)

图片.png
(2)在创建子类实例时,不能向父类传参

4.2 借用构造函数

使用父类的构造函数增强子类的实例,即复制父类的实例给子类

function Father(name) {
    this.name = name
    this.color = ['red','black','pink']
    this.getName = function () {
        console.log('我是父类构造函数的方法')
    }
}

function Son(name) {
    Father.call(this, name) //核心代码 在新创建的对象上执行构造函数
}

var stu1 = new Son('李四')
var stu2 = new Son('王五')
console.log(stu1.name)
console.log(stu1.color)
console.log(stu2.name)
console.log(stu2.color)
stu1.getName()

图片.png

  • 优点 (1)避免了引用类型的属性被所有实例共享
var stu1 = new Son('李四')
var stu2 = new Son('王五')
stu2.color.push('gray')
console.log(stu1.name)
console.log(stu1.color)
console.log(stu2.name)
console.log(stu2.color)

图片.png
(2)可在子类中向父类传参
(3)可以实现多继承(call多个父类对象)

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

4.3 组合继承

上述两种方式的结合,通过原型链实现对原型属性或方法的继承,构造函数实现实例的属性或方法的继承

function Father(name) {
    this.name = name
    this.color = ['red','black','pink']
}
Father.prototype.getName = () => {
    console.log('我是父亲的方法')
}

function Son(name) {
    Father.call(this, name) //第二次调用,从父类拷贝一份实例属性给子类
}
Son.prototype = new Father() //第一次调用,创建父类的实例作为子类的原型

var stu1 = new Son('张三')
var stu2 = new Son('李四')
stu2.color.push('yellow')
console.log(stu1.name)
console.log(stu1.color)
stu1.getName()
console.log(stu2.name)
console.log(stu2.color)
  • 优点 (1)不存在引用属性共享的问题
    (2)可以传递参数
    (3)函数可以复用
  • 缺点 使用子类创建实例对象时,父类被调用了两次,子类原型中会存在两份相同的属性和方法

图片.png

4.4 原型式继承

利用一个空对象作为中介,将某个对象直接赋值给空对象构造函数的原型

function Object(obj) {
    function F() {}
    F.prototype = obj
    return new F()
}

function Father(name) {
    this.name = name
    this.color = ['red','black','pink']
}
Father.prototype.getName = () => {
    console.log('我是父亲的方法')
}

var father = new Father('李四')
var stu1 = Object(father)
var stu2 = Object(father)
stu2.color.push('yellow')
console.log(stu1)
console.log(stu1.name)
console.log(stu1.color)
console.log(stu2.color)

图片.png
注:ES5中的Object.create()能够代替上述的Object方法

var stu1 = Object.create(father)

图片.png

  • 缺点 与原型链继承相同

4.5 寄生式继承

在原型式继承的基础上,增强对象,返回构造函数

function Object(obj) {
    function F() {}
    F.prototype = obj
    return new F()
}

function Father(name) {
    this.name = name
    this.color = ['red','black','pink']
}
Father.prototype.getName = () => {
    console.log('我是父亲的方法')
}

function creatAnother(original) {
    var clone = Object(original)
    clone.sayHi = function () {
        console.log('hi')
    }
    return clone
}

var stu1 = creatAnother(new Father('王五'))
var stu2 = creatAnother(new Father('李四'))
stu2.color.push('yellow')
console.log(stu1)
console.log(stu1.name)
console.log(stu1.color)
stu1.sayHi()
stu1.getName()
console.log(stu2.name)
console.log(stu2.color)

图片.png

  • 缺点 (1)无法实现函数的复用
    (2)无法传参

4.6 寄生组合式

结合借用构造函数传递参数和寄生模式,即 取得父类原型的副本,使用寄生式来继承父类的原型,然后将返回的新对象赋值给子类的原型

function Father(name) {
    this.name = name;
    this.color = ['red','black','pink']
}
Father.prototype.getName = function () {
    console.log('我是父类的方法')
}

function Son(name, age) {
    Father.call(this, name) //核心代码 借用父类构造函数增强子类实例
    this.age = age
}

let prototype = Object.create(Father.prototype) //取得父类原型副本
prototype.constructor = Son //重写constructor,增强对象
Son.prototype = prototype //返回父类原型对象副本赋值给子类的原型

Son.prototype.sayHi = function () {
    console.log('hi')
}

let stu1 = new Son('张三',18)
console.log(stu1)
console.log(stu1.name, stu1.age)
console.log(stu1.color)
stu1.getName()
stu1.sayHi()

图片.png

  • 优点 是目前效率最高的继承方式,既只调用了一次父类构造函数,同时还能保持原型链不变

参考