今天看啥  ›  专栏  ›  程序猿蔡徐坤

聊一聊JS继承

程序猿蔡徐坤  · 掘金  ·  · 2020-04-07 07:27
阅读 36

聊一聊JS继承

在开始之前需要先了解构造函数、原型和原型链的相关内容

构造函数

构造函数和普通函数的区别在调用方式,构造函数使用new操作符来调用,而普通函数则是直接执行

function Person(name, age) {
    this.name = name;
    this.age = age;
}

var Jason = new Person('Jason', 25);
console.log(Jason.constructor === Person); //true
复制代码

这里有一点要说明,除了基础数据类型的constructor外,constructor是可以被改写的,因此检测对象的时候,采用instanceof更靠谱

function Person(name) {
    this.name = name;
}
function SuperType() { }
var Jason = new Person('Jason');
console.log(Jason.constructor); //[Function: Person]
Jason.constructor = SuperType;
console.log(Jason.constructor); //[Function: SuperType]
复制代码

原型

我们创建的每个函数都有 prototype 属性,这个属性指向函数的原型对象。原型对象的用途是包含可以由特定类型的所有实例共享的属性和方法。

在默认情况下,所有原型对象都会自动获得一个 constructor 属性,这个属性包含一个指向 prototype 属性所在函数的指针。

当调用构造函数创建一个新实例后,该实例的内部将包含一个指针,指向构造函数的原型对象(可以通过实例的 proto 来访问构造函数的原型对象)。

function Person(name) {
    this.name = name;
}
Person.prototype.sayName = function() {
    console.log(this.name);
}
var person1 = new Person('Jason');
var person2 = new Person('ken');
//构造函数原型对象上的方法和属性被实例共享
person1.sayName();
person1.sayName(); 
复制代码

实例.proto === 构造函数.prototype

console.log(Object.prototype.__proto__ === null)  //true
console.log(Object.__proto__ === Function.prototype) //true
console.log(Function.prototype.__proto__ === Object.prototype) //true
复制代码

原型链

简单回顾一下构造函数、原型和实例的关系:

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个可以执行原型对象的内部指针(可以通过 __proto 访问)。

假如我们让原型对象等于另一个类型的实例,那么此时原型对象包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。加入另一个原型又是另一个类型的实例,那么上述关系仍然成立,如此层层递进,就构成了实例与原型的链条,这就是原型链的基本概念。

function SuperType() {
    this.type = 'animal';
}
SuperType.prototype.getType = function() {
    console.log(this.type);
}
function SubType() {

}
SubType.prototype = new SuperType();
SubType.prototype.sayHello = function() {
    console.log('hello');
}
function SimType(name) {
    this.name = name;
}
SimType.prototype = new SubType();
SimType.prototype.sayHi = function() {
    console.log('hi');
}
var instance = new SimType('刘小夕');
instance.getType();
复制代码

调用 instance.getType() 会调用以下的搜索步骤:

  • 搜索 instance 实例
  • 搜索 SimType.prototype
  • 搜索 SubType.prototype
  • 搜索 SuperType.prototype,找到了 getType 方法
  • 在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链的末端才会停下来。

所有引用类型都继承了 Object,这个继承也是通过原型链实现的。如果在 SuperType.prototype 还没有找到 getType,就会到 Object.prototype中找(图中少画了一环)。

原型链继承

原型链继承的基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。 如 SubType.prototype = new SuperType();

function SuperType() {
    this.name = 'Yvette';
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.getName = function () {
    return this.name;
}
function SubType() {
    this.age = 22;
}
SubType.prototype = new SuperType();
SubType.prototype.getAge = function() {
    return this.age;
}
SubType.prototype.constructor = SubType;
let instance1 = new SubType();
instance1.colors.push('yellow');
console.log(instance1.getName()); //'Yvette'
console.log(instance1.colors);//[ 'pink', 'blue', 'green', 'yellow' ]

let instance2 = new SubType();
console.log(instance2.colors);//[ 'pink', 'blue', 'green', 'yellow' ]
复制代码

可以看出 colors 属性会被所有的实例共享(instance1、instance2、...)。

缺点:

  • 通过原型来实现继承时,原型会变成另一个类型的实例,原先的实例属性变成了现在的原型属性,该原型的引用类型属性会被所有的实例共享。(引用类型值被所有实例共享)
  • 在创建子类型的实例时,没有办法在不影响所有对象实例的情况下给超类型的构造函数中传递参数。(传参共享)

借用构造函数

在子类型的构造函数中调用超类型构造函数。

function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
function SubType(name) {
    SuperType.call(this, name);
}
let instance1 = new SubType('Jason');
instance1.colors.push('yellow');
console.log(instance1.colors);//['pink', 'blue', 'green', yellow]

let instance2 = new SubType('Jack');
console.log(instance2.colors); //['pink', 'blue', 'green']
复制代码

优点:

  • 可以向超类传递参数
  • 解决了原型中包含引用类型值被所有实例共享的问题 缺点:
  • 方法都在构造函数中定义,函数复用无从谈起。
  • 超类型原型中定义的方法对于子类型而言都是不可见的。

组合继承

组合继承指的是将原型链和借用构造函数技术组合到一块,从而发挥二者之长的一种继承模式。基本思路:

使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,既通过在原型上定义方法来实现了函数复用,又保证了每个实例都有自己的属性。

function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}
function SuberType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SuberType.prototype = new SuperType()
SuberType.prototype.constructor = SuberType
SuberType.prototype.sayAge = function () {
    console.log(this.age);
}
let instance1 = new SuberType('Jason', 25);
instance1.colors.push('yellow');
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); //Jason

let instance2 = new SuberType('ken', 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]
instance2.sayName();//ken
复制代码

缺点:

  • 无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。

优点:

  • 可以向超类传递参数
  • 每个实例都有自己的属性
  • 实现了函数复用

寄生组合式继承

寄生组合继承是引用类型最理性的继承范式,使用Object.create在组合继承的基础上进行优化。

function SuperType(name) {
    this.name = name;
    this.colors = ['pink', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
    console.log(this.name);
}
function SuberType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}
SuberType.prototype = Object.create(SuperType.prototype)
SuberType.prototype.constructor = SuberType
SuberType.prototype.sayAge = function () {
    console.log(this.age);
}
let instance1 = new SuberType('Jason', 25);
instance1.colors.push('yellow');
console.log(instance1.colors); //[ 'pink', 'blue', 'green', 'yellow' ]
instance1.sayName(); //Jason

let instance2 = new SuberType('ken', 22);
console.log(instance2.colors); //[ 'pink', 'blue', 'green' ]
instance2.sayName();//ken
复制代码

ES6 继承

Class 可以通过extends关键字实现继承,如:

class SuperType {
    constructor(age) {
        this.age = age;
    }

    getAge() {
        console.log(this.age);
    }
}

class SubType extends SuperType {
    constructor(age, name) {
        super(age); // 调用父类的constructor(x, y)
        this.name = name;
    }

    getName() {
        console.log(this.name);
    }
}

let instance = new SubType(22, 'Jason');
instance.getAge(); //22
复制代码
  • 类的数据类型就是函数,类本身就指向构造函数。
console.log(typeof SuperType);//function
console.log(SuperType === SuperType.prototype.constructor); //true
复制代码
  • 类的内部所有定义的方法,都是不可枚举的。(ES5原型上的方法默认是可枚举的)
Object.keys(SuperType.prototype);
复制代码
  • constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有constructor 方法,如果没有显式定义,一个空的 constructor 方法会被默认添加。
  • Class 不能像构造函数那样直接调用,会抛出错误。
  • 子类必须在 constructor 中调用 super 方法,否则新建实例时会报错。如果没有子类没有定义 constructor 方法,那么这个方法会被默认添加。在子类的构造函数中,只有调用 super 之后,才能使用 this关键字,否则报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。



原文地址:访问原文地址
快照地址: 访问文章快照