JavaScript中的类和继承

JavaScript中的类和继承

工厂模式

function createPerson(name, age, job) {
	var o = new Object();
	o.name = name;
	o.age = age;
	o.job = job;
	o.sayName = function() {
		alert(this.name);
	};
	return o;
}
var person1 = createPerson('Nicholas', 29, 'Software Engineer');
var person2 = createPerson('Greg', 27, 'Doctor');

方便创建多个相似对象

没有解决对象识别的问题(即如何知道一个对象的类型)

构造函数模式

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = function() {
        alert(this.name);
    }
}
var person1 = new Person('Nicholas', 29, 'Software Engineer');
var person2 = new Person('Greg', 27, 'Doctor');

alert(person1.constructor === Person); // true
alert(person1 instanceof Object); // true
alert(person1 instanceof Person); // true

每个方法都要在每个实例上重新创建一遍

原型模式

function Person(){}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function() {
    alert(this.name);
};

var person1 = new Person();

ok:constructor instanceof

属性屏蔽

所有属性都是由实例共享(如果实例这样修改基本属性person1.name还可以,相当于屏蔽了原型里的name属性;函数由于本身就应该是共享的所以也可以;但是如果是引用类型的属性Person.prototype.friends = ['aa', 'bb']这种,如果person1.friends.push('cc')这样修改则会修改原型上的friends,导致person2的friends也会被修改)

[常用]组合使用构造函数模式和原型模式

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
}
Person.prototype = {
    constructor: Person,
    sayName: function() {
        alert(this.name);
    }
}

构造函数模式定义实例属性,原型模式定义方法和共享的属性

动态原型模式

function Person(name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    if (typeof this.sayName !== 'function') {
        Person.prototype.sayName = function() {
            alert(this.name);
        };
    }
}

寄生构造函数模式

function Person(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
		alert(this.name);
	};
    return o;
}
var person1 = new Person('Nicholas', 29, 'Software Engineer');

alert(person1.constructor === Person); // false
alert(person1 instanceof Object); // true
alert(person1 instanceof Person); // false

工厂模式+new = 构造函数有返回值

不能instanceof,因为这个构造函数有返回值所以new优先使用返回值(Object类型)

稳妥构造函数模式

function Person(name, age, job) {
    // 创建要返回的对象
    var o = new Object();
    
    // 可以在这里定义私有变量和函数
    
    // 添加方法
    o.sayName = function() {
		alert(this.name);
	};
    
    // 返回对象
    return o;
}
var person1 = Person('Nicholas', 29, 'Software Engineer');

稳妥对象:没有公共属性,而且其方法也不引用this的对象

除了使用sayName方法,没有其他方法能访问name的值

不能instanceof

es6的类

class PersonClass {
    constructor(name) { // 等价于PersonType构造函数
        this.name = name;
    }
    sayName() { // 等价于PersonType.prototype.sayName
        console.log(this.name);
    }
    static create(name) { // 静态成员,等价于PersonType.create
        return new PersonClass(name);
    }
}
let person = new PersonClass('Nicholas');
person.sayName(); // 'Nicholas'

console.log(person instanceof PersonClass); // true
console.log(person instanceof Object);      // true

let person2 = PersonClass.create('Nicholas');

只是语法糖,typeof PersonClass得到的是'function',本质就是[常用]组合使用构造函数模式和原型模式

表达式写法

let PersonClass = class {
    constructor(name) { // 等价于Person构造函数
        this.name = name;
    }
    sayName() { // 等价于Person.prototype.sayName
        console.log(this.name);
    }
}

与我们自己写的普通函数不同的是:

  • 类声明不会像函数一样被提升
  • 自动严格模式
  • 所有方法不可枚举(用Object.defineProperty定义)(通过for…in和in会取到原型链上的属性)
  • 不用new调用的话会抛出错误(用new.target判断)
  • 在类中修改类名会导致报错,在外部修改就可以(用const实现)

等价的实现

let PersonClass2 = (function() {
    'use strict';
    
    const PersonType2 = function(name) {
        // 确保通过new调用
        if (typeof new.target === 'undefined') {
            throw new Error('必须通过new调用构造函数');
        }
        
        this.name = name;
    }
    
    Object.defineProperty(PersonType2.prototype, 'sayName', {
        value: function() {
            // 确保不会通过new调用
            if (typeof new.target !== 'undefined') {
                throw new Error('不可使用new调用该方法');
            }
            
            console.log(this.name);
        },
        enumerable: false,
        writable: true,
        configurable: true
    });
    
    return PersonType2;
})();

参考:

  • 《JavaScript高级程序设计》第6章 面向对象的程序设计
  • 《深入理解es6》第9章 JavaScript中的类

继承

原型链继承

function SuperType() { // 组合使用构造函数模式和原型模式
    this.property = true;
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
};

function SubType() {
    this.subproperty = false;
}
SubType.prototype = new SuperType(); // 继承
SubType.prototype.getSubValue = function() {
    return this.subproperty;
}

var instance = new SubType();
alert(instance.getSuperValue()); // true

本质是重写原型对象SubType.prototype,代之以一个新类型的实例

注意:

  • 因为使用了new所以SubType.__proto__=SuperType.prototype
  • 如果取instance.constructor会通过原型链找到SuperType

instanceof Object / SuperType / SubType都为true

Object / SuperType / SubType .prototype.isPrototypeOf(instance)都为true

缺点:

  • 父类中的引用类型属性会被所有子类共享(因为SubType.prototype实际上是SuperType的实例,所以原先父类的实例属性变成了原型属性,就会出现原型模式中的属性共享问题)
  • 无法传递参数给SuperType
  • instance.constructor指向问题

借用构造函数继承

function SuperType() {
    this.colors = ['red', 'blue', 'green'];
}

function SubType() {
    SuperType.call(this); // 继承
}

var instance1 = new SubType();
instance1.colors.push('black');
alert(instance1.colors) // 'red,blue,green,black'

var instance2 = new SubType();
alert(instance2.colors) // 'red,blue,green'

可以在调用SuperType.call时传递参数,解决了原型链继承的属性共享问题

但是只这样写不能继承父类的原型属性/方法,只能继承实例属性和方法

而且同构造函数模式一样,每个子类都有父类实例函数的副本

instance1 instanceof SuperType会返回false

[常用]组合继承

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
    alert(this.name);
};

function SubType(name, age) {
    SuperType.call(this, name); // 继承属性,第二次调用SuperType()
    this.age = age;
}
SubType.prototype = new SuperType(); // 继承方法,第一次调用SuperType()
SubType.prototype.constructor = SubType; // 修复constructor指向
SubType.prototype.sayAge = function() {
    alert(this.age);
}

var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
alert(instance1.colors); // 'red,blue,green,black'
instance1.sayName(); // 'Nicholas'
instance1.sayAge(); // 29

var instance2 = new SubType('Greg', 27);
alert(instance2.colors); // 'red,blue,green'
instance2.sayName(); // 'Greg'
instance2.sayAge(); // 27

会调用两次SuperType(),导致instanceSubType.prototype中都有name和colors

原型式继承

function object(obj) {
	function F(){} // 自己造的构造函数
    F.prototype = obj; // 构造函数原型指向原对象
    return new F(); // 因此new调用时会将新对象的__proto__指向F.prototype也就是obj
}

var person = {
  name: 'Nicholas',
  friends: ['Shelby', 'Court', 'Van']
};

var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');

var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');

alert(person.friends);   // 'Shelby,Court,Van,Rob,Barbie'

在一个对象的基础上生成另一个对象,本质是对obj进行了浅复制

同原型链继承,存在属性共享问题和无法传递参数

等于Object.create()方法

  • person.isPrototypeOf(anotherPerson) = true
  • anotherPerson.__proto__ === person
  • instanceof不起效,因为anotherPerson的原型是个对象不是函数,根本没有prototype这个属性,见instanceof和isPrototypeOf()

寄生式继承

function createAnother(original) {
    var clone = object(original); // 上面的原型式继承创建一个新对象
    clone.sayHi = function() { // 增强新对象
        alert('hi');
    };
    return clone;
}

是原型式继承的扩展,缺点也是一样的

[常用]寄生组合式继承

function inheritPrototype(subType, superType) {
    var prototype = object(superType.prototype); // 创建对象
    prototype.constructor = subType;             // 增强对象,修复constructor指向
    subType.prototype = prototype                // 指定对象
}

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
    alert(this.name);
};

function SubType(name, age) {
    SuperType.call(this, name); // 继承属性
    this.age = age;
}
inheritPrototype(SubType, SuperType); // 继承方法
SubType.prototype.sayAge = function() {
    alert(this.age);
}

inheritPrototype(SubType, SuperType)这里没有向组合继承一样使用new SuperType(),因为只是想得到SuperType.prototype上的方法,所以只要通过原型式继承搞个浅复制的副本然后再像寄生式继承一样增强一下(修复constructor的指向,和组合继承一样)就可以了

总结起来就是,在[常用]组合使用构造函数模式和原型模式的基础上:

  • 构造函数模式对应借用构造函数继承(SuperType.call()),可以解决实例属性继承的问题(不会发生属性共享)
  • 原型模式对应原型链继承(new SuperType()),但我们其实只需要SuperType.prototype上的方法所以没必要把整个SuperType实例都搞过来(冗余问题)
  • 所以又根据原型式继承和寄生式继承,对原型链继承进行了优化,只对SuperType.prototype进行浅拷贝+增强,最终的SubType.prototype长得就和正常的prototype一样(有我们增强时候加上的constructor,有原型链继承来的方法,而没有冗余的属性)

用Object.create() / Object.setPrototypeOf()的写法

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
    alert(this.name);
};

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

// es5: Object.create()
SubType.prototype = Object.create(SuperType.prototype, {
    constructor: {
        value: SubType,
        enumrable: true,
        writaable: true,
        configurable: true
    }
});
// es6: Object.setPrototypeOf()
Object.setPrototypeOf(SubType.prototype, SuperType.prototype);

SubType.prototype.sayAge = function() {
    alert(this.age);
}

es6的继承

class Rectangle {
	constructor(length, width)  {
        this.length = length;
        this.width = width;
    }
    getArea() {
        return this.length * this.width;
    }
}

class Square extends Rectangle {
    constructor(length) {
        super(length, length); // 等价于Rectangle.call(this, length, length)
    }
}

var square = new Square(3);
console.log(square.getArea()); // 9
console.log(square instanceof Square); // true

extends关键字

super方法:

  • 在子类的构造函数中一定要调用super(),如果不写构造函数则会自动调用super()并传入所有参数
  • super()负责初始化this

super是指向父类原型的指针(等于Object.getPrototypeOf(this)),因此发生屏蔽时可以通过super.getArea()访问父类方法

extends还可以用于继承es5风格的构造函数,事实上只要表达式可以被解析为一个函数并具有[[Construct]]属性和原型就可以用extends进行派生

  • ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this)).
  • ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。

行为委托模式

行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。js中的[[Prototype]]机制本质上就是行为委托机制。主要通过Object.create()实现

Foo = {
    init: function(who) {
        this.me = who;
    },
    identfy: function() {
        return 'I am ' + this.me;
    }
};

Bar = Object.create(Foo);
Bar.speak = function() {
    alert('Hello, ' + this.identfy() + '.');
}

var b1 = Object.create(Bar);
b1.init('b1');
var b2 = Object.create(Bar);
b2.init('b2');

b1.speak();
b2.speak();

Foo.isPrototypeOf(Bar); // true
Object.getPrototypeOf(Bar) === Foo; // true

Foo.isPrototypeOf(b1); // true
Bar.isPrototypeOf(b1); // true
Object.getPrototypeOf(b1) === Bar; // true

参考:

  • 《JavaScript高级程序设计》第6章 面向对象的程序设计
  • 《深入理解es6》第9章 JavaScript中的类
  • JavaScript常用八种继承方案
  • 《你不知道的JavaScript 上卷》第二部分第6章 行为委托
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值