手记

理解JavaScript的类与继承

大纲

  • 继承

1.1.类的声明

// 类的声明
function Animal(name) {
    this.name = name;
}

// ES6的class的声明
class Animal2 {
    // 构造函数
    constructor(name) {
        this.name = name;
    }
}

1.2.通过类来实例化一个对象

// 通过new关键字实例化一个对象
var animal1 = new Animal('dog');
var animal2 = new Aniaml2('pig');

二、继承

2.1.借助构造函数实现继承

// 借助构造函数实现继承
function Parent1() {
    this.name = 'parent1';
    this.say = function () {
        console.log('hello');
    }
}

Parent1.prototype.getName = function () {
    return this.name;
}

function Child1() {
    // call, apply: 改变函数运行的上下文
    Parent1.call(this);
    this.type = 'child1';
}

console.log(new Child1()); // Child1 {name: "parent1", say: ƒ, type: "child1"}
  • 原理:将父级的构造函数的this执行子类构造函数的实例上
  • 优点:可以继承父类构造函数的属性与方法
  • 缺点:但是没有继承到Parent1原型对象上的方法

2.2.借助原型链实现继承

// 借助原型链实现继承
function Parent2() {
    this.name = 'parent2';
    this.arr = [1, 2, 3];
    this.say = function () {
        console.log('hello');
    }
}

Parent2.prototype.getName = function () {
    return this.name;
}

function Child2() {
    this.type = 'child2';
}

Child2.prototype = new Parent2();

Child2.prototype.getChildName = function () {
    return 'childName'
}

let c1 = new Child2();
let c2 = new Child2();
console.log(c1.__proto__ === Child2.prototype); // true

// 优点是能继承原型的属性和方法
// 缺点:因为通过子类构造出来的实例的__proto__隐形原型都相同,即:
console.log(c1.__proto__ === c2.__proto__); // true
// 导致了如果有一个实例修改了属性,那么所有通过实例出来的对象都会跟着改变
console.log(c1.arr); // [1, 2, 3]
console.log(c2.arr); // [1, 2, 3]

// 只是在c1对象中添加4
c1.arr.push(4);
// 然而c1,c2对象都已经跟着改变了
console.log(c1.arr); // [1, 2, 3, 4]
console.log(c2.arr); // [1, 2, 3, 4]
  • 原理:在子类的原型链上面继承父类的实例对象,到达了在原型链上面能查询到父类属性和方法
  • 缺点:因为通过子类构造出来的实例的__proto__隐形原型都相同即,导致了如果有一个实例修改了属性,那么所有通过实例出来的对象都会跟着改变

2.3.构造函数与原型组合方式继承

// 组合方式
function Parent3() {
    this.name = 'parent3';
    this.arr = [1, 2, 3];
    this.say = function () {
        console.log('hello');
    }
}

function Child3() {
    // 继承父类的构造函数的属性和方法
    Parent3.call(this);
    this.type = 'child3';
}

// 继承父类的原型对象的属性和方法
Child3.prototype = new Parent3();
let c1 = new Child3();
let c2 = new Child3();

console.log(c1.arr); // [1, 2, 3]
console.log(c2.arr); // [1, 2, 3]

// 这种继承方法通常叫组合方式继承,弥补了原型模式修改对象属性影响其他的实例问题
c1.arr.push(4);
console.log(c1.arr); // [1, 2, 3, 4]
console.log(c2.arr); // [1, 2, 3]
  • 优点:弥补了原型模式修改对象属性影响其他的实例问题
  • 缺点:每一次new调用都需要调用一次父类的调用:Parent3.call(this); 还有Child3.prototype = new Parent3();实际实例1次子类就实例化了2次父类。

2.4.构造函数与原型组合方式继承的优化方法1

  • 优化方法1:在子类的原型对象上引用父类的原型对象
// 组合方式
function Parent4() {
    this.name = 'parent3';
    this.arr = [1, 2, 3];
}

function Child4() {
    // 继承父类的构造函数的属性和方法
    Parent4.call(this);
    this.type = 'child3';
}

// 继承父类的原型对象的属性和方法
Child4.prototype = Parent4.prototype;
let c1 = new Child4();
let c2 = new Child4();

console.log(c1.arr); // [1, 2, 3]
console.log(c2.arr); // [1, 2, 3]

c1.arr.push(4);
console.log(c1.arr); // [1, 2, 3, 4]
console.log(c2.arr); // [1, 2, 3]

console.log(c1 instanceof Child4); // true
console.log(c1 instanceof Parent4); // true

console.log(c1.__proto__ === Child4.prototype); // true
console.log(c1.__proto__ === Parent4.prototype); // true

// 如何区分c1到底是子类实例化还是父类实例化?
console.log(c1.constructor); // 指向的是Parent4
// 理想上c1实例化的构造函数应该是指向Child4,实际上指向的是Parent4
  • 原理:当通过new关键字实例化Child4时,已经生成了Parent4实例,让子类的原型对象指向父类的原型对象即可
  • 优点:弥补了构造函数与原型组合方式继承多次调用父类实例的问题
  • 缺点:无法识别通过子类实例化出来的对象的构造函数是由谁实例化的

2.5.构造函数与原型组合方式继承的优化方法2

  • 优化方法:使用Object.create()方法创建新的对象,使子类的显式原型对象(prototype)等于通过Object.create()传入父类的原型对象新建出来的对象的隐式原型(__proto__)关联起来,然后再重新再把子类的(constructor)原型对象指引回它的构造函数。
  • Object.create();
    • 使用现有的对象来提供新创建的对象的__proto__
// 组合方式优化2

function Parent5() {
    this.name = 'parent3';
    this.arr = [1, 2, 3];
}

function Child5() {
    // 继承父类的构造函数的属性和方法
    Parent5.call(this);
    this.type = 'child3';
}

// 继承父类的原型对象的属性和方法
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
Child5.prototype = Object.create(Parent5.prototype);
Child5.prototype.constructor = Child5;

let c1 = new Child5();
let c2 = new Child5();

console.log(c1.arr); // [1, 2, 3]
console.log(c2.arr); // [1, 2, 3]

c1.arr.push(4);
console.log(c1.arr); // [1, 2, 3, 4]
console.log(c2.arr); // [1, 2, 3]

console.log(c1 instanceof Child5); // true
console.log(c1 instanceof Parent5); // true

console.log(c1.constructor); // 指向的是Child5
  • 原理:使Object.create()传入了父类原型对象,使子类的prototype与新创建的对象的__proto__又重新关联起来了,那么再通过修改子类的prototype.constructor重新指向回它的构造函数,实现继承。
console.log(c1.__proto__ === Child5.prototype); // true
console.log(c1.__proto__ === Parent5.prototype); // false
  • 优点:优化了构造函数和原型方式的缺点

三、封装DOM查询的原型链继承的例子

function Elem(id) {
    this.elem = document.getElementById(id)
}

Elem.prototype.html = function (val) {
    var elem = this.elem;
    if (val) {
        elem.innerHTML = val;
        return this; // 链式操作
    } else {
        return elem.innerHTML;
    }
}

Elem.prototype.on = function (type, fn) {
    var elem = this.elem;
    elem.addEventListener(type, fn);

    return this;
}

// 用法
var box = new Elem('box');
console.log(box.html())

box.html("<p>hello world</p>").on('click', function () {
    console.log('click');
}).html("<p>hello javascript</p>")

希望对你学习有帮助,共勉,Thanks!

0人推荐
随时随地看视频
慕课网APP