Js继承和内置构造函数全解

912 阅读8分钟

内置构造函数和继承

先来看两幅图

两个无关的类

0TQfX9.png

思考下People类和Student类的关系

image-20211204172329995

People类拥有的属性和方法Student类都有,Student类还扩展了一些属性和方法

Student是一种People,两类之间是“**is a kind of”**关系

这就是继承关系: Student类继承自People类

继承

继承描述了两个类之间的“is a kind of”关系

比如学生“是一种“人“,所以人类和学生类之间就构成继承关系

People是**“父类”(或“超类”、“基类”) ; Student是“子类”**(或“派生类”)

子类丰富了父类,让类描述得更具体、更细化

0T1SKJ.png

更多的继承关系举例

0T12L9.png

1.通过原型链实现继承

image-20211204123418581

实现继承的关键在于:子类必须拥有父类的全部属性和方法,同时子类还应该能定义自己特有的属性和方法

让子类构造函数的prototype,指向父类的一个实例

使用JavaScript特有的原型链特性来实现继承,是普遍的做法

0TUTr8.png

//父类 人类
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    // this.arr = [33, 44, 55];
}

People.prototype.sayHello = function () {
    console.log(`我是${this.name},今年${this.age}啦`);
};

People.prototype.sleep = function () {
    console.log(this.name + "开始睡觉觉了zzzz");
};

//子类 学生类
function Student(name, age, sex, scholl, studentNumber) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.scholl = scholl;
    this.studentNumber = studentNumber;
}
// 核心语句 实现继承
Student.prototype = new People();

Student.prototype.study = function(){
    console.log(this.name + "正在学习");
}
Student.prototype.exam = function(){
    console.log(this.name + "正在考试,奥利给!");
}

//子类可以重写,复写(override) 父类方法sayHello
Student.prototype.sayHello = function(){
    console.log( `敬礼! 您好,我是${this.name},${this.sex},我来自${this.scholl},我${this.age}了`);
}

// 实例化
let hanmeimei = new Student("韩梅梅" , 12, "女", "中心小学" , 12110);

hanmeimei.study();
hanmeimei.exam();
hanmeimei.sayHello();
hanmeimei.sleep();

let yunmu = new People("云牧", 18, "男");
yunmu.sayHello();

运行结果如下

下载

通过原型链实现继承的问题

  • 问题1:如果父类的属性中有引用类型值,则这个属性会被所有子类的实例共享;
  • 问题2:子类的构造函数中,往往需要重复定义很多超类定义过的属性。即子类的构造函数写的不够优雅;

2.借用构造函数

  • 为了解决原型中包含引用类型值所带来问题和子类构造函数不优雅的问题,开发人员通常使用一种叫做 “借助构造函数” 的技术,也被称为 “伪造对象”“经典继承”
  • 借用构造函数的思想非常简单:在子类构造函数的内部调用超类的构造函数,但是要注意使用call()绑定上下文

我们将上文例子稍微改造

//父类 人类
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.arr = [33, 44, 55];
}

//子类 学生类
function Student(name, age, sex, scholl, studentNumber) {
    //子类中使用call执行父类构造函数 绑定this并传值
    People.call(this, name, age, sex);
    this.scholl = scholl;
    this.studentNumber = studentNumber;
}

const xiaoming = new Student("小明", "男", 12, "中心学校", 100666);
console.log(xiaoming);
xiaoming.arr.push(77); 
console.log(xiaoming.arr); // [33, 44, 55, 77]
console.log(xiaoming.hasOwnProperty("arr")); // true

const xiaohong = new Student("小红", "女", 11, "中心学校", 100667);
console.log(xiaohong.arr); // [33, 44, 55]
console.log(xiaohong.hasOwnProperty("arr")); // true

3.组合继承

就是将原型链继承和构造函数继承组合在一起,继承两个优点,也做伪经典继承

通过调用父类构造函数,继承父类的属性并保留传参的优点,

然后再通过将父类实例作为子类原型,实现函数复用

组合继承是]avaScript中最常用的继承模式

//子类 学生类
function Student(name, age, sex, scholl, studentNumber) {
    //子类中使用call执行父类构造函数 绑定this并传值
    People.call(this, name, age, sex);
    this.scholl = scholl;
    this.studentNumber = studentNumber;
}
// 核心语句 实现继承
Student.prototype = new People();

组合继承的缺点

  • 组合继承最大的问题就是无论什么情况下,都会调用两次超类的构造函数
  • 一次是在创建子类原型的时候,另一次是在子类构造函数的内部

4.原型式继承

IE9+开始支持0bject.create()方法,可以根据指定的对象为原型创建出新对象

const obj2 = Object.create(obj1);

下载 (1)

const obj1 = {
    a: 1,
    b: 2,
    c: 3,
    test() {
        console.log(this.a + this.b);
    }
};

const obj2 = Object.create(obj1, {
    // 为obj2添加新属性
    d: {
        value: 99
    }, 
    // 访问时会遮蔽原型上的a属性
    a: {
        value: 666
    }
});


console.log(obj2.__proto__ === obj1);       // true
console.log(obj2.a); // 666
console.log(obj2.b); // 2
console.log(obj2.c); // 3
console.log(obj2.d); // 99
obj2.test(); // 668

如果想要新对象与现有对象"类似", 使用Object.create(),不必大张旗鼓创建构造函数

Object.create()的兼容性写法

  • 如何在低版本浏览器中实现0bject.create()呢?
// 函数的功能是以obj为原型创建新对象
function object(obj) { 
    // 创建一个临时构造函数
    function F() {}
    // 让这个临时构造函数的prototype指向o,这样一来它new出来的对象,__proto__指向了o
    F.prototype = obj;
    // 返回F的实例
    return new F();
}

5.寄生式继承

  • 编写一个函数,它接收一个参数o,返回以o为原型的新对象p,同时给p上添加预置的新方法

image-20211204144529995

const o1 = {
    name: "小明",
    age: 12,
    sex: "男",
};

const o2 = {
    name: "小红",
    age: 11,d
    sex: "女",
};

// 寄生式继承
function f(o) {
    // 以o为原型创建出新对象
    const p = Object.create(o);
    // 补充两个方法
    p.sayHello = function () {
        console.log("你好,我是" + this.name + "今年" + this.age + "岁了");
    };
    p.sleep = function () {
        console.log(this.name + "正在睡觉");
    };

    return p;
}

// 以o1对象为原型得到p1对象
const p1 = f(o1);
p1.sayHello(); // 你好,我是小明今年12岁了

// 以o2对象为原型得到p2对象
const p2 = f(o2);
p2.sayHello(); // 你好,我是小红今年11岁了
  • 寄生式继承就是编写一个函数,它可以“增强对象”

  • 只要把对象传入这个函数,这个函数将以此对象为“基础”创建出新对象,并为新对象赋予新的预置方法

  • 在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式

寄生式继承的缺点

  • 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,即“方法没有写到prototype上”
console.log(p1.sayHello == p2.sayHello); // false

6.寄生组合式继承

  • 寄生组合式继承:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法
  • 其背后的基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,
  • **我们所需要的无非就是超类型原型的一个副本而已。
  • 本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
function People(name) {
    this.name = name;
}

People.prototype.eat = function () {
    console.log(this.name + " is eating");
};

function Stundent(name, age) {
    People.call(this, name);
    this.age = age;
}
Stundent.prototype = Object.create(People.prototype);
// prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
Stundent.prototype.contructor = Stundent;

Stundent.prototype.study = function () {
    console.log(this.name + " is studying");
};

// 测试
let xiaoming = new Stundent("xiaoming", 16);
console.log(xiaoming.name); // xiaoming
xiaoming.eat(); // xiaoming is eating
xiaoming.study(); // xiaoming is studying

图示如下:

image-20211204154315064

7.ES6中class 的继承

  • ES6中引入了class关键字,class可以通过extends关键字实现继承
  • 还可以通过static关键字定义类的静态方法
  • 这比 ES5 的通过修改原型链实现继承,要清晰和方便很多
  • 需要注意的是,class关键字只是原型的语法糖,JavaScript继承仍然是基于原型实现的
class Person {
    //类的构造方法
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    //定义方法
    showName() {
        console.log("调用父类的方法");
        console.log(this.name, this.age);
    }
}
const p1 = new Person("kobe", 39);
console.log(p1);

//定义一个子类
class Student extends Person {
    constructor(name, age, salary) {
        super(name, age); //通过super调用父类的构造方法
        this.salary = salary;
    }
    showName() {
        //在子类自身定义方法
        console.log("调用子类的方法");
        console.log(this.name, this.age, this.salary);
    }
}
const s1 = new Student("yunmu", 18, 1000000);
console.log(s1);
s1.showName(); // yunmu 18 1000000

instance运算符

  • instanceof运算符用来检测“某对象是不是某个类的实例”,比如:

image-20211204154351358

function People() {}
function Student() {}
Student.prototype = Object.create(People.prototype);
const xiaoming = new Student();

// 测试instanceof
console.log(xiaoming instanceof Student); // true
console.log(xiaoming instanceof People); // true

还是此图,小明可以沿着原型链找到Student和People的prototype属性 所以instanceof结果为true

image-20211204154315064

内置构造函数

  • JavaScript有很多内置构造函数,比如Array就是数组类型的构造函数,Function就是函数类型的构造函数,object就是对象类型的构造函数
  • 内置构造函数非常有用,所有该类型的方法都是定义在它的内置构造函数的prototype上的,我们可以给这个对象添加新的方法,从而拓展某类型的功能

image-20211204160110373

// 数组的内置构造函数,任何的数组字面量都可以看做是Array的实例
console.log([1, 2] instanceof Array); // true
console.log([] instanceof Array); // true

// 创建了长度为5的空数组
const arr = new Array(5);
console.log(arr); //[ <5 empty items> ]
console.log(arr.length); // 5

// 函数的内置构造函数,任何的函数字面量都可以看做是Function的实例
function fun() {}
function add(a, b) {
    return a + b;
}
console.log(fun instanceof Function); // true
console.log(add instanceof Function); // true

const decrement = new Function("a", "b", "return a - b");
console.log(decrement(8, 3)); // 5

// 对象的内置构造函数
console.log({ a: 1 } instanceof Object); // true
console.log({} instanceof Object); // true
const o = new Object();
o.a = 2;
o.b = 6;
console.log(o); // { a: 2, b: 6 }

扩展方法

// push pop 等方法实际都是在数组构造函数原型上的方法
console.log(Array.prototype.hasOwnProperty("push")); // true
console.log(Array.prototype.hasOwnProperty("pop")); // true
console.log(Array.prototype.hasOwnProperty("splice")); // true

// 拓展qiuhe方法
Array.prototype.total = function () {
    // 累加器
    let sum = 0;
    // this表示调用total()方法的数组
    for (let i = 0; i < this.length; i++) {
        sum += this[i];
    }
    // 返回结果
    return sum;
};

const arr1 = [1, 2, 3, 4, 5];
const result1 = arr1.total();
console.log(result1); // 15

const arr2 = [2, 8];
const result2 = arr2.total();
console.log(result2); // 10

基本类型值的“包装类”

  • Number、String、Boolean是三个基本类型值的包装类,用new调用它们可以生成“对象”版本的基本类型值
const o = new Number(3);
console.log(o);
console.log(typeof o); // object
console.log(5 + o); // 8

const s = new String("abc");
console.log(s);
console.log(typeof s); // object
console.log(String.prototype.hasOwnProperty("slice")); // true
console.log(String.prototype.hasOwnProperty("substring")); // true
console.log(String.prototype.hasOwnProperty("substr")); // true

const b = new Boolean(true);
console.log(b);
console.log(typeof b); // object

内置构造函数的关系

  • 0bject.prototype是万物原型链的终点。JavaScript中函数、数组皆为对象,以数组为例,完整原型链是这样的:

image-20211204163050910

测试:

console.log([1, 2].__proto__ === Array.prototype); // true
console.log([1, 2].__proto__.__proto__ === Object.prototype); // true

console.log([] instanceof Object); // true
console.log([] instanceof Array); // true

console.log(Object.prototype.__proto__); // null null没有__proto__
  • 任何函数都可以看做是 Function “new出来的”,那我们开一个脑洞:
  • Object也是函数呀,它是不是Function “new出来的”呢?答案是肯定的
  • Function也是个函数,可以看作它自己 “new出来的” 自己,自己也是一个实例

image-20211204163400465

测试:

console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true
console.log(Object.__proto__.__proto__ === Object.prototype); // true

console.log(Function instanceof Object); // true
console.log(Object instanceof Function); // true
console.log(Function instanceof Function); // true
console.log(Object instanceof Object); // true

如果觉得本文不错,可以点个赞哦。